@aztec/wallet-sdk 0.0.1-commit.fff30aa → 0.0.1-dev
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 +10 -2
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +19 -4
- 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 +26 -3
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -1
- package/dest/extension/provider/extension_wallet.js +78 -7
- package/dest/iframe/handlers/iframe_connection_handler.d.ts +6 -2
- package/dest/iframe/handlers/iframe_connection_handler.d.ts.map +1 -1
- package/dest/iframe/handlers/iframe_connection_handler.js +16 -4
- package/dest/iframe/provider/iframe_wallet.d.ts +20 -3
- package/dest/iframe/provider/iframe_wallet.d.ts.map +1 -1
- package/dest/iframe/provider/iframe_wallet.js +77 -8
- package/dest/types.d.ts +52 -2
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +25 -0
- package/package.json +8 -8
- package/src/base-wallet/base_wallet.ts +19 -2
- 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 +92 -6
- package/src/iframe/handlers/iframe_connection_handler.ts +19 -5
- package/src/iframe/provider/iframe_wallet.ts +101 -7
- package/src/types.ts +59 -0
|
@@ -84,7 +84,7 @@ export type FeeOptions = {
|
|
|
84
84
|
/** Options for `simulateViaEntrypoint`. */
|
|
85
85
|
export type SimulateViaEntrypointOptions = Pick<
|
|
86
86
|
SimulateOptions,
|
|
87
|
-
'from' | 'additionalScopes' | 'skipTxValidation' | 'skipFeeEnforcement'
|
|
87
|
+
'from' | 'additionalScopes' | 'skipTxValidation' | 'skipFeeEnforcement' | 'sendMessagesAs'
|
|
88
88
|
> & {
|
|
89
89
|
/** Fee options for the entrypoint */
|
|
90
90
|
feeOptions: FeeOptions;
|
|
@@ -125,6 +125,17 @@ export abstract class BaseWallet implements Wallet {
|
|
|
125
125
|
return [...scopeSet].map(AztecAddress.fromString);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Picks the sender address PXE should tag private messages with. Returns `undefined` when there is no signing
|
|
130
|
+
* account (`from === NO_FROM`) and no explicit override; in that case any private log emitted by the tx will fail
|
|
131
|
+
* the contract-side `Sender for tags is not set` assertion unless `set_sender_for_tags` is called first.
|
|
132
|
+
* @param from - Tx sender, or `NO_FROM`.
|
|
133
|
+
* @param sendMessagesAs - Explicit override.
|
|
134
|
+
*/
|
|
135
|
+
protected senderForTagsFrom(from: AztecAddress | NoFrom, sendMessagesAs?: AztecAddress): AztecAddress | undefined {
|
|
136
|
+
return sendMessagesAs ?? (from === NO_FROM ? undefined : from);
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
protected abstract getAccountFromAddress(address: AztecAddress): Promise<Account>;
|
|
129
140
|
|
|
130
141
|
abstract getAccounts(): Promise<Aliased<AztecAddress>[]>;
|
|
@@ -325,6 +336,7 @@ export abstract class BaseWallet implements Wallet {
|
|
|
325
336
|
skipTxValidation: opts.skipTxValidation,
|
|
326
337
|
skipFeeEnforcement: opts.skipFeeEnforcement,
|
|
327
338
|
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
|
|
339
|
+
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
|
|
328
340
|
});
|
|
329
341
|
const appCallOffset = await this.computeAppCallOffset(opts.from, opts.feeOptions);
|
|
330
342
|
return TxSimulationResultWithAppOffset.fromResultAndOffset(result, appCallOffset);
|
|
@@ -396,6 +408,7 @@ export abstract class BaseWallet implements Wallet {
|
|
|
396
408
|
additionalScopes: opts.additionalScopes,
|
|
397
409
|
skipTxValidation: opts.skipTxValidation,
|
|
398
410
|
skipFeeEnforcement: opts.skipFeeEnforcement ?? true,
|
|
411
|
+
sendMessagesAs: opts.sendMessagesAs,
|
|
399
412
|
})
|
|
400
413
|
: Promise.resolve(null),
|
|
401
414
|
]);
|
|
@@ -414,6 +427,7 @@ export abstract class BaseWallet implements Wallet {
|
|
|
414
427
|
profileMode: opts.profileMode,
|
|
415
428
|
skipProofGeneration: opts.skipProofGeneration ?? true,
|
|
416
429
|
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
|
|
430
|
+
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
|
|
417
431
|
});
|
|
418
432
|
}
|
|
419
433
|
|
|
@@ -427,7 +441,10 @@ export abstract class BaseWallet implements Wallet {
|
|
|
427
441
|
gasSettings: opts.fee?.gasSettings,
|
|
428
442
|
});
|
|
429
443
|
const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, opts.from, feeOptions);
|
|
430
|
-
const provenTx = await this.pxe.proveTx(txRequest,
|
|
444
|
+
const provenTx = await this.pxe.proveTx(txRequest, {
|
|
445
|
+
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
|
|
446
|
+
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
|
|
447
|
+
});
|
|
431
448
|
const offchainOutput = extractOffchainOutput(
|
|
432
449
|
provenTx.getOffchainEffects(),
|
|
433
450
|
provenTx.publicInputs.constants.anchorBlockHeader.globalVariables.timestamp,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type WalletMessage,
|
|
18
18
|
WalletMessageType,
|
|
19
19
|
type WalletResponse,
|
|
20
|
+
type WalletSdkLogger,
|
|
20
21
|
} from '../../types.js';
|
|
21
22
|
import {
|
|
22
23
|
type BackgroundMessage,
|
|
@@ -131,6 +132,8 @@ export interface BackgroundConnectionConfig {
|
|
|
131
132
|
walletVersion: string;
|
|
132
133
|
/** Optional wallet icon URL. */
|
|
133
134
|
walletIcon?: string;
|
|
135
|
+
/** Logger used for diagnostics. */
|
|
136
|
+
logger: WalletSdkLogger;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
/**
|
|
@@ -149,6 +152,7 @@ export interface BackgroundConnectionConfig {
|
|
|
149
152
|
* walletId: 'my-wallet',
|
|
150
153
|
* walletName: 'My Wallet',
|
|
151
154
|
* walletVersion: '1.0.0',
|
|
155
|
+
* logger: console,
|
|
152
156
|
* },
|
|
153
157
|
* {
|
|
154
158
|
* sendToTab: (tabId, message) => browser.tabs.sendMessage(tabId, message),
|
|
@@ -167,12 +171,15 @@ export interface BackgroundConnectionConfig {
|
|
|
167
171
|
export class BackgroundConnectionHandler {
|
|
168
172
|
private pendingDiscoveries = new Map<string, PendingDiscovery>();
|
|
169
173
|
private activeSessions = new Map<string, ActiveSession>();
|
|
174
|
+
private log: WalletSdkLogger;
|
|
170
175
|
|
|
171
176
|
constructor(
|
|
172
177
|
private config: BackgroundConnectionConfig,
|
|
173
178
|
private transport: BackgroundTransport,
|
|
174
179
|
private callbacks: BackgroundConnectionCallbacks = {},
|
|
175
|
-
) {
|
|
180
|
+
) {
|
|
181
|
+
this.log = config.logger;
|
|
182
|
+
}
|
|
176
183
|
|
|
177
184
|
initialize(): void {
|
|
178
185
|
this.transport.addContentListener(this.handleMessage);
|
|
@@ -198,8 +205,8 @@ export class BackgroundConnectionHandler {
|
|
|
198
205
|
break;
|
|
199
206
|
case InternalMessageType.KEY_EXCHANGE_REQUEST:
|
|
200
207
|
if (sessionId) {
|
|
201
|
-
this.handleKeyExchangeRequest(sessionId, content as KeyExchangeRequest).catch(
|
|
202
|
-
|
|
208
|
+
this.handleKeyExchangeRequest(sessionId, content as KeyExchangeRequest).catch(err => {
|
|
209
|
+
this.log.warn('Key exchange failed — session will not be established', { sessionId, err });
|
|
203
210
|
});
|
|
204
211
|
}
|
|
205
212
|
break;
|
|
@@ -213,9 +220,31 @@ export class BackgroundConnectionHandler {
|
|
|
213
220
|
void this.handleEncryptedMessage(sessionId, content as EncryptedPayload);
|
|
214
221
|
}
|
|
215
222
|
break;
|
|
223
|
+
case InternalMessageType.PING:
|
|
224
|
+
if (sessionId) {
|
|
225
|
+
this.handlePing(sessionId);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
216
228
|
}
|
|
217
229
|
};
|
|
218
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Reply to a dApp PING with a PONG. Used as a liveness probe so the dApp can
|
|
233
|
+
* tell the difference between a slow request and a dead extension.
|
|
234
|
+
* @param sessionId - The session that sent the PING.
|
|
235
|
+
*/
|
|
236
|
+
private handlePing(sessionId: string): void {
|
|
237
|
+
const session = this.activeSessions.get(sessionId);
|
|
238
|
+
if (!session) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.transport.sendToTab(session.tabId, {
|
|
242
|
+
origin: MessageOrigin.BACKGROUND,
|
|
243
|
+
type: InternalMessageType.PONG,
|
|
244
|
+
sessionId,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
219
248
|
getWalletInfo(): WalletInfo {
|
|
220
249
|
return {
|
|
221
250
|
id: this.config.walletId,
|
|
@@ -315,8 +344,8 @@ export class BackgroundConnectionHandler {
|
|
|
315
344
|
});
|
|
316
345
|
|
|
317
346
|
this.callbacks.onSessionEstablished?.(session);
|
|
318
|
-
} catch {
|
|
319
|
-
|
|
347
|
+
} catch (err) {
|
|
348
|
+
this.log.warn('Key exchange failed — session will not be established', { sessionId, err });
|
|
320
349
|
}
|
|
321
350
|
}
|
|
322
351
|
|
|
@@ -329,8 +358,8 @@ export class BackgroundConnectionHandler {
|
|
|
329
358
|
try {
|
|
330
359
|
const message = await decrypt<WalletMessage>(session.sharedKey, encrypted);
|
|
331
360
|
this.callbacks.onWalletMessage?.(session, message);
|
|
332
|
-
} catch {
|
|
333
|
-
|
|
361
|
+
} catch (err) {
|
|
362
|
+
this.log.warn('Failed to decrypt incoming wallet message', { sessionId, err });
|
|
334
363
|
}
|
|
335
364
|
}
|
|
336
365
|
|
|
@@ -348,8 +377,12 @@ export class BackgroundConnectionHandler {
|
|
|
348
377
|
sessionId,
|
|
349
378
|
content: encrypted,
|
|
350
379
|
});
|
|
351
|
-
} catch {
|
|
352
|
-
|
|
380
|
+
} catch (err) {
|
|
381
|
+
this.log.error('Failed to encrypt wallet response — response will not be sent', {
|
|
382
|
+
sessionId,
|
|
383
|
+
messageId: response.messageId,
|
|
384
|
+
err,
|
|
385
|
+
});
|
|
353
386
|
}
|
|
354
387
|
}
|
|
355
388
|
|
|
@@ -139,9 +139,20 @@ export class ContentScriptConnectionHandler {
|
|
|
139
139
|
case InternalMessageType.SESSION_DISCONNECTED:
|
|
140
140
|
this.handleSessionDisconnected(sessionId);
|
|
141
141
|
break;
|
|
142
|
+
case InternalMessageType.PONG:
|
|
143
|
+
this.handlePong(sessionId);
|
|
144
|
+
break;
|
|
142
145
|
}
|
|
143
146
|
};
|
|
144
147
|
|
|
148
|
+
private handlePong(sessionId: string): void {
|
|
149
|
+
const connection = this.ports.get(sessionId);
|
|
150
|
+
if (!connection) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
connection.port.postMessage({ type: WalletMessageType.PONG });
|
|
154
|
+
}
|
|
155
|
+
|
|
145
156
|
private handleDiscoveryRequest(request: DiscoveryRequest): void {
|
|
146
157
|
this.transport.sendToBackground({
|
|
147
158
|
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
@@ -178,6 +189,13 @@ export class ContentScriptConnectionHandler {
|
|
|
178
189
|
content: data,
|
|
179
190
|
});
|
|
180
191
|
break;
|
|
192
|
+
case WalletMessageType.PING:
|
|
193
|
+
this.transport.sendToBackground({
|
|
194
|
+
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
195
|
+
type: InternalMessageType.PING,
|
|
196
|
+
sessionId,
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
181
199
|
default:
|
|
182
200
|
this.transport.sendToBackground({
|
|
183
201
|
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
@@ -9,11 +9,13 @@ export const InternalMessageType = {
|
|
|
9
9
|
KEY_EXCHANGE_REQUEST: 'key-exchange-request',
|
|
10
10
|
SECURE_MESSAGE: 'secure-message',
|
|
11
11
|
DISCONNECT_REQUEST: 'disconnect-request',
|
|
12
|
+
PING: 'ping',
|
|
12
13
|
// Background → Content script
|
|
13
14
|
DISCOVERY_APPROVED: 'discovery-approved',
|
|
14
15
|
KEY_EXCHANGE_RESPONSE: 'key-exchange-response',
|
|
15
16
|
SECURE_RESPONSE: 'secure-response',
|
|
16
17
|
SESSION_DISCONNECTED: 'session-disconnected',
|
|
18
|
+
PONG: 'pong',
|
|
17
19
|
} as const;
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -6,7 +6,17 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
|
6
6
|
import type { FunctionsOf } from '@aztec/foundation/types';
|
|
7
7
|
|
|
8
8
|
import { type EncryptedPayload, decrypt, encrypt } from '../../crypto.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_HEARTBEAT_DEAD_AFTER_MS,
|
|
11
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
12
|
+
type DisconnectCallback,
|
|
13
|
+
type HeartbeatOptions,
|
|
14
|
+
NOOP_LOGGER,
|
|
15
|
+
type WalletMessage,
|
|
16
|
+
WalletMessageType,
|
|
17
|
+
type WalletResponse,
|
|
18
|
+
type WalletSdkLogger,
|
|
19
|
+
} from '../../types.js';
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* Internal type representing a wallet method call before encryption.
|
|
@@ -55,6 +65,11 @@ export class ExtensionWallet {
|
|
|
55
65
|
private inFlight = new Map<string, PromiseWithResolvers<unknown>>();
|
|
56
66
|
private disconnected = false;
|
|
57
67
|
private disconnectCallbacks: DisconnectCallback[] = [];
|
|
68
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
69
|
+
private lastInboundAt = 0;
|
|
70
|
+
private log: WalletSdkLogger;
|
|
71
|
+
private heartbeatIntervalMs: number;
|
|
72
|
+
private heartbeatDeadAfterMs: number;
|
|
58
73
|
|
|
59
74
|
/**
|
|
60
75
|
* Private constructor - use {@link ExtensionWallet.create} to instantiate.
|
|
@@ -63,6 +78,8 @@ export class ExtensionWallet {
|
|
|
63
78
|
* @param extensionId - The unique identifier of the target wallet extension
|
|
64
79
|
* @param port - The MessagePort for private communication with the wallet
|
|
65
80
|
* @param sharedKey - The derived AES-256-GCM shared key for encryption
|
|
81
|
+
* @param logger - Optional logger; defaults to a no-op logger
|
|
82
|
+
* @param heartbeatOptions - Optional heartbeat tuning (mostly useful for tests)
|
|
66
83
|
*/
|
|
67
84
|
private constructor(
|
|
68
85
|
private chainInfo: ChainInfo,
|
|
@@ -70,7 +87,13 @@ export class ExtensionWallet {
|
|
|
70
87
|
private extensionId: string,
|
|
71
88
|
private port: MessagePort,
|
|
72
89
|
private sharedKey: CryptoKey,
|
|
73
|
-
|
|
90
|
+
logger?: WalletSdkLogger,
|
|
91
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
92
|
+
) {
|
|
93
|
+
this.log = logger ?? NOOP_LOGGER;
|
|
94
|
+
this.heartbeatIntervalMs = heartbeatOptions?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
95
|
+
this.heartbeatDeadAfterMs = heartbeatOptions?.deadAfterMs ?? DEFAULT_HEARTBEAT_DEAD_AFTER_MS;
|
|
96
|
+
}
|
|
74
97
|
|
|
75
98
|
/**
|
|
76
99
|
* Creates a Wallet that communicates with a browser extension
|
|
@@ -81,6 +104,8 @@ export class ExtensionWallet {
|
|
|
81
104
|
* @param sharedKey - The derived AES-256-GCM shared key for encryption
|
|
82
105
|
* @param chainInfo - The chain information (chainId and version) for request context
|
|
83
106
|
* @param appId - Application identifier used to identify the requesting dApp to the wallet
|
|
107
|
+
* @param logger - Optional logger; defaults to a no-op logger to keep extension/page bundles small
|
|
108
|
+
* @param heartbeatOptions - Optional override for heartbeat tuning (mostly useful for tests)
|
|
84
109
|
* @returns A Wallet interface where all method calls are encrypted
|
|
85
110
|
*
|
|
86
111
|
* @example
|
|
@@ -104,13 +129,17 @@ export class ExtensionWallet {
|
|
|
104
129
|
sharedKey: CryptoKey,
|
|
105
130
|
chainInfo: ChainInfo,
|
|
106
131
|
appId: string,
|
|
132
|
+
logger?: WalletSdkLogger,
|
|
133
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
107
134
|
): ExtensionWallet {
|
|
108
|
-
const wallet = new ExtensionWallet(chainInfo, appId, extensionId, port, sharedKey);
|
|
135
|
+
const wallet = new ExtensionWallet(chainInfo, appId, extensionId, port, sharedKey, logger, heartbeatOptions);
|
|
109
136
|
|
|
110
137
|
// Set up message handler for encrypted responses and unencrypted control messages
|
|
111
138
|
wallet.port.onmessage = (event: MessageEvent) => {
|
|
112
139
|
const data = event.data;
|
|
113
|
-
//
|
|
140
|
+
// Any inbound traffic counts as proof of liveness.
|
|
141
|
+
wallet.lastInboundAt = Date.now();
|
|
142
|
+
|
|
114
143
|
if (data && typeof data === 'object' && 'type' in data && data.type === WalletMessageType.DISCONNECT) {
|
|
115
144
|
wallet.handleDisconnect();
|
|
116
145
|
return;
|
|
@@ -184,8 +213,10 @@ export class ExtensionWallet {
|
|
|
184
213
|
resolve(result);
|
|
185
214
|
}
|
|
186
215
|
this.inFlight.delete(messageId);
|
|
187
|
-
|
|
188
|
-
} catch {
|
|
216
|
+
this.maybeStopHeartbeat();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
this.log.warn('Failed to decrypt wallet response', { err });
|
|
219
|
+
}
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
/**
|
|
@@ -223,9 +254,59 @@ export class ExtensionWallet {
|
|
|
223
254
|
|
|
224
255
|
const { promise, resolve, reject } = promiseWithResolvers<unknown>();
|
|
225
256
|
this.inFlight.set(messageId, { promise, resolve, reject });
|
|
257
|
+
this.startHeartbeat();
|
|
226
258
|
return promise;
|
|
227
259
|
}
|
|
228
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Start the heartbeat probe loop while at least one request is in flight.
|
|
263
|
+
* Idempotent — calling while already running is a no-op.
|
|
264
|
+
*
|
|
265
|
+
* Heartbeat is opt-in via wire protocol: PINGs are unencrypted control messages
|
|
266
|
+
* (like DISCONNECT). Older wallets that do not understand PING simply drop it,
|
|
267
|
+
* which is safe — we only declare disconnect when **no** inbound traffic of any
|
|
268
|
+
* kind (PONG, encrypted response, DISCONNECT) arrives within the dead window.
|
|
269
|
+
* A wallet that is processing a slow request will reset the timer when it
|
|
270
|
+
* eventually responds, so this never causes false disconnects on legacy peers.
|
|
271
|
+
*/
|
|
272
|
+
private startHeartbeat(): void {
|
|
273
|
+
if (this.heartbeatTimer !== null || this.disconnected) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.lastInboundAt = Date.now();
|
|
277
|
+
this.heartbeatTimer = setInterval(() => this.heartbeatTick(), this.heartbeatIntervalMs);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private maybeStopHeartbeat(): void {
|
|
281
|
+
if (this.inFlight.size === 0 && this.heartbeatTimer !== null) {
|
|
282
|
+
clearInterval(this.heartbeatTimer);
|
|
283
|
+
this.heartbeatTimer = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private heartbeatTick(): void {
|
|
288
|
+
if (this.disconnected || this.inFlight.size === 0) {
|
|
289
|
+
this.maybeStopHeartbeat();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const idleMs = Date.now() - this.lastInboundAt;
|
|
294
|
+
if (idleMs >= this.heartbeatDeadAfterMs) {
|
|
295
|
+
this.log.warn('Wallet channel unresponsive — declaring disconnect', {
|
|
296
|
+
idleMs,
|
|
297
|
+
inFlight: this.inFlight.size,
|
|
298
|
+
});
|
|
299
|
+
this.handleDisconnect();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
this.port.postMessage({ type: WalletMessageType.PING });
|
|
305
|
+
} catch (err) {
|
|
306
|
+
this.log.warn('Failed to send heartbeat PING', { err });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
229
310
|
/**
|
|
230
311
|
* Handles wallet disconnection.
|
|
231
312
|
* Rejects all pending requests and notifies registered callbacks.
|
|
@@ -237,6 +318,11 @@ export class ExtensionWallet {
|
|
|
237
318
|
}
|
|
238
319
|
this.disconnected = true;
|
|
239
320
|
|
|
321
|
+
if (this.heartbeatTimer !== null) {
|
|
322
|
+
clearInterval(this.heartbeatTimer);
|
|
323
|
+
this.heartbeatTimer = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
240
326
|
if (this.port) {
|
|
241
327
|
this.port.onmessage = null;
|
|
242
328
|
this.port.close();
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* so the dApp knows it can send a discovery request.
|
|
15
15
|
*/
|
|
16
16
|
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
17
|
-
import { createLogger } from '@aztec/aztec.js/log';
|
|
18
17
|
import type { Wallet } from '@aztec/aztec.js/wallet';
|
|
19
18
|
import { WalletSchema } from '@aztec/aztec.js/wallet';
|
|
20
19
|
import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
@@ -29,7 +28,7 @@ import {
|
|
|
29
28
|
generateKeyPair,
|
|
30
29
|
importPublicKey,
|
|
31
30
|
} from '../../crypto.js';
|
|
32
|
-
import { type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js';
|
|
31
|
+
import { type WalletMessage, WalletMessageType, type WalletResponse, type WalletSdkLogger } from '../../types.js';
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
34
|
* A pending discovery request from a dApp (before user approval).
|
|
@@ -75,6 +74,8 @@ export interface IframeConnectionConfig {
|
|
|
75
74
|
walletIcon?: string;
|
|
76
75
|
/** Origins allowed to connect. If empty or undefined, all origins are allowed (dev mode). */
|
|
77
76
|
allowedOrigins?: string[];
|
|
77
|
+
/** Logger used for diagnostics. */
|
|
78
|
+
logger: WalletSdkLogger;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -105,7 +106,7 @@ export interface IframeConnectionCallbacks {
|
|
|
105
106
|
* @example
|
|
106
107
|
* ```typescript
|
|
107
108
|
* const handler = new IframeConnectionHandler(
|
|
108
|
-
* { walletId: 'my-wallet', walletName: 'My Wallet', walletVersion: '1.0.0' },
|
|
109
|
+
* { walletId: 'my-wallet', walletName: 'My Wallet', walletVersion: '1.0.0', logger: console },
|
|
109
110
|
* {
|
|
110
111
|
* onPendingDiscovery: (session) => showApprovalUI(session),
|
|
111
112
|
* getWallet: (appId, chainInfo) => createWalletForApp(appId, chainInfo),
|
|
@@ -117,12 +118,14 @@ export interface IframeConnectionCallbacks {
|
|
|
117
118
|
export class IframeConnectionHandler {
|
|
118
119
|
private pendingSessions = new Map<string, PendingSession>();
|
|
119
120
|
private activeSessions = new Map<string, ActiveSession>();
|
|
120
|
-
private log
|
|
121
|
+
private log: WalletSdkLogger;
|
|
121
122
|
|
|
122
123
|
constructor(
|
|
123
124
|
private config: IframeConnectionConfig,
|
|
124
125
|
private callbacks: IframeConnectionCallbacks,
|
|
125
|
-
) {
|
|
126
|
+
) {
|
|
127
|
+
this.log = config.logger;
|
|
128
|
+
}
|
|
126
129
|
|
|
127
130
|
start(): void {
|
|
128
131
|
window.addEventListener('message', this.handleMessage);
|
|
@@ -203,7 +206,18 @@ export class IframeConnectionHandler {
|
|
|
203
206
|
case WalletMessageType.DISCONNECT:
|
|
204
207
|
this.terminateSession(msg.sessionId);
|
|
205
208
|
break;
|
|
209
|
+
case WalletMessageType.PING:
|
|
210
|
+
this.handlePing(msg.sessionId);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private handlePing(sessionId: string): void {
|
|
216
|
+
const session = this.activeSessions.get(sessionId);
|
|
217
|
+
if (!session) {
|
|
218
|
+
return;
|
|
206
219
|
}
|
|
220
|
+
this.postToOrigin(session.origin, { type: WalletMessageType.PONG, sessionId });
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
private handleDiscoveryRequest(msg: Record<string, unknown>, origin: string): void {
|
|
@@ -14,7 +14,17 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
|
14
14
|
import type { FunctionsOf } from '@aztec/foundation/types';
|
|
15
15
|
|
|
16
16
|
import { type EncryptedPayload, decrypt, encrypt } from '../../crypto.js';
|
|
17
|
-
import {
|
|
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';
|
|
18
28
|
|
|
19
29
|
/**
|
|
20
30
|
* Internal type representing a wallet method call before encryption.
|
|
@@ -46,6 +56,11 @@ export class IframeWallet {
|
|
|
46
56
|
private disconnected = false;
|
|
47
57
|
private disconnectCallbacks: DisconnectCallback[] = [];
|
|
48
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;
|
|
49
64
|
|
|
50
65
|
private constructor(
|
|
51
66
|
private chainInfo: ChainInfo,
|
|
@@ -55,7 +70,13 @@ export class IframeWallet {
|
|
|
55
70
|
private iframeWindow: Window,
|
|
56
71
|
private walletOrigin: string,
|
|
57
72
|
private sharedKey: CryptoKey,
|
|
58
|
-
|
|
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
|
+
}
|
|
59
80
|
|
|
60
81
|
/**
|
|
61
82
|
* Creates a proxied IframeWallet that implements the {@link Wallet} interface.
|
|
@@ -71,6 +92,8 @@ export class IframeWallet {
|
|
|
71
92
|
* @param sharedKey - AES-256-GCM key derived from ECDH key exchange
|
|
72
93
|
* @param chainInfo - Network information (chainId and version)
|
|
73
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)
|
|
74
97
|
* @returns A proxied IframeWallet — call `.asWallet()` to get the typed `Wallet`
|
|
75
98
|
*/
|
|
76
99
|
static create(
|
|
@@ -81,8 +104,20 @@ export class IframeWallet {
|
|
|
81
104
|
sharedKey: CryptoKey,
|
|
82
105
|
chainInfo: ChainInfo,
|
|
83
106
|
appId: string,
|
|
107
|
+
logger?: WalletSdkLogger,
|
|
108
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
84
109
|
): IframeWallet {
|
|
85
|
-
const wallet = new 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
|
+
);
|
|
86
121
|
|
|
87
122
|
wallet.messageListener = (event: MessageEvent) => {
|
|
88
123
|
if (event.origin !== walletOrigin) {
|
|
@@ -93,9 +128,16 @@ export class IframeWallet {
|
|
|
93
128
|
return;
|
|
94
129
|
}
|
|
95
130
|
|
|
96
|
-
if (msg.
|
|
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) {
|
|
97
139
|
void wallet.handleEncryptedResponse(msg.encrypted as EncryptedPayload);
|
|
98
|
-
} else if (msg.type === WalletMessageType.SESSION_DISCONNECTED
|
|
140
|
+
} else if (msg.type === WalletMessageType.SESSION_DISCONNECTED) {
|
|
99
141
|
wallet.handleDisconnect();
|
|
100
142
|
}
|
|
101
143
|
};
|
|
@@ -148,8 +190,9 @@ export class IframeWallet {
|
|
|
148
190
|
pending.resolve(result);
|
|
149
191
|
}
|
|
150
192
|
this.inFlight.delete(messageId);
|
|
151
|
-
|
|
152
|
-
|
|
193
|
+
this.maybeStopHeartbeat();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
this.log.warn('Failed to decrypt wallet response', { err });
|
|
153
196
|
}
|
|
154
197
|
}
|
|
155
198
|
|
|
@@ -176,15 +219,66 @@ export class IframeWallet {
|
|
|
176
219
|
|
|
177
220
|
const { promise, resolve, reject } = promiseWithResolvers<unknown>();
|
|
178
221
|
this.inFlight.set(messageId, { promise, resolve, reject });
|
|
222
|
+
this.startHeartbeat();
|
|
179
223
|
return promise;
|
|
180
224
|
}
|
|
181
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
|
+
|
|
182
271
|
private handleDisconnect(): void {
|
|
183
272
|
if (this.disconnected) {
|
|
184
273
|
return;
|
|
185
274
|
}
|
|
186
275
|
this.disconnected = true;
|
|
187
276
|
|
|
277
|
+
if (this.heartbeatTimer !== null) {
|
|
278
|
+
clearInterval(this.heartbeatTimer);
|
|
279
|
+
this.heartbeatTimer = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
188
282
|
if (this.messageListener) {
|
|
189
283
|
window.removeEventListener('message', this.messageListener);
|
|
190
284
|
this.messageListener = null;
|