@aztec/wallet-sdk 0.0.1-commit.fffb133c → 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 +83 -35
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +231 -70
- 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 +50 -0
- package/dest/base-wallet/utils.d.ts.map +1 -0
- package/dest/base-wallet/utils.js +133 -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 +87 -9
- 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 +240 -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 +318 -124
- package/src/base-wallet/index.ts +7 -1
- package/src/base-wallet/utils.ts +240 -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 +104 -15
- package/src/extension/provider/index.ts +1 -1
- package/src/iframe/handlers/iframe_connection_handler.ts +342 -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
|
@@ -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.
|
|
@@ -19,11 +29,6 @@ type WalletMethodCall = {
|
|
|
19
29
|
args: unknown[];
|
|
20
30
|
};
|
|
21
31
|
|
|
22
|
-
/**
|
|
23
|
-
* Callback type for wallet disconnect events.
|
|
24
|
-
*/
|
|
25
|
-
export type DisconnectCallback = () => void;
|
|
26
|
-
|
|
27
32
|
/**
|
|
28
33
|
* A wallet implementation that communicates with browser extension wallets
|
|
29
34
|
* using an encrypted MessageChannel.
|
|
@@ -60,6 +65,11 @@ export class ExtensionWallet {
|
|
|
60
65
|
private inFlight = new Map<string, PromiseWithResolvers<unknown>>();
|
|
61
66
|
private disconnected = false;
|
|
62
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;
|
|
63
73
|
|
|
64
74
|
/**
|
|
65
75
|
* Private constructor - use {@link ExtensionWallet.create} to instantiate.
|
|
@@ -68,6 +78,8 @@ export class ExtensionWallet {
|
|
|
68
78
|
* @param extensionId - The unique identifier of the target wallet extension
|
|
69
79
|
* @param port - The MessagePort for private communication with the wallet
|
|
70
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)
|
|
71
83
|
*/
|
|
72
84
|
private constructor(
|
|
73
85
|
private chainInfo: ChainInfo,
|
|
@@ -75,7 +87,13 @@ export class ExtensionWallet {
|
|
|
75
87
|
private extensionId: string,
|
|
76
88
|
private port: MessagePort,
|
|
77
89
|
private sharedKey: CryptoKey,
|
|
78
|
-
|
|
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
|
+
}
|
|
79
97
|
|
|
80
98
|
/**
|
|
81
99
|
* Creates a Wallet that communicates with a browser extension
|
|
@@ -86,6 +104,8 @@ export class ExtensionWallet {
|
|
|
86
104
|
* @param sharedKey - The derived AES-256-GCM shared key for encryption
|
|
87
105
|
* @param chainInfo - The chain information (chainId and version) for request context
|
|
88
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)
|
|
89
109
|
* @returns A Wallet interface where all method calls are encrypted
|
|
90
110
|
*
|
|
91
111
|
* @example
|
|
@@ -109,13 +129,17 @@ export class ExtensionWallet {
|
|
|
109
129
|
sharedKey: CryptoKey,
|
|
110
130
|
chainInfo: ChainInfo,
|
|
111
131
|
appId: string,
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
logger?: WalletSdkLogger,
|
|
133
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
134
|
+
): ExtensionWallet {
|
|
135
|
+
const wallet = new ExtensionWallet(chainInfo, appId, extensionId, port, sharedKey, logger, heartbeatOptions);
|
|
114
136
|
|
|
115
137
|
// Set up message handler for encrypted responses and unencrypted control messages
|
|
116
138
|
wallet.port.onmessage = (event: MessageEvent) => {
|
|
117
139
|
const data = event.data;
|
|
118
|
-
//
|
|
140
|
+
// Any inbound traffic counts as proof of liveness.
|
|
141
|
+
wallet.lastInboundAt = Date.now();
|
|
142
|
+
|
|
119
143
|
if (data && typeof data === 'object' && 'type' in data && data.type === WalletMessageType.DISCONNECT) {
|
|
120
144
|
wallet.handleDisconnect();
|
|
121
145
|
return;
|
|
@@ -127,8 +151,10 @@ export class ExtensionWallet {
|
|
|
127
151
|
wallet.port.start();
|
|
128
152
|
|
|
129
153
|
return new Proxy(wallet, {
|
|
130
|
-
get: (target, prop) => {
|
|
131
|
-
if (
|
|
154
|
+
get: (target, prop, receiver) => {
|
|
155
|
+
if (prop === 'asWallet') {
|
|
156
|
+
return () => receiver as unknown as Wallet;
|
|
157
|
+
} else if (schemaHasMethod(WalletSchema, prop.toString())) {
|
|
132
158
|
return async (...args: unknown[]) => {
|
|
133
159
|
const result = await target.postMessage({
|
|
134
160
|
type: prop.toString() as keyof FunctionsOf<Wallet>,
|
|
@@ -140,7 +166,13 @@ export class ExtensionWallet {
|
|
|
140
166
|
return target[prop as keyof ExtensionWallet];
|
|
141
167
|
}
|
|
142
168
|
},
|
|
143
|
-
})
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
asWallet(): Wallet {
|
|
173
|
+
// Overridden by the proxy in create() to return the proxy itself.
|
|
174
|
+
// This body is never reached when accessed through create().
|
|
175
|
+
return this as unknown as Wallet;
|
|
144
176
|
}
|
|
145
177
|
|
|
146
178
|
/**
|
|
@@ -181,8 +213,10 @@ export class ExtensionWallet {
|
|
|
181
213
|
resolve(result);
|
|
182
214
|
}
|
|
183
215
|
this.inFlight.delete(messageId);
|
|
184
|
-
|
|
185
|
-
} catch {
|
|
216
|
+
this.maybeStopHeartbeat();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
this.log.warn('Failed to decrypt wallet response', { err });
|
|
219
|
+
}
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
/**
|
|
@@ -220,9 +254,59 @@ export class ExtensionWallet {
|
|
|
220
254
|
|
|
221
255
|
const { promise, resolve, reject } = promiseWithResolvers<unknown>();
|
|
222
256
|
this.inFlight.set(messageId, { promise, resolve, reject });
|
|
257
|
+
this.startHeartbeat();
|
|
223
258
|
return promise;
|
|
224
259
|
}
|
|
225
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
|
+
|
|
226
310
|
/**
|
|
227
311
|
* Handles wallet disconnection.
|
|
228
312
|
* Rejects all pending requests and notifies registered callbacks.
|
|
@@ -234,6 +318,11 @@ export class ExtensionWallet {
|
|
|
234
318
|
}
|
|
235
319
|
this.disconnected = true;
|
|
236
320
|
|
|
321
|
+
if (this.heartbeatTimer !== null) {
|
|
322
|
+
clearInterval(this.heartbeatTimer);
|
|
323
|
+
this.heartbeatTimer = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
237
326
|
if (this.port) {
|
|
238
327
|
this.port.onmessage = null;
|
|
239
328
|
this.port.close();
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IframeConnectionHandler — wallet-side of the cross-origin iframe protocol.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors {@link BackgroundConnectionHandler} from `@aztec/wallet-sdk/extension/handlers`
|
|
5
|
+
* but uses `window.postMessage` instead of browser.runtime messaging.
|
|
6
|
+
*
|
|
7
|
+
* Message flow (wallet receives):
|
|
8
|
+
* parent → DISCOVERY → show approval UI → send DISCOVERY_RESPONSE
|
|
9
|
+
* parent → KEY_EXCHANGE_REQUEST → ECDH → send KEY_EXCHANGE_RESPONSE
|
|
10
|
+
* parent → SECURE_MESSAGE → decrypt → Wallet → encrypt → SECURE_RESPONSE
|
|
11
|
+
* parent → DISCONNECT → terminate session
|
|
12
|
+
*
|
|
13
|
+
* The wallet announces itself by posting WALLET_READY as soon as the handler starts,
|
|
14
|
+
* so the dApp knows it can send a discovery request.
|
|
15
|
+
*/
|
|
16
|
+
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
17
|
+
import type { Wallet } from '@aztec/aztec.js/wallet';
|
|
18
|
+
import { WalletSchema } from '@aztec/aztec.js/wallet';
|
|
19
|
+
import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
20
|
+
import { parseWithOptionals, schemaHasMethod } from '@aztec/foundation/schemas';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
type EncryptedPayload,
|
|
24
|
+
decrypt,
|
|
25
|
+
deriveSessionKeys,
|
|
26
|
+
encrypt,
|
|
27
|
+
exportPublicKey,
|
|
28
|
+
generateKeyPair,
|
|
29
|
+
importPublicKey,
|
|
30
|
+
} from '../../crypto.js';
|
|
31
|
+
import { type WalletMessage, WalletMessageType, type WalletResponse, type WalletSdkLogger } from '../../types.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A pending discovery request from a dApp (before user approval).
|
|
35
|
+
*/
|
|
36
|
+
export interface PendingSession {
|
|
37
|
+
/** Unique request identifier */
|
|
38
|
+
requestId: string;
|
|
39
|
+
/** Application identifier */
|
|
40
|
+
appId: string;
|
|
41
|
+
/** Origin URL of the requesting page */
|
|
42
|
+
origin: string;
|
|
43
|
+
/** Approval status */
|
|
44
|
+
status: 'pending' | 'approved';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An active session (after key exchange).
|
|
49
|
+
*/
|
|
50
|
+
export interface ActiveSession {
|
|
51
|
+
/** Session identifier (same as the discovery requestId) */
|
|
52
|
+
sessionId: string;
|
|
53
|
+
/** AES-256-GCM shared key for this session */
|
|
54
|
+
sharedKey: CryptoKey;
|
|
55
|
+
/** Verification hash for emoji display */
|
|
56
|
+
verificationHash: string;
|
|
57
|
+
/** Origin URL of the connected dApp */
|
|
58
|
+
origin: string;
|
|
59
|
+
/** Application identifier */
|
|
60
|
+
appId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for the iframe connection handler.
|
|
65
|
+
*/
|
|
66
|
+
export interface IframeConnectionConfig {
|
|
67
|
+
/** Unique wallet identifier */
|
|
68
|
+
walletId: string;
|
|
69
|
+
/** Display name for the wallet */
|
|
70
|
+
walletName: string;
|
|
71
|
+
/** Wallet version string */
|
|
72
|
+
walletVersion: string;
|
|
73
|
+
/** Optional wallet icon URL */
|
|
74
|
+
walletIcon?: string;
|
|
75
|
+
/** Origins allowed to connect. If empty or undefined, all origins are allowed (dev mode). */
|
|
76
|
+
allowedOrigins?: string[];
|
|
77
|
+
/** Logger used for diagnostics. */
|
|
78
|
+
logger: WalletSdkLogger;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Event callbacks for the iframe connection handler.
|
|
83
|
+
*/
|
|
84
|
+
export interface IframeConnectionCallbacks {
|
|
85
|
+
/** Called when a new discovery request arrives — wallet can show approval UI */
|
|
86
|
+
onPendingDiscovery?: (session: PendingSession) => void;
|
|
87
|
+
/** Called when a session is established (key exchange complete) */
|
|
88
|
+
onSessionEstablished?: (session: ActiveSession) => void;
|
|
89
|
+
/** Called when a session is terminated */
|
|
90
|
+
onSessionTerminated?: (sessionId: string) => void;
|
|
91
|
+
/** Called when a key exchange completes — show verificationHash as emojis to the user */
|
|
92
|
+
onVerificationHash?: (verificationHash: string) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the Wallet instance to use for a given dApp and chain.
|
|
95
|
+
* Called when an encrypted message arrives and needs to be dispatched.
|
|
96
|
+
*/
|
|
97
|
+
getWallet: (appId: string, chainInfo: ChainInfo) => Promise<Wallet>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handles the wallet side of the cross-origin iframe protocol.
|
|
102
|
+
*
|
|
103
|
+
* Manages the full lifecycle: discovery, ECDH key exchange, encrypted message
|
|
104
|
+
* dispatch to a {@link Wallet} instance, and session termination.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const handler = new IframeConnectionHandler(
|
|
109
|
+
* { walletId: 'my-wallet', walletName: 'My Wallet', walletVersion: '1.0.0', logger: console },
|
|
110
|
+
* {
|
|
111
|
+
* onPendingDiscovery: (session) => showApprovalUI(session),
|
|
112
|
+
* getWallet: (appId, chainInfo) => createWalletForApp(appId, chainInfo),
|
|
113
|
+
* },
|
|
114
|
+
* );
|
|
115
|
+
* handler.start();
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export class IframeConnectionHandler {
|
|
119
|
+
private pendingSessions = new Map<string, PendingSession>();
|
|
120
|
+
private activeSessions = new Map<string, ActiveSession>();
|
|
121
|
+
private log: WalletSdkLogger;
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
private config: IframeConnectionConfig,
|
|
125
|
+
private callbacks: IframeConnectionCallbacks,
|
|
126
|
+
) {
|
|
127
|
+
this.log = config.logger;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
start(): void {
|
|
131
|
+
window.addEventListener('message', this.handleMessage);
|
|
132
|
+
this.postToParent({ type: WalletMessageType.WALLET_READY });
|
|
133
|
+
this.log.info('IframeConnectionHandler started, posted WALLET_READY');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
stop(): void {
|
|
137
|
+
window.removeEventListener('message', this.handleMessage);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
approveDiscovery(requestId: string): void {
|
|
141
|
+
const pending = this.pendingSessions.get(requestId);
|
|
142
|
+
if (!pending || pending.status !== 'pending') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pending.status = 'approved';
|
|
147
|
+
this.postToOrigin(pending.origin, {
|
|
148
|
+
type: WalletMessageType.DISCOVERY_RESPONSE,
|
|
149
|
+
requestId,
|
|
150
|
+
walletInfo: {
|
|
151
|
+
id: this.config.walletId,
|
|
152
|
+
name: this.config.walletName,
|
|
153
|
+
version: this.config.walletVersion,
|
|
154
|
+
icon: this.config.walletIcon,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
this.log.info(`Discovery approved for requestId=${requestId}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
rejectDiscovery(requestId: string): void {
|
|
161
|
+
this.pendingSessions.delete(requestId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
terminateSession(sessionId: string): void {
|
|
165
|
+
const session = this.activeSessions.get(sessionId);
|
|
166
|
+
if (session) {
|
|
167
|
+
this.postToOrigin(session.origin, {
|
|
168
|
+
type: WalletMessageType.SESSION_DISCONNECTED,
|
|
169
|
+
sessionId,
|
|
170
|
+
});
|
|
171
|
+
this.activeSessions.delete(sessionId);
|
|
172
|
+
this.callbacks.onSessionTerminated?.(sessionId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getPendingSessions(): PendingSession[] {
|
|
177
|
+
return Array.from(this.pendingSessions.values()).filter(s => s.status === 'pending');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private handleMessage = (event: MessageEvent): void => {
|
|
181
|
+
void this.handleMessageAsync(event);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
private async handleMessageAsync(event: MessageEvent): Promise<void> {
|
|
185
|
+
if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {
|
|
186
|
+
if (!this.config.allowedOrigins.includes(event.origin)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const msg = event.data;
|
|
192
|
+
if (!msg || typeof msg !== 'object' || !msg.type) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
switch (msg.type) {
|
|
197
|
+
case WalletMessageType.DISCOVERY:
|
|
198
|
+
this.handleDiscoveryRequest(msg, event.origin);
|
|
199
|
+
break;
|
|
200
|
+
case WalletMessageType.KEY_EXCHANGE_REQUEST:
|
|
201
|
+
await this.handleKeyExchangeRequest(msg, event.origin);
|
|
202
|
+
break;
|
|
203
|
+
case WalletMessageType.SECURE_MESSAGE:
|
|
204
|
+
await this.handleSecureMessage(msg);
|
|
205
|
+
break;
|
|
206
|
+
case WalletMessageType.DISCONNECT:
|
|
207
|
+
this.terminateSession(msg.sessionId);
|
|
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;
|
|
219
|
+
}
|
|
220
|
+
this.postToOrigin(session.origin, { type: WalletMessageType.PONG, sessionId });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private handleDiscoveryRequest(msg: Record<string, unknown>, origin: string): void {
|
|
224
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
225
|
+
const { requestId, appId } = msg as { requestId: string; appId: string };
|
|
226
|
+
const pending: PendingSession = { requestId, appId, origin, status: 'pending' };
|
|
227
|
+
this.pendingSessions.set(requestId, pending);
|
|
228
|
+
this.log.info(`Discovery request from appId=${appId} origin=${origin}`);
|
|
229
|
+
this.callbacks.onPendingDiscovery?.(pending);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async handleKeyExchangeRequest(msg: Record<string, unknown>, origin: string): Promise<void> {
|
|
233
|
+
const { requestId, publicKey: appPublicKeyRaw } = msg as {
|
|
234
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
235
|
+
requestId: string;
|
|
236
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
237
|
+
publicKey: { kty: string; crv: string; x: string; y: string };
|
|
238
|
+
};
|
|
239
|
+
const pending = this.pendingSessions.get(requestId);
|
|
240
|
+
if (!pending || pending.status !== 'approved') {
|
|
241
|
+
this.log.warn(`Key exchange for unknown/unapproved requestId=${requestId}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const keyPair = await generateKeyPair();
|
|
247
|
+
const walletPublicKey = await exportPublicKey(keyPair.publicKey);
|
|
248
|
+
const appPublicKey = await importPublicKey(appPublicKeyRaw);
|
|
249
|
+
const sessionKeys = await deriveSessionKeys(keyPair, appPublicKey, false);
|
|
250
|
+
|
|
251
|
+
const session: ActiveSession = {
|
|
252
|
+
sessionId: requestId,
|
|
253
|
+
sharedKey: sessionKeys.encryptionKey,
|
|
254
|
+
verificationHash: sessionKeys.verificationHash,
|
|
255
|
+
origin: pending.origin,
|
|
256
|
+
appId: pending.appId,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
this.activeSessions.set(requestId, session);
|
|
260
|
+
this.pendingSessions.delete(requestId);
|
|
261
|
+
|
|
262
|
+
this.postToOrigin(origin, {
|
|
263
|
+
type: WalletMessageType.KEY_EXCHANGE_RESPONSE,
|
|
264
|
+
requestId,
|
|
265
|
+
publicKey: walletPublicKey,
|
|
266
|
+
verificationHash: sessionKeys.verificationHash,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.callbacks.onVerificationHash?.(sessionKeys.verificationHash);
|
|
270
|
+
this.callbacks.onSessionEstablished?.(session);
|
|
271
|
+
this.log.info(`Key exchange complete, sessionId=${requestId}`);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.log.error(`Key exchange failed: ${err}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async handleSecureMessage(msg: Record<string, unknown>): Promise<void> {
|
|
278
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
279
|
+
const { sessionId, encrypted } = msg as { sessionId: string; encrypted: EncryptedPayload };
|
|
280
|
+
const session = this.activeSessions.get(sessionId);
|
|
281
|
+
if (!session) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let walletMessage: WalletMessage;
|
|
286
|
+
try {
|
|
287
|
+
walletMessage = await decrypt<WalletMessage>(session.sharedKey, encrypted);
|
|
288
|
+
} catch {
|
|
289
|
+
this.log.warn(`Decryption failed for sessionId=${sessionId}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { messageId, type, args, chainInfo, appId } = walletMessage;
|
|
294
|
+
|
|
295
|
+
let result: unknown;
|
|
296
|
+
let error: string | undefined;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const wallet = await this.callbacks.getWallet(appId, chainInfo);
|
|
300
|
+
|
|
301
|
+
if (!schemaHasMethod(WalletSchema, type)) {
|
|
302
|
+
throw new Error(`Unknown wallet method: ${type}`);
|
|
303
|
+
}
|
|
304
|
+
// Zod's AnyZodTuple rejects optional tuple items typed as `T | undefined`
|
|
305
|
+
const sanitizedArgs = await parseWithOptionals(args, WalletSchema[type].parameters() as any);
|
|
306
|
+
result = await (wallet as Record<string, (...a: unknown[]) => Promise<unknown>>)[type](...sanitizedArgs);
|
|
307
|
+
} catch (err: unknown) {
|
|
308
|
+
error = err instanceof Error ? err.message : String(err);
|
|
309
|
+
this.log.error(`Error handling ${type}: ${error}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const response: WalletResponse = {
|
|
313
|
+
messageId,
|
|
314
|
+
walletId: this.config.walletId,
|
|
315
|
+
result,
|
|
316
|
+
error,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const encryptedResponse = await encrypt(session.sharedKey, jsonStringify(response));
|
|
321
|
+
this.postToOrigin(session.origin, {
|
|
322
|
+
type: WalletMessageType.SECURE_RESPONSE,
|
|
323
|
+
sessionId,
|
|
324
|
+
encrypted: encryptedResponse,
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.log.error(`Encryption of response failed: ${err}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private postToParent(msg: object): void {
|
|
332
|
+
if (window.parent !== window) {
|
|
333
|
+
window.parent.postMessage(msg, '*');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private postToOrigin(origin: string, msg: object): void {
|
|
338
|
+
if (window.parent !== window) {
|
|
339
|
+
window.parent.postMessage(msg, origin);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|