@aztec/wallet-sdk 5.0.0-private.20260319 → 5.0.0-rc.2
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 +125 -0
- package/dest/base-wallet/base_wallet.d.ts +65 -40
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +196 -80
- package/dest/base-wallet/get_gas_limits.d.ts +36 -0
- package/dest/base-wallet/get_gas_limits.d.ts.map +1 -0
- package/dest/base-wallet/get_gas_limits.js +55 -0
- 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 +7 -4
- package/dest/base-wallet/utils.d.ts.map +1 -1
- package/dest/base-wallet/utils.js +11 -5
- 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 +26 -6
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -1
- package/dest/extension/provider/extension_wallet.js +80 -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 +239 -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 +3 -2
- 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 +46 -16
- package/dest/types.d.ts +64 -2
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +29 -0
- package/package.json +12 -8
- package/src/base-wallet/base_wallet.ts +257 -125
- package/src/base-wallet/get_gas_limits.ts +88 -0
- package/src/base-wallet/index.ts +7 -1
- package/src/base-wallet/utils.ts +15 -5
- 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 +94 -13
- package/src/extension/provider/index.ts +1 -1
- package/src/iframe/handlers/iframe_connection_handler.ts +341 -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 +2 -1
- package/src/manager/wallet_manager.ts +48 -14
- package/src/types.ts +72 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants';
|
|
2
|
+
import { Gas, type GasUsed } from '@aztec/stdlib/gas';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns suggested total and teardown gas limits for a simulated tx, clamped to the network's per-tx
|
|
6
|
+
* admission limits.
|
|
7
|
+
*
|
|
8
|
+
* The network only admits transactions that declare up to `maxTxGasLimits` per dimension (the
|
|
9
|
+
* node-advertised `txsLimits.gas`). Wallets pass the value read from their own node info, but since node info
|
|
10
|
+
* is remote input it is defensively clamped here to the per-tx protocol maxima so a value above them is never
|
|
11
|
+
* honored. If the simulated usage already exceeds the resulting admission limits the tx can never be included,
|
|
12
|
+
* so this throws a descriptive error instead of returning a limit the node would reject. Otherwise it pads the
|
|
13
|
+
* usage and clamps each dimension to the admission limit.
|
|
14
|
+
* @param gasUsed - The gas actually consumed during simulation.
|
|
15
|
+
* @param maxTxGasLimits - The maximum gas a single tx may declare on this network (the node-advertised `txsLimits.gas`).
|
|
16
|
+
* @param pad - Fraction to pad the suggested gas limits by (as a decimal, e.g. 0.1 for 10%). The effective
|
|
17
|
+
* padding shrinks to zero as usage approaches the network limit, since the network will not admit a higher
|
|
18
|
+
* declared limit regardless of the buffer.
|
|
19
|
+
*/
|
|
20
|
+
export function getGasLimits(
|
|
21
|
+
gasUsed: GasUsed,
|
|
22
|
+
maxTxGasLimits: Gas,
|
|
23
|
+
pad = 0.1,
|
|
24
|
+
): {
|
|
25
|
+
/**
|
|
26
|
+
* Gas limit for the tx, excluding teardown gas
|
|
27
|
+
*/
|
|
28
|
+
gasLimits: Gas;
|
|
29
|
+
/**
|
|
30
|
+
* Gas limit for the teardown phase
|
|
31
|
+
*/
|
|
32
|
+
teardownGasLimits: Gas;
|
|
33
|
+
} {
|
|
34
|
+
const { totalGas, teardownGas } = gasUsed;
|
|
35
|
+
|
|
36
|
+
// `maxTxGasLimits` is the node-advertised admission limit. Node info is remote input, so we defensively
|
|
37
|
+
// clamp to the per-tx protocol maxima so a value above them can never be honored.
|
|
38
|
+
const maxLimits = new Gas(
|
|
39
|
+
Math.min(maxTxGasLimits.daGas, MAX_TX_DA_GAS),
|
|
40
|
+
Math.min(maxTxGasLimits.l2Gas, MAX_PROCESSABLE_L2_GAS),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// The simulated usage must fit within the admission limits, otherwise the tx can never be included.
|
|
44
|
+
if (totalGas.daGas > maxLimits.daGas) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Transaction consumes ${totalGas.daGas} DA gas but the network only admits transactions declaring up to ${maxLimits.daGas} DA gas`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (totalGas.l2Gas > maxLimits.l2Gas) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Transaction consumes ${totalGas.l2Gas} L2 gas but the network only admits transactions declaring up to ${maxLimits.l2Gas} L2 gas`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Pad the limits by the buffer, then cap each dimension at the admission limit so the buffer cannot push a
|
|
56
|
+
// declared limit past what inbound validation accepts. Teardown is part of the total, so clamping it to the
|
|
57
|
+
// admission limit is safe.
|
|
58
|
+
return {
|
|
59
|
+
gasLimits: padGas(totalGas, pad, maxLimits),
|
|
60
|
+
teardownGasLimits: padGas(teardownGas, pad, maxLimits),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Pads each gas dimension, capping it at the network admission limit. */
|
|
65
|
+
function padGas(gas: Gas, pad: number, cap: Gas): Gas {
|
|
66
|
+
const padded = gas.mul(1 + pad);
|
|
67
|
+
return new Gas(Math.min(padded.daGas, cap.daGas), Math.min(padded.l2Gas, cap.l2Gas));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates that caller-declared gas limits do not exceed the network's per-tx admission limits, throwing a
|
|
72
|
+
* descriptive error per dimension when they do. The node's inbound validation checks declared
|
|
73
|
+
* `gasSettings.gasLimits`, so we mirror that here to surface the rejection locally before the tx is sent.
|
|
74
|
+
* @param gasLimits - The gas limits the transaction will declare.
|
|
75
|
+
* @param maxTxGasLimits - The maximum gas a single tx may declare on this network (the node-advertised `txsLimits.gas`).
|
|
76
|
+
*/
|
|
77
|
+
export function assertGasLimitsWithinNetworkLimits(gasLimits: Gas, maxTxGasLimits: Gas): void {
|
|
78
|
+
if (gasLimits.daGas > maxTxGasLimits.daGas) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Declared DA gas limit (${gasLimits.daGas}) exceeds the maximum this network allows per tx (${maxTxGasLimits.daGas})`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (gasLimits.l2Gas > maxTxGasLimits.l2Gas) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Declared L2 gas limit (${gasLimits.l2Gas}) exceeds the maximum this network allows per tx (${maxTxGasLimits.l2Gas})`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/base-wallet/index.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
BaseWallet,
|
|
3
|
+
type CompleteFeeOptionsConfig,
|
|
4
|
+
type FeeOptions,
|
|
5
|
+
type SimulateViaEntrypointOptions,
|
|
6
|
+
} from './base_wallet.js';
|
|
2
7
|
export { simulateViaNode, buildMergedSimulationResult, extractOptimizablePublicStaticCalls } from './utils.js';
|
|
8
|
+
export { getGasLimits, assertGasLimitsWithinNetworkLimits } from './get_gas_limits.js';
|
package/src/base-wallet/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AztecNode } from '@aztec/aztec.js/node';
|
|
2
|
+
import { TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet';
|
|
2
3
|
import { MAX_ENQUEUED_CALLS_PER_CALL } from '@aztec/constants';
|
|
3
4
|
import type { ChainInfo } from '@aztec/entrypoints/interfaces';
|
|
4
5
|
import { makeTuple } from '@aztec/foundation/array';
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
PrivateCallExecutionResult,
|
|
25
26
|
PrivateExecutionResult,
|
|
26
27
|
PublicSimulationOutput,
|
|
28
|
+
type SimulationOverrides,
|
|
27
29
|
Tx,
|
|
28
30
|
TxContext,
|
|
29
31
|
TxSimulationResult,
|
|
@@ -64,6 +66,8 @@ export function extractOptimizablePublicStaticCalls(payload: ExecutionPayload):
|
|
|
64
66
|
* @param gasSettings - Gas settings for the transaction.
|
|
65
67
|
* @param blockHeader - Block header to use as anchor.
|
|
66
68
|
* @param skipFeeEnforcement - Whether to skip fee enforcement during simulation.
|
|
69
|
+
* @param getContractName - Resolver for contract names (used for debug log display).
|
|
70
|
+
* @param overrides - Optional pre-simulation overrides applied to the ephemeral fork and contract DB.
|
|
67
71
|
* @returns TxSimulationResult with public return values.
|
|
68
72
|
*/
|
|
69
73
|
async function simulateBatchViaNode(
|
|
@@ -75,6 +79,7 @@ async function simulateBatchViaNode(
|
|
|
75
79
|
blockHeader: BlockHeader,
|
|
76
80
|
skipFeeEnforcement: boolean,
|
|
77
81
|
getContractName: ContractNameResolver,
|
|
82
|
+
overrides?: SimulationOverrides,
|
|
78
83
|
): Promise<TxSimulationResult> {
|
|
79
84
|
const txContext = new TxContext(chainInfo.chainId, chainInfo.version, gasSettings);
|
|
80
85
|
|
|
@@ -142,7 +147,7 @@ async function simulateBatchViaNode(
|
|
|
142
147
|
publicFunctionCalldata: publicFunctionCalldata,
|
|
143
148
|
});
|
|
144
149
|
|
|
145
|
-
const publicOutput = await node.simulatePublicCalls(tx, skipFeeEnforcement);
|
|
150
|
+
const publicOutput = await node.simulatePublicCalls(tx, skipFeeEnforcement, overrides);
|
|
146
151
|
|
|
147
152
|
if (publicOutput.revertReason) {
|
|
148
153
|
throw publicOutput.revertReason;
|
|
@@ -165,6 +170,8 @@ async function simulateBatchViaNode(
|
|
|
165
170
|
* @param gasSettings - Gas settings for the transaction.
|
|
166
171
|
* @param blockHeader - Block header to use as anchor.
|
|
167
172
|
* @param skipFeeEnforcement - Whether to skip fee enforcement during simulation.
|
|
173
|
+
* @param getContractName - Resolver for contract names (used for debug log display).
|
|
174
|
+
* @param overrides - Optional pre-simulation overrides applied to the ephemeral fork and contract DB.
|
|
168
175
|
* @returns Array of TxSimulationResult, one per batch.
|
|
169
176
|
*/
|
|
170
177
|
export async function simulateViaNode(
|
|
@@ -176,6 +183,7 @@ export async function simulateViaNode(
|
|
|
176
183
|
blockHeader: BlockHeader,
|
|
177
184
|
skipFeeEnforcement: boolean = true,
|
|
178
185
|
getContractName: ContractNameResolver,
|
|
186
|
+
overrides?: SimulationOverrides,
|
|
179
187
|
): Promise<TxSimulationResult[]> {
|
|
180
188
|
const batches: FunctionCall[][] = [];
|
|
181
189
|
|
|
@@ -195,6 +203,7 @@ export async function simulateViaNode(
|
|
|
195
203
|
blockHeader,
|
|
196
204
|
skipFeeEnforcement,
|
|
197
205
|
getContractName,
|
|
206
|
+
overrides,
|
|
198
207
|
);
|
|
199
208
|
results.push(result);
|
|
200
209
|
}
|
|
@@ -214,13 +223,13 @@ export async function simulateViaNode(
|
|
|
214
223
|
*/
|
|
215
224
|
export function buildMergedSimulationResult(
|
|
216
225
|
optimizedResults: TxSimulationResult[],
|
|
217
|
-
normalResult:
|
|
218
|
-
):
|
|
226
|
+
normalResult: TxSimulationResultWithAppOffset | null,
|
|
227
|
+
): TxSimulationResultWithAppOffset {
|
|
219
228
|
const optimizedReturnValues = optimizedResults.flatMap(r => r.publicOutput?.publicReturnValues ?? []);
|
|
220
229
|
const normalReturnValues = normalResult?.publicOutput?.publicReturnValues ?? [];
|
|
221
230
|
const allReturnValues = [...optimizedReturnValues, ...normalReturnValues];
|
|
222
231
|
|
|
223
|
-
const baseResult = normalResult ?? optimizedResults[0];
|
|
232
|
+
const baseResult: TxSimulationResult = normalResult ?? optimizedResults[0];
|
|
224
233
|
|
|
225
234
|
const mergedPublicOutput: PublicSimulationOutput | undefined = baseResult.publicOutput
|
|
226
235
|
? {
|
|
@@ -229,10 +238,11 @@ export function buildMergedSimulationResult(
|
|
|
229
238
|
}
|
|
230
239
|
: undefined;
|
|
231
240
|
|
|
232
|
-
|
|
241
|
+
const merged = new TxSimulationResult(
|
|
233
242
|
baseResult.privateExecutionResult,
|
|
234
243
|
baseResult.publicInputs,
|
|
235
244
|
mergedPublicOutput,
|
|
236
245
|
normalResult?.stats,
|
|
237
246
|
);
|
|
247
|
+
return TxSimulationResultWithAppOffset.fromResultAndOffset(merged, normalResult?.appCallOffset ?? 0);
|
|
238
248
|
}
|
package/src/crypto.ts
CHANGED
|
@@ -497,3 +497,107 @@ export function hashToEmoji(hash: string, count: number = DEFAULT_EMOJI_GRID_SIZ
|
|
|
497
497
|
}
|
|
498
498
|
return emojis.join('');
|
|
499
499
|
}
|
|
500
|
+
|
|
501
|
+
// ─── Passphrase-based encryption (PBKDF2 + AES-256-GCM) ───────────────────
|
|
502
|
+
|
|
503
|
+
/** Default PBKDF2 iteration count. High to compensate for short PINs (~1-2s on modern hardware). */
|
|
504
|
+
const DEFAULT_PBKDF2_ITERATIONS = 2_000_000;
|
|
505
|
+
const PBKDF2_SALT_BYTES = 16;
|
|
506
|
+
const PBKDF2_IV_BYTES = 12;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Derives an AES-256-GCM key from a passphrase using PBKDF2-SHA256.
|
|
510
|
+
*
|
|
511
|
+
* @param passphrase - The user-provided passphrase or PIN
|
|
512
|
+
* @param salt - Random salt bytes
|
|
513
|
+
* @param iterations - PBKDF2 iteration count (default: 2,000,000)
|
|
514
|
+
* @returns An AES-256-GCM CryptoKey
|
|
515
|
+
*/
|
|
516
|
+
export async function deriveKeyFromPassphrase(
|
|
517
|
+
passphrase: string,
|
|
518
|
+
salt: Uint8Array,
|
|
519
|
+
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
|
|
520
|
+
): Promise<CryptoKey> {
|
|
521
|
+
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, [
|
|
522
|
+
'deriveKey',
|
|
523
|
+
]);
|
|
524
|
+
return crypto.subtle.deriveKey(
|
|
525
|
+
{ name: 'PBKDF2', salt: salt as BufferSource, iterations, hash: 'SHA-256' },
|
|
526
|
+
keyMaterial,
|
|
527
|
+
{ name: 'AES-GCM', length: 256 },
|
|
528
|
+
false,
|
|
529
|
+
['encrypt', 'decrypt'],
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Encrypts arbitrary bytes with a passphrase using PBKDF2 + AES-256-GCM.
|
|
535
|
+
*
|
|
536
|
+
* Output layout: `[salt (16)] [iv (12)] [ciphertext (...)]`
|
|
537
|
+
*
|
|
538
|
+
* @param plaintext - Data to encrypt
|
|
539
|
+
* @param passphrase - User passphrase or PIN
|
|
540
|
+
* @param iterations - PBKDF2 iteration count (default: 2,000,000)
|
|
541
|
+
* @returns A Uint8Array containing salt + iv + ciphertext
|
|
542
|
+
*/
|
|
543
|
+
export async function encryptWithPassphrase(
|
|
544
|
+
plaintext: Uint8Array,
|
|
545
|
+
passphrase: string,
|
|
546
|
+
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
|
|
547
|
+
): Promise<Uint8Array> {
|
|
548
|
+
const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_BYTES));
|
|
549
|
+
const iv = crypto.getRandomValues(new Uint8Array(PBKDF2_IV_BYTES));
|
|
550
|
+
const key = await deriveKeyFromPassphrase(passphrase, salt, iterations);
|
|
551
|
+
const ciphertext = new Uint8Array(
|
|
552
|
+
await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext as BufferSource),
|
|
553
|
+
);
|
|
554
|
+
const result = new Uint8Array(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES + ciphertext.length);
|
|
555
|
+
result.set(salt, 0);
|
|
556
|
+
result.set(iv, PBKDF2_SALT_BYTES);
|
|
557
|
+
result.set(ciphertext, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Decrypts data produced by {@link encryptWithPassphrase}.
|
|
563
|
+
*
|
|
564
|
+
* @param data - The encrypted blob (salt + iv + ciphertext)
|
|
565
|
+
* @param passphrase - The passphrase used during encryption
|
|
566
|
+
* @param iterations - PBKDF2 iteration count (must match encryption)
|
|
567
|
+
* @returns The decrypted plaintext bytes
|
|
568
|
+
* @throws On wrong passphrase (AES-GCM auth tag mismatch)
|
|
569
|
+
*/
|
|
570
|
+
export async function decryptWithPassphrase(
|
|
571
|
+
data: Uint8Array,
|
|
572
|
+
passphrase: string,
|
|
573
|
+
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
|
|
574
|
+
): Promise<Uint8Array> {
|
|
575
|
+
const salt = data.slice(0, PBKDF2_SALT_BYTES);
|
|
576
|
+
const iv = data.slice(PBKDF2_SALT_BYTES, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
|
|
577
|
+
const ciphertext = data.slice(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
|
|
578
|
+
const key = await deriveKeyFromPassphrase(passphrase, salt, iterations);
|
|
579
|
+
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext as BufferSource));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Converts a Uint8Array to a base64 string.
|
|
584
|
+
*/
|
|
585
|
+
export function uint8ToBase64(bytes: Uint8Array): string {
|
|
586
|
+
let binary = '';
|
|
587
|
+
for (const b of bytes) {
|
|
588
|
+
binary += String.fromCharCode(b);
|
|
589
|
+
}
|
|
590
|
+
return btoa(binary);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Converts a base64 string to a Uint8Array.
|
|
595
|
+
*/
|
|
596
|
+
export function base64ToUint8(b64: string): Uint8Array {
|
|
597
|
+
const binary = atob(b64);
|
|
598
|
+
const bytes = new Uint8Array(binary.length);
|
|
599
|
+
for (let i = 0; i < binary.length; i++) {
|
|
600
|
+
bytes[i] = binary.charCodeAt(i);
|
|
601
|
+
}
|
|
602
|
+
return bytes;
|
|
603
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type WalletMessage,
|
|
18
18
|
WalletMessageType,
|
|
19
19
|
type WalletResponse,
|
|
20
|
+
type WalletSdkLogger,
|
|
20
21
|
} from '../../types.js';
|
|
21
22
|
import {
|
|
22
23
|
type BackgroundMessage,
|
|
@@ -131,6 +132,8 @@ export interface BackgroundConnectionConfig {
|
|
|
131
132
|
walletVersion: string;
|
|
132
133
|
/** Optional wallet icon URL. */
|
|
133
134
|
walletIcon?: string;
|
|
135
|
+
/** Logger used for diagnostics. */
|
|
136
|
+
logger: WalletSdkLogger;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
/**
|
|
@@ -149,6 +152,7 @@ export interface BackgroundConnectionConfig {
|
|
|
149
152
|
* walletId: 'my-wallet',
|
|
150
153
|
* walletName: 'My Wallet',
|
|
151
154
|
* walletVersion: '1.0.0',
|
|
155
|
+
* logger: console,
|
|
152
156
|
* },
|
|
153
157
|
* {
|
|
154
158
|
* sendToTab: (tabId, message) => browser.tabs.sendMessage(tabId, message),
|
|
@@ -167,12 +171,15 @@ export interface BackgroundConnectionConfig {
|
|
|
167
171
|
export class BackgroundConnectionHandler {
|
|
168
172
|
private pendingDiscoveries = new Map<string, PendingDiscovery>();
|
|
169
173
|
private activeSessions = new Map<string, ActiveSession>();
|
|
174
|
+
private log: WalletSdkLogger;
|
|
170
175
|
|
|
171
176
|
constructor(
|
|
172
177
|
private config: BackgroundConnectionConfig,
|
|
173
178
|
private transport: BackgroundTransport,
|
|
174
179
|
private callbacks: BackgroundConnectionCallbacks = {},
|
|
175
|
-
) {
|
|
180
|
+
) {
|
|
181
|
+
this.log = config.logger;
|
|
182
|
+
}
|
|
176
183
|
|
|
177
184
|
initialize(): void {
|
|
178
185
|
this.transport.addContentListener(this.handleMessage);
|
|
@@ -198,8 +205,8 @@ export class BackgroundConnectionHandler {
|
|
|
198
205
|
break;
|
|
199
206
|
case InternalMessageType.KEY_EXCHANGE_REQUEST:
|
|
200
207
|
if (sessionId) {
|
|
201
|
-
this.handleKeyExchangeRequest(sessionId, content as KeyExchangeRequest).catch(
|
|
202
|
-
|
|
208
|
+
this.handleKeyExchangeRequest(sessionId, content as KeyExchangeRequest).catch(err => {
|
|
209
|
+
this.log.warn('Key exchange failed — session will not be established', { sessionId, err });
|
|
203
210
|
});
|
|
204
211
|
}
|
|
205
212
|
break;
|
|
@@ -213,9 +220,31 @@ export class BackgroundConnectionHandler {
|
|
|
213
220
|
void this.handleEncryptedMessage(sessionId, content as EncryptedPayload);
|
|
214
221
|
}
|
|
215
222
|
break;
|
|
223
|
+
case InternalMessageType.PING:
|
|
224
|
+
if (sessionId) {
|
|
225
|
+
this.handlePing(sessionId);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
216
228
|
}
|
|
217
229
|
};
|
|
218
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Reply to a dApp PING with a PONG. Used as a liveness probe so the dApp can
|
|
233
|
+
* tell the difference between a slow request and a dead extension.
|
|
234
|
+
* @param sessionId - The session that sent the PING.
|
|
235
|
+
*/
|
|
236
|
+
private handlePing(sessionId: string): void {
|
|
237
|
+
const session = this.activeSessions.get(sessionId);
|
|
238
|
+
if (!session) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.transport.sendToTab(session.tabId, {
|
|
242
|
+
origin: MessageOrigin.BACKGROUND,
|
|
243
|
+
type: InternalMessageType.PONG,
|
|
244
|
+
sessionId,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
219
248
|
getWalletInfo(): WalletInfo {
|
|
220
249
|
return {
|
|
221
250
|
id: this.config.walletId,
|
|
@@ -315,8 +344,8 @@ export class BackgroundConnectionHandler {
|
|
|
315
344
|
});
|
|
316
345
|
|
|
317
346
|
this.callbacks.onSessionEstablished?.(session);
|
|
318
|
-
} catch {
|
|
319
|
-
|
|
347
|
+
} catch (err) {
|
|
348
|
+
this.log.warn('Key exchange failed — session will not be established', { sessionId, err });
|
|
320
349
|
}
|
|
321
350
|
}
|
|
322
351
|
|
|
@@ -329,8 +358,8 @@ export class BackgroundConnectionHandler {
|
|
|
329
358
|
try {
|
|
330
359
|
const message = await decrypt<WalletMessage>(session.sharedKey, encrypted);
|
|
331
360
|
this.callbacks.onWalletMessage?.(session, message);
|
|
332
|
-
} catch {
|
|
333
|
-
|
|
361
|
+
} catch (err) {
|
|
362
|
+
this.log.warn('Failed to decrypt incoming wallet message', { sessionId, err });
|
|
334
363
|
}
|
|
335
364
|
}
|
|
336
365
|
|
|
@@ -348,8 +377,12 @@ export class BackgroundConnectionHandler {
|
|
|
348
377
|
sessionId,
|
|
349
378
|
content: encrypted,
|
|
350
379
|
});
|
|
351
|
-
} catch {
|
|
352
|
-
|
|
380
|
+
} catch (err) {
|
|
381
|
+
this.log.error('Failed to encrypt wallet response — response will not be sent', {
|
|
382
|
+
sessionId,
|
|
383
|
+
messageId: response.messageId,
|
|
384
|
+
err,
|
|
385
|
+
});
|
|
353
386
|
}
|
|
354
387
|
}
|
|
355
388
|
|
|
@@ -139,9 +139,20 @@ export class ContentScriptConnectionHandler {
|
|
|
139
139
|
case InternalMessageType.SESSION_DISCONNECTED:
|
|
140
140
|
this.handleSessionDisconnected(sessionId);
|
|
141
141
|
break;
|
|
142
|
+
case InternalMessageType.PONG:
|
|
143
|
+
this.handlePong(sessionId);
|
|
144
|
+
break;
|
|
142
145
|
}
|
|
143
146
|
};
|
|
144
147
|
|
|
148
|
+
private handlePong(sessionId: string): void {
|
|
149
|
+
const connection = this.ports.get(sessionId);
|
|
150
|
+
if (!connection) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
connection.port.postMessage({ type: WalletMessageType.PONG });
|
|
154
|
+
}
|
|
155
|
+
|
|
145
156
|
private handleDiscoveryRequest(request: DiscoveryRequest): void {
|
|
146
157
|
this.transport.sendToBackground({
|
|
147
158
|
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
@@ -178,6 +189,13 @@ export class ContentScriptConnectionHandler {
|
|
|
178
189
|
content: data,
|
|
179
190
|
});
|
|
180
191
|
break;
|
|
192
|
+
case WalletMessageType.PING:
|
|
193
|
+
this.transport.sendToBackground({
|
|
194
|
+
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
195
|
+
type: InternalMessageType.PING,
|
|
196
|
+
sessionId,
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
181
199
|
default:
|
|
182
200
|
this.transport.sendToBackground({
|
|
183
201
|
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
@@ -9,11 +9,13 @@ export const InternalMessageType = {
|
|
|
9
9
|
KEY_EXCHANGE_REQUEST: 'key-exchange-request',
|
|
10
10
|
SECURE_MESSAGE: 'secure-message',
|
|
11
11
|
DISCONNECT_REQUEST: 'disconnect-request',
|
|
12
|
+
PING: 'ping',
|
|
12
13
|
// Background → Content script
|
|
13
14
|
DISCOVERY_APPROVED: 'discovery-approved',
|
|
14
15
|
KEY_EXCHANGE_RESPONSE: 'key-exchange-response',
|
|
15
16
|
SECURE_RESPONSE: 'secure-response',
|
|
16
17
|
SESSION_DISCONNECTED: 'session-disconnected',
|
|
18
|
+
PONG: 'pong',
|
|
17
19
|
} as const;
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -2,11 +2,21 @@ import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
|
2
2
|
import { type Wallet, WalletSchema } from '@aztec/aztec.js/wallet';
|
|
3
3
|
import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
4
4
|
import { type PromiseWithResolvers, promiseWithResolvers } from '@aztec/foundation/promise';
|
|
5
|
-
import { schemaHasMethod } from '@aztec/foundation/schemas';
|
|
5
|
+
import { getSchemaReturnType, 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,
|
|
132
|
+
logger?: WalletSdkLogger,
|
|
133
|
+
heartbeatOptions?: HeartbeatOptions,
|
|
112
134
|
): ExtensionWallet {
|
|
113
|
-
const wallet = new ExtensionWallet(chainInfo, appId, extensionId, port, sharedKey);
|
|
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;
|
|
@@ -136,7 +160,7 @@ export class ExtensionWallet {
|
|
|
136
160
|
type: prop.toString() as keyof FunctionsOf<Wallet>,
|
|
137
161
|
args,
|
|
138
162
|
});
|
|
139
|
-
return WalletSchema[prop.toString() as keyof typeof WalletSchema]
|
|
163
|
+
return getSchemaReturnType(WalletSchema[prop.toString() as keyof typeof WalletSchema]).parseAsync(result);
|
|
140
164
|
};
|
|
141
165
|
} else {
|
|
142
166
|
return target[prop as keyof ExtensionWallet];
|
|
@@ -189,8 +213,10 @@ export class ExtensionWallet {
|
|
|
189
213
|
resolve(result);
|
|
190
214
|
}
|
|
191
215
|
this.inFlight.delete(messageId);
|
|
192
|
-
|
|
193
|
-
} catch {
|
|
216
|
+
this.maybeStopHeartbeat();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
this.log.warn('Failed to decrypt wallet response', { err });
|
|
219
|
+
}
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
/**
|
|
@@ -228,9 +254,59 @@ export class ExtensionWallet {
|
|
|
228
254
|
|
|
229
255
|
const { promise, resolve, reject } = promiseWithResolvers<unknown>();
|
|
230
256
|
this.inFlight.set(messageId, { promise, resolve, reject });
|
|
257
|
+
this.startHeartbeat();
|
|
231
258
|
return promise;
|
|
232
259
|
}
|
|
233
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
|
+
|
|
234
310
|
/**
|
|
235
311
|
* Handles wallet disconnection.
|
|
236
312
|
* Rejects all pending requests and notifies registered callbacks.
|
|
@@ -242,6 +318,11 @@ export class ExtensionWallet {
|
|
|
242
318
|
}
|
|
243
319
|
this.disconnected = true;
|
|
244
320
|
|
|
321
|
+
if (this.heartbeatTimer !== null) {
|
|
322
|
+
clearInterval(this.heartbeatTimer);
|
|
323
|
+
this.heartbeatTimer = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
245
326
|
if (this.port) {
|
|
246
327
|
this.port.onmessage = null;
|
|
247
328
|
this.port.close();
|