Skip to content

Commit 67cb83d

Browse files
authored
feat: find accounts by passkey (#227)
1 parent 9c31ecb commit 67cb83d

File tree

8 files changed

+472
-17
lines changed

8 files changed

+472
-17
lines changed

examples/demo-app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ forge-output-paymaster.json
2727
forge-output-erc1271.json
2828
contracts.json
2929
contracts-anvil.json
30+
public/contracts.json

examples/demo-app/pages/web-sdk-test.vue

Lines changed: 205 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
</div>
7777
<div>
7878
<strong>Account Address:</strong>
79-
<code class="bg-white px-2 py-1 rounded text-xs ml-2">{{ deploymentResult.address }}</code>
79+
<code class="bg-white px-2 py-1 rounded text-xs ml-2" data-testid="deployed-account-address">{{ deploymentResult.address }}</code>
8080
</div>
8181
<div v-if="deploymentResult.eoaSigner">
8282
<strong>EOA Signer:</strong>
@@ -132,6 +132,107 @@
132132
</div>
133133
</div>
134134

135+
<!-- Find Addresses by Passkey -->
136+
<div
137+
class="bg-indigo-50 p-4 rounded-lg mb-4 border border-indigo-200"
138+
data-testid="find-addresses-section"
139+
>
140+
<h2 class="text-lg font-semibold mb-3 text-indigo-800">
141+
Find Addresses by Passkey
142+
</h2>
143+
<p class="text-sm text-gray-600 mb-4">
144+
Authenticate with a passkey to find all smart account addresses associated with it.
145+
</p>
146+
147+
<div class="space-y-3">
148+
<!-- Scan Passkey Button (always visible) -->
149+
<button
150+
:disabled="loading"
151+
class="w-full px-4 py-2 bg-indigo-500 text-white rounded hover:bg-indigo-600 disabled:opacity-50"
152+
data-testid="scan-passkey-button"
153+
@click="scanPasskeyForFindAccounts"
154+
>
155+
{{ loading ? 'Authenticating...' : (findPasskeyScanned ? 'Scan Different Passkey' : 'Scan Passkey to Find Accounts') }}
156+
</button>
157+
158+
<!-- Passkey Info & Found Addresses (shown after scanning) -->
159+
<div
160+
v-if="findPasskeyScanned"
161+
class="p-3 bg-white rounded border border-indigo-300"
162+
>
163+
<div class="space-y-3">
164+
<div>
165+
<p class="text-xs text-gray-600 mb-1">
166+
<strong>Passkey Credential ID:</strong>
167+
</p>
168+
<code class="text-xs font-mono break-all">{{ findPasskeyCredentialId }}</code>
169+
</div>
170+
171+
<div>
172+
<p class="text-xs text-gray-600 mb-1">
173+
<strong>Origin Domain:</strong>
174+
</p>
175+
<code class="text-xs font-mono">{{ findPasskeyOriginDomain }}</code>
176+
</div>
177+
178+
<!-- Found Addresses -->
179+
<div v-if="foundAddresses !== null">
180+
<p class="text-xs text-gray-600 mb-1">
181+
<strong>Associated Accounts:</strong>
182+
</p>
183+
<div
184+
v-if="foundAddresses.length > 0"
185+
class="mt-2"
186+
data-testid="found-addresses-result"
187+
>
188+
<ul class="space-y-1" data-testid="found-addresses-list">
189+
<li
190+
v-for="(address, index) in foundAddresses"
191+
:key="address"
192+
class="text-xs font-mono bg-gray-100 px-2 py-1 rounded"
193+
data-testid="found-address-item"
194+
>
195+
{{ index + 1 }}. {{ address }}
196+
</li>
197+
</ul>
198+
</div>
199+
<div
200+
v-else
201+
class="mt-2 p-2 bg-yellow-50 rounded border border-yellow-200"
202+
data-testid="no-addresses-found"
203+
>
204+
<p class="text-xs text-yellow-800">
205+
No accounts found for this passkey.
206+
</p>
207+
</div>
208+
</div>
209+
</div>
210+
</div>
211+
212+
<!-- Errors -->
213+
<div
214+
v-if="findPasskeyScanError"
215+
class="p-3 bg-red-50 rounded border border-red-300"
216+
>
217+
<strong class="text-sm text-red-800">Scan Error:</strong>
218+
<p class="text-xs text-red-600 mt-1">
219+
{{ findPasskeyScanError }}
220+
</p>
221+
</div>
222+
223+
<div
224+
v-if="findAddressesError"
225+
class="p-3 bg-red-50 rounded border border-red-300"
226+
data-testid="find-addresses-error"
227+
>
228+
<strong class="text-sm text-red-800">Search Error:</strong>
229+
<p class="text-xs text-red-600 mt-1">
230+
{{ findAddressesError }}
231+
</p>
232+
</div>
233+
</div>
234+
</div>
235+
135236
<!-- Fund Smart Account -->
136237
<div
137238
v-if="deploymentResult && (!deploymentResult.passkeyEnabled || passkeyRegistered)"
@@ -292,7 +393,7 @@ import { createWalletClient, http, type Hash, type Hex, type Address, parseEther
292393
import { privateKeyToAccount } from "viem/accounts";
293394
import { createBundlerClient } from "viem/account-abstraction";
294395
295-
import { createEcdsaClient, prepareDeploySmartAccount, getAccountAddressFromLogs, generateAccountId } from "zksync-sso-4337/client";
396+
import { createEcdsaClient, prepareDeploySmartAccount, getAccountAddressFromLogs, generateAccountId, findAddressesByPasskey } from "zksync-sso-4337/client";
296397
297398
import { loadContracts, getBundlerUrl, getChainConfig, createPublicClient } from "~/utils/contracts";
298399
@@ -329,6 +430,14 @@ const passkeyRegistered = ref(false);
329430
const passkeyRegisterResult = ref("");
330431
const passkeyRegisterError = ref("");
331432
433+
// Find addresses by passkey state
434+
const findPasskeyScanned = ref(false);
435+
const findPasskeyCredentialId = ref("");
436+
const findPasskeyOriginDomain = ref("");
437+
const findPasskeyScanError = ref("");
438+
const foundAddresses = ref<Address[] | null>(null);
439+
const findAddressesError = ref("");
440+
332441
// Fund smart account parameters
333442
const fundParams = ref({
334443
amount: "0.1",
@@ -714,6 +823,100 @@ async function registerPasskey() {
714823
}
715824
}
716825
826+
/**
827+
* Scan/authenticate with a passkey and automatically find associated accounts
828+
*/
829+
async function scanPasskeyForFindAccounts() {
830+
loading.value = true;
831+
findPasskeyScanError.value = "";
832+
foundAddresses.value = null;
833+
findAddressesError.value = "";
834+
findPasskeyScanned.value = false;
835+
836+
try {
837+
// eslint-disable-next-line no-console
838+
console.log("Requesting WebAuthn authentication to scan passkey...");
839+
840+
// Create a challenge for authentication
841+
const challenge = new Uint8Array(32);
842+
crypto.getRandomValues(challenge);
843+
844+
// Request authentication to get the credential ID
845+
const credential = await navigator.credentials.get({
846+
publicKey: {
847+
challenge,
848+
timeout: 60000,
849+
rpId: window.location.hostname,
850+
userVerification: "required",
851+
},
852+
});
853+
854+
if (!credential || credential.type !== "public-key") {
855+
throw new Error("Failed to authenticate with passkey");
856+
}
857+
858+
const pkCredential = credential as PublicKeyCredential;
859+
860+
// Extract credential ID
861+
const credentialId = new Uint8Array(pkCredential.rawId);
862+
const credentialIdHex = `0x${Array.from(credentialId).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
863+
864+
// Set the scanned passkey details
865+
findPasskeyCredentialId.value = credentialIdHex;
866+
findPasskeyOriginDomain.value = window.location.origin;
867+
findPasskeyScanned.value = true;
868+
869+
// eslint-disable-next-line no-console
870+
console.log("Passkey scanned successfully:");
871+
// eslint-disable-next-line no-console
872+
console.log(" Credential ID:", credentialIdHex);
873+
// eslint-disable-next-line no-console
874+
console.log(" Origin:", window.location.origin);
875+
876+
// Automatically find accounts for this passkey
877+
// eslint-disable-next-line no-console
878+
console.log("Finding accounts for passkey...");
879+
880+
// Load contracts configuration
881+
const contracts = await loadContracts();
882+
883+
// Create public client
884+
const publicClient = await createPublicClient(contracts);
885+
886+
// eslint-disable-next-line no-console
887+
console.log(" WebAuthn Validator:", contracts.webauthnValidator);
888+
889+
// Call the findAddressesByPasskey action
890+
const result = await findAddressesByPasskey({
891+
client: publicClient,
892+
contracts: {
893+
webauthnValidator: contracts.webauthnValidator as Address,
894+
},
895+
passkey: {
896+
credentialId: credentialIdHex as Hex,
897+
originDomain: window.location.origin,
898+
},
899+
});
900+
901+
foundAddresses.value = result.addresses;
902+
903+
// eslint-disable-next-line no-console
904+
console.log(` Found ${result.addresses.length} account(s):`, result.addresses);
905+
} catch (err: unknown) {
906+
// eslint-disable-next-line no-console
907+
console.error("Failed to scan passkey or find accounts:", err);
908+
909+
// Determine if error was during scan or search
910+
if (!findPasskeyScanned.value) {
911+
findPasskeyScanError.value = `Failed to scan passkey: ${err instanceof Error ? err.message : String(err)}`;
912+
} else {
913+
findAddressesError.value = `Failed to find accounts: ${err instanceof Error ? err.message : String(err)}`;
914+
}
915+
} finally {
916+
loading.value = false;
917+
}
918+
}
919+
717920
// Fund the smart account with ETH from EOA wallet
718921
async function fundSmartAccount() {
719922
loading.value = true;

examples/demo-app/public/contracts.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

examples/demo-app/tests/web-sdk-test.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,109 @@ test("Deploy with passkey and send transaction using passkey", async ({ page })
208208

209209
console.log("✅ All passkey steps completed successfully!");
210210
});
211+
212+
test("Find addresses by passkey credential ID", async ({ page }) => {
213+
// Use Anvil account #4 for this test to avoid nonce conflicts
214+
await page.goto("/web-sdk-test?fundingAccount=4");
215+
await expect(page.getByText("ZKSync SSO Web SDK Test")).toBeVisible();
216+
217+
// Wait for SDK to load
218+
await expect(page.getByText("SDK Loaded:")).toBeVisible();
219+
await expect(page.getByText("Yes")).toBeVisible({ timeout: 10000 });
220+
221+
console.log("Step 1: Enabling passkey configuration...");
222+
223+
// Enable passkey deployment
224+
const passkeyCheckbox = page.getByLabel("Enable Passkey Deployment");
225+
await expect(passkeyCheckbox).toBeVisible();
226+
await passkeyCheckbox.check();
227+
228+
console.log("Step 2: Creating WebAuthn passkey...");
229+
230+
// Create a virtual authenticator for testing
231+
const client = await page.context().newCDPSession(page);
232+
await client.send("WebAuthn.enable");
233+
await client.send("WebAuthn.addVirtualAuthenticator", {
234+
options: {
235+
protocol: "ctap2",
236+
transport: "usb",
237+
hasResidentKey: true,
238+
hasUserVerification: true,
239+
isUserVerified: true,
240+
},
241+
});
242+
243+
// Click Create New WebAuthn Passkey button
244+
await page.getByRole("button", { name: "Create New WebAuthn Passkey" }).click();
245+
246+
// Wait for passkey creation to complete
247+
await expect(page.getByText("Passkey created successfully!")).toBeVisible({ timeout: 10000 });
248+
249+
console.log("✓ WebAuthn passkey created successfully");
250+
251+
// Step 3: Deploy Account with Passkey
252+
console.log("Step 3: Deploying smart account with passkey...");
253+
await page.getByRole("button", { name: "Deploy Account" }).click();
254+
255+
// Wait for deployment to complete
256+
await expect(page.getByText("Account Deployed Successfully!")).toBeVisible({ timeout: 30000 });
257+
258+
// Verify passkey is enabled in deployment result
259+
await expect(page.getByText("Passkey Enabled: Yes")).toBeVisible();
260+
261+
console.log("✓ Smart account with passkey deployed successfully");
262+
263+
// Get the deployed account address
264+
const deployedAddressElement = page.getByTestId("deployed-account-address");
265+
await expect(deployedAddressElement).toBeVisible();
266+
const deployedAddress = await deployedAddressElement.textContent();
267+
console.log(`Deployed account address: ${deployedAddress}`);
268+
269+
// Step 4: Find addresses by passkey
270+
console.log("Step 4: Finding addresses by passkey credential ID...");
271+
272+
// Verify the "Find Addresses by Passkey" section is visible
273+
const findAddressesSection = page.getByTestId("find-addresses-section");
274+
await expect(findAddressesSection).toBeVisible();
275+
276+
// Click the "Scan Passkey to Find Accounts" button - this will automatically find accounts
277+
const scanPasskeyButton = page.getByTestId("scan-passkey-button");
278+
await expect(scanPasskeyButton).toBeVisible();
279+
await scanPasskeyButton.click();
280+
281+
// Wait for the results to appear (scanning + finding happens automatically)
282+
const foundAddressesResult = page.getByTestId("found-addresses-result");
283+
await expect(foundAddressesResult).toBeVisible({ timeout: 15000 });
284+
285+
// Verify "Associated Accounts:" label is visible
286+
await expect(page.getByText("Associated Accounts:")).toBeVisible();
287+
288+
// Get all found addresses
289+
const foundAddressList = page.getByTestId("found-addresses-list");
290+
await expect(foundAddressList).toBeVisible();
291+
292+
const addressItems = foundAddressList.getByTestId("found-address-item");
293+
const addressCount = await addressItems.count();
294+
console.log(`Found ${addressCount} address(es)`);
295+
296+
// Verify that the deployed address is in the list
297+
let foundDeployedAddress = false;
298+
for (let i = 0; i < addressCount; i++) {
299+
const addressText = await addressItems.nth(i).textContent();
300+
console.log(` Address ${i + 1}: ${addressText}`);
301+
302+
// Check if this address item contains the deployed address
303+
if (addressText && addressText.includes(deployedAddress || "")) {
304+
foundDeployedAddress = true;
305+
console.log(" ✓ Deployed address found in list!");
306+
}
307+
}
308+
309+
// Assert that we found the deployed address
310+
expect(foundDeployedAddress, `Deployed address ${deployedAddress} should be in the found addresses list`).toBe(true);
311+
312+
console.log("✓ Successfully found addresses by passkey");
313+
console.log("✓ Verified deployed address is in the found addresses list");
314+
315+
console.log("✅ Find addresses by passkey test completed successfully!");
316+
});

packages/sdk-4337/src/client/actions/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ export type {
33
PrepareDeploySmartAccountResult,
44
} from "./deploy.js";
55
export { getAccountAddressFromLogs, prepareDeploySmartAccount } from "./deploy.js";
6-
export type { AddPasskeyParams, AddPasskeyResult } from "./passkey.js";
7-
export { addPasskey } from "./passkey.js";
6+
export type {
7+
AddPasskeyParams,
8+
AddPasskeyResult,
9+
FindAddressesByPasskeyParams,
10+
FindAddressesByPasskeyResult,
11+
} from "./passkey.js";
12+
export { addPasskey, findAddressesByPasskey } from "./passkey.js";
813
export { generateAccountId } from "./utils.js";

0 commit comments

Comments
 (0)