@aztec/wallet-sdk 0.0.1-commit.9593d84 → 0.0.1-commit.96bb3f7

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 (33) hide show
  1. package/README.md +173 -139
  2. package/dest/base-wallet/base_wallet.d.ts +5 -5
  3. package/dest/base-wallet/base_wallet.d.ts.map +1 -1
  4. package/dest/base-wallet/base_wallet.js +18 -9
  5. package/dest/crypto.d.ts +146 -0
  6. package/dest/crypto.d.ts.map +1 -0
  7. package/dest/crypto.js +216 -0
  8. package/dest/manager/index.d.ts +2 -2
  9. package/dest/manager/index.d.ts.map +1 -1
  10. package/dest/manager/wallet_manager.js +1 -1
  11. package/dest/providers/extension/extension_provider.d.ts +2 -2
  12. package/dest/providers/extension/extension_provider.d.ts.map +1 -1
  13. package/dest/providers/extension/extension_wallet.d.ts +79 -7
  14. package/dest/providers/extension/extension_wallet.d.ts.map +1 -1
  15. package/dest/providers/extension/extension_wallet.js +173 -44
  16. package/dest/providers/extension/index.d.ts +3 -2
  17. package/dest/providers/extension/index.d.ts.map +1 -1
  18. package/dest/providers/extension/index.js +1 -0
  19. package/dest/types.d.ts +83 -0
  20. package/dest/types.d.ts.map +1 -0
  21. package/dest/types.js +3 -0
  22. package/package.json +12 -10
  23. package/src/base-wallet/base_wallet.ts +19 -13
  24. package/src/crypto.ts +283 -0
  25. package/src/manager/index.ts +1 -7
  26. package/src/manager/wallet_manager.ts +1 -1
  27. package/src/providers/extension/extension_provider.ts +1 -1
  28. package/src/providers/extension/extension_wallet.ts +206 -55
  29. package/src/providers/extension/index.ts +9 -1
  30. package/src/{providers/types.ts → types.ts} +22 -4
  31. package/dest/providers/types.d.ts +0 -67
  32. package/dest/providers/types.d.ts.map +0 -1
  33. package/dest/providers/types.js +0 -3
package/src/crypto.ts ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Cryptographic utilities for secure wallet communication.
3
+ *
4
+ * This module provides ECDH key exchange and AES-GCM encryption primitives
5
+ * for establishing secure communication channels between dApps and wallet extensions.
6
+ *
7
+ * The crypto flow:
8
+ * 1. Both parties generate ECDH key pairs using {@link generateKeyPair}
9
+ * 2. Public keys are exchanged (exported via {@link exportPublicKey}, imported via {@link importPublicKey})
10
+ * 3. Both parties derive the same shared secret using {@link deriveSharedKey}
11
+ * 4. Messages are encrypted/decrypted using {@link encrypt} and {@link decrypt}
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Party A
16
+ * const keyPairA = await generateKeyPair();
17
+ * const publicKeyA = await exportPublicKey(keyPairA.publicKey);
18
+ *
19
+ * // Party B
20
+ * const keyPairB = await generateKeyPair();
21
+ * const publicKeyB = await exportPublicKey(keyPairB.publicKey);
22
+ *
23
+ * // Exchange public keys, then derive shared secret
24
+ * const importedB = await importPublicKey(publicKeyB);
25
+ * const sharedKeyA = await deriveSharedKey(keyPairA.privateKey, importedB);
26
+ *
27
+ * // Encrypt and decrypt
28
+ * const encrypted = await encrypt(sharedKeyA, { message: 'hello' });
29
+ * const decrypted = await decrypt(sharedKeyB, encrypted);
30
+ * ```
31
+ *
32
+ * @packageDocumentation
33
+ */
34
+ import { jsonStringify } from '@aztec/foundation/json-rpc';
35
+
36
+ /**
37
+ * Exported public key in JWK format for transmission over untrusted channels.
38
+ *
39
+ * Contains only the public components of an ECDH P-256 key, safe to share.
40
+ */
41
+ export interface ExportedPublicKey {
42
+ /** Key type - always "EC" for elliptic curve */
43
+ kty: string;
44
+ /** Curve name - always "P-256" */
45
+ crv: string;
46
+ /** X coordinate (base64url encoded) */
47
+ x: string;
48
+ /** Y coordinate (base64url encoded) */
49
+ y: string;
50
+ }
51
+
52
+ /**
53
+ * Encrypted message payload containing ciphertext and initialization vector.
54
+ *
55
+ * Both fields are base64-encoded for safe transmission as JSON.
56
+ */
57
+ export interface EncryptedPayload {
58
+ /** Initialization vector (base64 encoded, 12 bytes) */
59
+ iv: string;
60
+ /** Ciphertext (base64 encoded) */
61
+ ciphertext: string;
62
+ }
63
+
64
+ /**
65
+ * ECDH key pair for secure communication.
66
+ *
67
+ * The private key should never be exported or transmitted.
68
+ * The public key can be exported via {@link exportPublicKey} for exchange.
69
+ */
70
+ export interface SecureKeyPair {
71
+ /** Public key - safe to share */
72
+ publicKey: CryptoKey;
73
+ /** Private key - keep secret, used for key derivation */
74
+ privateKey: CryptoKey;
75
+ }
76
+
77
+ /**
78
+ * Generates an ECDH P-256 key pair for key exchange.
79
+ *
80
+ * The generated key pair can be used to derive a shared secret with another
81
+ * party's public key using {@link deriveSharedKey}.
82
+ *
83
+ * @returns A new ECDH key pair
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const keyPair = await generateKeyPair();
88
+ * const publicKey = await exportPublicKey(keyPair.publicKey);
89
+ * // Send publicKey to the other party
90
+ * ```
91
+ */
92
+ export async function generateKeyPair(): Promise<SecureKeyPair> {
93
+ const keyPair = await crypto.subtle.generateKey(
94
+ {
95
+ name: 'ECDH',
96
+ namedCurve: 'P-256',
97
+ },
98
+ true, // extractable (needed to export public key)
99
+ ['deriveKey'],
100
+ );
101
+ return {
102
+ publicKey: keyPair.publicKey,
103
+ privateKey: keyPair.privateKey,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Exports a public key to JWK format for transmission.
109
+ *
110
+ * The exported key contains only public components and is safe to transmit
111
+ * over untrusted channels.
112
+ *
113
+ * @param publicKey - The CryptoKey public key to export
114
+ * @returns The public key in JWK format
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const keyPair = await generateKeyPair();
119
+ * const exported = await exportPublicKey(keyPair.publicKey);
120
+ * // exported can be JSON serialized and sent to another party
121
+ * ```
122
+ */
123
+ export async function exportPublicKey(publicKey: CryptoKey): Promise<ExportedPublicKey> {
124
+ const jwk = await crypto.subtle.exportKey('jwk', publicKey);
125
+ return {
126
+ kty: jwk.kty!,
127
+ crv: jwk.crv!,
128
+ x: jwk.x!,
129
+ y: jwk.y!,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Imports a public key from JWK format.
135
+ *
136
+ * Used to import the other party's public key for deriving a shared secret.
137
+ *
138
+ * @param exported - The public key in JWK format
139
+ * @returns A CryptoKey that can be used with {@link deriveSharedKey}
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Receive exported public key from other party
144
+ * const theirPublicKey = await importPublicKey(receivedPublicKey);
145
+ * const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey);
146
+ * ```
147
+ */
148
+ export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey> {
149
+ return crypto.subtle.importKey(
150
+ 'jwk',
151
+ {
152
+ kty: exported.kty,
153
+ crv: exported.crv,
154
+ x: exported.x,
155
+ y: exported.y,
156
+ },
157
+ {
158
+ name: 'ECDH',
159
+ namedCurve: 'P-256',
160
+ },
161
+ false,
162
+ [],
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Derives a shared AES-256-GCM key from ECDH key exchange.
168
+ *
169
+ * Both parties will derive the same shared key when using their own private key
170
+ * and the other party's public key. This is the core of ECDH key agreement.
171
+ *
172
+ * @param privateKey - Your ECDH private key
173
+ * @param publicKey - The other party's ECDH public key
174
+ * @returns An AES-256-GCM key for encryption/decryption
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * // Both parties derive the same key
179
+ * const sharedKeyA = await deriveSharedKey(privateKeyA, publicKeyB);
180
+ * const sharedKeyB = await deriveSharedKey(privateKeyB, publicKeyA);
181
+ * // sharedKeyA and sharedKeyB are equivalent
182
+ * ```
183
+ */
184
+ export function deriveSharedKey(privateKey: CryptoKey, publicKey: CryptoKey): Promise<CryptoKey> {
185
+ return crypto.subtle.deriveKey(
186
+ {
187
+ name: 'ECDH',
188
+ public: publicKey,
189
+ },
190
+ privateKey,
191
+ {
192
+ name: 'AES-GCM',
193
+ length: 256,
194
+ },
195
+ false,
196
+ ['encrypt', 'decrypt'],
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Encrypts data using AES-256-GCM.
202
+ *
203
+ * The data is JSON serialized before encryption. A random 12-byte IV is
204
+ * generated for each encryption operation.
205
+ *
206
+ * AES-GCM provides both confidentiality and authenticity - any tampering
207
+ * with the ciphertext will cause decryption to fail.
208
+ *
209
+ * @param key - The AES-GCM key (from {@link deriveSharedKey})
210
+ * @param data - The data to encrypt (will be JSON serialized)
211
+ * @returns The encrypted payload with IV and ciphertext
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const encrypted = await encrypt(sharedKey, { action: 'transfer', amount: 100 });
216
+ * // encrypted.iv and encrypted.ciphertext are base64 strings
217
+ * ```
218
+ */
219
+ export async function encrypt(key: CryptoKey, data: unknown): Promise<EncryptedPayload> {
220
+ const iv = crypto.getRandomValues(new Uint8Array(12));
221
+ const encoded = new TextEncoder().encode(jsonStringify(data));
222
+
223
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
224
+
225
+ return {
226
+ iv: arrayBufferToBase64(iv),
227
+ ciphertext: arrayBufferToBase64(ciphertext),
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Decrypts data using AES-256-GCM.
233
+ *
234
+ * The decrypted data is JSON parsed before returning.
235
+ *
236
+ * @typeParam T - The expected type of the decrypted data
237
+ * @param key - The AES-GCM key (from {@link deriveSharedKey})
238
+ * @param payload - The encrypted payload from {@link encrypt}
239
+ * @returns The decrypted and parsed data
240
+ *
241
+ * @throws Error if decryption fails (wrong key or tampered ciphertext)
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const decrypted = await decrypt<{ action: string; amount: number }>(sharedKey, encrypted);
246
+ * console.log(decrypted.action); // 'transfer'
247
+ * ```
248
+ */
249
+ export async function decrypt<T = unknown>(key: CryptoKey, payload: EncryptedPayload): Promise<T> {
250
+ const iv = base64ToArrayBuffer(payload.iv);
251
+ const ciphertext = base64ToArrayBuffer(payload.ciphertext);
252
+
253
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
254
+
255
+ const decoded = new TextDecoder().decode(decrypted);
256
+ return JSON.parse(decoded) as T;
257
+ }
258
+
259
+ /**
260
+ * Converts ArrayBuffer to base64 string.
261
+ * @internal
262
+ */
263
+ function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
264
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
265
+ let binary = '';
266
+ for (let i = 0; i < bytes.byteLength; i++) {
267
+ binary += String.fromCharCode(bytes[i]);
268
+ }
269
+ return btoa(binary);
270
+ }
271
+
272
+ /**
273
+ * Converts base64 string to ArrayBuffer.
274
+ * @internal
275
+ */
276
+ function base64ToArrayBuffer(base64: string): ArrayBuffer {
277
+ const binary = atob(base64);
278
+ const bytes = new Uint8Array(binary.length);
279
+ for (let i = 0; i < binary.length; i++) {
280
+ bytes[i] = binary.charCodeAt(i);
281
+ }
282
+ return bytes.buffer;
283
+ }
@@ -9,13 +9,7 @@ export type {
9
9
  } from './types.js';
10
10
 
11
11
  // Re-export types from providers for convenience
12
- export type {
13
- WalletInfo,
14
- WalletMessage,
15
- WalletResponse,
16
- DiscoveryRequest,
17
- DiscoveryResponse,
18
- } from '../providers/types.js';
12
+ export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse } from '../types.js';
19
13
 
20
14
  // Re-export commonly needed utilities for wallet integration
21
15
  export { ChainInfoSchema } from '@aztec/aztec.js/account';
@@ -54,7 +54,7 @@ export class WalletManager {
54
54
  metadata: {
55
55
  version: ext.version,
56
56
  },
57
- connect: (appId: string) => Promise.resolve(ExtensionWallet.create(chainInfo, appId, ext.id)),
57
+ connect: (appId: string) => ExtensionWallet.create(ext, chainInfo, appId),
58
58
  });
59
59
  }
60
60
  }
@@ -2,7 +2,7 @@ import type { ChainInfo } from '@aztec/aztec.js/account';
2
2
  import { jsonStringify } from '@aztec/foundation/json-rpc';
3
3
  import { promiseWithResolvers } from '@aztec/foundation/promise';
4
4
 
5
- import type { DiscoveryRequest, DiscoveryResponse, WalletInfo } from '../types.js';
5
+ import type { DiscoveryRequest, DiscoveryResponse, WalletInfo } from '../../types.js';
6
6
 
7
7
  /**
8
8
  * Provider for discovering and managing Aztec wallet extensions
@@ -5,29 +5,74 @@ import { type PromiseWithResolvers, promiseWithResolvers } from '@aztec/foundati
5
5
  import { schemaHasMethod } from '@aztec/foundation/schemas';
6
6
  import type { FunctionsOf } from '@aztec/foundation/types';
7
7
 
8
- import type { WalletMessage, WalletResponse } from '../types.js';
8
+ import {
9
+ type EncryptedPayload,
10
+ type ExportedPublicKey,
11
+ decrypt,
12
+ deriveSharedKey,
13
+ encrypt,
14
+ exportPublicKey,
15
+ generateKeyPair,
16
+ importPublicKey,
17
+ } from '../../crypto.js';
18
+ import type { ConnectRequest, WalletInfo, WalletMessage, WalletResponse } from '../../types.js';
9
19
 
10
20
  /**
11
- * Message payload for posting to extension
21
+ * Internal type representing a wallet method call before encryption.
22
+ * @internal
12
23
  */
13
24
  type WalletMethodCall = {
14
- /**
15
- * The wallet method name to invoke
16
- */
25
+ /** The wallet method name to invoke */
17
26
  type: keyof FunctionsOf<Wallet>;
18
- /**
19
- * Arguments to pass to the wallet method
20
- */
27
+ /** Arguments to pass to the wallet method */
21
28
  args: unknown[];
22
29
  };
23
30
 
24
31
  /**
25
32
  * A wallet implementation that communicates with browser extension wallets
26
- * Supports multiple extensions by targeting specific extension IDs
33
+ * using a secure encrypted MessageChannel.
34
+ *
35
+ * This class establishes a private communication channel with a wallet extension
36
+ * using the following security mechanisms:
37
+ *
38
+ * 1. **MessageChannel**: Creates a private communication channel that is not
39
+ * visible to other scripts on the page (unlike window.postMessage).
40
+ *
41
+ * 2. **ECDH Key Exchange**: Uses Elliptic Curve Diffie-Hellman to derive a
42
+ * shared secret between the dApp and wallet without transmitting private keys.
43
+ *
44
+ * 3. **AES-GCM Encryption**: All messages after channel establishment are
45
+ * encrypted using AES-256-GCM, providing both confidentiality and authenticity.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // Discovery returns wallet info including the wallet's public key
50
+ * const wallets = await ExtensionProvider.discoverExtensions(chainInfo);
51
+ * const walletInfo = wallets[0];
52
+ *
53
+ * // Create a secure connection to the wallet
54
+ * const wallet = await ExtensionWallet.create(walletInfo, chainInfo, 'my-dapp');
55
+ *
56
+ * // All subsequent calls are encrypted
57
+ * const accounts = await wallet.getAccounts();
58
+ * ```
27
59
  */
28
60
  export class ExtensionWallet {
61
+ /** Map of pending requests awaiting responses, keyed by message ID */
29
62
  private inFlight = new Map<string, PromiseWithResolvers<unknown>>();
30
63
 
64
+ /** The MessagePort for private communication with the extension */
65
+ private port: MessagePort | null = null;
66
+
67
+ /** The derived AES-GCM key for encrypting/decrypting messages */
68
+ private sharedKey: CryptoKey | null = null;
69
+
70
+ /**
71
+ * Private constructor - use {@link ExtensionWallet.create} to instantiate.
72
+ * @param chainInfo - The chain information (chainId and version)
73
+ * @param appId - Application identifier for the requesting dApp
74
+ * @param extensionId - The unique identifier of the target wallet extension
75
+ */
31
76
  private constructor(
32
77
  private chainInfo: ChainInfo,
33
78
  private appId: string,
@@ -36,75 +81,157 @@ export class ExtensionWallet {
36
81
 
37
82
  /**
38
83
  * Creates an ExtensionWallet instance that proxies wallet calls to a browser extension
39
- * @param chainInfo - The chain information (chainId and version)
40
- * @param appId - Application identifier for the requesting dapp
41
- * @param extensionId - Specific extension ID to communicate with
42
- * @returns A Proxy object that implements the Wallet interface
84
+ * over a secure encrypted MessageChannel.
85
+ *
86
+ * The connection process:
87
+ * 1. Generates an ECDH key pair for this session
88
+ * 2. Derives a shared AES-256 key using the wallet's public key
89
+ * 3. Creates a MessageChannel and transfers one port to the extension
90
+ * 4. Returns a Proxy that encrypts all wallet method calls
91
+ *
92
+ * @param walletInfo - The discovered wallet information, including the wallet's ECDH public key
93
+ * @param chainInfo - The chain information (chainId and version) for request context
94
+ * @param appId - Application identifier used to identify the requesting dApp to the wallet
95
+ * @returns A Promise resolving to a Wallet implementation that encrypts all communication
96
+ *
97
+ * @throws Error if the secure channel cannot be established
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const wallet = await ExtensionWallet.create(
102
+ * walletInfo,
103
+ * { chainId: Fr(31337), version: Fr(0) },
104
+ * 'my-defi-app'
105
+ * );
106
+ * ```
43
107
  */
44
- static create(chainInfo: ChainInfo, appId: string, extensionId: string): Wallet {
45
- const wallet = new ExtensionWallet(chainInfo, appId, extensionId);
108
+ static async create(walletInfo: WalletInfo, chainInfo: ChainInfo, appId: string): Promise<Wallet> {
109
+ const wallet = new ExtensionWallet(chainInfo, appId, walletInfo.id);
46
110
 
47
- // Set up message listener for responses from extensions
48
- window.addEventListener('message', event => {
49
- if (event.source !== window) {
50
- return;
51
- }
111
+ if (!walletInfo.publicKey) {
112
+ throw new Error('Wallet does not support secure channel establishment (missing public key)');
113
+ }
52
114
 
53
- let data: WalletResponse;
54
- try {
55
- data = JSON.parse(event.data);
56
- } catch {
57
- return;
58
- }
115
+ await wallet.establishSecureChannel(walletInfo.publicKey);
59
116
 
60
- // Ignore request messages (only process responses)
61
- if ('type' in data) {
62
- return;
63
- }
117
+ // Create a Proxy that intercepts wallet method calls and forwards them to the extension
118
+ return new Proxy(wallet, {
119
+ get: (target, prop) => {
120
+ if (schemaHasMethod(WalletSchema, prop.toString())) {
121
+ return async (...args: unknown[]) => {
122
+ const result = await target.postMessage({
123
+ type: prop.toString() as keyof FunctionsOf<Wallet>,
124
+ args,
125
+ });
126
+ return WalletSchema[prop.toString() as keyof typeof WalletSchema].returnType().parseAsync(result);
127
+ };
128
+ } else {
129
+ return target[prop as keyof ExtensionWallet];
130
+ }
131
+ },
132
+ }) as unknown as Wallet;
133
+ }
64
134
 
65
- const { messageId, result, error, walletId: responseWalletId } = data;
135
+ /**
136
+ * Establishes a secure MessageChannel with ECDH key exchange.
137
+ *
138
+ * This method performs the cryptographic handshake:
139
+ * 1. Generates a new ECDH P-256 key pair for this session
140
+ * 2. Imports the wallet's public key and derives a shared secret
141
+ * 3. Creates a MessageChannel for private communication
142
+ * 4. Sends a connection request with our public key via window.postMessage
143
+ * (this is the only public message - subsequent communication uses the private channel)
144
+ *
145
+ * @param walletExportedPublicKey - The wallet's ECDH public key in JWK format
146
+ */
147
+ private async establishSecureChannel(walletExportedPublicKey: ExportedPublicKey): Promise<void> {
148
+ const keyPair = await generateKeyPair();
149
+ const exportedPublicKey = await exportPublicKey(keyPair.publicKey);
150
+
151
+ const walletPublicKey = await importPublicKey(walletExportedPublicKey);
152
+ this.sharedKey = await deriveSharedKey(keyPair.privateKey, walletPublicKey);
153
+
154
+ const channel = new MessageChannel();
155
+ this.port = channel.port1;
156
+
157
+ this.port.onmessage = async (event: MessageEvent<EncryptedPayload>) => {
158
+ await this.handleEncryptedResponse(event.data);
159
+ };
160
+
161
+ this.port.start();
162
+
163
+ // Send connection request with our public key and transfer port2 to content script
164
+ // This is the only public postMessage - it contains our public key (safe to expose)
165
+ // and transfers the MessagePort for subsequent private communication
166
+ const connectRequest: ConnectRequest = {
167
+ type: 'aztec-wallet-connect',
168
+ walletId: this.extensionId,
169
+ appId: this.appId,
170
+ publicKey: exportedPublicKey,
171
+ };
172
+
173
+ window.postMessage(jsonStringify(connectRequest), '*', [channel.port2]);
174
+ }
175
+
176
+ /**
177
+ * Handles an encrypted response received from the wallet extension.
178
+ *
179
+ * Decrypts the response using the shared AES key and resolves or rejects
180
+ * the corresponding pending promise based on the response content.
181
+ *
182
+ * @param encrypted - The encrypted response from the wallet
183
+ */
184
+ private async handleEncryptedResponse(encrypted: EncryptedPayload): Promise<void> {
185
+ if (!this.sharedKey) {
186
+ return;
187
+ }
188
+
189
+ try {
190
+ const response = await decrypt<WalletResponse>(this.sharedKey, encrypted);
191
+
192
+ const { messageId, result, error, walletId: responseWalletId } = response;
66
193
 
67
194
  if (!messageId || !responseWalletId) {
68
195
  return;
69
196
  }
70
197
 
71
- if (wallet.extensionId !== responseWalletId) {
198
+ if (this.extensionId !== responseWalletId) {
72
199
  return;
73
200
  }
74
201
 
75
- if (!wallet.inFlight.has(messageId)) {
202
+ if (!this.inFlight.has(messageId)) {
76
203
  return;
77
204
  }
78
205
 
79
- const { resolve, reject } = wallet.inFlight.get(messageId)!;
206
+ const { resolve, reject } = this.inFlight.get(messageId)!;
80
207
 
81
208
  if (error) {
82
209
  reject(new Error(jsonStringify(error)));
83
210
  } else {
84
211
  resolve(result);
85
212
  }
86
- wallet.inFlight.delete(messageId);
87
- });
88
-
89
- // Create a Proxy that intercepts wallet method calls and forwards them to the extension
90
- return new Proxy(wallet, {
91
- get: (target, prop) => {
92
- if (schemaHasMethod(WalletSchema, prop.toString())) {
93
- return async (...args: unknown[]) => {
94
- const result = await target.postMessage({
95
- type: prop.toString() as keyof FunctionsOf<Wallet>,
96
- args,
97
- });
98
- return WalletSchema[prop.toString() as keyof typeof WalletSchema].returnType().parseAsync(result);
99
- };
100
- } else {
101
- return target[prop as keyof ExtensionWallet];
102
- }
103
- },
104
- }) as unknown as Wallet;
213
+ this.inFlight.delete(messageId);
214
+ // eslint-disable-next-line no-empty
215
+ } catch {}
105
216
  }
106
217
 
107
- private postMessage(call: WalletMethodCall): Promise<unknown> {
218
+ /**
219
+ * Sends an encrypted wallet method call over the secure MessageChannel.
220
+ *
221
+ * The message is encrypted using AES-256-GCM with the shared key derived
222
+ * during channel establishment. A unique message ID is generated to correlate
223
+ * the response.
224
+ *
225
+ * @param call - The wallet method call containing method name and arguments
226
+ * @returns A Promise that resolves with the decrypted result from the wallet
227
+ *
228
+ * @throws Error if the secure channel has not been established
229
+ */
230
+ private async postMessage(call: WalletMethodCall): Promise<unknown> {
231
+ if (!this.port || !this.sharedKey) {
232
+ throw new Error('Secure channel not established');
233
+ }
234
+
108
235
  const messageId = globalThis.crypto.randomUUID();
109
236
  const message: WalletMessage = {
110
237
  type: call.type,
@@ -115,10 +242,34 @@ export class ExtensionWallet {
115
242
  walletId: this.extensionId,
116
243
  };
117
244
 
118
- window.postMessage(jsonStringify(message), '*');
245
+ // Encrypt the message and send over the private MessageChannel
246
+ const encrypted = await encrypt(this.sharedKey, message);
247
+ this.port.postMessage(encrypted);
119
248
 
120
249
  const { promise, resolve, reject } = promiseWithResolvers<unknown>();
121
250
  this.inFlight.set(messageId, { promise, resolve, reject });
122
251
  return promise;
123
252
  }
253
+
254
+ /**
255
+ * Closes the secure channel and cleans up resources.
256
+ *
257
+ * After calling this method, the wallet instance can no longer be used.
258
+ * Any pending requests will not receive responses.
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * const wallet = await ExtensionWallet.create(walletInfo, chainInfo, 'my-app');
263
+ * // ... use wallet ...
264
+ * wallet.close(); // Clean up when done
265
+ * ```
266
+ */
267
+ close(): void {
268
+ if (this.port) {
269
+ this.port.close();
270
+ this.port = null;
271
+ }
272
+ this.sharedKey = null;
273
+ this.inFlight.clear();
274
+ }
124
275
  }
@@ -1,3 +1,11 @@
1
1
  export { ExtensionWallet } from './extension_wallet.js';
2
2
  export { ExtensionProvider } from './extension_provider.js';
3
- export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse } from '../types.js';
3
+ export * from '../../crypto.js';
4
+ export type {
5
+ WalletInfo,
6
+ WalletMessage,
7
+ WalletResponse,
8
+ DiscoveryRequest,
9
+ DiscoveryResponse,
10
+ ConnectRequest,
11
+ } from '../../types.js';