@aztec/wallet-sdk 0.0.1-commit.d431d1c → 0.0.1-commit.d58ff9d0

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 (103) hide show
  1. package/README.md +318 -270
  2. package/dest/base-wallet/base_wallet.d.ts +115 -40
  3. package/dest/base-wallet/base_wallet.d.ts.map +1 -1
  4. package/dest/base-wallet/base_wallet.js +353 -105
  5. package/dest/base-wallet/get_gas_limits.d.ts +36 -0
  6. package/dest/base-wallet/get_gas_limits.d.ts.map +1 -0
  7. package/dest/base-wallet/get_gas_limits.js +55 -0
  8. package/dest/base-wallet/index.d.ts +4 -2
  9. package/dest/base-wallet/index.d.ts.map +1 -1
  10. package/dest/base-wallet/index.js +2 -0
  11. package/dest/base-wallet/utils.d.ts +52 -0
  12. package/dest/base-wallet/utils.d.ts.map +1 -0
  13. package/dest/base-wallet/utils.js +137 -0
  14. package/dest/crypto.d.ts +97 -50
  15. package/dest/crypto.d.ts.map +1 -1
  16. package/dest/crypto.js +290 -108
  17. package/dest/emoji_alphabet.d.ts +35 -0
  18. package/dest/emoji_alphabet.d.ts.map +1 -0
  19. package/dest/emoji_alphabet.js +299 -0
  20. package/dest/extension/handlers/background_connection_handler.d.ts +168 -0
  21. package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -0
  22. package/dest/extension/handlers/background_connection_handler.js +294 -0
  23. package/dest/extension/handlers/content_script_connection_handler.d.ts +57 -0
  24. package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -0
  25. package/dest/extension/handlers/content_script_connection_handler.js +193 -0
  26. package/dest/extension/handlers/index.d.ts +12 -0
  27. package/dest/extension/handlers/index.d.ts.map +1 -0
  28. package/dest/extension/handlers/index.js +10 -0
  29. package/dest/extension/handlers/internal_message_types.d.ts +65 -0
  30. package/dest/extension/handlers/internal_message_types.d.ts.map +1 -0
  31. package/dest/extension/handlers/internal_message_types.js +24 -0
  32. package/dest/extension/provider/extension_provider.d.ts +107 -0
  33. package/dest/extension/provider/extension_provider.d.ts.map +1 -0
  34. package/dest/extension/provider/extension_provider.js +160 -0
  35. package/dest/extension/provider/extension_wallet.d.ts +152 -0
  36. package/dest/extension/provider/extension_wallet.d.ts.map +1 -0
  37. package/dest/{providers/extension → extension/provider}/extension_wallet.js +134 -103
  38. package/dest/extension/provider/index.d.ts +3 -0
  39. package/dest/extension/provider/index.d.ts.map +1 -0
  40. package/dest/{providers/extension → extension/provider}/index.js +0 -2
  41. package/dest/iframe/handlers/iframe_connection_handler.d.ts +122 -0
  42. package/dest/iframe/handlers/iframe_connection_handler.d.ts.map +1 -0
  43. package/dest/iframe/handlers/iframe_connection_handler.js +239 -0
  44. package/dest/iframe/handlers/index.d.ts +2 -0
  45. package/dest/iframe/handlers/index.d.ts.map +1 -0
  46. package/dest/iframe/handlers/index.js +1 -0
  47. package/dest/iframe/provider/iframe_discovery.d.ts +25 -0
  48. package/dest/iframe/provider/iframe_discovery.d.ts.map +1 -0
  49. package/dest/iframe/provider/iframe_discovery.js +167 -0
  50. package/dest/iframe/provider/iframe_provider.d.ts +65 -0
  51. package/dest/iframe/provider/iframe_provider.d.ts.map +1 -0
  52. package/dest/iframe/provider/iframe_provider.js +257 -0
  53. package/dest/iframe/provider/iframe_wallet.d.ts +85 -0
  54. package/dest/iframe/provider/iframe_wallet.d.ts.map +1 -0
  55. package/dest/iframe/provider/iframe_wallet.js +269 -0
  56. package/dest/iframe/provider/index.d.ts +4 -0
  57. package/dest/iframe/provider/index.d.ts.map +1 -0
  58. package/dest/iframe/provider/index.js +3 -0
  59. package/dest/manager/index.d.ts +2 -8
  60. package/dest/manager/index.d.ts.map +1 -1
  61. package/dest/manager/index.js +0 -6
  62. package/dest/manager/types.d.ts +92 -9
  63. package/dest/manager/types.d.ts.map +1 -1
  64. package/dest/manager/types.js +17 -1
  65. package/dest/manager/wallet_manager.d.ts +50 -7
  66. package/dest/manager/wallet_manager.d.ts.map +1 -1
  67. package/dest/manager/wallet_manager.js +203 -43
  68. package/dest/types.d.ts +104 -11
  69. package/dest/types.d.ts.map +1 -1
  70. package/dest/types.js +32 -2
  71. package/package.json +21 -10
  72. package/src/base-wallet/base_wallet.ts +456 -167
  73. package/src/base-wallet/get_gas_limits.ts +88 -0
  74. package/src/base-wallet/index.ts +8 -1
  75. package/src/base-wallet/utils.ts +248 -0
  76. package/src/crypto.ts +341 -113
  77. package/src/emoji_alphabet.ts +317 -0
  78. package/src/extension/handlers/background_connection_handler.ts +456 -0
  79. package/src/extension/handlers/content_script_connection_handler.ts +264 -0
  80. package/src/extension/handlers/index.ts +25 -0
  81. package/src/extension/handlers/internal_message_types.ts +71 -0
  82. package/src/extension/provider/extension_provider.ts +233 -0
  83. package/src/{providers/extension → extension/provider}/extension_wallet.ts +151 -120
  84. package/src/extension/provider/index.ts +7 -0
  85. package/src/iframe/handlers/iframe_connection_handler.ts +341 -0
  86. package/src/iframe/handlers/index.ts +7 -0
  87. package/src/iframe/provider/iframe_discovery.ts +185 -0
  88. package/src/iframe/provider/iframe_provider.ts +331 -0
  89. package/src/iframe/provider/iframe_wallet.ts +323 -0
  90. package/src/iframe/provider/index.ts +3 -0
  91. package/src/manager/index.ts +2 -10
  92. package/src/manager/types.ts +95 -8
  93. package/src/manager/wallet_manager.ts +223 -45
  94. package/src/types.ts +116 -10
  95. package/dest/providers/extension/extension_provider.d.ts +0 -63
  96. package/dest/providers/extension/extension_provider.d.ts.map +0 -1
  97. package/dest/providers/extension/extension_provider.js +0 -124
  98. package/dest/providers/extension/extension_wallet.d.ts +0 -155
  99. package/dest/providers/extension/extension_wallet.d.ts.map +0 -1
  100. package/dest/providers/extension/index.d.ts +0 -6
  101. package/dest/providers/extension/index.d.ts.map +0 -1
  102. package/src/providers/extension/extension_provider.ts +0 -167
  103. package/src/providers/extension/index.ts +0 -5
package/README.md CHANGED
@@ -10,6 +10,8 @@ All types and utilities needed for wallet integration are exported from `@aztec/
10
10
  import type {
11
11
  DiscoveryRequest,
12
12
  DiscoveryResponse,
13
+ KeyExchangeRequest,
14
+ KeyExchangeResponse,
13
15
  WalletInfo,
14
16
  WalletMessage,
15
17
  WalletResponse,
@@ -22,377 +24,423 @@ Cryptographic utilities for secure channel establishment are exported from `@azt
22
24
  import type { EncryptedPayload, ExportedPublicKey } from '@aztec/wallet-sdk/crypto';
23
25
  import {
24
26
  decrypt,
25
- deriveSharedKey,
27
+ deriveSessionKeys,
26
28
  encrypt,
27
29
  exportPublicKey,
28
30
  generateKeyPair,
29
- hashSharedSecret,
30
31
  hashToEmoji,
31
32
  importPublicKey,
32
33
  } from '@aztec/wallet-sdk/crypto';
33
34
  ```
34
35
 
35
- ## Overview
36
+ **For extension wallets**, pre-built connection handlers are available:
36
37
 
37
- The Wallet SDK uses a **unified discovery and connection** model with **end-to-end encryption**:
38
+ ```typescript
39
+ import {
40
+ BackgroundConnectionHandler,
41
+ ContentScriptConnectionHandler,
42
+ } from '@aztec/wallet-sdk/extension/handlers';
43
+ ```
38
44
 
39
- 1. **dApp requests wallets** for a specific chain/version via `WalletManager.getAvailableWallets({ chainInfo })`
40
- 2. **SDK broadcasts** a discovery message with chain information and the dApp's ECDH public key
41
- 3. **Your wallet responds** with its ECDH public key and a MessagePort ONLY if it supports that network
42
- 4. **Both parties derive** the same shared secret via ECDH key exchange
43
- 5. **SDK receives** discovered wallets with secure channel already established (port + sharedKey)
44
- 6. **All subsequent communication** is encrypted using AES-256-GCM over the private MessagePort
45
+ ## Overview
45
46
 
46
- ### Key Features
47
+ The Wallet SDK uses a **two-phase connection model** with **end-to-end encryption**:
47
48
 
48
- - **No separate connection step**: The secure channel is established during discovery
49
- - **MessagePort transferred immediately**: The discovery response includes a MessagePort for private communication
50
- - **Anti-MITM verification**: Both parties can display emoji verification codes derived from the shared secret
49
+ ### Phase 1: Discovery
51
50
 
52
- ### Transport Mechanisms
51
+ 1. **dApp broadcasts** a discovery request with chain information (NO public keys)
52
+ 2. **Your wallet shows** a pending connection request to the user
53
+ 3. **User approves** the connection request
54
+ 4. **Your wallet responds** with basic wallet info and a MessagePort
53
55
 
54
- This guide uses **browser extension wallets** as the primary example, which communicate via `window.postMessage` for discovery and MessageChannel for secure communication. The same message protocol can be adapted for other transport mechanisms.
56
+ ### Phase 2: Secure Channel Establishment
55
57
 
56
- ## Discovery Protocol
58
+ 5. **dApp initiates key exchange** by sending its ECDH public key over the MessagePort
59
+ 6. **Wallet generates** ephemeral key pair and derives session keys using HKDF
60
+ 7. **Both parties compute** the same verification hash independently
61
+ 8. **User verifies** the has matches on both sides. A util for conversion to an emoji grid is provided
62
+ 9. **User confirms** the connection in the dApp
63
+ 10. **All subsequent communication** is encrypted using AES-256-GCM
57
64
 
58
- ### 1. Listen for Discovery Requests
65
+ ### Key Security Features
59
66
 
60
- **Extension wallet (content script):**
67
+ - **User approval required**: Wallet never reveals itself without explicit user consent
68
+ - **Ephemeral keys**: New key pairs generated for each session
69
+ - **Anti-MITM verification**: 3x3 emoji grid (72 bits of security) for visual confirmation
61
70
 
62
- ```typescript
63
- window.addEventListener('message', async (event) => {
64
- if (event.source !== window) return;
65
-
66
- let data: DiscoveryRequest;
67
- try {
68
- data = JSON.parse(event.data);
69
- } catch {
70
- return;
71
- }
71
+ ## Architecture for Extension Wallets
72
72
 
73
- if (data.type === 'aztec-wallet-discovery') {
74
- await handleDiscoveryRequest(data);
75
- }
76
- });
77
73
  ```
78
-
79
- ### 2. Discovery Message Format
80
-
81
- Discovery messages have this structure:
82
-
83
- ```typescript
84
- interface DiscoveryRequest {
85
- type: 'aztec-wallet-discovery';
86
- requestId: string; // UUID for tracking this request
87
- chainInfo: ChainInfo; // Chain ID and protocol version
88
- publicKey: ExportedPublicKey; // dApp's ECDH public key for key exchange
89
- }
74
+ ┌─────────────┐ window.postMessage ┌─────────────────┐ browser.runtime ┌──────────────────┐
75
+ │ dApp │◄──(discovery + port)────►│ Content Script │◄────────────────────►│ Background Script│
76
+ │ (web page) │ │ (message relay)│ │ (crypto+state) │
77
+ └─────────────┘ └─────────────────┘ └──────────────────┘
78
+ │ │
79
+ │ MessagePort │
80
+ └──────────(key exchange + encrypted)──────┘
90
81
  ```
91
82
 
92
- ### 3. Handle Discovery and Establish Secure Channel
83
+ **Security model:**
84
+
85
+ - The MessagePort is transferred via `window.postMessage` - other scripts on the page could intercept it
86
+ - **Security comes from encryption**: After key exchange, all communication is AES-256-GCM encrypted
87
+ - Content script never has access to private keys or session secrets
88
+ - All cryptographic operations happen in the background script (service worker)
89
+ - Anti-MITM verification (emoji grid) ensures both parties derived the same keys
93
90
 
94
- When your wallet receives a discovery request:
91
+ ## Using Pre-built Connection Handlers
95
92
 
96
- 1. Check if you support the requested network
97
- 2. Derive the shared secret from the dApp's public key
98
- 3. Create a MessageChannel for secure communication
99
- 4. Respond with your wallet info and transfer one end of the channel
93
+ The SDK provides `BackgroundConnectionHandler` and `ContentScriptConnectionHandler` to handle the connection flow. These are the recommended way to build extension wallets.
100
94
 
101
- **Extension wallet (background script):**
95
+ ### Background Script Setup
102
96
 
103
97
  ```typescript
104
98
  import {
105
- deriveSharedKey,
106
- exportPublicKey,
107
- generateKeyPair,
108
- hashSharedSecret,
109
- importPublicKey,
110
- } from '@aztec/wallet-sdk/crypto';
99
+ BackgroundConnectionHandler,
100
+ type BackgroundConnectionConfig,
101
+ type BackgroundConnectionCallbacks,
102
+ type BackgroundTransport,
103
+ } from '@aztec/wallet-sdk/extension/handlers';
104
+ import { hashToEmoji } from '@aztec/wallet-sdk/crypto';
111
105
 
112
- // Generate key pair on wallet initialization (per session)
113
- let walletKeyPair = await generateKeyPair();
114
- let walletPublicKey = await exportPublicKey(walletKeyPair.publicKey);
106
+ // Configuration for your wallet
107
+ const config: BackgroundConnectionConfig = {
108
+ walletId: 'my-aztec-wallet',
109
+ walletName: 'My Aztec Wallet',
110
+ walletVersion: '1.0.0',
111
+ walletIcon: 'https://example.com/icon.png',
112
+ };
113
+
114
+ // Transport for browser extension APIs
115
+ const transport: BackgroundTransport = {
116
+ sendToTab: (tabId, message) => browser.tabs.sendMessage(tabId, message),
117
+ addContentListener: (handler) => browser.runtime.onMessage.addListener(handler),
118
+ };
119
+
120
+ // Event callbacks (all optional)
121
+ const callbacks: BackgroundConnectionCallbacks = {
122
+ // Called when a new discovery request is received
123
+ onPendingDiscovery: (discovery) => {
124
+ // Show pending connection in wallet UI
125
+ // Check if wallet supports this network (chainId AND version)
126
+ const supported = supportedNetworks.some(
127
+ n => n.chainId === discovery.chainInfo.chainId.toString() &&
128
+ n.version === discovery.chainInfo.version.toString()
129
+ );
130
+ if (supported) {
131
+ // Show the user so they can approve or reject
132
+ }
133
+ },
115
134
 
116
- // Store sessions by requestId
117
- const sessions = new Map<string, { sharedKey: CryptoKey; verificationHash: string; tabId: number }>();
135
+ // Called when key exchange completes and session is ready
136
+ onSessionEstablished: (session) => {
137
+ // Display verification emojis for user reference
138
+ console.log('Session emojis:', hashToEmoji(session.verificationHash));
139
+ },
118
140
 
119
- async function handleDiscovery(
120
- request: DiscoveryRequest,
121
- tabId: number
122
- ): Promise<{ success: true; response: DiscoveryResponse }> {
123
- // Check network support
124
- if (!supportsNetwork(request.chainInfo)) {
125
- throw new Error('Network not supported');
126
- }
141
+ // Called when a session is terminated
142
+ onSessionTerminated: (requestId) => {
143
+ console.log('Session terminated:', requestId);
144
+ },
127
145
 
128
- // Import dApp's public key and derive shared secret
129
- const dAppPublicKey = await importPublicKey(request.publicKey);
130
- const sharedKey = await deriveSharedKey(walletKeyPair.privateKey, dAppPublicKey);
146
+ // Called when a decrypted wallet message is received
147
+ onWalletMessage: (session, message) => {
148
+ // Forward to your wallet backend
149
+ wallet.postMessage(message);
150
+ },
151
+ };
131
152
 
132
- // Compute verification hash for anti-MITM verification
133
- const verificationHash = await hashSharedSecret(sharedKey);
153
+ const handler = new BackgroundConnectionHandler(config, transport, callbacks);
134
154
 
135
- // Store the session with verificationHash (emoji computed lazily for display)
136
- sessions.set(request.requestId, { sharedKey, verificationHash, tabId });
155
+ // Initialize the handler to start listening
156
+ handler.initialize();
137
157
 
138
- const response: DiscoveryResponse = {
139
- type: 'aztec-wallet-discovery-response',
140
- requestId: request.requestId,
141
- walletInfo: {
142
- id: 'my-aztec-wallet',
143
- name: 'My Aztec Wallet',
144
- version: '1.0.0',
145
- publicKey: walletPublicKey,
146
- },
147
- };
158
+ // User approves connection from wallet UI
159
+ function approveConnection(requestId: string) {
160
+ handler.approveDiscovery(requestId);
161
+ }
148
162
 
149
- return { success: true, response };
163
+ // User denies connection
164
+ function denyConnection(requestId: string) {
165
+ handler.rejectDiscovery(requestId);
150
166
  }
167
+
168
+ // Send response back to dApp
169
+ async function sendWalletResponse(requestId: string, response: WalletResponse) {
170
+ await handler.sendResponse(requestId, response);
171
+ }
172
+
173
+ // Clean up on tab close/navigate
174
+ browser.tabs.onRemoved.addListener((tabId) => {
175
+ handler.terminateForTab(tabId);
176
+ });
151
177
  ```
152
178
 
153
- **Content script (creates MessageChannel and sends response):**
179
+ ### Content Script Setup
154
180
 
155
181
  ```typescript
156
- async function handleDiscoveryRequest(request: DiscoveryRequest) {
157
- // Forward to background script for key derivation
158
- const result = await browser.runtime.sendMessage({
159
- type: 'aztec-wallet-discovery',
160
- content: request,
161
- });
162
-
163
- if (!result?.success) return;
164
-
165
- // Create MessageChannel for secure communication
166
- const channel = new MessageChannel();
167
-
168
- // Set up relay from page to background
169
- channel.port1.onmessage = (event) => {
170
- browser.runtime.sendMessage({
171
- type: 'secure-message',
172
- requestId: request.requestId,
173
- content: event.data, // Encrypted payload
174
- });
175
- };
176
- channel.port1.start();
182
+ import {
183
+ ContentScriptConnectionHandler,
184
+ type ContentScriptTransport,
185
+ } from '@aztec/wallet-sdk/extension/handlers';
177
186
 
178
- // Send response with port2 to the page
179
- window.postMessage(JSON.stringify(result.response), '*', [channel.port2]);
180
- }
187
+ const transport: ContentScriptTransport = {
188
+ sendToBackground: (message) => browser.runtime.sendMessage(message),
189
+ addBackgroundListener: (handler) => browser.runtime.onMessage.addListener(handler),
190
+ };
191
+
192
+ const handler = new ContentScriptConnectionHandler(transport);
193
+
194
+ // Start listening for discovery requests and background messages
195
+ handler.start();
181
196
  ```
182
197
 
183
- ### 4. Discovery Response Format
198
+ ## Testing Your Integration (dApp Side)
199
+
200
+ The `WalletManager` supports two patterns for consuming discovered wallets.
201
+
202
+ ### Async Iterator Pattern
184
203
 
185
204
  ```typescript
186
- interface DiscoveryResponse {
187
- type: 'aztec-wallet-discovery-response';
188
- requestId: string; // Must match the request
189
- walletInfo: WalletInfo; // Wallet info including public key
190
- }
205
+ import { Fr } from '@aztec/foundation/fields';
206
+ import { WalletManager } from '@aztec/wallet-sdk/manager';
207
+ import { hashToEmoji } from '@aztec/wallet-sdk/crypto';
191
208
 
192
- interface WalletInfo {
193
- id: string; // Unique wallet identifier
194
- name: string; // Display name
195
- icon?: string; // Optional icon URL
196
- version: string; // Wallet version
197
- publicKey: ExportedPublicKey; // ECDH public key for key exchange
198
- }
199
- ```
209
+ const discovery = WalletManager.configure({
210
+ extensions: { enabled: true },
211
+ }).getAvailableWallets({
212
+ chainInfo: {
213
+ chainId: new Fr(31337),
214
+ version: new Fr(1),
215
+ },
216
+ appId: 'my-dapp',
217
+ timeout: 60000,
218
+ });
200
219
 
201
- **Important:** The response is sent via `window.postMessage` with a MessagePort transferred as the third argument. The SDK receives the port and uses it for all subsequent encrypted communication.
220
+ // Iterate over discovered wallets as they're approved
221
+ for await (const provider of discovery.wallets) {
222
+ console.log(`Found: ${provider.name}`);
202
223
 
203
- ## Secure Communication
224
+ // Establish secure channel (key exchange)
225
+ const pending = await provider.establishSecureChannel('my-dapp');
204
226
 
205
- ### Architecture for Extension Wallets
227
+ // Display verification emojis to user
228
+ const emojis = hashToEmoji(pending.verificationHash);
229
+ console.log('Verify this matches your wallet:', emojis);
206
230
 
207
- ```
208
- ┌─────────────┐ window.postMessage ┌─────────────────┐ browser.runtime ┌──────────────────┐
209
- │ dApp │◄───(discovery only)─────►│ Content Script │◄────────────────────►│ Background Script│
210
- (web page) │ │ (message relay)│ │ (decrypt+process)│
211
- └─────────────┘ └─────────────────┘ └──────────────────┘
212
- │ │
213
- │ MessagePort (private channel) │
214
- └──────────(encrypted messages)────────────┘
231
+ // User confirms emojis match
232
+ const wallet = await pending.confirm();
233
+
234
+ // All calls are now encrypted
235
+ const accounts = await wallet.getAccounts();
236
+ console.log('Accounts:', accounts);
237
+ }
238
+
239
+ // Cancel discovery when done or on cleanup
240
+ discovery.cancel();
215
241
  ```
216
242
 
217
- **Security benefits:**
243
+ ### Callback Pattern
218
244
 
219
- - Content script never has access to private keys or shared secrets
220
- - All cryptographic operations happen in the background script (service worker)
221
- - MessagePort provides a private channel not visible to other page scripts
222
- - Only discovery uses `window.postMessage`; all wallet calls are encrypted on the MessagePort
245
+ ```typescript
246
+ import { Fr } from '@aztec/foundation/fields';
247
+ import { WalletManager, type WalletProvider } from '@aztec/wallet-sdk/manager';
248
+ import { hashToEmoji } from '@aztec/wallet-sdk/crypto';
223
249
 
224
- ### Handle Encrypted Messages
250
+ const discoveredProviders: WalletProvider[] = [];
225
251
 
226
- All wallet method calls arrive as encrypted payloads on the MessagePort:
252
+ const discovery = WalletManager.configure({
253
+ extensions: { enabled: true },
254
+ }).getAvailableWallets({
255
+ chainInfo: {
256
+ chainId: new Fr(31337),
257
+ version: new Fr(1),
258
+ },
259
+ appId: 'my-dapp',
260
+ timeout: 60000,
261
+ // Callback fires as each wallet is discovered
262
+ onWalletDiscovered: (provider) => {
263
+ discoveredProviders.push(provider);
264
+ updateUI(); // Your UI update function
265
+ },
266
+ });
227
267
 
228
- ```typescript
229
- interface EncryptedPayload {
230
- iv: string; // Base64-encoded initialization vector
231
- ciphertext: string; // Base64-encoded encrypted data
268
+ // Wait for discovery to complete (or cancel early with discovery.cancel())
269
+ await discovery.done;
270
+ console.log('Discovery complete, found:', discoveredProviders.length);
271
+
272
+ // Connect to a selected provider
273
+ async function connectToWallet(provider: WalletProvider) {
274
+ const pending = await provider.establishSecureChannel('my-dapp');
275
+
276
+ // Show verification UI
277
+ const emojis = hashToEmoji(pending.verificationHash);
278
+ showVerificationDialog(emojis);
279
+
280
+ // User confirms
281
+ const wallet = await pending.confirm();
282
+ return wallet;
232
283
  }
233
284
  ```
234
285
 
235
- **Background script:**
286
+ ### React Hook Example
236
287
 
237
288
  ```typescript
238
- import { decrypt, encrypt } from '@aztec/wallet-sdk/crypto';
239
-
240
- async function handleSecureMessage(requestId: string, encrypted: EncryptedPayload) {
241
- const session = sessions.get(requestId);
242
- if (!session) return;
243
-
244
- try {
245
- // Decrypt the incoming message
246
- const message = await decrypt<WalletMessage>(session.sharedKey, encrypted);
247
- const { type, messageId, args, chainInfo, walletId } = message;
248
-
249
- // Process the wallet method call
250
- const wallet = await getWalletForChain(chainInfo);
251
- const result = await wallet[type](...args);
252
-
253
- // Create and encrypt response
254
- const response: WalletResponse = { messageId, result, walletId };
255
- const encryptedResponse = await encrypt(session.sharedKey, response);
256
-
257
- // Send back through content script
258
- browser.tabs.sendMessage(session.tabId, {
259
- type: 'secure-response',
260
- requestId,
261
- content: encryptedResponse,
289
+ function useWalletDiscovery(chainInfo: ChainInfo, appId: string) {
290
+ const [providers, setProviders] = useState<WalletProvider[]>([]);
291
+ const [isDiscovering, setIsDiscovering] = useState(true);
292
+ const discoveryRef = useRef<DiscoverySession | null>(null);
293
+
294
+ useEffect(() => {
295
+ setProviders([]);
296
+ setIsDiscovering(true);
297
+
298
+ const discovery = WalletManager.configure({
299
+ extensions: { enabled: true },
300
+ }).getAvailableWallets({
301
+ chainInfo,
302
+ appId,
303
+ timeout: 60000,
304
+ onWalletDiscovered: (provider) => {
305
+ setProviders(prev => [...prev, provider]);
306
+ },
262
307
  });
263
- } catch (error) {
264
- // Send encrypted error response
265
- const errorResponse: WalletResponse = {
266
- messageId: message?.messageId ?? '',
267
- error: { message: error.message },
268
- walletId: message?.walletId ?? '',
308
+
309
+ discoveryRef.current = discovery;
310
+
311
+ discovery.done.then(() => setIsDiscovering(false));
312
+
313
+ return () => {
314
+ discovery.cancel();
315
+ discoveryRef.current = null;
269
316
  };
270
- const encryptedError = await encrypt(session.sharedKey, errorResponse);
271
- // ... send error response
272
- }
317
+ }, [chainInfo.chainId.toString(), chainInfo.version.toString(), appId]);
318
+
319
+ return { providers, isDiscovering, cancel: () => discoveryRef.current?.cancel() };
273
320
  }
274
321
  ```
275
322
 
276
- ## Message Formats
323
+ ## Storage backends
324
+
325
+ Your wallet and the PXE it embeds persist state through a pluggable key-value store (`@aztec/kv-store`). In the browser there are two backends:
326
+
327
+ - **IndexedDB** (`@aztec/kv-store/deprecated/indexeddb`): the default in browser environments up to Aztec Alpha v4, now moved to a deprecated subpath. We plan to remove this backend, so new browser code should use the SQLite backend below.
328
+ - **SQLite-OPFS** (`@aztec/kv-store/sqlite-opfs`): the default KV store backend from Aztec Alpha v5 on. It's backed by the durable Origin Private File System web standard, and it offers a number of advantages over IndexedDB: a sane transaction model (IDB transactions auto-close the moment the event loop yields, which constrains the store layer), support for encryption at rest, and better performance in the access patterns we exercise the most from both wallet and PXE.
329
+
330
+ The backend is chosen by *which store you construct and hand to the wallet* there is no runtime flag or environment variable.
277
331
 
278
- ### Wallet Method Request (Decrypted)
332
+ > **Data migration is not supported between backends, by design.** The v4→v5 protocol upgrade discards all local state regardless, so switching to SQLite-OPFS simply means starting from a fresh store.
333
+
334
+ ### Quick start: embedded wallet with an encrypted SQLite store
335
+
336
+ If you build on `@aztec/wallets`' `EmbeddedWallet`, open its two stores (PXE state + the wallet DB) with `openEncryptedEmbeddedStores`, then pass them in:
279
337
 
280
338
  ```typescript
281
- interface WalletMessage {
282
- type: string; // Wallet method name (e.g., 'getAccounts', 'sendTx')
283
- messageId: string; // UUID for tracking this request
284
- args: unknown[]; // Method arguments
285
- chainInfo: ChainInfo;
286
- appId: string; // Application identifier
287
- walletId: string; // Your wallet's ID
288
- }
339
+ import { EmbeddedWallet } from '@aztec/wallets/embedded';
340
+ import { openEncryptedEmbeddedStores } from '@aztec/wallets/embedded/store-encryption';
341
+ import { createLogger } from '@aztec/foundation/log';
342
+
343
+ const log = createLogger('wallet:storage');
344
+
345
+ // Your wallet derives a 32-byte key (see "Key management" below).
346
+ // IMPORTANT: return a *fresh* Uint8Array each call. Opening a store consumes (empties)
347
+ // the key, so a reused array would be empty on the second open (see "important" below).
348
+ const getEncryptionKey = async () => new Uint8Array(myDerivedKey);
349
+
350
+ const { pxeStore, walletStore } = await openEncryptedEmbeddedStores(
351
+ {
352
+ pxe: { name: `pxe-${rollupAddress}`, poolDirectory: '/pxe' },
353
+ wallet: { name: `wallet-${rollupAddress}`, poolDirectory: '/wallet' },
354
+ },
355
+ getEncryptionKey,
356
+ log,
357
+ );
358
+
359
+ const wallet = await EmbeddedWallet.create(nodeUrl, {
360
+ pxe: { store: pxeStore },
361
+ walletDb: { store: walletStore },
362
+ });
289
363
  ```
290
364
 
291
- ### Wallet Method Response
365
+ If the supplied key cannot decrypt an existing store, `openEncryptedEmbeddedStores` throws `EmbeddedWalletEncryptionError` with `storeName: 'pxe' | 'wallet'`, which you can then surface as a "wrong password" error in your UI:
292
366
 
293
367
  ```typescript
294
- interface WalletResponse {
295
- messageId: string; // Must match the request
296
- result?: unknown; // Method result (if successful)
297
- error?: unknown; // Error (if failed)
298
- walletId: string; // Your wallet's ID
368
+ import { EmbeddedWalletEncryptionError } from '@aztec/wallets/embedded/store-encryption';
369
+
370
+ try {
371
+ await openEncryptedEmbeddedStores(/* ... */);
372
+ } catch (err) {
373
+ if (err instanceof EmbeddedWalletEncryptionError) {
374
+ showWrongPasswordError(); // err.storeName tells you which store failed
375
+ } else {
376
+ throw err;
377
+ }
299
378
  }
300
379
  ```
301
380
 
302
- ## Anti-MITM Verification
381
+ ### Important
303
382
 
304
- Both the dApp and wallet independently compute a `verificationHash` from the shared secret. If both parties compute the same hash, they know there's no man-in-the-middle attack.
383
+ 1. **Opening a store consumes the key, it does not copy it.** So that raw key material does not linger in page memory, the SDK moves your key into the storage worker and detaches the buffer on your side. The `Uint8Array` you passed comes back empty, so the same array cannot be reused to open a second store. To open more than one store with the same key, hand each open a fresh copy (`new Uint8Array(key)`). `openEncryptedEmbeddedStores` does this for you by invoking your `getEncryptionKey` callback once per store.
384
+ 2. **Each coexisting store needs its own `poolDirectory`.** The OPFS SAH Pool holds an *exclusive* lock on its directory, so two stores sharing the default pool fail with "Access Handles cannot be created if there is another open Access Handle…". Give every store a distinct, stable `poolDirectory` (stable so the same files re-open next session).
305
385
 
306
- ```typescript
307
- import { hashSharedSecret } from '@aztec/wallet-sdk/crypto';
386
+ ### No multi-tab access: assume one tab at a time
308
387
 
309
- // Compute verification hash from shared key
310
- const verificationHash = await hashSharedSecret(sharedKey);
388
+ A store can be opened by **one browser tab at a time per origin**. If the user opens your wallet in a second tab of the same origin pointing at the same store, the second open contends for that lock.
311
389
 
312
- // Store verificationHash in session - this is the cryptographic proof
313
- sessions.set(requestId, { sharedKey, verificationHash, tabId });
314
- ```
390
+ Thanks to the lock, the data is never corrupted, but the second open fails or hangs rather than succeeding, and there is no graceful "already open elsewhere" signal yet.
315
391
 
316
- For user-friendly display, convert the hash to an emoji sequence:
392
+ Until it does, design for a single active tab: detect a second instance (e.g. with the [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) or a `BroadcastChannel`) and steer the user back to the existing tab, or open the store read-only there.
317
393
 
318
- ```typescript
319
- import { hashToEmoji } from '@aztec/wallet-sdk/crypto';
394
+ If you need genuine concurrent multi-tab access, route all storage access through a single `SharedWorker` that you own and that holds the one connection.
320
395
 
321
- // Convert to emoji only when displaying to the user
322
- const emoji = hashToEmoji(verificationHash); // e.g., "🔵🦋🎯🐼"
323
- ```
396
+ ### Opting out of encryption
324
397
 
325
- The dApp displays the same emoji sequence. If they match, the connection is secure.
398
+ If you do not need at-rest encryption (you rely on full-disk encryption, or the device is trusted), an *unencrypted* SQLite-OPFS store is still a better default than IndexedDB.
326
399
 
327
- ## Session Management
400
+ The `createStore` convenience helper always uses the default OPFS pool directory and does not currently let you change it, so it only works for a single store per tab. The embedded wallet runs two stores (PXE + walletDB), so open them directly from `AztecSQLiteOPFSStore` with a distinct `poolDirectory` each:
328
401
 
329
- Sessions should be cleaned up when:
402
+ ```typescript
403
+ import { EmbeddedWallet } from '@aztec/wallets/embedded';
404
+ import { AztecSQLiteOPFSStore } from '@aztec/kv-store/sqlite-opfs';
405
+ import { createLogger } from '@aztec/foundation/log';
330
406
 
331
- - **Tab closes**: Browser tabs API `onRemoved` event
332
- - **Tab navigates**: Browser tabs API `onUpdated` event with `status === 'loading'`
407
+ const log = createLogger('wallet:storage');
333
408
 
334
- ```typescript
335
- // Clean up when tab closes
336
- browser.tabs.onRemoved.addListener((tabId) => {
337
- for (const [requestId, session] of sessions) {
338
- if (session.tabId === tabId) {
339
- sessions.delete(requestId);
340
- }
341
- }
342
- });
409
+ // No key; just name, ephemeral=false, and a distinct poolDirectory per store.
410
+ const pxeStore = await AztecSQLiteOPFSStore.open(log, `pxe-${rollupAddress}`, false, '/pxe');
411
+ const walletStore = await AztecSQLiteOPFSStore.open(log, `wallet-${rollupAddress}`, false, '/wallet');
343
412
 
344
- // Clean up when tab navigates
345
- browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
346
- if (changeInfo.status === 'loading') {
347
- for (const [requestId, session] of sessions) {
348
- if (session.tabId === tabId) {
349
- sessions.delete(requestId);
350
- }
351
- }
352
- }
413
+ const wallet = await EmbeddedWallet.create(nodeUrl, {
414
+ pxe: { store: pxeStore },
415
+ walletDb: { store: walletStore },
353
416
  });
354
417
  ```
355
418
 
356
- ## Testing Your Integration
419
+ ### Building your own wallet (lower-level API)
357
420
 
358
- ### Using WalletManager
421
+ If you are not using `EmbeddedWallet`, construct stores directly from `@aztec/kv-store/sqlite-opfs` and pass them wherever a store is accepted (e.g. `PXECreationOptions.store`):
359
422
 
360
423
  ```typescript
361
- import { Fr } from '@aztec/foundation/fields';
362
- import { WalletManager, hashToEmoji } from '@aztec/wallet-sdk/manager';
424
+ import { openEncryptedStore, createStore, SqliteEncryptionError } from '@aztec/kv-store/sqlite-opfs';
363
425
 
364
- const manager = WalletManager.configure({
365
- extensions: { enabled: true },
366
- });
426
+ // Encrypted, persistent:
427
+ const store = await openEncryptedStore(new Uint8Array(myDerivedKey), 'my-store', '/my-pool');
367
428
 
368
- // Discover wallets (secure channel established automatically)
369
- const wallets = await manager.getAvailableWallets({
370
- chainInfo: {
371
- chainId: new Fr(31337),
372
- version: new Fr(0),
373
- },
374
- timeout: 2000,
375
- });
429
+ // Or unencrypted:
430
+ const plain = await createStore('my-store', { dataStoreMapSizeKb: 2e10 });
431
+ ```
376
432
 
377
- // Each wallet provider has verification info
378
- for (const provider of wallets) {
379
- const emoji = hashToEmoji(provider.metadata.verificationHash);
380
- console.log(`${provider.name}: ${emoji}`);
381
- }
433
+ Note: `dataStoreMapSizeKb` is an LMDB-specific ceiling (the maximum memory-map size). SQLite-OPFS grows its file dynamically and ignores the value, but it is a required field of the shared `DataStoreConfig` type, so you must still pass something (any number is fine). We will fix this implementation leak in coming versions.
382
434
 
383
- // Connect and use
384
- const walletProvider = wallets.find(w => w.id === 'my-aztec-wallet');
385
- if (walletProvider) {
386
- const wallet = await walletProvider.connect('my-app-id');
435
+ Note: `openEncryptedStore` throws `SqliteEncryptionError` (with a typed `code`, e.g. `'decrypt_failed'`) on a bad key.
387
436
 
388
- // All calls are automatically encrypted
389
- const accounts = await wallet.getAccounts();
390
- console.log('Accounts:', accounts);
391
- }
392
- ```
437
+ ### Using SQLite-OPFS in a browser extension (MV3)
438
+
439
+ SQLite-OPFS needs OPFS, a Web Worker, and cross-origin isolation (SharedArrayBuffer). In a Chrome MV3 extension:
393
440
 
394
- ## Reference Implementation
441
+ - **Run it in an offscreen document, not the background service worker.** The service worker is ephemeral and does not reliably provide OPFS/SharedArrayBuffer; an offscreen document does, and it is where your PXE and stores should live.
442
+ - **No COOP/COEP header setup is needed inside the extension.** Extension pages are cross-origin-isolated by default. (A plain web page hosting the wallet *does* need those headers.)
395
443
 
396
- For a complete reference implementation, see the demo wallet at:
444
+ ### Key management is your responsibility
397
445
 
398
- - Repository: `~/repos/demo-wallet`
446
+ The store encrypts data at rest given a 32-byte key, but deriving and safeguarding that key is the wallet's job. A common pattern is to derive the key from a user password with a memory-hard KDF (e.g. Argon2id) and hold it only in memory while the wallet is unlocked. Adapt this to your own security model.