@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.
- package/README.md +318 -270
- package/dest/base-wallet/base_wallet.d.ts +115 -40
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +353 -105
- package/dest/base-wallet/get_gas_limits.d.ts +36 -0
- package/dest/base-wallet/get_gas_limits.d.ts.map +1 -0
- package/dest/base-wallet/get_gas_limits.js +55 -0
- package/dest/base-wallet/index.d.ts +4 -2
- package/dest/base-wallet/index.d.ts.map +1 -1
- package/dest/base-wallet/index.js +2 -0
- package/dest/base-wallet/utils.d.ts +52 -0
- package/dest/base-wallet/utils.d.ts.map +1 -0
- package/dest/base-wallet/utils.js +137 -0
- package/dest/crypto.d.ts +97 -50
- package/dest/crypto.d.ts.map +1 -1
- package/dest/crypto.js +290 -108
- package/dest/emoji_alphabet.d.ts +35 -0
- package/dest/emoji_alphabet.d.ts.map +1 -0
- package/dest/emoji_alphabet.js +299 -0
- package/dest/extension/handlers/background_connection_handler.d.ts +168 -0
- package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -0
- package/dest/extension/handlers/background_connection_handler.js +294 -0
- package/dest/extension/handlers/content_script_connection_handler.d.ts +57 -0
- package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -0
- package/dest/extension/handlers/content_script_connection_handler.js +193 -0
- package/dest/extension/handlers/index.d.ts +12 -0
- package/dest/extension/handlers/index.d.ts.map +1 -0
- package/dest/extension/handlers/index.js +10 -0
- package/dest/extension/handlers/internal_message_types.d.ts +65 -0
- package/dest/extension/handlers/internal_message_types.d.ts.map +1 -0
- package/dest/extension/handlers/internal_message_types.js +24 -0
- package/dest/extension/provider/extension_provider.d.ts +107 -0
- package/dest/extension/provider/extension_provider.d.ts.map +1 -0
- package/dest/extension/provider/extension_provider.js +160 -0
- package/dest/extension/provider/extension_wallet.d.ts +152 -0
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -0
- package/dest/{providers/extension → extension/provider}/extension_wallet.js +134 -103
- package/dest/extension/provider/index.d.ts +3 -0
- package/dest/extension/provider/index.d.ts.map +1 -0
- package/dest/{providers/extension → extension/provider}/index.js +0 -2
- package/dest/iframe/handlers/iframe_connection_handler.d.ts +122 -0
- package/dest/iframe/handlers/iframe_connection_handler.d.ts.map +1 -0
- package/dest/iframe/handlers/iframe_connection_handler.js +239 -0
- package/dest/iframe/handlers/index.d.ts +2 -0
- package/dest/iframe/handlers/index.d.ts.map +1 -0
- package/dest/iframe/handlers/index.js +1 -0
- package/dest/iframe/provider/iframe_discovery.d.ts +25 -0
- package/dest/iframe/provider/iframe_discovery.d.ts.map +1 -0
- package/dest/iframe/provider/iframe_discovery.js +167 -0
- package/dest/iframe/provider/iframe_provider.d.ts +65 -0
- package/dest/iframe/provider/iframe_provider.d.ts.map +1 -0
- package/dest/iframe/provider/iframe_provider.js +257 -0
- package/dest/iframe/provider/iframe_wallet.d.ts +85 -0
- package/dest/iframe/provider/iframe_wallet.d.ts.map +1 -0
- package/dest/iframe/provider/iframe_wallet.js +269 -0
- package/dest/iframe/provider/index.d.ts +4 -0
- package/dest/iframe/provider/index.d.ts.map +1 -0
- package/dest/iframe/provider/index.js +3 -0
- package/dest/manager/index.d.ts +2 -8
- package/dest/manager/index.d.ts.map +1 -1
- package/dest/manager/index.js +0 -6
- package/dest/manager/types.d.ts +92 -9
- package/dest/manager/types.d.ts.map +1 -1
- package/dest/manager/types.js +17 -1
- package/dest/manager/wallet_manager.d.ts +50 -7
- package/dest/manager/wallet_manager.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +203 -43
- package/dest/types.d.ts +104 -11
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +32 -2
- package/package.json +21 -10
- package/src/base-wallet/base_wallet.ts +456 -167
- package/src/base-wallet/get_gas_limits.ts +88 -0
- package/src/base-wallet/index.ts +8 -1
- package/src/base-wallet/utils.ts +248 -0
- package/src/crypto.ts +341 -113
- package/src/emoji_alphabet.ts +317 -0
- package/src/extension/handlers/background_connection_handler.ts +456 -0
- package/src/extension/handlers/content_script_connection_handler.ts +264 -0
- package/src/extension/handlers/index.ts +25 -0
- package/src/extension/handlers/internal_message_types.ts +71 -0
- package/src/extension/provider/extension_provider.ts +233 -0
- package/src/{providers/extension → extension/provider}/extension_wallet.ts +151 -120
- package/src/extension/provider/index.ts +7 -0
- package/src/iframe/handlers/iframe_connection_handler.ts +341 -0
- package/src/iframe/handlers/index.ts +7 -0
- package/src/iframe/provider/iframe_discovery.ts +185 -0
- package/src/iframe/provider/iframe_provider.ts +331 -0
- package/src/iframe/provider/iframe_wallet.ts +323 -0
- package/src/iframe/provider/index.ts +3 -0
- package/src/manager/index.ts +2 -10
- package/src/manager/types.ts +95 -8
- package/src/manager/wallet_manager.ts +223 -45
- package/src/types.ts +116 -10
- package/dest/providers/extension/extension_provider.d.ts +0 -63
- package/dest/providers/extension/extension_provider.d.ts.map +0 -1
- package/dest/providers/extension/extension_provider.js +0 -124
- package/dest/providers/extension/extension_wallet.d.ts +0 -155
- package/dest/providers/extension/extension_wallet.d.ts.map +0 -1
- package/dest/providers/extension/index.d.ts +0 -6
- package/dest/providers/extension/index.d.ts.map +0 -1
- package/src/providers/extension/extension_provider.ts +0 -167
- 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
|
-
|
|
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
|
-
|
|
36
|
+
**For extension wallets**, pre-built connection handlers are available:
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
BackgroundConnectionHandler,
|
|
41
|
+
ContentScriptConnectionHandler,
|
|
42
|
+
} from '@aztec/wallet-sdk/extension/handlers';
|
|
43
|
+
```
|
|
38
44
|
|
|
39
|
-
|
|
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
|
-
|
|
47
|
+
The Wallet SDK uses a **two-phase connection model** with **end-to-end encryption**:
|
|
47
48
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
### Phase 2: Secure Channel Establishment
|
|
55
57
|
|
|
56
|
-
|
|
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
|
-
###
|
|
65
|
+
### Key Security Features
|
|
59
66
|
|
|
60
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
## Using Pre-built Connection Handlers
|
|
95
92
|
|
|
96
|
-
|
|
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
|
-
|
|
95
|
+
### Background Script Setup
|
|
102
96
|
|
|
103
97
|
```typescript
|
|
104
98
|
import {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
//
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
const verificationHash = await hashSharedSecret(sharedKey);
|
|
153
|
+
const handler = new BackgroundConnectionHandler(config, transport, callbacks);
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
// Initialize the handler to start listening
|
|
156
|
+
handler.initialize();
|
|
137
157
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
### Content Script Setup
|
|
154
180
|
|
|
155
181
|
```typescript
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
// Establish secure channel (key exchange)
|
|
225
|
+
const pending = await provider.establishSecureChannel('my-dapp');
|
|
204
226
|
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
243
|
+
### Callback Pattern
|
|
218
244
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
250
|
+
const discoveredProviders: WalletProvider[] = [];
|
|
225
251
|
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
286
|
+
### React Hook Example
|
|
236
287
|
|
|
237
288
|
```typescript
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
381
|
+
### Important
|
|
303
382
|
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
import { hashSharedSecret } from '@aztec/wallet-sdk/crypto';
|
|
386
|
+
### No multi-tab access: assume one tab at a time
|
|
308
387
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
const emoji = hashToEmoji(verificationHash); // e.g., "🔵🦋🎯🐼"
|
|
323
|
-
```
|
|
396
|
+
### Opting out of encryption
|
|
324
397
|
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
- **Tab navigates**: Browser tabs API `onUpdated` event with `status === 'loading'`
|
|
407
|
+
const log = createLogger('wallet:storage');
|
|
333
408
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
419
|
+
### Building your own wallet (lower-level API)
|
|
357
420
|
|
|
358
|
-
|
|
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 {
|
|
362
|
-
import { WalletManager, hashToEmoji } from '@aztec/wallet-sdk/manager';
|
|
424
|
+
import { openEncryptedStore, createStore, SqliteEncryptionError } from '@aztec/kv-store/sqlite-opfs';
|
|
363
425
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
});
|
|
426
|
+
// Encrypted, persistent:
|
|
427
|
+
const store = await openEncryptedStore(new Uint8Array(myDerivedKey), 'my-store', '/my-pool');
|
|
367
428
|
|
|
368
|
-
//
|
|
369
|
-
const
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
+
### Key management is your responsibility
|
|
397
445
|
|
|
398
|
-
-
|
|
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.
|