Skip to content

Commit 8349b92

Browse files
authored
E2E web-sdk passkey-paymaster (#247)
next step is getting auth-servers working
1 parent 48aa667 commit 8349b92

File tree

46 files changed

+1985
-586
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1985
-586
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,24 @@ jobs:
9898
- name: Install Playwright Chromium Browser
9999
run: pnpm exec playwright install chromium
100100
working-directory: examples/demo-app
101-
- name: Run e2e tests
101+
- name: Run ERC4337 e2e tests
102102
run: pnpm nx e2e:erc4337 demo-app
103103
- uses: actions/upload-artifact@v4
104104
if: ${{ !cancelled() }}
105105
with:
106106
name: demo-app-4337-playwright-report
107107
path: examples/demo-app/playwright-report/
108108
retention-days: 3
109-
109+
110+
- name: Run demo-only e2e tests (session + passkey)
111+
run: pnpm nx e2e:demo-only demo-app
112+
- uses: actions/upload-artifact@v4
113+
if: ${{ !cancelled() }}
114+
with:
115+
name: demo-app-demo-only-playwright-report
116+
path: examples/demo-app/playwright-report/
117+
retention-days: 3
118+
110119
# e2e-nft-quest:
111120
# runs-on: ubuntu-latest
112121
# defaults:
@@ -166,4 +175,4 @@ jobs:
166175
# with:
167176
# name: nft-quest-playwright-report
168177
# path: examples/nft-quest/playwright-report/
169-
# retention-days: 3
178+
# retention-days: 3

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,11 @@ This monorepo is comprised of the following packages, products, and examples:
175175
pnpm nx dev demo-app
176176
```
177177

178-
Your local Auth Server will be running at `http://localhost:3002/`, and the demo
179-
app will be running at `http://localhost:3004/`.
178+
local port list:
179+
180+
- auth server api: 3004
181+
- auth server: 3002
182+
- demo app: 3005
180183

181184
## Running commands
182185

examples/demo-app/components/SessionCreator.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ async function createSessionOnChain() {
190190
address: props.accountAddress as Address,
191191
signerPrivateKey: props.eoaPrivateKey as `0x${string}`,
192192
eoaValidatorAddress: props.eoaValidatorAddress as Address,
193+
entryPointAddress: contracts.entryPoint as Address,
193194
});
194195
195196
// eslint-disable-next-line no-console

examples/demo-app/components/SessionTransactionSender.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ async function sendTransaction() {
187187
bundlerClient,
188188
chain,
189189
transport: http(),
190+
entryPointAddress: contracts.entryPoint as Address,
190191
});
191192
192193
// If an allowed recipient is configured, enforce it matches the selected target

examples/demo-app/components/TransactionSender.vue

Lines changed: 133 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@
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"
@@ -92,15 +107,23 @@
92107

93108
<script setup lang="ts">
94109
import { ref } from "vue";
95-
import { formatEther, http, parseEther } from "viem";
96-
import { createBundlerClient } from "viem/account-abstraction";
110+
import { formatEther, parseEther } from "viem";
97111
import type { Address } from "viem";
98112
99-
import { createEcdsaClient, createPasskeyClient } from "zksync-sso-4337/client";
100113
import { 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
102116
import { 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
105128
const props = defineProps({
106129
deploymentResult: {
@@ -117,6 +140,7 @@ const props = defineProps({
117140
const signingMethod = ref("eoa");
118141
const to = ref("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); // Anvil account #2
119142
const amount = ref("0.001");
143+
const usePaymaster = ref(false);
120144
const loading = ref(false);
121145
const txResult = ref("");
122146
const 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

Comments
 (0)