5757 >
5858 </div >
5959
60+ <!-- Optional Paymaster -->
61+ <div class =" p-3 bg-white rounded border border-indigo-300" >
62+ <label class =" flex items-center" >
63+ <input
64+ v-model =" usePaymaster"
65+ type =" checkbox"
66+ class =" mr-2"
67+ >
68+ <span class =" text-sm" >Use Demo Paymaster (sponsor gas)</span >
69+ </label >
70+ <p class =" text-xs text-gray-600 mt-2" >
71+ When enabled, the user operation will include `paymaster` parameters and higher verification gas.
72+ </p >
73+ </div >
74+
6075 <!-- Send Button -->
6176 <button
6277 :disabled =" loading || !deploymentResult"
92107
93108<script setup lang="ts">
94109import { ref } from " vue" ;
95- import { formatEther , http , parseEther } from " viem" ;
96- import { createBundlerClient } from " viem/account-abstraction" ;
110+ import { formatEther , parseEther } from " viem" ;
97111import type { Address } from " viem" ;
98112
99- import { createEcdsaClient , createPasskeyClient } from " zksync-sso-4337/client" ;
100113import { WebAuthnValidatorAbi } from " zksync-sso-4337/abi" ;
114+ import { PaymasterParams , prepare_passkey_user_operation , submit_passkey_user_operation , SendTransactionConfig , send_transaction_eoa , signWithPasskey } from " zksync-sso-web-sdk/bundler" ;
101115
102116import { loadContracts , getBundlerUrl , getChainConfig , createPublicClient } from " ~/utils/contracts" ;
103117
118+ const getRpId = (origin : string ) => {
119+ try {
120+ return new URL (origin ).hostname ;
121+ } catch {
122+ // Fallback to raw origin string if parsing fails
123+ return origin ;
124+ }
125+ };
126+
104127// Props
105128const props = defineProps ({
106129 deploymentResult: {
@@ -117,6 +140,7 @@ const props = defineProps({
117140const signingMethod = ref (" eoa" );
118141const to = ref (" 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" ); // Anvil account #2
119142const amount = ref (" 0.001" );
143+ const usePaymaster = ref (false );
120144const loading = ref (false );
121145const txResult = ref (" " );
122146const txError = ref (" " );
@@ -149,7 +173,7 @@ async function sendTransaction() {
149173 const balance = await publicClient .getBalance ({
150174 address: props .deploymentResult .address as Address ,
151175 });
152- const balanceEth = formatEther (balance );
176+ const balanceEth = Number ( formatEther (balance )). toFixed ( 6 );
153177
154178 // eslint-disable-next-line no-console
155179 console .log (" Smart account balance:" , balanceEth , " ETH" );
@@ -173,10 +197,10 @@ async function sendTransaction() {
173197 } else {
174198 await sendFromSmartAccountWithEOA ();
175199 }
176- } catch (err ) {
200+ } catch (error ) {
177201 // eslint-disable-next-line no-console
178- console .error (" Transaction failed:" , err );
179- txError .value = ` Failed to send transaction: ${(err as Error ).message } ` ;
202+ console .error (" Transaction failed:" , error );
203+ txError .value = ` Failed to send transaction: ${(error as Error ).message } ` ;
180204 } finally {
181205 loading .value = false ;
182206 }
@@ -188,44 +212,49 @@ async function sendFromSmartAccountWithEOA() {
188212 const contracts = await loadContracts ();
189213 const chain = getChainConfig (contracts );
190214
191- // Create public client for network calls
192- const publicClient = await createPublicClient (contracts );
193-
194- // Create bundler client with entry point configuration
195- const bundlerClient = createBundlerClient ({
196- client: publicClient ,
197- chain ,
198- transport: http (getBundlerUrl (contracts )),
199- });
200-
201- // Create ECDSA client using the new SDK
202215 // eslint-disable-next-line no-console
203- console .log (" Creating ECDSA client..." );
204- const client = createEcdsaClient ({
205- account: {
206- address: props .deploymentResult .address as Address ,
207- // EOA signer private key (Anvil account #1) - this will sign the UserOperation
208- signerPrivateKey: " 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" ,
209- eoaValidatorAddress: contracts .eoaValidator as Address ,
210- },
211- bundlerClient ,
212- chain ,
213- transport: http (), // Use default RPC URL (not bundler URL)
214- });
216+ console .log (" Sending transaction via Rust FFI path..." );
217+
218+ // Prepare params for sendUserOperation
219+ const rpcUrl = chain .rpcUrls .default .http [0 ];
220+ const bundlerUrl = getBundlerUrl (contracts );
221+ const entryPoint = contracts .entryPoint as Address ;
222+ const account = props .deploymentResult .address as Address ;
223+ const validator = contracts .eoaValidator as Address ;
224+ const valueWei = parseEther (amount .value ).toString ();
225+
226+ const privateKey = " 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" as ` 0x${string } ` ;
227+
228+ // Use paymaster if checkbox is enabled
229+ let paymasterParams = null ;
230+ if (usePaymaster .value ) {
231+ if (! props .passkeyConfig .paymasterAddress ) {
232+ throw new Error (" Paymaster address is not configured. Please configure paymaster in the Passkey Config section." );
233+ }
234+ if (! props .passkeyConfig .paymasterFlow ) {
235+ throw new Error (" Paymaster flow is not configured. Please configure paymaster in the Passkey Config section." );
236+ }
237+ paymasterParams = new PaymasterParams (
238+ props .passkeyConfig .paymasterAddress ,
239+ props .passkeyConfig .paymasterFlow ,
240+ );
241+ }
215242
216- // Send transaction (returns actual tx hash, not userOp hash)
217- // eslint-disable-next-line no-console
218- console .log (" Sending transaction..." );
219- const txHash = await client .sendTransaction ({
220- to: to .value as Address ,
221- value: parseEther (amount .value ),
222- data: " 0x" ,
223- });
243+ const config = new SendTransactionConfig (rpcUrl , bundlerUrl , entryPoint );
244+ const userOpHashOrReceipt = await send_transaction_eoa (
245+ config ,
246+ validator ,
247+ privateKey ,
248+ account ,
249+ to .value ,
250+ valueWei ,
251+ " 0x" ,
252+ paymasterParams ,
253+ );
224254
225255 // eslint-disable-next-line no-console
226- console .log (" Transaction confirmed! Hash:" , txHash );
227-
228- txResult .value = ` Transaction successful! Tx hash: ${txHash } ` ;
256+ console .log (" User operation submitted:" , userOpHashOrReceipt );
257+ txResult .value = ` UserOperation submitted: ${userOpHashOrReceipt } ` ;
229258}
230259
231260// Send transaction using Passkey validator (NEW SDK)
@@ -250,6 +279,8 @@ async function sendFromSmartAccountWithPasskey() {
250279 console .log (" Amount:" , amount .value , " ETH" );
251280 // eslint-disable-next-line no-console
252281 console .log (" WebAuthn Validator:" , webauthnValidatorAddress );
282+ // eslint-disable-next-line no-console
283+ console .log (" Use Paymaster:" , usePaymaster .value );
253284
254285 // Verify the public key is registered on-chain
255286 // eslint-disable-next-line no-console
@@ -295,56 +326,72 @@ async function sendFromSmartAccountWithPasskey() {
295326 // eslint-disable-next-line no-console
296327 console .log (" ✓ Public key verification passed!" );
297328
298- // Create bundler client with entry point configuration
329+ const rpId = getRpId (props .passkeyConfig .originDomain );
330+ // Send transaction using Rust passkey flow (without paymaster for normal transactions)
331+ const rpcUrl = chain .rpcUrls .default .http [0 ];
332+ const bundlerUrl = getBundlerUrl (contracts );
333+ const entryPoint = contracts .entryPoint as Address ;
334+ const config = new SendTransactionConfig (rpcUrl , bundlerUrl , entryPoint );
335+
336+ const valueWei = parseEther (amount .value ).toString ();
337+
338+ // Don't use paymaster for normal passkey transactions
339+ // (paymaster is tested separately in web-sdk-test.vue)
340+
341+ // Step 1: prepare userOp and hash (optionally with paymaster)
342+ const paymasterParams = usePaymaster .value
343+ ? new PaymasterParams (props .passkeyConfig .paymasterAddress , null , null , null )
344+ : undefined ;
345+
346+ const preparedJson = await prepare_passkey_user_operation (
347+ config ,
348+ webauthnValidatorAddress ,
349+ props .deploymentResult .address ,
350+ to .value ,
351+ valueWei ,
352+ " 0x" ,
353+ paymasterParams ,
354+ );
355+
356+ // Debug + guards to ensure prepare returned a valid payload
299357 // eslint-disable-next-line no-console
300- console .log (" Creating bundler client..." );
301- const bundlerClient = createBundlerClient ({
302- client: publicClient ,
303- chain ,
304- transport: http (getBundlerUrl (contracts )),
305- userOperation: {
306- // Use fixed gas values matching old Rust SDK implementation
307- // (old SDK used: 2M callGas, 2M verificationGas, 1M preVerificationGas)
308- async estimateFeesPerGas() {
309- const feesPerGas = await publicClient .estimateFeesPerGas ();
310- return {
311- callGasLimit: 2_000_000n ,
312- verificationGasLimit: 2_000_000n ,
313- preVerificationGas: 1_000_000n ,
314- ... feesPerGas ,
315- };
316- },
317- },
318- });
358+ console .log (" prepare_passkey_user_operation result:" , preparedJson );
359+ if (typeof preparedJson !== " string" ) {
360+ throw new Error (" Unexpected prepare result type" );
361+ }
362+ if (preparedJson .startsWith (" Failed" ) || preparedJson .startsWith (" Error" )) {
363+ throw new Error (preparedJson );
364+ }
319365
320- // Create passkey client (wraps account + bundler)
321- // eslint-disable-next-line no-console
322- console .log (" Creating passkey client with createPasskeyClient..." );
323- const client = createPasskeyClient ({
324- account: {
325- address: props .deploymentResult .address as Address ,
326- validatorAddress: webauthnValidatorAddress as Address ,
327- credentialId: props .passkeyConfig .credentialId ,
328- rpId: window .location .hostname ,
329- origin: window .location .origin ,
330- },
331- bundlerClient ,
332- chain ,
333- transport: http (),
334- });
366+ const { hash, userOp } = JSON .parse (preparedJson ) as { hash: string ; userOp: unknown };
367+ if (! hash ) {
368+ throw new Error (" Prepare step did not return a hash" );
369+ }
335370
336- // Send transaction (client handles bundler operations and returns actual tx hash)
337- // eslint-disable-next-line no-console
338- console .log (" Sending transaction..." );
339- const txHash = await client .sendTransaction ({
340- to: to .value as Address ,
341- value: parseEther (amount .value ),
342- data: " 0x" ,
371+ // Step 2: sign the hash with WebAuthn passkey (signature already includes validator prefix)
372+ const signResult = await signWithPasskey ({
373+ hash ,
374+ credentialId: props .passkeyConfig .credentialId as ` 0x${string } ` ,
375+ rpId ,
376+ origin: props .passkeyConfig .originDomain ,
343377 });
344378
345- // eslint-disable-next-line no-console
346- console .log (" Transaction confirmed! Hash:" , txHash );
379+ if (! signResult || ! signResult .signature ) {
380+ throw new Error (" No passkey signature returned from WebAuthn" );
381+ }
382+
383+ const { signature } = signResult ;
384+
385+ // Step 3: submit signed userOp (paymaster already embedded if used)
386+ // Important: create a fresh config for submit. The config used in
387+ // prepare may be consumed by WASM and invalid for reuse.
388+ const submitConfig = new SendTransactionConfig (rpcUrl , bundlerUrl , entryPoint );
389+ const receipt = await submit_passkey_user_operation (
390+ submitConfig ,
391+ JSON .stringify (userOp ),
392+ signature ,
393+ );
347394
348- txResult .value = ` Transaction successful! Tx hash: ${ txHash } ` ;
395+ txResult .value = receipt as string ;
349396}
350397 </script >
0 commit comments