@aztec/wallet-sdk 0.0.1-commit.0c875d939 → 0.0.1-commit.0ec55a70b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dest/base-wallet/base_wallet.d.ts +66 -41
  2. package/dest/base-wallet/base_wallet.d.ts.map +1 -1
  3. package/dest/base-wallet/base_wallet.js +197 -82
  4. package/dest/base-wallet/index.d.ts +2 -2
  5. package/dest/base-wallet/index.d.ts.map +1 -1
  6. package/dest/base-wallet/utils.d.ts +5 -3
  7. package/dest/base-wallet/utils.d.ts.map +1 -1
  8. package/dest/base-wallet/utils.js +9 -4
  9. package/dest/crypto.d.ts +39 -1
  10. package/dest/crypto.d.ts.map +1 -1
  11. package/dest/crypto.js +88 -0
  12. package/dest/extension/provider/extension_wallet.d.ts +4 -6
  13. package/dest/extension/provider/extension_wallet.d.ts.map +1 -1
  14. package/dest/extension/provider/extension_wallet.js +9 -2
  15. package/dest/extension/provider/index.d.ts +2 -2
  16. package/dest/extension/provider/index.d.ts.map +1 -1
  17. package/dest/iframe/handlers/iframe_connection_handler.d.ts +118 -0
  18. package/dest/iframe/handlers/iframe_connection_handler.d.ts.map +1 -0
  19. package/dest/iframe/handlers/iframe_connection_handler.js +228 -0
  20. package/dest/iframe/handlers/index.d.ts +2 -0
  21. package/dest/iframe/handlers/index.d.ts.map +1 -0
  22. package/dest/iframe/handlers/index.js +1 -0
  23. package/dest/iframe/provider/iframe_discovery.d.ts +25 -0
  24. package/dest/iframe/provider/iframe_discovery.d.ts.map +1 -0
  25. package/dest/iframe/provider/iframe_discovery.js +167 -0
  26. package/dest/iframe/provider/iframe_provider.d.ts +65 -0
  27. package/dest/iframe/provider/iframe_provider.d.ts.map +1 -0
  28. package/dest/iframe/provider/iframe_provider.js +257 -0
  29. package/dest/iframe/provider/iframe_wallet.d.ts +68 -0
  30. package/dest/iframe/provider/iframe_wallet.d.ts.map +1 -0
  31. package/dest/iframe/provider/iframe_wallet.js +200 -0
  32. package/dest/iframe/provider/index.d.ts +4 -0
  33. package/dest/iframe/provider/index.d.ts.map +1 -0
  34. package/dest/iframe/provider/index.js +3 -0
  35. package/dest/manager/types.d.ts +6 -5
  36. package/dest/manager/types.d.ts.map +1 -1
  37. package/dest/manager/wallet_manager.d.ts +1 -1
  38. package/dest/manager/wallet_manager.d.ts.map +1 -1
  39. package/dest/manager/wallet_manager.js +48 -18
  40. package/dest/types.d.ts +14 -2
  41. package/dest/types.d.ts.map +1 -1
  42. package/dest/types.js +4 -0
  43. package/package.json +12 -8
  44. package/src/base-wallet/base_wallet.ts +275 -135
  45. package/src/base-wallet/index.ts +6 -1
  46. package/src/base-wallet/utils.ts +14 -4
  47. package/src/crypto.ts +104 -0
  48. package/src/extension/provider/extension_wallet.ts +13 -10
  49. package/src/extension/provider/index.ts +1 -1
  50. package/src/iframe/handlers/iframe_connection_handler.ts +328 -0
  51. package/src/iframe/handlers/index.ts +7 -0
  52. package/src/iframe/provider/iframe_discovery.ts +185 -0
  53. package/src/iframe/provider/iframe_provider.ts +331 -0
  54. package/src/iframe/provider/iframe_wallet.ts +229 -0
  55. package/src/iframe/provider/index.ts +3 -0
  56. package/src/manager/types.ts +5 -4
  57. package/src/manager/wallet_manager.ts +55 -23
  58. package/src/types.ts +13 -0
@@ -0,0 +1,229 @@
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 { 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 { type DisconnectCallback, type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js';
18
+
19
+ /**
20
+ * Internal type representing a wallet method call before encryption.
21
+ * @internal
22
+ */
23
+ type WalletMethodCall = {
24
+ /** Wallet method name to invoke. */
25
+ type: keyof FunctionsOf<Wallet>;
26
+ /** Arguments to pass to the wallet method. */
27
+ args: unknown[];
28
+ };
29
+
30
+ /**
31
+ * A wallet implementation that communicates with a web wallet loaded in an iframe
32
+ * using encrypted postMessage.
33
+ *
34
+ * Uses the same Proxy pattern as {@link ExtensionWallet}: intercepts property access,
35
+ * checks if the property is a Wallet method via {@link WalletSchema}, and routes the
36
+ * call through an encrypted postMessage channel.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const wallet = IframeWallet.create(walletId, sessionId, iframeWindow, walletOrigin, sharedKey, chainInfo, appId);
41
+ * const accounts = await wallet.asWallet().getAccounts();
42
+ * ```
43
+ */
44
+ export class IframeWallet {
45
+ private inFlight = new Map<string, PromiseWithResolvers<unknown>>();
46
+ private disconnected = false;
47
+ private disconnectCallbacks: DisconnectCallback[] = [];
48
+ private messageListener: ((e: MessageEvent) => void) | null = null;
49
+
50
+ private constructor(
51
+ private chainInfo: ChainInfo,
52
+ private appId: string,
53
+ private walletId: string,
54
+ private sessionId: string,
55
+ private iframeWindow: Window,
56
+ private walletOrigin: string,
57
+ private sharedKey: CryptoKey,
58
+ ) {}
59
+
60
+ /**
61
+ * Creates a proxied IframeWallet that implements the {@link Wallet} interface.
62
+ *
63
+ * All Wallet method calls are intercepted by a Proxy, encrypted with the shared
64
+ * AES-256-GCM key, sent via postMessage, and the response is decrypted and
65
+ * validated against {@link WalletSchema}.
66
+ *
67
+ * @param walletId - Unique identifier of the remote wallet
68
+ * @param sessionId - Session identifier from the key exchange
69
+ * @param iframeWindow - The iframe's contentWindow to post messages to
70
+ * @param walletOrigin - Origin of the wallet iframe (for postMessage targeting)
71
+ * @param sharedKey - AES-256-GCM key derived from ECDH key exchange
72
+ * @param chainInfo - Network information (chainId and version)
73
+ * @param appId - Application identifier for the requesting dApp
74
+ * @returns A proxied IframeWallet — call `.asWallet()` to get the typed `Wallet`
75
+ */
76
+ static create(
77
+ walletId: string,
78
+ sessionId: string,
79
+ iframeWindow: Window,
80
+ walletOrigin: string,
81
+ sharedKey: CryptoKey,
82
+ chainInfo: ChainInfo,
83
+ appId: string,
84
+ ): IframeWallet {
85
+ const wallet = new IframeWallet(chainInfo, appId, walletId, sessionId, iframeWindow, walletOrigin, sharedKey);
86
+
87
+ wallet.messageListener = (event: MessageEvent) => {
88
+ if (event.origin !== walletOrigin) {
89
+ return;
90
+ }
91
+ const msg = event.data;
92
+ if (!msg || typeof msg !== 'object') {
93
+ return;
94
+ }
95
+
96
+ if (msg.type === WalletMessageType.SECURE_RESPONSE && msg.sessionId === sessionId) {
97
+ void wallet.handleEncryptedResponse(msg.encrypted as EncryptedPayload);
98
+ } else if (msg.type === WalletMessageType.SESSION_DISCONNECTED && msg.sessionId === sessionId) {
99
+ wallet.handleDisconnect();
100
+ }
101
+ };
102
+ window.addEventListener('message', wallet.messageListener);
103
+
104
+ return new Proxy(wallet, {
105
+ get: (target, prop, receiver) => {
106
+ if (prop === 'asWallet') {
107
+ return () => receiver as unknown as Wallet;
108
+ } else if (schemaHasMethod(WalletSchema, prop.toString())) {
109
+ return async (...args: unknown[]) => {
110
+ const result = await target.postMessage({
111
+ type: prop.toString() as keyof FunctionsOf<Wallet>,
112
+ args,
113
+ });
114
+ return WalletSchema[prop.toString() as keyof typeof WalletSchema].returnType().parseAsync(result);
115
+ };
116
+ } else {
117
+ return target[prop as keyof IframeWallet];
118
+ }
119
+ },
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Returns this wallet as a typed {@link Wallet} interface.
125
+ * When accessed through the Proxy (via `create()`), returns the proxy itself.
126
+ */
127
+ asWallet(): Wallet {
128
+ return this as unknown as Wallet;
129
+ }
130
+
131
+ private async handleEncryptedResponse(encrypted: EncryptedPayload): Promise<void> {
132
+ try {
133
+ const response = await decrypt<WalletResponse>(this.sharedKey, encrypted);
134
+ const { messageId, result, error, walletId: responseWalletId } = response;
135
+
136
+ if (!messageId || responseWalletId !== this.walletId) {
137
+ return;
138
+ }
139
+
140
+ const pending = this.inFlight.get(messageId);
141
+ if (!pending) {
142
+ return;
143
+ }
144
+
145
+ if (error) {
146
+ pending.reject(new Error(jsonStringify(error)));
147
+ } else {
148
+ pending.resolve(result);
149
+ }
150
+ this.inFlight.delete(messageId);
151
+ } catch {
152
+ // Decryption errors are silently ignored (message not for us or corrupted)
153
+ }
154
+ }
155
+
156
+ private async postMessage(call: WalletMethodCall): Promise<unknown> {
157
+ if (this.disconnected) {
158
+ throw new Error('Wallet has been disconnected');
159
+ }
160
+
161
+ const messageId = globalThis.crypto.randomUUID();
162
+ const message: WalletMessage = {
163
+ type: call.type,
164
+ args: call.args,
165
+ messageId,
166
+ chainInfo: this.chainInfo,
167
+ appId: this.appId,
168
+ walletId: this.walletId,
169
+ };
170
+
171
+ const encrypted = await encrypt(this.sharedKey, jsonStringify(message));
172
+ this.iframeWindow.postMessage(
173
+ { type: WalletMessageType.SECURE_MESSAGE, sessionId: this.sessionId, encrypted },
174
+ this.walletOrigin,
175
+ );
176
+
177
+ const { promise, resolve, reject } = promiseWithResolvers<unknown>();
178
+ this.inFlight.set(messageId, { promise, resolve, reject });
179
+ return promise;
180
+ }
181
+
182
+ private handleDisconnect(): void {
183
+ if (this.disconnected) {
184
+ return;
185
+ }
186
+ this.disconnected = true;
187
+
188
+ if (this.messageListener) {
189
+ window.removeEventListener('message', this.messageListener);
190
+ this.messageListener = null;
191
+ }
192
+
193
+ const error = new Error('Wallet disconnected');
194
+ for (const { reject } of this.inFlight.values()) {
195
+ reject(error);
196
+ }
197
+ this.inFlight.clear();
198
+
199
+ for (const cb of this.disconnectCallbacks) {
200
+ try {
201
+ cb();
202
+ } catch {
203
+ // Ignore errors in disconnect callbacks
204
+ }
205
+ }
206
+ }
207
+
208
+ onDisconnect(callback: DisconnectCallback): () => void {
209
+ this.disconnectCallbacks.push(callback);
210
+ return () => {
211
+ const i = this.disconnectCallbacks.indexOf(callback);
212
+ if (i !== -1) {
213
+ this.disconnectCallbacks.splice(i, 1);
214
+ }
215
+ };
216
+ }
217
+
218
+ isDisconnected(): boolean {
219
+ return this.disconnected;
220
+ }
221
+
222
+ disconnect(): void {
223
+ if (this.disconnected) {
224
+ return;
225
+ }
226
+ this.iframeWindow.postMessage({ type: WalletMessageType.DISCONNECT, sessionId: this.sessionId }, this.walletOrigin);
227
+ this.handleDisconnect();
228
+ }
229
+ }
@@ -0,0 +1,3 @@
1
+ export { IframeWallet } from './iframe_wallet.js';
2
+ export { IframeWalletProvider, type IframeConnectionOptions } from './iframe_provider.js';
3
+ export { discoverWebWallets } from './iframe_discovery.js';
@@ -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?(): Promise<void>;
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?(callback: ProviderDisconnectionCallback): () => void;
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?(): boolean;
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 (!provider) {
111
- return;
133
+ if (provider) {
134
+ emit(provider);
112
135
  }
136
+ },
137
+ }).then(() => sourceComplete('extensions'));
138
+ }
113
139
 
114
- // Call user's callback if provided
115
- options.onWalletDiscovered?.(provider);
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
- // Also queue for async iterator
118
- if (pendingResolve) {
119
- const resolve = pendingResolve;
120
- pendingResolve = null;
121
- resolve({ value: provider, done: false });
122
- } else {
123
- pendingProviders.push(provider);
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
- }).then(markComplete);
127
- } else {
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
- return Promise.resolve(
200
- ExtensionWallet.create(
201
- connection.info.id,
202
- connection.port,
203
- connection.sharedKey,
204
- chainInfo,
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,14 @@ 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',
20
28
  }
21
29
 
22
30
  /**
@@ -130,3 +138,8 @@ export interface KeyExchangeResponse {
130
138
  /** Wallet's ECDH public key for deriving shared secret */
131
139
  publicKey: ExportedPublicKey;
132
140
  }
141
+
142
+ /**
143
+ * Callback invoked when a wallet connection is disconnected.
144
+ */
145
+ export type DisconnectCallback = () => void;