@aztec/wallet-sdk 4.0.0-nightly.20260108 → 4.0.0-nightly.20260110
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/README.md +172 -138
- package/dest/crypto.d.ts +146 -0
- package/dest/crypto.d.ts.map +1 -0
- package/dest/crypto.js +216 -0
- package/dest/manager/index.d.ts +2 -2
- package/dest/manager/index.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +1 -1
- package/dest/providers/extension/extension_provider.d.ts +2 -2
- package/dest/providers/extension/extension_provider.d.ts.map +1 -1
- package/dest/providers/extension/extension_wallet.d.ts +79 -7
- package/dest/providers/extension/extension_wallet.d.ts.map +1 -1
- package/dest/providers/extension/extension_wallet.js +173 -44
- package/dest/providers/extension/index.d.ts +3 -2
- package/dest/providers/extension/index.d.ts.map +1 -1
- package/dest/providers/extension/index.js +1 -0
- package/dest/types.d.ts +83 -0
- package/dest/types.d.ts.map +1 -0
- package/dest/types.js +3 -0
- package/package.json +10 -8
- package/src/crypto.ts +283 -0
- package/src/manager/index.ts +1 -7
- package/src/manager/wallet_manager.ts +1 -1
- package/src/providers/extension/extension_provider.ts +1 -1
- package/src/providers/extension/extension_wallet.ts +206 -55
- package/src/providers/extension/index.ts +9 -1
- package/src/{providers/types.ts → types.ts} +22 -4
- package/dest/providers/types.d.ts +0 -67
- package/dest/providers/types.d.ts.map +0 -1
- package/dest/providers/types.js +0 -3
package/dest/crypto.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
*/ import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
34
|
+
/**
|
|
35
|
+
* Generates an ECDH P-256 key pair for key exchange.
|
|
36
|
+
*
|
|
37
|
+
* The generated key pair can be used to derive a shared secret with another
|
|
38
|
+
* party's public key using {@link deriveSharedKey}.
|
|
39
|
+
*
|
|
40
|
+
* @returns A new ECDH key pair
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const keyPair = await generateKeyPair();
|
|
45
|
+
* const publicKey = await exportPublicKey(keyPair.publicKey);
|
|
46
|
+
* // Send publicKey to the other party
|
|
47
|
+
* ```
|
|
48
|
+
*/ export async function generateKeyPair() {
|
|
49
|
+
const keyPair = await crypto.subtle.generateKey({
|
|
50
|
+
name: 'ECDH',
|
|
51
|
+
namedCurve: 'P-256'
|
|
52
|
+
}, true, [
|
|
53
|
+
'deriveKey'
|
|
54
|
+
]);
|
|
55
|
+
return {
|
|
56
|
+
publicKey: keyPair.publicKey,
|
|
57
|
+
privateKey: keyPair.privateKey
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Exports a public key to JWK format for transmission.
|
|
62
|
+
*
|
|
63
|
+
* The exported key contains only public components and is safe to transmit
|
|
64
|
+
* over untrusted channels.
|
|
65
|
+
*
|
|
66
|
+
* @param publicKey - The CryptoKey public key to export
|
|
67
|
+
* @returns The public key in JWK format
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const keyPair = await generateKeyPair();
|
|
72
|
+
* const exported = await exportPublicKey(keyPair.publicKey);
|
|
73
|
+
* // exported can be JSON serialized and sent to another party
|
|
74
|
+
* ```
|
|
75
|
+
*/ export async function exportPublicKey(publicKey) {
|
|
76
|
+
const jwk = await crypto.subtle.exportKey('jwk', publicKey);
|
|
77
|
+
return {
|
|
78
|
+
kty: jwk.kty,
|
|
79
|
+
crv: jwk.crv,
|
|
80
|
+
x: jwk.x,
|
|
81
|
+
y: jwk.y
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Imports a public key from JWK format.
|
|
86
|
+
*
|
|
87
|
+
* Used to import the other party's public key for deriving a shared secret.
|
|
88
|
+
*
|
|
89
|
+
* @param exported - The public key in JWK format
|
|
90
|
+
* @returns A CryptoKey that can be used with {@link deriveSharedKey}
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // Receive exported public key from other party
|
|
95
|
+
* const theirPublicKey = await importPublicKey(receivedPublicKey);
|
|
96
|
+
* const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey);
|
|
97
|
+
* ```
|
|
98
|
+
*/ export function importPublicKey(exported) {
|
|
99
|
+
return crypto.subtle.importKey('jwk', {
|
|
100
|
+
kty: exported.kty,
|
|
101
|
+
crv: exported.crv,
|
|
102
|
+
x: exported.x,
|
|
103
|
+
y: exported.y
|
|
104
|
+
}, {
|
|
105
|
+
name: 'ECDH',
|
|
106
|
+
namedCurve: 'P-256'
|
|
107
|
+
}, false, []);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Derives a shared AES-256-GCM key from ECDH key exchange.
|
|
111
|
+
*
|
|
112
|
+
* Both parties will derive the same shared key when using their own private key
|
|
113
|
+
* and the other party's public key. This is the core of ECDH key agreement.
|
|
114
|
+
*
|
|
115
|
+
* @param privateKey - Your ECDH private key
|
|
116
|
+
* @param publicKey - The other party's ECDH public key
|
|
117
|
+
* @returns An AES-256-GCM key for encryption/decryption
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // Both parties derive the same key
|
|
122
|
+
* const sharedKeyA = await deriveSharedKey(privateKeyA, publicKeyB);
|
|
123
|
+
* const sharedKeyB = await deriveSharedKey(privateKeyB, publicKeyA);
|
|
124
|
+
* // sharedKeyA and sharedKeyB are equivalent
|
|
125
|
+
* ```
|
|
126
|
+
*/ export function deriveSharedKey(privateKey, publicKey) {
|
|
127
|
+
return crypto.subtle.deriveKey({
|
|
128
|
+
name: 'ECDH',
|
|
129
|
+
public: publicKey
|
|
130
|
+
}, privateKey, {
|
|
131
|
+
name: 'AES-GCM',
|
|
132
|
+
length: 256
|
|
133
|
+
}, false, [
|
|
134
|
+
'encrypt',
|
|
135
|
+
'decrypt'
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Encrypts data using AES-256-GCM.
|
|
140
|
+
*
|
|
141
|
+
* The data is JSON serialized before encryption. A random 12-byte IV is
|
|
142
|
+
* generated for each encryption operation.
|
|
143
|
+
*
|
|
144
|
+
* AES-GCM provides both confidentiality and authenticity - any tampering
|
|
145
|
+
* with the ciphertext will cause decryption to fail.
|
|
146
|
+
*
|
|
147
|
+
* @param key - The AES-GCM key (from {@link deriveSharedKey})
|
|
148
|
+
* @param data - The data to encrypt (will be JSON serialized)
|
|
149
|
+
* @returns The encrypted payload with IV and ciphertext
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const encrypted = await encrypt(sharedKey, { action: 'transfer', amount: 100 });
|
|
154
|
+
* // encrypted.iv and encrypted.ciphertext are base64 strings
|
|
155
|
+
* ```
|
|
156
|
+
*/ export async function encrypt(key, data) {
|
|
157
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
158
|
+
const encoded = new TextEncoder().encode(jsonStringify(data));
|
|
159
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
160
|
+
name: 'AES-GCM',
|
|
161
|
+
iv
|
|
162
|
+
}, key, encoded);
|
|
163
|
+
return {
|
|
164
|
+
iv: arrayBufferToBase64(iv),
|
|
165
|
+
ciphertext: arrayBufferToBase64(ciphertext)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Decrypts data using AES-256-GCM.
|
|
170
|
+
*
|
|
171
|
+
* The decrypted data is JSON parsed before returning.
|
|
172
|
+
*
|
|
173
|
+
* @typeParam T - The expected type of the decrypted data
|
|
174
|
+
* @param key - The AES-GCM key (from {@link deriveSharedKey})
|
|
175
|
+
* @param payload - The encrypted payload from {@link encrypt}
|
|
176
|
+
* @returns The decrypted and parsed data
|
|
177
|
+
*
|
|
178
|
+
* @throws Error if decryption fails (wrong key or tampered ciphertext)
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const decrypted = await decrypt<{ action: string; amount: number }>(sharedKey, encrypted);
|
|
183
|
+
* console.log(decrypted.action); // 'transfer'
|
|
184
|
+
* ```
|
|
185
|
+
*/ export async function decrypt(key, payload) {
|
|
186
|
+
const iv = base64ToArrayBuffer(payload.iv);
|
|
187
|
+
const ciphertext = base64ToArrayBuffer(payload.ciphertext);
|
|
188
|
+
const decrypted = await crypto.subtle.decrypt({
|
|
189
|
+
name: 'AES-GCM',
|
|
190
|
+
iv
|
|
191
|
+
}, key, ciphertext);
|
|
192
|
+
const decoded = new TextDecoder().decode(decrypted);
|
|
193
|
+
return JSON.parse(decoded);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Converts ArrayBuffer to base64 string.
|
|
197
|
+
* @internal
|
|
198
|
+
*/ function arrayBufferToBase64(buffer) {
|
|
199
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
200
|
+
let binary = '';
|
|
201
|
+
for(let i = 0; i < bytes.byteLength; i++){
|
|
202
|
+
binary += String.fromCharCode(bytes[i]);
|
|
203
|
+
}
|
|
204
|
+
return btoa(binary);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Converts base64 string to ArrayBuffer.
|
|
208
|
+
* @internal
|
|
209
|
+
*/ function base64ToArrayBuffer(base64) {
|
|
210
|
+
const binary = atob(base64);
|
|
211
|
+
const bytes = new Uint8Array(binary.length);
|
|
212
|
+
for(let i = 0; i < binary.length; i++){
|
|
213
|
+
bytes[i] = binary.charCodeAt(i);
|
|
214
|
+
}
|
|
215
|
+
return bytes.buffer;
|
|
216
|
+
}
|
package/dest/manager/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { WalletManager } from './wallet_manager.js';
|
|
2
2
|
export type { WalletManagerConfig, ExtensionWalletConfig, WebWalletConfig, WalletProviderType, WalletProvider, DiscoverWalletsOptions, } from './types.js';
|
|
3
|
-
export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse
|
|
3
|
+
export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse } from '../types.js';
|
|
4
4
|
export { ChainInfoSchema } from '@aztec/aztec.js/account';
|
|
5
5
|
export type { ChainInfo } from '@aztec/aztec.js/account';
|
|
6
6
|
export { WalletSchema } from '@aztec/aztec.js/wallet';
|
|
7
7
|
export { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
8
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
8
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9tYW5hZ2VyL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNwRCxZQUFZLEVBQ1YsbUJBQW1CLEVBQ25CLHFCQUFxQixFQUNyQixlQUFlLEVBQ2Ysa0JBQWtCLEVBQ2xCLGNBQWMsRUFDZCxzQkFBc0IsR0FDdkIsTUFBTSxZQUFZLENBQUM7QUFHcEIsWUFBWSxFQUFFLFVBQVUsRUFBRSxhQUFhLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLE1BQU0sYUFBYSxDQUFDO0FBR2xILE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUMxRCxZQUFZLEVBQUUsU0FBUyxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDekQsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQ3RELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQyJ9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGlH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -51,7 +51,7 @@ import { ExtensionProvider, ExtensionWallet } from '../providers/extension/index
|
|
|
51
51
|
metadata: {
|
|
52
52
|
version: ext.version
|
|
53
53
|
},
|
|
54
|
-
connect: (appId)=>
|
|
54
|
+
connect: (appId)=>ExtensionWallet.create(ext, chainInfo, appId)
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
2
|
-
import type { WalletInfo } from '
|
|
2
|
+
import type { WalletInfo } from '../../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Provider for discovering and managing Aztec wallet extensions
|
|
5
5
|
*/
|
|
@@ -14,4 +14,4 @@ export declare class ExtensionProvider {
|
|
|
14
14
|
*/
|
|
15
15
|
static discoverExtensions(chainInfo: ChainInfo, timeout?: number): Promise<WalletInfo[]>;
|
|
16
16
|
}
|
|
17
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
17
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXh0ZW5zaW9uX3Byb3ZpZGVyLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvcHJvdmlkZXJzL2V4dGVuc2lvbi9leHRlbnNpb25fcHJvdmlkZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFJekQsT0FBTyxLQUFLLEVBQXVDLFVBQVUsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRXRGOztHQUVHO0FBQ0gscUJBQWEsaUJBQWlCO0lBQzVCLE9BQU8sQ0FBQyxNQUFNLENBQUMsb0JBQW9CLENBQXNDO0lBQ3pFLE9BQU8sQ0FBQyxNQUFNLENBQUMsbUJBQW1CLENBQVM7SUFFM0M7Ozs7O09BS0c7SUFDSCxPQUFhLGtCQUFrQixDQUFDLFNBQVMsRUFBRSxTQUFTLEVBQUUsT0FBTyxHQUFFLE1BQWEsR0FBRyxPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FtRG5HO0NBQ0YifQ==
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension_provider.d.ts","sourceRoot":"","sources":["../../../src/providers/extension/extension_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAIzD,OAAO,KAAK,EAAuC,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"extension_provider.d.ts","sourceRoot":"","sources":["../../../src/providers/extension/extension_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAIzD,OAAO,KAAK,EAAuC,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEtF;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAsC;IACzE,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAS;IAE3C;;;;;OAKG;IACH,OAAa,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,GAAE,MAAa,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAmDnG;CACF"}
|
|
@@ -1,23 +1,95 @@
|
|
|
1
1
|
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
2
2
|
import { type Wallet } from '@aztec/aztec.js/wallet';
|
|
3
|
+
import type { WalletInfo } from '../../types.js';
|
|
3
4
|
/**
|
|
4
5
|
* A wallet implementation that communicates with browser extension wallets
|
|
5
|
-
*
|
|
6
|
+
* using a secure encrypted MessageChannel.
|
|
7
|
+
*
|
|
8
|
+
* This class establishes a private communication channel with a wallet extension
|
|
9
|
+
* using the following security mechanisms:
|
|
10
|
+
*
|
|
11
|
+
* 1. **MessageChannel**: Creates a private communication channel that is not
|
|
12
|
+
* visible to other scripts on the page (unlike window.postMessage).
|
|
13
|
+
*
|
|
14
|
+
* 2. **ECDH Key Exchange**: Uses Elliptic Curve Diffie-Hellman to derive a
|
|
15
|
+
* shared secret between the dApp and wallet without transmitting private keys.
|
|
16
|
+
*
|
|
17
|
+
* 3. **AES-GCM Encryption**: All messages after channel establishment are
|
|
18
|
+
* encrypted using AES-256-GCM, providing both confidentiality and authenticity.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Discovery returns wallet info including the wallet's public key
|
|
23
|
+
* const wallets = await ExtensionProvider.discoverExtensions(chainInfo);
|
|
24
|
+
* const walletInfo = wallets[0];
|
|
25
|
+
*
|
|
26
|
+
* // Create a secure connection to the wallet
|
|
27
|
+
* const wallet = await ExtensionWallet.create(walletInfo, chainInfo, 'my-dapp');
|
|
28
|
+
*
|
|
29
|
+
* // All subsequent calls are encrypted
|
|
30
|
+
* const accounts = await wallet.getAccounts();
|
|
31
|
+
* ```
|
|
6
32
|
*/
|
|
7
33
|
export declare class ExtensionWallet {
|
|
8
34
|
private chainInfo;
|
|
9
35
|
private appId;
|
|
10
36
|
private extensionId;
|
|
37
|
+
/** Map of pending requests awaiting responses, keyed by message ID */
|
|
11
38
|
private inFlight;
|
|
39
|
+
/** The MessagePort for private communication with the extension */
|
|
40
|
+
private port;
|
|
41
|
+
/** The derived AES-GCM key for encrypting/decrypting messages */
|
|
42
|
+
private sharedKey;
|
|
43
|
+
/**
|
|
44
|
+
* Private constructor - use {@link ExtensionWallet.create} to instantiate.
|
|
45
|
+
* @param chainInfo - The chain information (chainId and version)
|
|
46
|
+
* @param appId - Application identifier for the requesting dApp
|
|
47
|
+
* @param extensionId - The unique identifier of the target wallet extension
|
|
48
|
+
*/
|
|
12
49
|
private constructor();
|
|
13
50
|
/**
|
|
14
51
|
* Creates an ExtensionWallet instance that proxies wallet calls to a browser extension
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
52
|
+
* over a secure encrypted MessageChannel.
|
|
53
|
+
*
|
|
54
|
+
* The connection process:
|
|
55
|
+
* 1. Generates an ECDH key pair for this session
|
|
56
|
+
* 2. Derives a shared AES-256 key using the wallet's public key
|
|
57
|
+
* 3. Creates a MessageChannel and transfers one port to the extension
|
|
58
|
+
* 4. Returns a Proxy that encrypts all wallet method calls
|
|
59
|
+
*
|
|
60
|
+
* @param walletInfo - The discovered wallet information, including the wallet's ECDH public key
|
|
61
|
+
* @param chainInfo - The chain information (chainId and version) for request context
|
|
62
|
+
* @param appId - Application identifier used to identify the requesting dApp to the wallet
|
|
63
|
+
* @returns A Promise resolving to a Wallet implementation that encrypts all communication
|
|
64
|
+
*
|
|
65
|
+
* @throws Error if the secure channel cannot be established
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const wallet = await ExtensionWallet.create(
|
|
70
|
+
* walletInfo,
|
|
71
|
+
* { chainId: Fr(31337), version: Fr(0) },
|
|
72
|
+
* 'my-defi-app'
|
|
73
|
+
* );
|
|
74
|
+
* ```
|
|
19
75
|
*/
|
|
20
|
-
static create(
|
|
76
|
+
static create(walletInfo: WalletInfo, chainInfo: ChainInfo, appId: string): Promise<Wallet>;
|
|
77
|
+
private establishSecureChannel;
|
|
78
|
+
private handleEncryptedResponse;
|
|
21
79
|
private postMessage;
|
|
80
|
+
/**
|
|
81
|
+
* Closes the secure channel and cleans up resources.
|
|
82
|
+
*
|
|
83
|
+
* After calling this method, the wallet instance can no longer be used.
|
|
84
|
+
* Any pending requests will not receive responses.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const wallet = await ExtensionWallet.create(walletInfo, chainInfo, 'my-app');
|
|
89
|
+
* // ... use wallet ...
|
|
90
|
+
* wallet.close(); // Clean up when done
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
close(): void;
|
|
22
94
|
}
|
|
23
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
95
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXh0ZW5zaW9uX3dhbGxldC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3Byb3ZpZGVycy9leHRlbnNpb24vZXh0ZW5zaW9uX3dhbGxldC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN6RCxPQUFPLEVBQUUsS0FBSyxNQUFNLEVBQWdCLE1BQU0sd0JBQXdCLENBQUM7QUFnQm5FLE9BQU8sS0FBSyxFQUFrQixVQUFVLEVBQWlDLE1BQU0sZ0JBQWdCLENBQUM7QUFhaEc7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E0Qkc7QUFDSCxxQkFBYSxlQUFlO0lBaUJ4QixPQUFPLENBQUMsU0FBUztJQUNqQixPQUFPLENBQUMsS0FBSztJQUNiLE9BQU8sQ0FBQyxXQUFXO0lBbEJyQixzRUFBc0U7SUFDdEUsT0FBTyxDQUFDLFFBQVEsQ0FBb0Q7SUFFcEUsbUVBQW1FO0lBQ25FLE9BQU8sQ0FBQyxJQUFJLENBQTRCO0lBRXhDLGlFQUFpRTtJQUNqRSxPQUFPLENBQUMsU0FBUyxDQUEwQjtJQUUzQzs7Ozs7T0FLRztJQUNILE9BQU8sZUFJSDtJQUVKOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O09BeUJHO0lBQ0gsT0FBYSxNQUFNLENBQUMsVUFBVSxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLEtBQUssRUFBRSxNQUFNLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQXlCaEc7WUFjYSxzQkFBc0I7WUFxQ3RCLHVCQUF1QjtZQThDdkIsV0FBVztJQXdCekI7Ozs7Ozs7Ozs7OztPQVlHO0lBQ0gsS0FBSyxJQUFJLElBQUksQ0FPWjtDQUNGIn0=
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension_wallet.d.ts","sourceRoot":"","sources":["../../../src/providers/extension/extension_wallet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"extension_wallet.d.ts","sourceRoot":"","sources":["../../../src/providers/extension/extension_wallet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,wBAAwB,CAAC;AAgBnE,OAAO,KAAK,EAAkB,UAAU,EAAiC,MAAM,gBAAgB,CAAC;AAahG;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,eAAe;IAiBxB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,WAAW;IAlBrB,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAoD;IAEpE,mEAAmE;IACnE,OAAO,CAAC,IAAI,CAA4B;IAExC,iEAAiE;IACjE,OAAO,CAAC,SAAS,CAA0B;IAE3C;;;;;OAKG;IACH,OAAO,eAIH;IAEJ;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAa,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhG;YAca,sBAAsB;YAqCtB,uBAAuB;YA8CvB,WAAW;IAwBzB;;;;;;;;;;;;OAYG;IACH,KAAK,IAAI,IAAI,CAOZ;CACF"}
|
|
@@ -2,61 +2,86 @@ import { WalletSchema } from '@aztec/aztec.js/wallet';
|
|
|
2
2
|
import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
3
3
|
import { promiseWithResolvers } from '@aztec/foundation/promise';
|
|
4
4
|
import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
5
|
+
import { decrypt, deriveSharedKey, encrypt, exportPublicKey, generateKeyPair, importPublicKey } from '../../crypto.js';
|
|
5
6
|
/**
|
|
6
7
|
* A wallet implementation that communicates with browser extension wallets
|
|
7
|
-
*
|
|
8
|
+
* using a secure encrypted MessageChannel.
|
|
9
|
+
*
|
|
10
|
+
* This class establishes a private communication channel with a wallet extension
|
|
11
|
+
* using the following security mechanisms:
|
|
12
|
+
*
|
|
13
|
+
* 1. **MessageChannel**: Creates a private communication channel that is not
|
|
14
|
+
* visible to other scripts on the page (unlike window.postMessage).
|
|
15
|
+
*
|
|
16
|
+
* 2. **ECDH Key Exchange**: Uses Elliptic Curve Diffie-Hellman to derive a
|
|
17
|
+
* shared secret between the dApp and wallet without transmitting private keys.
|
|
18
|
+
*
|
|
19
|
+
* 3. **AES-GCM Encryption**: All messages after channel establishment are
|
|
20
|
+
* encrypted using AES-256-GCM, providing both confidentiality and authenticity.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Discovery returns wallet info including the wallet's public key
|
|
25
|
+
* const wallets = await ExtensionProvider.discoverExtensions(chainInfo);
|
|
26
|
+
* const walletInfo = wallets[0];
|
|
27
|
+
*
|
|
28
|
+
* // Create a secure connection to the wallet
|
|
29
|
+
* const wallet = await ExtensionWallet.create(walletInfo, chainInfo, 'my-dapp');
|
|
30
|
+
*
|
|
31
|
+
* // All subsequent calls are encrypted
|
|
32
|
+
* const accounts = await wallet.getAccounts();
|
|
33
|
+
* ```
|
|
8
34
|
*/ export class ExtensionWallet {
|
|
9
35
|
chainInfo;
|
|
10
36
|
appId;
|
|
11
37
|
extensionId;
|
|
12
|
-
inFlight;
|
|
13
|
-
|
|
38
|
+
/** Map of pending requests awaiting responses, keyed by message ID */ inFlight;
|
|
39
|
+
/** The MessagePort for private communication with the extension */ port;
|
|
40
|
+
/** The derived AES-GCM key for encrypting/decrypting messages */ sharedKey;
|
|
41
|
+
/**
|
|
42
|
+
* Private constructor - use {@link ExtensionWallet.create} to instantiate.
|
|
43
|
+
* @param chainInfo - The chain information (chainId and version)
|
|
44
|
+
* @param appId - Application identifier for the requesting dApp
|
|
45
|
+
* @param extensionId - The unique identifier of the target wallet extension
|
|
46
|
+
*/ constructor(chainInfo, appId, extensionId){
|
|
14
47
|
this.chainInfo = chainInfo;
|
|
15
48
|
this.appId = appId;
|
|
16
49
|
this.extensionId = extensionId;
|
|
17
50
|
this.inFlight = new Map();
|
|
51
|
+
this.port = null;
|
|
52
|
+
this.sharedKey = null;
|
|
18
53
|
}
|
|
19
54
|
/**
|
|
20
55
|
* Creates an ExtensionWallet instance that proxies wallet calls to a browser extension
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const { resolve, reject } = wallet.inFlight.get(messageId);
|
|
53
|
-
if (error) {
|
|
54
|
-
reject(new Error(jsonStringify(error)));
|
|
55
|
-
} else {
|
|
56
|
-
resolve(result);
|
|
57
|
-
}
|
|
58
|
-
wallet.inFlight.delete(messageId);
|
|
59
|
-
});
|
|
56
|
+
* over a secure encrypted MessageChannel.
|
|
57
|
+
*
|
|
58
|
+
* The connection process:
|
|
59
|
+
* 1. Generates an ECDH key pair for this session
|
|
60
|
+
* 2. Derives a shared AES-256 key using the wallet's public key
|
|
61
|
+
* 3. Creates a MessageChannel and transfers one port to the extension
|
|
62
|
+
* 4. Returns a Proxy that encrypts all wallet method calls
|
|
63
|
+
*
|
|
64
|
+
* @param walletInfo - The discovered wallet information, including the wallet's ECDH public key
|
|
65
|
+
* @param chainInfo - The chain information (chainId and version) for request context
|
|
66
|
+
* @param appId - Application identifier used to identify the requesting dApp to the wallet
|
|
67
|
+
* @returns A Promise resolving to a Wallet implementation that encrypts all communication
|
|
68
|
+
*
|
|
69
|
+
* @throws Error if the secure channel cannot be established
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const wallet = await ExtensionWallet.create(
|
|
74
|
+
* walletInfo,
|
|
75
|
+
* { chainId: Fr(31337), version: Fr(0) },
|
|
76
|
+
* 'my-defi-app'
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*/ static async create(walletInfo, chainInfo, appId) {
|
|
80
|
+
const wallet = new ExtensionWallet(chainInfo, appId, walletInfo.id);
|
|
81
|
+
if (!walletInfo.publicKey) {
|
|
82
|
+
throw new Error('Wallet does not support secure channel establishment (missing public key)');
|
|
83
|
+
}
|
|
84
|
+
await wallet.establishSecureChannel(walletInfo.publicKey);
|
|
60
85
|
// Create a Proxy that intercepts wallet method calls and forwards them to the extension
|
|
61
86
|
return new Proxy(wallet, {
|
|
62
87
|
get: (target, prop)=>{
|
|
@@ -74,7 +99,89 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
|
74
99
|
}
|
|
75
100
|
});
|
|
76
101
|
}
|
|
77
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Establishes a secure MessageChannel with ECDH key exchange.
|
|
104
|
+
*
|
|
105
|
+
* This method performs the cryptographic handshake:
|
|
106
|
+
* 1. Generates a new ECDH P-256 key pair for this session
|
|
107
|
+
* 2. Imports the wallet's public key and derives a shared secret
|
|
108
|
+
* 3. Creates a MessageChannel for private communication
|
|
109
|
+
* 4. Sends a connection request with our public key via window.postMessage
|
|
110
|
+
* (this is the only public message - subsequent communication uses the private channel)
|
|
111
|
+
*
|
|
112
|
+
* @param walletExportedPublicKey - The wallet's ECDH public key in JWK format
|
|
113
|
+
*/ async establishSecureChannel(walletExportedPublicKey) {
|
|
114
|
+
const keyPair = await generateKeyPair();
|
|
115
|
+
const exportedPublicKey = await exportPublicKey(keyPair.publicKey);
|
|
116
|
+
const walletPublicKey = await importPublicKey(walletExportedPublicKey);
|
|
117
|
+
this.sharedKey = await deriveSharedKey(keyPair.privateKey, walletPublicKey);
|
|
118
|
+
const channel = new MessageChannel();
|
|
119
|
+
this.port = channel.port1;
|
|
120
|
+
this.port.onmessage = async (event)=>{
|
|
121
|
+
await this.handleEncryptedResponse(event.data);
|
|
122
|
+
};
|
|
123
|
+
this.port.start();
|
|
124
|
+
// Send connection request with our public key and transfer port2 to content script
|
|
125
|
+
// This is the only public postMessage - it contains our public key (safe to expose)
|
|
126
|
+
// and transfers the MessagePort for subsequent private communication
|
|
127
|
+
const connectRequest = {
|
|
128
|
+
type: 'aztec-wallet-connect',
|
|
129
|
+
walletId: this.extensionId,
|
|
130
|
+
appId: this.appId,
|
|
131
|
+
publicKey: exportedPublicKey
|
|
132
|
+
};
|
|
133
|
+
window.postMessage(jsonStringify(connectRequest), '*', [
|
|
134
|
+
channel.port2
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handles an encrypted response received from the wallet extension.
|
|
139
|
+
*
|
|
140
|
+
* Decrypts the response using the shared AES key and resolves or rejects
|
|
141
|
+
* the corresponding pending promise based on the response content.
|
|
142
|
+
*
|
|
143
|
+
* @param encrypted - The encrypted response from the wallet
|
|
144
|
+
*/ async handleEncryptedResponse(encrypted) {
|
|
145
|
+
if (!this.sharedKey) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const response = await decrypt(this.sharedKey, encrypted);
|
|
150
|
+
const { messageId, result, error, walletId: responseWalletId } = response;
|
|
151
|
+
if (!messageId || !responseWalletId) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this.extensionId !== responseWalletId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!this.inFlight.has(messageId)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { resolve, reject } = this.inFlight.get(messageId);
|
|
161
|
+
if (error) {
|
|
162
|
+
reject(new Error(jsonStringify(error)));
|
|
163
|
+
} else {
|
|
164
|
+
resolve(result);
|
|
165
|
+
}
|
|
166
|
+
this.inFlight.delete(messageId);
|
|
167
|
+
// eslint-disable-next-line no-empty
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Sends an encrypted wallet method call over the secure MessageChannel.
|
|
172
|
+
*
|
|
173
|
+
* The message is encrypted using AES-256-GCM with the shared key derived
|
|
174
|
+
* during channel establishment. A unique message ID is generated to correlate
|
|
175
|
+
* the response.
|
|
176
|
+
*
|
|
177
|
+
* @param call - The wallet method call containing method name and arguments
|
|
178
|
+
* @returns A Promise that resolves with the decrypted result from the wallet
|
|
179
|
+
*
|
|
180
|
+
* @throws Error if the secure channel has not been established
|
|
181
|
+
*/ async postMessage(call) {
|
|
182
|
+
if (!this.port || !this.sharedKey) {
|
|
183
|
+
throw new Error('Secure channel not established');
|
|
184
|
+
}
|
|
78
185
|
const messageId = globalThis.crypto.randomUUID();
|
|
79
186
|
const message = {
|
|
80
187
|
type: call.type,
|
|
@@ -84,7 +191,9 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
|
84
191
|
appId: this.appId,
|
|
85
192
|
walletId: this.extensionId
|
|
86
193
|
};
|
|
87
|
-
|
|
194
|
+
// Encrypt the message and send over the private MessageChannel
|
|
195
|
+
const encrypted = await encrypt(this.sharedKey, message);
|
|
196
|
+
this.port.postMessage(encrypted);
|
|
88
197
|
const { promise, resolve, reject } = promiseWithResolvers();
|
|
89
198
|
this.inFlight.set(messageId, {
|
|
90
199
|
promise,
|
|
@@ -93,4 +202,24 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
|
93
202
|
});
|
|
94
203
|
return promise;
|
|
95
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Closes the secure channel and cleans up resources.
|
|
207
|
+
*
|
|
208
|
+
* After calling this method, the wallet instance can no longer be used.
|
|
209
|
+
* Any pending requests will not receive responses.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const wallet = await ExtensionWallet.create(walletInfo, chainInfo, 'my-app');
|
|
214
|
+
* // ... use wallet ...
|
|
215
|
+
* wallet.close(); // Clean up when done
|
|
216
|
+
* ```
|
|
217
|
+
*/ close() {
|
|
218
|
+
if (this.port) {
|
|
219
|
+
this.port.close();
|
|
220
|
+
this.port = null;
|
|
221
|
+
}
|
|
222
|
+
this.sharedKey = null;
|
|
223
|
+
this.inFlight.clear();
|
|
224
|
+
}
|
|
96
225
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ExtensionWallet } from './extension_wallet.js';
|
|
2
2
|
export { ExtensionProvider } from './extension_provider.js';
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export * from '../../crypto.js';
|
|
4
|
+
export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse, ConnectRequest, } from '../../types.js';
|
|
5
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9wcm92aWRlcnMvZXh0ZW5zaW9uL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUN4RCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUM1RCxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLFlBQVksRUFDVixVQUFVLEVBQ1YsYUFBYSxFQUNiLGNBQWMsRUFDZCxnQkFBZ0IsRUFDaEIsaUJBQWlCLEVBQ2pCLGNBQWMsR0FDZixNQUFNLGdCQUFnQixDQUFDIn0=
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/extension/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/extension/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,cAAc,iBAAiB,CAAC;AAChC,YAAY,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,GACf,MAAM,gBAAgB,CAAC"}
|