@aztec/wallet-sdk 0.0.1-commit.fcb71a6 → 3.0.0-devnet.2-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +241 -267
  2. package/dest/crypto.d.ts +183 -0
  3. package/dest/crypto.d.ts.map +1 -0
  4. package/dest/crypto.js +300 -0
  5. package/dest/manager/index.d.ts +4 -3
  6. package/dest/manager/index.d.ts.map +1 -1
  7. package/dest/manager/index.js +2 -0
  8. package/dest/manager/types.d.ts +22 -1
  9. package/dest/manager/types.d.ts.map +1 -1
  10. package/dest/manager/wallet_manager.d.ts +1 -1
  11. package/dest/manager/wallet_manager.d.ts.map +1 -1
  12. package/dest/manager/wallet_manager.js +34 -15
  13. package/dest/providers/extension/extension_provider.d.ts +53 -7
  14. package/dest/providers/extension/extension_provider.d.ts.map +1 -1
  15. package/dest/providers/extension/extension_provider.js +81 -13
  16. package/dest/providers/extension/extension_wallet.d.ts +140 -8
  17. package/dest/providers/extension/extension_wallet.d.ts.map +1 -1
  18. package/dest/providers/extension/extension_wallet.js +268 -46
  19. package/dest/providers/extension/index.d.ts +6 -4
  20. package/dest/providers/extension/index.d.ts.map +1 -1
  21. package/dest/providers/extension/index.js +2 -0
  22. package/dest/types.d.ts +92 -0
  23. package/dest/types.d.ts.map +1 -0
  24. package/dest/types.js +10 -0
  25. package/package.json +10 -8
  26. package/src/crypto.ts +375 -0
  27. package/src/manager/index.ts +4 -8
  28. package/src/manager/types.ts +22 -0
  29. package/src/manager/wallet_manager.ts +43 -16
  30. package/src/providers/extension/extension_provider.ts +112 -17
  31. package/src/providers/extension/extension_wallet.ts +310 -55
  32. package/src/providers/extension/index.ts +5 -3
  33. package/src/{providers/types.ts → types.ts} +33 -6
  34. package/dest/providers/types.d.ts +0 -67
  35. package/dest/providers/types.d.ts.map +0 -1
  36. package/dest/providers/types.js +0 -3
package/src/crypto.ts ADDED
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Cryptographic utilities for secure wallet communication.
3
+ *
4
+ * This module provides ECDH key exchange and AES-GCM encryption primitives
5
+ * for establishing secure communication channels between dApps and wallet extensions.
6
+ *
7
+ * The crypto flow:
8
+ * 1. Both parties generate ECDH key pairs using {@link generateKeyPair}
9
+ * 2. Public keys are exchanged (exported via {@link exportPublicKey}, imported via {@link importPublicKey})
10
+ * 3. Both parties derive the same shared secret using {@link deriveSharedKey}
11
+ * 4. Messages are encrypted/decrypted using {@link encrypt} and {@link decrypt}
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Party A
16
+ * const keyPairA = await generateKeyPair();
17
+ * const publicKeyA = await exportPublicKey(keyPairA.publicKey);
18
+ *
19
+ * // Party B
20
+ * const keyPairB = await generateKeyPair();
21
+ * const publicKeyB = await exportPublicKey(keyPairB.publicKey);
22
+ *
23
+ * // Exchange public keys, then derive shared secret
24
+ * const importedB = await importPublicKey(publicKeyB);
25
+ * const sharedKeyA = await deriveSharedKey(keyPairA.privateKey, importedB);
26
+ *
27
+ * // Encrypt and decrypt
28
+ * const encrypted = await encrypt(sharedKeyA, { message: 'hello' });
29
+ * const decrypted = await decrypt(sharedKeyB, encrypted);
30
+ * ```
31
+ *
32
+ * @packageDocumentation
33
+ */
34
+ import { jsonStringify } from '@aztec/foundation/json-rpc';
35
+
36
+ /**
37
+ * Exported public key in JWK format for transmission over untrusted channels.
38
+ *
39
+ * Contains only the public components of an ECDH P-256 key, safe to share.
40
+ */
41
+ export interface ExportedPublicKey {
42
+ /** Key type - always "EC" for elliptic curve */
43
+ kty: string;
44
+ /** Curve name - always "P-256" */
45
+ crv: string;
46
+ /** X coordinate (base64url encoded) */
47
+ x: string;
48
+ /** Y coordinate (base64url encoded) */
49
+ y: string;
50
+ }
51
+
52
+ /**
53
+ * Encrypted message payload containing ciphertext and initialization vector.
54
+ *
55
+ * Both fields are base64-encoded for safe transmission as JSON.
56
+ */
57
+ export interface EncryptedPayload {
58
+ /** Initialization vector (base64 encoded, 12 bytes) */
59
+ iv: string;
60
+ /** Ciphertext (base64 encoded) */
61
+ ciphertext: string;
62
+ }
63
+
64
+ /**
65
+ * ECDH key pair for secure communication.
66
+ *
67
+ * The private key should never be exported or transmitted.
68
+ * The public key can be exported via {@link exportPublicKey} for exchange.
69
+ */
70
+ export interface SecureKeyPair {
71
+ /** Public key - safe to share */
72
+ publicKey: CryptoKey;
73
+ /** Private key - keep secret, used for key derivation */
74
+ privateKey: CryptoKey;
75
+ }
76
+
77
+ /**
78
+ * Generates an ECDH P-256 key pair for key exchange.
79
+ *
80
+ * The generated key pair can be used to derive a shared secret with another
81
+ * party's public key using {@link deriveSharedKey}.
82
+ *
83
+ * @returns A new ECDH key pair
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const keyPair = await generateKeyPair();
88
+ * const publicKey = await exportPublicKey(keyPair.publicKey);
89
+ * // Send publicKey to the other party
90
+ * ```
91
+ */
92
+ export async function generateKeyPair(): Promise<SecureKeyPair> {
93
+ const keyPair = await crypto.subtle.generateKey(
94
+ {
95
+ name: 'ECDH',
96
+ namedCurve: 'P-256',
97
+ },
98
+ true, // extractable (needed to export public key)
99
+ ['deriveKey'],
100
+ );
101
+ return {
102
+ publicKey: keyPair.publicKey,
103
+ privateKey: keyPair.privateKey,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Exports a public key to JWK format for transmission.
109
+ *
110
+ * The exported key contains only public components and is safe to transmit
111
+ * over untrusted channels.
112
+ *
113
+ * @param publicKey - The CryptoKey public key to export
114
+ * @returns The public key in JWK format
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const keyPair = await generateKeyPair();
119
+ * const exported = await exportPublicKey(keyPair.publicKey);
120
+ * // exported can be JSON serialized and sent to another party
121
+ * ```
122
+ */
123
+ export async function exportPublicKey(publicKey: CryptoKey): Promise<ExportedPublicKey> {
124
+ const jwk = await crypto.subtle.exportKey('jwk', publicKey);
125
+ return {
126
+ kty: jwk.kty!,
127
+ crv: jwk.crv!,
128
+ x: jwk.x!,
129
+ y: jwk.y!,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Imports a public key from JWK format.
135
+ *
136
+ * Used to import the other party's public key for deriving a shared secret.
137
+ *
138
+ * @param exported - The public key in JWK format
139
+ * @returns A CryptoKey that can be used with {@link deriveSharedKey}
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Receive exported public key from other party
144
+ * const theirPublicKey = await importPublicKey(receivedPublicKey);
145
+ * const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey);
146
+ * ```
147
+ */
148
+ export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey> {
149
+ return crypto.subtle.importKey(
150
+ 'jwk',
151
+ {
152
+ kty: exported.kty,
153
+ crv: exported.crv,
154
+ x: exported.x,
155
+ y: exported.y,
156
+ },
157
+ {
158
+ name: 'ECDH',
159
+ namedCurve: 'P-256',
160
+ },
161
+ false,
162
+ [],
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Derives a shared AES-256-GCM key from ECDH key exchange.
168
+ *
169
+ * Both parties will derive the same shared key when using their own private key
170
+ * and the other party's public key. This is the core of ECDH key agreement.
171
+ *
172
+ * @param privateKey - Your ECDH private key
173
+ * @param publicKey - The other party's ECDH public key
174
+ * @returns An AES-256-GCM key for encryption/decryption
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * // Both parties derive the same key
179
+ * const sharedKeyA = await deriveSharedKey(privateKeyA, publicKeyB);
180
+ * const sharedKeyB = await deriveSharedKey(privateKeyB, publicKeyA);
181
+ * // sharedKeyA and sharedKeyB are equivalent
182
+ * ```
183
+ */
184
+ export function deriveSharedKey(privateKey: CryptoKey, publicKey: CryptoKey): Promise<CryptoKey> {
185
+ return crypto.subtle.deriveKey(
186
+ {
187
+ name: 'ECDH',
188
+ public: publicKey,
189
+ },
190
+ privateKey,
191
+ {
192
+ name: 'AES-GCM',
193
+ length: 256,
194
+ },
195
+ true, // extractable - needed for hashing
196
+ ['encrypt', 'decrypt'],
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Encrypts data using AES-256-GCM.
202
+ *
203
+ * The data is JSON serialized before encryption. A random 12-byte IV is
204
+ * generated for each encryption operation.
205
+ *
206
+ * AES-GCM provides both confidentiality and authenticity - any tampering
207
+ * with the ciphertext will cause decryption to fail.
208
+ *
209
+ * @param key - The AES-GCM key (from {@link deriveSharedKey})
210
+ * @param data - The data to encrypt (will be JSON serialized)
211
+ * @returns The encrypted payload with IV and ciphertext
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const encrypted = await encrypt(sharedKey, { action: 'transfer', amount: 100 });
216
+ * // encrypted.iv and encrypted.ciphertext are base64 strings
217
+ * ```
218
+ */
219
+ export async function encrypt(key: CryptoKey, data: unknown): Promise<EncryptedPayload> {
220
+ const iv = crypto.getRandomValues(new Uint8Array(12));
221
+ const encoded = new TextEncoder().encode(jsonStringify(data));
222
+
223
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
224
+
225
+ return {
226
+ iv: arrayBufferToBase64(iv),
227
+ ciphertext: arrayBufferToBase64(ciphertext),
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Decrypts data using AES-256-GCM.
233
+ *
234
+ * The decrypted data is JSON parsed before returning.
235
+ *
236
+ * @typeParam T - The expected type of the decrypted data
237
+ * @param key - The AES-GCM key (from {@link deriveSharedKey})
238
+ * @param payload - The encrypted payload from {@link encrypt}
239
+ * @returns The decrypted and parsed data
240
+ *
241
+ * @throws Error if decryption fails (wrong key or tampered ciphertext)
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const decrypted = await decrypt<{ action: string; amount: number }>(sharedKey, encrypted);
246
+ * console.log(decrypted.action); // 'transfer'
247
+ * ```
248
+ */
249
+ export async function decrypt<T = unknown>(key: CryptoKey, payload: EncryptedPayload): Promise<T> {
250
+ const iv = base64ToArrayBuffer(payload.iv);
251
+ const ciphertext = base64ToArrayBuffer(payload.ciphertext);
252
+
253
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
254
+
255
+ const decoded = new TextDecoder().decode(decrypted);
256
+ return JSON.parse(decoded) as T;
257
+ }
258
+
259
+ /**
260
+ * Converts ArrayBuffer to base64 string.
261
+ * @internal
262
+ */
263
+ function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
264
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
265
+ let binary = '';
266
+ for (let i = 0; i < bytes.byteLength; i++) {
267
+ binary += String.fromCharCode(bytes[i]);
268
+ }
269
+ return btoa(binary);
270
+ }
271
+
272
+ /**
273
+ * Converts base64 string to ArrayBuffer.
274
+ * @internal
275
+ */
276
+ function base64ToArrayBuffer(base64: string): ArrayBuffer {
277
+ const binary = atob(base64);
278
+ const bytes = new Uint8Array(binary.length);
279
+ for (let i = 0; i < binary.length; i++) {
280
+ bytes[i] = binary.charCodeAt(i);
281
+ }
282
+ return bytes.buffer;
283
+ }
284
+
285
+ /**
286
+ * Emoji alphabet for visual verification of shared secrets.
287
+ * 32 distinct, easily recognizable emojis for anti-spoofing verification.
288
+ * @internal
289
+ */
290
+ const EMOJI_ALPHABET = [
291
+ '🔵',
292
+ '🟢',
293
+ '🔴',
294
+ '🟡',
295
+ '🟣',
296
+ '🟠',
297
+ '⚫',
298
+ '⚪',
299
+ '🌟',
300
+ '🌙',
301
+ '☀️',
302
+ '🌈',
303
+ '🔥',
304
+ '💧',
305
+ '🌸',
306
+ '🍀',
307
+ '🦋',
308
+ '🐬',
309
+ '🦊',
310
+ '🐼',
311
+ '🦁',
312
+ '🐯',
313
+ '🐸',
314
+ '🦉',
315
+ '🎵',
316
+ '🎨',
317
+ '🎯',
318
+ '🎲',
319
+ '🔔',
320
+ '💎',
321
+ '🔑',
322
+ '🏆',
323
+ ];
324
+
325
+ /**
326
+ * Hashes a shared AES key to a hex string for verification.
327
+ *
328
+ * This extracts the raw key material and hashes it with SHA-256,
329
+ * returning the first 16 bytes as a hex string.
330
+ *
331
+ * @param sharedKey - The AES-GCM shared key (must be extractable)
332
+ * @returns A hex string representation of the hash
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * const hash = await hashSharedSecret(sharedKey);
337
+ * const emoji = hashToEmoji(hash);
338
+ * ```
339
+ */
340
+ export async function hashSharedSecret(sharedKey: CryptoKey): Promise<string> {
341
+ const rawKey = await crypto.subtle.exportKey('raw', sharedKey);
342
+ const hash = await crypto.subtle.digest('SHA-256', rawKey);
343
+ const bytes = new Uint8Array(hash.slice(0, 16));
344
+ return Array.from(bytes)
345
+ .map(b => b.toString(16).padStart(2, '0'))
346
+ .join('');
347
+ }
348
+
349
+ /**
350
+ * Converts a hex hash to an emoji sequence for visual verification.
351
+ *
352
+ * This is used for anti-MITM verification - both the dApp and wallet
353
+ * independently compute the same emoji sequence from the shared secret.
354
+ * Users can visually compare the sequences to detect interception.
355
+ *
356
+ * Similar to SAS (Short Authentication String) in ZRTP/Signal.
357
+ *
358
+ * @param hash - Hex string from {@link hashSharedSecret}
359
+ * @param length - Number of emojis to generate (default: 4)
360
+ * @returns A string of emojis representing the hash
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * const hash = await hashSharedSecret(sharedKey);
365
+ * const emoji = hashToEmoji(hash); // e.g., "🔵🦋🎯🐼"
366
+ * // Display to user for verification
367
+ * ```
368
+ */
369
+ export function hashToEmoji(hash: string, length: number = 4): string {
370
+ const bytes: number[] = [];
371
+ for (let i = 0; i < hash.length && bytes.length < length; i += 2) {
372
+ bytes.push(parseInt(hash.slice(i, i + 2), 16));
373
+ }
374
+ return bytes.map(b => EMOJI_ALPHABET[b % EMOJI_ALPHABET.length]).join('');
375
+ }
@@ -5,17 +5,13 @@ export type {
5
5
  WebWalletConfig,
6
6
  WalletProviderType,
7
7
  WalletProvider,
8
+ ProviderDisconnectionCallback,
8
9
  DiscoverWalletsOptions,
9
10
  } from './types.js';
10
11
 
11
- // Re-export types from providers for convenience
12
- export type {
13
- WalletInfo,
14
- WalletMessage,
15
- WalletResponse,
16
- DiscoveryRequest,
17
- DiscoveryResponse,
18
- } from '../providers/types.js';
12
+ // Re-export types and enums from providers for convenience
13
+ export { WalletMessageType } from '../types.js';
14
+ export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse } from '../types.js';
19
15
 
20
16
  // Re-export commonly needed utilities for wallet integration
21
17
  export { ChainInfoSchema } from '@aztec/aztec.js/account';
@@ -36,6 +36,11 @@ export interface WalletManagerConfig {
36
36
  */
37
37
  export type WalletProviderType = 'extension' | 'web' | 'embedded';
38
38
 
39
+ /**
40
+ * Callback type for wallet disconnect events at the provider level.
41
+ */
42
+ export type ProviderDisconnectionCallback = () => void;
43
+
39
44
  /**
40
45
  * A wallet provider that can connect to create a wallet instance.
41
46
  * Chain information is already baked in from the discovery process.
@@ -56,6 +61,23 @@ export interface WalletProvider {
56
61
  * @param appId - Application identifier for the requesting dapp
57
62
  */
58
63
  connect(appId: string): Promise<Wallet>;
64
+ /**
65
+ * Disconnects the current wallet and cleans up resources.
66
+ * After calling this, the wallet returned from connect() should no longer be used.
67
+ * @returns A promise that resolves when disconnection is complete
68
+ */
69
+ disconnect?(): Promise<void>;
70
+ /**
71
+ * Registers a callback to be invoked when the wallet disconnects unexpectedly.
72
+ * @param callback - Function to call when wallet disconnects
73
+ * @returns A function to unregister the callback
74
+ */
75
+ onDisconnect?(callback: ProviderDisconnectionCallback): () => void;
76
+ /**
77
+ * Returns whether the provider's wallet connection has been disconnected.
78
+ * @returns true if the wallet is no longer connected
79
+ */
80
+ isDisconnected?(): boolean;
59
81
  }
60
82
 
61
83
  /**
@@ -1,5 +1,11 @@
1
1
  import { ExtensionProvider, ExtensionWallet } from '../providers/extension/index.js';
2
- import type { DiscoverWalletsOptions, ExtensionWalletConfig, WalletManagerConfig, WalletProvider } from './types.js';
2
+ import type {
3
+ DiscoverWalletsOptions,
4
+ ExtensionWalletConfig,
5
+ ProviderDisconnectionCallback,
6
+ WalletManagerConfig,
7
+ WalletProvider,
8
+ } from './types.js';
3
9
 
4
10
  /**
5
11
  * Manager for wallet discovery, configuration, and connection
@@ -35,27 +41,51 @@ export class WalletManager {
35
41
  const providers: WalletProvider[] = [];
36
42
  const { chainInfo } = options;
37
43
 
38
- // Discover extension wallets
39
44
  if (this.config.extensions?.enabled) {
40
- const extensions = await ExtensionProvider.discoverExtensions(chainInfo, options.timeout);
45
+ const discoveredWallets = await ExtensionProvider.discoverExtensions(chainInfo, options.timeout);
41
46
  const extensionConfig = this.config.extensions;
42
47
 
43
- for (const ext of extensions) {
44
- // Apply allow/block lists
45
- if (!this.isExtensionAllowed(ext.id, extensionConfig)) {
48
+ for (const { info, port, sharedKey } of discoveredWallets) {
49
+ if (!this.isExtensionAllowed(info.id, extensionConfig)) {
46
50
  continue;
47
51
  }
48
52
 
49
- providers.push({
50
- id: ext.id,
53
+ let extensionWallet: ExtensionWallet | null = null;
54
+
55
+ const provider: WalletProvider = {
56
+ id: info.id,
51
57
  type: 'extension',
52
- name: ext.name,
53
- icon: ext.icon,
58
+ name: info.name,
59
+ icon: info.icon,
54
60
  metadata: {
55
- version: ext.version,
61
+ version: info.version,
62
+ verificationHash: info.verificationHash,
63
+ },
64
+ connect: (appId: string) => {
65
+ extensionWallet = ExtensionWallet.create(info, chainInfo, port, sharedKey, appId);
66
+ return Promise.resolve(extensionWallet.getWallet());
67
+ },
68
+ disconnect: async () => {
69
+ if (extensionWallet) {
70
+ await extensionWallet.disconnect();
71
+ extensionWallet = null;
72
+ }
56
73
  },
57
- connect: (appId: string) => Promise.resolve(ExtensionWallet.create(chainInfo, appId, ext.id)),
58
- });
74
+ onDisconnect: (callback: ProviderDisconnectionCallback) => {
75
+ if (extensionWallet) {
76
+ return extensionWallet.onDisconnect(callback);
77
+ }
78
+ return () => {};
79
+ },
80
+ isDisconnected: () => {
81
+ if (extensionWallet) {
82
+ return extensionWallet.isDisconnected();
83
+ }
84
+ return true;
85
+ },
86
+ };
87
+
88
+ providers.push(provider);
59
89
  }
60
90
  }
61
91
 
@@ -70,17 +100,14 @@ export class WalletManager {
70
100
  * @param config - Extension wallet configuration containing allow/block lists
71
101
  */
72
102
  private isExtensionAllowed(extensionId: string, config: ExtensionWalletConfig): boolean {
73
- // Check block list first
74
103
  if (config.blockList && config.blockList.includes(extensionId)) {
75
104
  return false;
76
105
  }
77
106
 
78
- // If allow list exists, extension must be in it
79
107
  if (config.allowList && config.allowList.length > 0) {
80
108
  return config.allowList.includes(extensionId);
81
109
  }
82
110
 
83
- // If no allow list, extension is allowed (unless blocked)
84
111
  return true;
85
112
  }
86
113
  }
@@ -2,34 +2,95 @@ import type { ChainInfo } from '@aztec/aztec.js/account';
2
2
  import { jsonStringify } from '@aztec/foundation/json-rpc';
3
3
  import { promiseWithResolvers } from '@aztec/foundation/promise';
4
4
 
5
- import type { DiscoveryRequest, DiscoveryResponse, WalletInfo } from '../types.js';
5
+ import { deriveSharedKey, exportPublicKey, generateKeyPair, hashSharedSecret, importPublicKey } from '../../crypto.js';
6
+ import { type DiscoveryRequest, type DiscoveryResponse, type WalletInfo, WalletMessageType } from '../../types.js';
6
7
 
7
8
  /**
8
- * Provider for discovering and managing Aztec wallet extensions
9
+ * A discovered wallet with its secure channel components.
10
+ * Returned by {@link ExtensionProvider.discoverExtensions}.
11
+ */
12
+ export interface DiscoveredWallet {
13
+ /** Basic wallet information (id, name, icon, version, publicKey, verificationHash) */
14
+ info: WalletInfo;
15
+ /** The MessagePort for private communication with the wallet */
16
+ port: MessagePort;
17
+ /** The derived AES-256-GCM shared key for encryption */
18
+ sharedKey: CryptoKey;
19
+ }
20
+
21
+ /**
22
+ * Internal type for discovery response with MessagePort
23
+ * @internal
24
+ */
25
+ interface DiscoveryResponseWithPort extends DiscoveryResponse {
26
+ /** The MessagePort transferred from the wallet */
27
+ port?: MessagePort;
28
+ }
29
+
30
+ /**
31
+ * Provider for discovering Aztec wallet extensions.
32
+ *
33
+ * This class handles the discovery phase of wallet communication:
34
+ * 1. Broadcasts a discovery request with the dApp's public key
35
+ * 2. Receives responses from installed wallets with their public keys
36
+ * 3. Derives shared secrets and computes verification hashes
37
+ * 4. Returns discovered wallets with their secure channel components
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const wallets = await ExtensionProvider.discoverExtensions(chainInfo);
42
+ * // Display wallets to user with optional emoji verification
43
+ * for (const wallet of wallets) {
44
+ * const emoji = hashToEmoji(wallet.info.verificationHash!);
45
+ * console.log(`${wallet.info.name}: ${emoji}`);
46
+ * }
47
+ * // User selects a wallet after verifying
48
+ * const wallet = await ExtensionWallet.create(wallets[0], chainInfo, 'my-app');
49
+ * ```
9
50
  */
10
51
  export class ExtensionProvider {
11
- private static discoveredExtensions: Map<string, WalletInfo> = new Map();
12
52
  private static discoveryInProgress = false;
13
53
 
14
54
  /**
15
- * Discovers all installed Aztec wallet extensions
55
+ * Discovers all installed Aztec wallet extensions and establishes secure channel components.
56
+ *
57
+ * This method:
58
+ * 1. Generates an ECDH key pair for this discovery session
59
+ * 2. Broadcasts a discovery request with the dApp's public key
60
+ * 3. Receives responses from wallets with their public keys and MessagePorts
61
+ * 4. Derives shared secrets and computes verification hashes
62
+ *
16
63
  * @param chainInfo - Chain information to check if extensions support this network
17
64
  * @param timeout - How long to wait for extensions to respond (ms)
18
- * @returns Array of discovered extension information
65
+ * @returns Array of discovered wallets with their secure channel components
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const wallets = await ExtensionProvider.discoverExtensions({
70
+ * chainId: Fr(31337),
71
+ * version: Fr(0)
72
+ * });
73
+ * // Access wallet info and secure channel
74
+ * const { info, port, sharedKey } = wallets[0];
75
+ * ```
19
76
  */
20
- static async discoverExtensions(chainInfo: ChainInfo, timeout: number = 1000): Promise<WalletInfo[]> {
21
- // If discovery is in progress, wait for it to complete
77
+ static async discoverExtensions(chainInfo: ChainInfo, timeout: number = 1000): Promise<DiscoveredWallet[]> {
78
+ // If discovery is already in progress, wait and return empty
79
+ // (caller should retry or handle appropriately)
22
80
  if (this.discoveryInProgress) {
23
81
  await new Promise(resolve => setTimeout(resolve, timeout));
24
- return Array.from(this.discoveredExtensions.values());
82
+ return [];
25
83
  }
26
84
 
27
85
  this.discoveryInProgress = true;
28
- this.discoveredExtensions.clear();
29
86
 
30
- const { promise, resolve } = promiseWithResolvers<WalletInfo[]>();
87
+ // Generate key pair for this discovery session
88
+ const keyPair = await generateKeyPair();
89
+ const exportedPublicKey = await exportPublicKey(keyPair.publicKey);
90
+
91
+ const { promise, resolve } = promiseWithResolvers<DiscoveredWallet[]>();
31
92
  const requestId = globalThis.crypto.randomUUID();
32
- const responses: WalletInfo[] = [];
93
+ const responses: DiscoveredWallet[] = [];
33
94
 
34
95
  // Set up listener for discovery responses
35
96
  const handleMessage = (event: MessageEvent) => {
@@ -37,26 +98,60 @@ export class ExtensionProvider {
37
98
  return;
38
99
  }
39
100
 
40
- let data: DiscoveryResponse;
101
+ let data: DiscoveryResponseWithPort;
41
102
  try {
42
103
  data = JSON.parse(event.data);
43
104
  } catch {
44
105
  return;
45
106
  }
46
107
 
47
- if (data.type === 'aztec-wallet-discovery-response' && data.requestId === requestId) {
48
- responses.push(data.walletInfo);
49
- this.discoveredExtensions.set(data.walletInfo.id, data.walletInfo);
108
+ if (data.type === WalletMessageType.DISCOVERY_RESPONSE && data.requestId === requestId) {
109
+ // Get the MessagePort from the event
110
+ const port = event.ports?.[0];
111
+ if (!port) {
112
+ return;
113
+ }
114
+
115
+ // Derive shared key from wallet's public key
116
+ const walletPublicKey = data.walletInfo.publicKey;
117
+ if (!walletPublicKey) {
118
+ return;
119
+ }
120
+
121
+ void (async () => {
122
+ try {
123
+ const importedWalletKey = await importPublicKey(walletPublicKey);
124
+ const sharedKey = await deriveSharedKey(keyPair.privateKey, importedWalletKey);
125
+
126
+ // Compute verification hash
127
+ const verificationHash = await hashSharedSecret(sharedKey);
128
+
129
+ // Create wallet info with verification hash
130
+ const walletInfo: WalletInfo = {
131
+ ...data.walletInfo,
132
+ verificationHash,
133
+ };
134
+
135
+ responses.push({
136
+ info: walletInfo,
137
+ port,
138
+ sharedKey,
139
+ });
140
+ } catch {
141
+ // Failed to derive key, skip this wallet
142
+ }
143
+ })();
50
144
  }
51
145
  };
52
146
 
53
147
  window.addEventListener('message', handleMessage);
54
148
 
55
- // Send discovery message
149
+ // Send discovery message with our public key
56
150
  const discoveryMessage: DiscoveryRequest = {
57
- type: 'aztec-wallet-discovery',
151
+ type: WalletMessageType.DISCOVERY,
58
152
  requestId,
59
153
  chainInfo,
154
+ publicKey: exportedPublicKey,
60
155
  };
61
156
  window.postMessage(jsonStringify(discoveryMessage), '*');
62
157