|
76 | 76 | </div> |
77 | 77 | <div> |
78 | 78 | <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> |
80 | 80 | </div> |
81 | 81 | <div v-if="deploymentResult.eoaSigner"> |
82 | 82 | <strong>EOA Signer:</strong> |
|
132 | 132 | </div> |
133 | 133 | </div> |
134 | 134 |
|
| 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 | + |
135 | 236 | <!-- Fund Smart Account --> |
136 | 237 | <div |
137 | 238 | v-if="deploymentResult && (!deploymentResult.passkeyEnabled || passkeyRegistered)" |
@@ -292,7 +393,7 @@ import { createWalletClient, http, type Hash, type Hex, type Address, parseEther |
292 | 393 | import { privateKeyToAccount } from "viem/accounts"; |
293 | 394 | import { createBundlerClient } from "viem/account-abstraction"; |
294 | 395 |
|
295 | | -import { createEcdsaClient, prepareDeploySmartAccount, getAccountAddressFromLogs, generateAccountId } from "zksync-sso-4337/client"; |
| 396 | +import { createEcdsaClient, prepareDeploySmartAccount, getAccountAddressFromLogs, generateAccountId, findAddressesByPasskey } from "zksync-sso-4337/client"; |
296 | 397 |
|
297 | 398 | import { loadContracts, getBundlerUrl, getChainConfig, createPublicClient } from "~/utils/contracts"; |
298 | 399 |
|
@@ -329,6 +430,14 @@ const passkeyRegistered = ref(false); |
329 | 430 | const passkeyRegisterResult = ref(""); |
330 | 431 | const passkeyRegisterError = ref(""); |
331 | 432 |
|
| 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 | +
|
332 | 441 | // Fund smart account parameters |
333 | 442 | const fundParams = ref({ |
334 | 443 | amount: "0.1", |
@@ -714,6 +823,100 @@ async function registerPasskey() { |
714 | 823 | } |
715 | 824 | } |
716 | 825 |
|
| 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 | +
|
717 | 920 | // Fund the smart account with ETH from EOA wallet |
718 | 921 | async function fundSmartAccount() { |
719 | 922 | loading.value = true; |
|
0 commit comments