@aztec/wallet-sdk 0.0.1-commit.fffb133c → 0.0.1-private.5d121bfd
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/dest/base-wallet/base_wallet.d.ts +94 -35
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +258 -69
- package/dest/base-wallet/index.d.ts +3 -2
- package/dest/base-wallet/index.d.ts.map +1 -1
- package/dest/base-wallet/index.js +1 -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 +39 -1
- package/dest/crypto.d.ts.map +1 -1
- package/dest/crypto.js +88 -0
- package/dest/extension/handlers/background_connection_handler.d.ts +12 -2
- package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -1
- package/dest/extension/handlers/background_connection_handler.js +44 -8
- package/dest/extension/handlers/content_script_connection_handler.d.ts +2 -1
- package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -1
- package/dest/extension/handlers/content_script_connection_handler.js +19 -0
- package/dest/extension/handlers/internal_message_types.d.ts +3 -1
- package/dest/extension/handlers/internal_message_types.d.ts.map +1 -1
- package/dest/extension/handlers/internal_message_types.js +3 -1
- package/dest/extension/provider/extension_wallet.d.ts +27 -6
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -1
- package/dest/extension/provider/extension_wallet.js +89 -11
- package/dest/extension/provider/index.d.ts +2 -2
- package/dest/extension/provider/index.d.ts.map +1 -1
- 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/types.d.ts +6 -5
- package/dest/manager/types.d.ts.map +1 -1
- package/dest/manager/wallet_manager.d.ts +1 -1
- package/dest/manager/wallet_manager.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +48 -18
- package/dest/types.d.ts +64 -2
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +29 -0
- package/package.json +19 -9
- package/src/base-wallet/base_wallet.ts +354 -122
- package/src/base-wallet/index.ts +7 -1
- package/src/base-wallet/utils.ts +248 -0
- package/src/crypto.ts +104 -0
- package/src/extension/handlers/background_connection_handler.ts +42 -9
- package/src/extension/handlers/content_script_connection_handler.ts +18 -0
- package/src/extension/handlers/internal_message_types.ts +2 -0
- package/src/extension/provider/extension_wallet.ts +106 -17
- package/src/extension/provider/index.ts +1 -1
- 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/types.ts +5 -4
- package/src/manager/wallet_manager.ts +55 -23
- package/src/types.ts +72 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IframeWallet — Wallet proxy that communicates with a web wallet loaded in an iframe.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors {@link ExtensionWallet} from `@aztec/wallet-sdk/extension/provider` but uses
|
|
5
|
+
* `window.postMessage` / `window.addEventListener('message')` instead of MessagePort.
|
|
6
|
+
*
|
|
7
|
+
* The wire protocol (encrypted {@link WalletMessage} / {@link WalletResponse}) is identical.
|
|
8
|
+
*/
|
|
9
|
+
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
10
|
+
import { type Wallet, WalletSchema } from '@aztec/aztec.js/wallet';
|
|
11
|
+
import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
12
|
+
import { type PromiseWithResolvers, promiseWithResolvers } from '@aztec/foundation/promise';
|
|
13
|
+
import { getSchemaReturnType, schemaHasMethod } from '@aztec/foundation/schemas';
|
|
14
|
+
import type { FunctionsOf } from '@aztec/foundation/types';
|
|
15
|
+
|
|
16
|
+
import { type EncryptedPayload, decrypt, encrypt } from '../../crypto.js';
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_HEARTBEAT_DEAD_AFTER_MS,
|
|
19
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
20
|
+
type DisconnectCallback,
|
|
21
|
+
type HeartbeatOptions,
|
|
22
|
+
NOOP_LOGGER,
|
|
23
|
+
type WalletMessage,
|
|
24
|
+
WalletMessageType,
|
|
25
|
+
type WalletResponse,
|
|
26
|
+
type WalletSdkLogger,
|
|
27
|
+
} from '../../types.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Internal type representing a wallet method call before encryption.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
type WalletMethodCall = {
|
|
34
|
+
/** Wallet method name to invoke. */
|
|
35
|
+
type: keyof FunctionsOf<Wallet>;
|
|
36
|
+
/** Arguments to pass to the wallet method. */
|
|
37
|
+
args: unknown[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A wallet implementation that communicates with a web wallet loaded in an iframe
|
|
42
|
+
* using encrypted postMessage.
|
|
43
|
+
*
|
|
44
|
+
* Uses the same Proxy pattern as {@link ExtensionWallet}: intercepts property access,
|
|
45
|
+
* checks if the property is a Wallet method via {@link WalletSchema}, and routes the
|
|
46
|
+
* call through an encrypted postMessage channel.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const wallet = IframeWallet.create(walletId, sessionId, iframeWindow, walletOrigin, sharedKey, chainInfo, appId);
|
|
51
|
+
* const accounts = await wallet.asWallet().getAccounts();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export class IframeWallet {
|
|
55
|
+
private inFlight = new Map<string, PromiseWithResolvers<unknown>>();
|
|
56
|
+
private disconnected = false;
|
|
57
|
+
private disconnectCallbacks: DisconnectCallback[] = [];
|
|
58
|
+
private messageListener: ((e: MessageEvent) => void) | null = null;
|
|
59
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
60
|
+
private lastInboundAt = 0;
|
|
61
|
+
private log: WalletSdkLogger;
|
|
62
|
+
private heartbeatIntervalMs: number;
|
|
63
|
+
private heartbeatDeadAfterMs: number;
|
|
64
|
+
|
|
65
|
+
private constructor(
|
|
66
|
+
private chainInfo: ChainInfo,
|
|
67
|
+
private appId: string,
|
|
68
|
+
private walletId: string,
|
|
69
|
+
private sessionId: string,
|
|
70
|
+
private iframeWindow: Window,
|
|
71
|
+
private walletOrigin: string,
|
|
72
|
+
private sharedKey: CryptoKey,
|
|
73
|
+
logger?: WalletSdkLogger,
|
|
74
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
75
|
+
) {
|
|
76
|
+
this.log = logger ?? NOOP_LOGGER;
|
|
77
|
+
this.heartbeatIntervalMs = heartbeatOptions?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
78
|
+
this.heartbeatDeadAfterMs = heartbeatOptions?.deadAfterMs ?? DEFAULT_HEARTBEAT_DEAD_AFTER_MS;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a proxied IframeWallet that implements the {@link Wallet} interface.
|
|
83
|
+
*
|
|
84
|
+
* All Wallet method calls are intercepted by a Proxy, encrypted with the shared
|
|
85
|
+
* AES-256-GCM key, sent via postMessage, and the response is decrypted and
|
|
86
|
+
* validated against {@link WalletSchema}.
|
|
87
|
+
*
|
|
88
|
+
* @param walletId - Unique identifier of the remote wallet
|
|
89
|
+
* @param sessionId - Session identifier from the key exchange
|
|
90
|
+
* @param iframeWindow - The iframe's contentWindow to post messages to
|
|
91
|
+
* @param walletOrigin - Origin of the wallet iframe (for postMessage targeting)
|
|
92
|
+
* @param sharedKey - AES-256-GCM key derived from ECDH key exchange
|
|
93
|
+
* @param chainInfo - Network information (chainId and version)
|
|
94
|
+
* @param appId - Application identifier for the requesting dApp
|
|
95
|
+
* @param logger - Optional logger; defaults to a no-op logger to keep extension/page bundles small
|
|
96
|
+
* @param heartbeatOptions - Optional override for heartbeat tuning (mostly useful for tests)
|
|
97
|
+
* @returns A proxied IframeWallet — call `.asWallet()` to get the typed `Wallet`
|
|
98
|
+
*/
|
|
99
|
+
static create(
|
|
100
|
+
walletId: string,
|
|
101
|
+
sessionId: string,
|
|
102
|
+
iframeWindow: Window,
|
|
103
|
+
walletOrigin: string,
|
|
104
|
+
sharedKey: CryptoKey,
|
|
105
|
+
chainInfo: ChainInfo,
|
|
106
|
+
appId: string,
|
|
107
|
+
logger?: WalletSdkLogger,
|
|
108
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
109
|
+
): IframeWallet {
|
|
110
|
+
const wallet = new IframeWallet(
|
|
111
|
+
chainInfo,
|
|
112
|
+
appId,
|
|
113
|
+
walletId,
|
|
114
|
+
sessionId,
|
|
115
|
+
iframeWindow,
|
|
116
|
+
walletOrigin,
|
|
117
|
+
sharedKey,
|
|
118
|
+
logger,
|
|
119
|
+
heartbeatOptions,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
wallet.messageListener = (event: MessageEvent) => {
|
|
123
|
+
if (event.origin !== walletOrigin) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const msg = event.data;
|
|
127
|
+
if (!msg || typeof msg !== 'object') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (msg.sessionId !== sessionId) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Any inbound traffic on our session counts as proof of liveness.
|
|
136
|
+
wallet.lastInboundAt = Date.now();
|
|
137
|
+
|
|
138
|
+
if (msg.type === WalletMessageType.SECURE_RESPONSE) {
|
|
139
|
+
void wallet.handleEncryptedResponse(msg.encrypted as EncryptedPayload);
|
|
140
|
+
} else if (msg.type === WalletMessageType.SESSION_DISCONNECTED) {
|
|
141
|
+
wallet.handleDisconnect();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
window.addEventListener('message', wallet.messageListener);
|
|
145
|
+
|
|
146
|
+
return new Proxy(wallet, {
|
|
147
|
+
get: (target, prop, receiver) => {
|
|
148
|
+
if (prop === 'asWallet') {
|
|
149
|
+
return () => receiver as unknown as Wallet;
|
|
150
|
+
} else if (schemaHasMethod(WalletSchema, prop.toString())) {
|
|
151
|
+
return async (...args: unknown[]) => {
|
|
152
|
+
const result = await target.postMessage({
|
|
153
|
+
type: prop.toString() as keyof FunctionsOf<Wallet>,
|
|
154
|
+
args,
|
|
155
|
+
});
|
|
156
|
+
return getSchemaReturnType(WalletSchema[prop.toString() as keyof typeof WalletSchema]).parseAsync(result);
|
|
157
|
+
};
|
|
158
|
+
} else {
|
|
159
|
+
return target[prop as keyof IframeWallet];
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns this wallet as a typed {@link Wallet} interface.
|
|
167
|
+
* When accessed through the Proxy (via `create()`), returns the proxy itself.
|
|
168
|
+
*/
|
|
169
|
+
asWallet(): Wallet {
|
|
170
|
+
return this as unknown as Wallet;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async handleEncryptedResponse(encrypted: EncryptedPayload): Promise<void> {
|
|
174
|
+
try {
|
|
175
|
+
const response = await decrypt<WalletResponse>(this.sharedKey, encrypted);
|
|
176
|
+
const { messageId, result, error, walletId: responseWalletId } = response;
|
|
177
|
+
|
|
178
|
+
if (!messageId || responseWalletId !== this.walletId) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pending = this.inFlight.get(messageId);
|
|
183
|
+
if (!pending) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (error) {
|
|
188
|
+
pending.reject(new Error(jsonStringify(error)));
|
|
189
|
+
} else {
|
|
190
|
+
pending.resolve(result);
|
|
191
|
+
}
|
|
192
|
+
this.inFlight.delete(messageId);
|
|
193
|
+
this.maybeStopHeartbeat();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
this.log.warn('Failed to decrypt wallet response', { err });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async postMessage(call: WalletMethodCall): Promise<unknown> {
|
|
200
|
+
if (this.disconnected) {
|
|
201
|
+
throw new Error('Wallet has been disconnected');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const messageId = globalThis.crypto.randomUUID();
|
|
205
|
+
const message: WalletMessage = {
|
|
206
|
+
type: call.type,
|
|
207
|
+
args: call.args,
|
|
208
|
+
messageId,
|
|
209
|
+
chainInfo: this.chainInfo,
|
|
210
|
+
appId: this.appId,
|
|
211
|
+
walletId: this.walletId,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const encrypted = await encrypt(this.sharedKey, jsonStringify(message));
|
|
215
|
+
this.iframeWindow.postMessage(
|
|
216
|
+
{ type: WalletMessageType.SECURE_MESSAGE, sessionId: this.sessionId, encrypted },
|
|
217
|
+
this.walletOrigin,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const { promise, resolve, reject } = promiseWithResolvers<unknown>();
|
|
221
|
+
this.inFlight.set(messageId, { promise, resolve, reject });
|
|
222
|
+
this.startHeartbeat();
|
|
223
|
+
return promise;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Start liveness probing while at least one request is in flight. PINGs are
|
|
228
|
+
* unencrypted control messages — older wallet handlers that don't understand
|
|
229
|
+
* them simply drop them, but any inbound traffic (PONG, encrypted response,
|
|
230
|
+
* disconnect notice) resets the idle timer, so a slow-but-alive legacy wallet
|
|
231
|
+
* never trips a false disconnect.
|
|
232
|
+
*/
|
|
233
|
+
private startHeartbeat(): void {
|
|
234
|
+
if (this.heartbeatTimer !== null || this.disconnected) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.lastInboundAt = Date.now();
|
|
238
|
+
this.heartbeatTimer = setInterval(() => this.heartbeatTick(), this.heartbeatIntervalMs);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private maybeStopHeartbeat(): void {
|
|
242
|
+
if (this.inFlight.size === 0 && this.heartbeatTimer !== null) {
|
|
243
|
+
clearInterval(this.heartbeatTimer);
|
|
244
|
+
this.heartbeatTimer = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private heartbeatTick(): void {
|
|
249
|
+
if (this.disconnected || this.inFlight.size === 0) {
|
|
250
|
+
this.maybeStopHeartbeat();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const idleMs = Date.now() - this.lastInboundAt;
|
|
255
|
+
if (idleMs >= this.heartbeatDeadAfterMs) {
|
|
256
|
+
this.log.warn('Iframe wallet channel unresponsive — declaring disconnect', {
|
|
257
|
+
idleMs,
|
|
258
|
+
inFlight: this.inFlight.size,
|
|
259
|
+
});
|
|
260
|
+
this.handleDisconnect();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
this.iframeWindow.postMessage({ type: WalletMessageType.PING, sessionId: this.sessionId }, this.walletOrigin);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
this.log.warn('Failed to send heartbeat PING', { err });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private handleDisconnect(): void {
|
|
272
|
+
if (this.disconnected) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.disconnected = true;
|
|
276
|
+
|
|
277
|
+
if (this.heartbeatTimer !== null) {
|
|
278
|
+
clearInterval(this.heartbeatTimer);
|
|
279
|
+
this.heartbeatTimer = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (this.messageListener) {
|
|
283
|
+
window.removeEventListener('message', this.messageListener);
|
|
284
|
+
this.messageListener = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const error = new Error('Wallet disconnected');
|
|
288
|
+
for (const { reject } of this.inFlight.values()) {
|
|
289
|
+
reject(error);
|
|
290
|
+
}
|
|
291
|
+
this.inFlight.clear();
|
|
292
|
+
|
|
293
|
+
for (const cb of this.disconnectCallbacks) {
|
|
294
|
+
try {
|
|
295
|
+
cb();
|
|
296
|
+
} catch {
|
|
297
|
+
// Ignore errors in disconnect callbacks
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
onDisconnect(callback: DisconnectCallback): () => void {
|
|
303
|
+
this.disconnectCallbacks.push(callback);
|
|
304
|
+
return () => {
|
|
305
|
+
const i = this.disconnectCallbacks.indexOf(callback);
|
|
306
|
+
if (i !== -1) {
|
|
307
|
+
this.disconnectCallbacks.splice(i, 1);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
isDisconnected(): boolean {
|
|
313
|
+
return this.disconnected;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
disconnect(): void {
|
|
317
|
+
if (this.disconnected) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
this.iframeWindow.postMessage({ type: WalletMessageType.DISCONNECT, sessionId: this.sessionId }, this.walletOrigin);
|
|
321
|
+
this.handleDisconnect();
|
|
322
|
+
}
|
|
323
|
+
}
|
package/src/manager/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface WalletProvider {
|
|
|
96
96
|
* matches their wallet before calling `confirm()`.
|
|
97
97
|
*
|
|
98
98
|
* @param appId - Application identifier for the requesting dapp
|
|
99
|
+
* @param options - Optional provider-specific options (e.g. container element for iframe wallets)
|
|
99
100
|
* @returns A pending connection with verification hash and confirm/cancel methods
|
|
100
101
|
*
|
|
101
102
|
* @example
|
|
@@ -110,24 +111,24 @@ export interface WalletProvider {
|
|
|
110
111
|
* const wallet = await pending.confirm();
|
|
111
112
|
* ```
|
|
112
113
|
*/
|
|
113
|
-
establishSecureChannel(appId: string): Promise<PendingConnection>;
|
|
114
|
+
establishSecureChannel(appId: string, options?: Record<string, unknown>): Promise<PendingConnection>;
|
|
114
115
|
/**
|
|
115
116
|
* Disconnects the current wallet and cleans up resources.
|
|
116
117
|
* After calling this, the wallet returned from confirm() should no longer be used.
|
|
117
118
|
* @returns A promise that resolves when disconnection is complete
|
|
118
119
|
*/
|
|
119
|
-
disconnect
|
|
120
|
+
disconnect(): Promise<void>;
|
|
120
121
|
/**
|
|
121
122
|
* Registers a callback to be invoked when the wallet disconnects unexpectedly.
|
|
122
123
|
* @param callback - Function to call when wallet disconnects
|
|
123
124
|
* @returns A function to unregister the callback
|
|
124
125
|
*/
|
|
125
|
-
onDisconnect
|
|
126
|
+
onDisconnect(callback: ProviderDisconnectionCallback): () => void;
|
|
126
127
|
/**
|
|
127
128
|
* Returns whether the provider's wallet connection has been disconnected.
|
|
128
129
|
* @returns true if the wallet is no longer connected
|
|
129
130
|
*/
|
|
130
|
-
isDisconnected
|
|
131
|
+
isDisconnected(): boolean;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
@@ -2,6 +2,7 @@ import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
|
2
2
|
import { promiseWithResolvers } from '@aztec/foundation/promise';
|
|
3
3
|
|
|
4
4
|
import { type DiscoveredWallet, ExtensionProvider, ExtensionWallet } from '../extension/provider/index.js';
|
|
5
|
+
import { discoverWebWallets } from '../iframe/provider/iframe_discovery.js';
|
|
5
6
|
import { WalletMessageType } from '../types.js';
|
|
6
7
|
import type {
|
|
7
8
|
DiscoverWalletsOptions,
|
|
@@ -88,6 +89,20 @@ export class WalletManager {
|
|
|
88
89
|
|
|
89
90
|
const { promise: donePromise, resolve: resolveDone } = promiseWithResolvers<void>();
|
|
90
91
|
|
|
92
|
+
const pendingSources = new Set<string>();
|
|
93
|
+
|
|
94
|
+
const emit = (provider: WalletProvider) => {
|
|
95
|
+
options.onWalletDiscovered?.(provider);
|
|
96
|
+
|
|
97
|
+
if (pendingResolve) {
|
|
98
|
+
const resolve = pendingResolve;
|
|
99
|
+
pendingResolve = null;
|
|
100
|
+
resolve({ value: provider, done: false });
|
|
101
|
+
} else {
|
|
102
|
+
pendingProviders.push(provider);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
91
106
|
const markComplete = () => {
|
|
92
107
|
completed = true;
|
|
93
108
|
resolveDone();
|
|
@@ -98,7 +113,15 @@ export class WalletManager {
|
|
|
98
113
|
}
|
|
99
114
|
};
|
|
100
115
|
|
|
116
|
+
const sourceComplete = (source: string) => {
|
|
117
|
+
pendingSources.delete(source);
|
|
118
|
+
if (pendingSources.size === 0) {
|
|
119
|
+
markComplete();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
101
123
|
if (this.config.extensions?.enabled) {
|
|
124
|
+
pendingSources.add('extensions');
|
|
102
125
|
const extensionConfig = this.config.extensions;
|
|
103
126
|
|
|
104
127
|
void ExtensionProvider.discoverWallets(chainInfo, {
|
|
@@ -107,24 +130,35 @@ export class WalletManager {
|
|
|
107
130
|
signal: abortController.signal,
|
|
108
131
|
onWalletDiscovered: discoveredWallet => {
|
|
109
132
|
const provider = this.createProviderFromDiscoveredWallet(discoveredWallet, chainInfo, extensionConfig);
|
|
110
|
-
if (
|
|
111
|
-
|
|
133
|
+
if (provider) {
|
|
134
|
+
emit(provider);
|
|
112
135
|
}
|
|
136
|
+
},
|
|
137
|
+
}).then(() => sourceComplete('extensions'));
|
|
138
|
+
}
|
|
113
139
|
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
if (this.config.webWallets?.urls && this.config.webWallets.urls.length > 0) {
|
|
141
|
+
pendingSources.add('webWallets');
|
|
142
|
+
const webSession = discoverWebWallets(this.config.webWallets.urls, chainInfo);
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
// Forward discovered web wallets into the shared iterator
|
|
145
|
+
void (async () => {
|
|
146
|
+
try {
|
|
147
|
+
for await (const provider of webSession.wallets) {
|
|
148
|
+
if (abortController.signal.aborted) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
emit(provider);
|
|
124
152
|
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
153
|
+
} finally {
|
|
154
|
+
sourceComplete('webWallets');
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
|
|
158
|
+
abortController.signal.addEventListener('abort', () => webSession.cancel(), { once: true });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (pendingSources.size === 0) {
|
|
128
162
|
markComplete();
|
|
129
163
|
}
|
|
130
164
|
|
|
@@ -196,15 +230,14 @@ export class WalletManager {
|
|
|
196
230
|
return {
|
|
197
231
|
verificationHash: connection.info.verificationHash!,
|
|
198
232
|
confirm: () => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
connectAppId,
|
|
206
|
-
),
|
|
233
|
+
extensionWallet = ExtensionWallet.create(
|
|
234
|
+
connection.info.id,
|
|
235
|
+
connection.port,
|
|
236
|
+
connection.sharedKey,
|
|
237
|
+
chainInfo,
|
|
238
|
+
connectAppId,
|
|
207
239
|
);
|
|
240
|
+
return Promise.resolve(extensionWallet.asWallet());
|
|
208
241
|
},
|
|
209
242
|
cancel: () => {
|
|
210
243
|
// Send disconnect to terminate the session on the extension side
|
|
@@ -213,7 +246,6 @@ export class WalletManager {
|
|
|
213
246
|
type: WalletMessageType.DISCONNECT,
|
|
214
247
|
requestId: discoveredWallet.requestId,
|
|
215
248
|
});
|
|
216
|
-
// Don't close the port - allow retry with fresh key exchange
|
|
217
249
|
},
|
|
218
250
|
};
|
|
219
251
|
},
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,21 @@ export enum WalletMessageType {
|
|
|
17
17
|
KEY_EXCHANGE_REQUEST = 'aztec-wallet-key-exchange-request',
|
|
18
18
|
/** Key exchange response sent over MessageChannel */
|
|
19
19
|
KEY_EXCHANGE_RESPONSE = 'aztec-wallet-key-exchange-response',
|
|
20
|
+
/** Wallet ready signal */
|
|
21
|
+
WALLET_READY = 'aztec-wallet-ready',
|
|
22
|
+
/** Encrypted wallet message wrapper */
|
|
23
|
+
SECURE_MESSAGE = 'aztec-wallet-secure-message',
|
|
24
|
+
/** Encrypted wallet response wrapper */
|
|
25
|
+
SECURE_RESPONSE = 'aztec-wallet-secure-response',
|
|
26
|
+
/** Session disconnected notification */
|
|
27
|
+
SESSION_DISCONNECTED = 'aztec-wallet-session-disconnected',
|
|
28
|
+
/** Liveness probe sent by the dApp while a request is in flight */
|
|
29
|
+
PING = 'aztec-wallet-ping',
|
|
30
|
+
/** Liveness response from the wallet. Any inbound message (including a regular
|
|
31
|
+
* encrypted response) is treated as proof of liveness, so a missing PONG alone
|
|
32
|
+
* never trips disconnect — the dApp also resets its liveness timer on traffic.
|
|
33
|
+
*/
|
|
34
|
+
PONG = 'aztec-wallet-pong',
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
/**
|
|
@@ -130,3 +145,60 @@ export interface KeyExchangeResponse {
|
|
|
130
145
|
/** Wallet's ECDH public key for deriving shared secret */
|
|
131
146
|
publicKey: ExportedPublicKey;
|
|
132
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Callback invoked when a wallet connection is disconnected.
|
|
151
|
+
*/
|
|
152
|
+
export type DisconnectCallback = () => void;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Default heartbeat tuning shared by both transports.
|
|
156
|
+
*
|
|
157
|
+
* - `intervalMs`: how often the dApp sends PING probes while a request is in flight.
|
|
158
|
+
* - `deadAfterMs`: how long the channel can stay silent (no PONG, no encrypted
|
|
159
|
+
* response, no DISCONNECT) before the dApp declares the wallet unreachable
|
|
160
|
+
* and rejects all in-flight requests. Generous enough that long-running
|
|
161
|
+
* operations (proveTx, sendTx) on legacy wallets that don't reply to PING
|
|
162
|
+
* still succeed — any inbound traffic resets the timer.
|
|
163
|
+
*/
|
|
164
|
+
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
165
|
+
export const DEFAULT_HEARTBEAT_DEAD_AFTER_MS = 300_000;
|
|
166
|
+
|
|
167
|
+
/** Override knobs for the heartbeat — mostly useful for tests. */
|
|
168
|
+
export interface HeartbeatOptions {
|
|
169
|
+
/** How often to send PING probes while a request is in flight (ms). */
|
|
170
|
+
intervalMs?: number;
|
|
171
|
+
/** Idle ceiling before declaring disconnect (ms). */
|
|
172
|
+
deadAfterMs?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Minimal logger surface used by the wallet SDK.
|
|
177
|
+
*
|
|
178
|
+
* Defined locally so that wallet hosts (browser extensions, iframe wallet pages)
|
|
179
|
+
* can pass a simple `console`-backed logger without pulling in the full
|
|
180
|
+
* `@aztec/foundation` logging runtime, which is non-trivial to bundle in those
|
|
181
|
+
* contexts. Structurally compatible with `Logger` from `@aztec/foundation/log`,
|
|
182
|
+
* so dApp-side callers can pass that type directly.
|
|
183
|
+
*/
|
|
184
|
+
export interface WalletSdkLogger {
|
|
185
|
+
/** Diagnostic messages — typically discarded in production. */
|
|
186
|
+
debug: (message: string, data?: unknown) => void;
|
|
187
|
+
/** Informational messages — significant lifecycle events. */
|
|
188
|
+
info: (message: string, data?: unknown) => void;
|
|
189
|
+
/** Recoverable problems — channel decryption failure, missed heartbeats, etc. */
|
|
190
|
+
warn: (message: string, data?: unknown) => void;
|
|
191
|
+
/** Errors that prevent normal operation. */
|
|
192
|
+
error: (message: string, errOrData?: unknown, data?: unknown) => void;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* No-op logger used as the default when callers don't provide one. Discards all
|
|
197
|
+
* messages — wallet hosts that want diagnostics should pass their own logger.
|
|
198
|
+
*/
|
|
199
|
+
export const NOOP_LOGGER: WalletSdkLogger = {
|
|
200
|
+
debug: () => {},
|
|
201
|
+
info: () => {},
|
|
202
|
+
warn: () => {},
|
|
203
|
+
error: () => {},
|
|
204
|
+
};
|