@aztec/end-to-end 5.0.0-rc.1 → 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.
Files changed (60) hide show
  1. package/dest/bench/client_flows/client_flows_benchmark.d.ts +4 -4
  2. package/dest/bench/client_flows/client_flows_benchmark.d.ts.map +1 -1
  3. package/dest/bench/client_flows/client_flows_benchmark.js +9 -6
  4. package/dest/e2e_cross_chain_messaging/cross_chain_messaging_test.d.ts +3 -2
  5. package/dest/e2e_cross_chain_messaging/cross_chain_messaging_test.d.ts.map +1 -1
  6. package/dest/e2e_cross_chain_messaging/cross_chain_messaging_test.js +8 -6
  7. package/dest/e2e_epochs/epochs_test.d.ts +1 -1
  8. package/dest/e2e_epochs/epochs_test.d.ts.map +1 -1
  9. package/dest/e2e_epochs/epochs_test.js +2 -2
  10. package/dest/e2e_fees/bridging_race.notest.js +10 -0
  11. package/dest/e2e_fees/fees_test.d.ts +1 -1
  12. package/dest/e2e_fees/fees_test.d.ts.map +1 -1
  13. package/dest/e2e_fees/fees_test.js +4 -2
  14. package/dest/e2e_l1_publisher/write_json.d.ts +1 -1
  15. package/dest/e2e_l1_publisher/write_json.d.ts.map +1 -1
  16. package/dest/e2e_l1_publisher/write_json.js +2 -1
  17. package/dest/e2e_p2p/p2p_network.d.ts +2 -2
  18. package/dest/e2e_p2p/p2p_network.d.ts.map +1 -1
  19. package/dest/e2e_p2p/p2p_network.js +10 -8
  20. package/dest/e2e_p2p/shared.d.ts +26 -1
  21. package/dest/e2e_p2p/shared.d.ts.map +1 -1
  22. package/dest/e2e_p2p/shared.js +40 -0
  23. package/dest/fixtures/e2e_prover_test.d.ts +1 -1
  24. package/dest/fixtures/e2e_prover_test.d.ts.map +1 -1
  25. package/dest/fixtures/e2e_prover_test.js +3 -3
  26. package/dest/fixtures/fixtures.d.ts +2 -1
  27. package/dest/fixtures/fixtures.d.ts.map +1 -1
  28. package/dest/fixtures/fixtures.js +6 -0
  29. package/dest/fixtures/setup.d.ts +4 -2
  30. package/dest/fixtures/setup.d.ts.map +1 -1
  31. package/dest/fixtures/setup.js +34 -6
  32. package/dest/fixtures/setup_p2p_test.d.ts +1 -1
  33. package/dest/fixtures/setup_p2p_test.d.ts.map +1 -1
  34. package/dest/fixtures/setup_p2p_test.js +3 -3
  35. package/dest/fixtures/token_utils.d.ts +22 -4
  36. package/dest/fixtures/token_utils.d.ts.map +1 -1
  37. package/dest/fixtures/token_utils.js +21 -1
  38. package/dest/shared/timing_env.js +254 -0
  39. package/dest/simulators/lending_simulator.js +1 -1
  40. package/dest/spartan/setup_test_wallets.d.ts +1 -1
  41. package/dest/spartan/setup_test_wallets.d.ts.map +1 -1
  42. package/dest/spartan/setup_test_wallets.js +5 -1
  43. package/package.json +41 -41
  44. package/src/bench/client_flows/client_flows_benchmark.ts +22 -8
  45. package/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +14 -5
  46. package/src/e2e_epochs/epochs_test.ts +2 -2
  47. package/src/e2e_fees/bridging_race.notest.ts +10 -0
  48. package/src/e2e_fees/fees_test.ts +10 -2
  49. package/src/e2e_l1_publisher/write_json.ts +1 -0
  50. package/src/e2e_p2p/p2p_network.ts +10 -7
  51. package/src/e2e_p2p/shared.ts +59 -0
  52. package/src/fixtures/e2e_prover_test.ts +4 -3
  53. package/src/fixtures/fixtures.ts +8 -0
  54. package/src/fixtures/setup.ts +40 -10
  55. package/src/fixtures/setup_p2p_test.ts +3 -3
  56. package/src/fixtures/token_utils.ts +39 -4
  57. package/src/guides/up_quick_start.sh +5 -0
  58. package/src/shared/timing_env.mjs +259 -0
  59. package/src/simulators/lending_simulator.ts +1 -1
  60. package/src/spartan/setup_test_wallets.ts +4 -1
@@ -431,6 +431,7 @@ export class P2PNetworkTest {
431
431
  timeoutSeconds = 30,
432
432
  checkIntervalSeconds = 0.1,
433
433
  topics: TopicType[] = [TopicType.tx],
434
+ minMeshPeerCount = 1,
434
435
  ) {
435
436
  const nodeCount = expectedNodeCount ?? nodes.length;
436
437
  const minPeerCount = nodeCount - 1;
@@ -457,11 +458,13 @@ export class P2PNetworkTest {
457
458
 
458
459
  this.logger.warn('All nodes connected to P2P mesh');
459
460
 
460
- // Wait for GossipSub mesh to form for all specified topics.
461
- // We only require at least 1 mesh peer per node because GossipSub
462
- // stops grafting once it reaches Dlo peers and won't fill the mesh to all available peers.
461
+ // Wait for the GossipSub mesh to form for all specified topics. By default we only require at
462
+ // least 1 mesh peer per node, since GossipSub stops grafting once it reaches Dlo peers and won't
463
+ // fill the mesh to every available peer. Callers that need a proposal to reach the whole
464
+ // committee within a slot (e.g. quorum-from-genesis tests) raise `minMeshPeerCount` so the mesh
465
+ // is fully formed — a single mesh peer can leave some committee members unreached at first.
463
466
  for (const topic of topics) {
464
- this.logger.warn(`Waiting for GossipSub mesh to form for ${topic} topic...`);
467
+ this.logger.warn(`Waiting for GossipSub mesh (>= ${minMeshPeerCount} peers per node) for ${topic} topic...`);
465
468
  await Promise.all(
466
469
  nodes.map(async (node, index) => {
467
470
  const p2p = node.getP2P();
@@ -469,15 +472,15 @@ export class P2PNetworkTest {
469
472
  async () => {
470
473
  const meshPeers = await p2p.getGossipMeshPeerCount(topic);
471
474
  this.logger.debug(`Node ${index} has ${meshPeers} gossip mesh peers for ${topic} topic`);
472
- return meshPeers >= 1 ? true : undefined;
475
+ return meshPeers >= minMeshPeerCount ? true : undefined;
473
476
  },
474
- `Node ${index} to have gossip mesh peers for ${topic} topic`,
477
+ `Node ${index} to have >= ${minMeshPeerCount} gossip mesh peers for ${topic} topic`,
475
478
  timeoutSeconds,
476
479
  checkIntervalSeconds,
477
480
  );
478
481
  }),
479
482
  );
480
- this.logger.warn(`All nodes have gossip mesh peers for ${topic} topic`);
483
+ this.logger.warn(`All nodes have >= ${minMeshPeerCount} gossip mesh peers for ${topic} topic`);
481
484
  }
482
485
  }
483
486
 
@@ -147,6 +147,65 @@ export async function awaitCommitteeExists({
147
147
  return committee!.map(c => c.toString() as `0x${string}`);
148
148
  }
149
149
 
150
+ /**
151
+ * Scans L2 slots forward from `minLeadSlots` ahead of the current slot, returning the first slot in
152
+ * which `targetProposer` is the proposer.
153
+ *
154
+ * Scanning starts at `currentSlot + minLeadSlots` and only ever moves forward, so every returned slot
155
+ * is at least `minLeadSlots` ahead — a caller can safely warp to `targetSlot - minLeadSlots` for a
156
+ * settle buffer without risking a backwards warp. Stepping by a single slot examines both epoch
157
+ * parities, which matters because the per-slot proposer is a different RANDAO-shuffled committee
158
+ * member: searching only a fixed offset within each epoch can leave a 1-of-N target unexamined when
159
+ * the epoch is short. A candidate in an epoch whose committee isn't sampled yet makes the proposer
160
+ * lookup revert with EpochNotStable; this warps one epoch forward and continues, keeping the
161
+ * candidate at least `minLeadSlots` ahead of the new current slot. Throws after `maxSlotsToScan`.
162
+ *
163
+ * Unlike {@link advanceToEpochBeforeProposer}, this does not stop an epoch early — callers that want
164
+ * to warp close to the target (rather than stage sequencers an epoch ahead) use this and warp the
165
+ * final `minLeadSlots` in themselves.
166
+ */
167
+ export async function findUpcomingProposerSlot({
168
+ epochCache,
169
+ cheatCodes,
170
+ targetProposer,
171
+ logger,
172
+ minLeadSlots,
173
+ maxSlotsToScan = 100,
174
+ }: {
175
+ epochCache: EpochCacheInterface;
176
+ cheatCodes: RollupCheatCodes;
177
+ targetProposer: EthAddress;
178
+ logger: Logger;
179
+ minLeadSlots: number;
180
+ maxSlotsToScan?: number;
181
+ }): Promise<SlotNumber> {
182
+ let candidate = Number(await cheatCodes.getSlot()) + minLeadSlots;
183
+
184
+ for (let scanned = 0; scanned < maxSlotsToScan; scanned++) {
185
+ let proposer: EthAddress | undefined;
186
+ try {
187
+ proposer = await epochCache.getProposerAttesterAddressInSlot(SlotNumber(candidate));
188
+ } catch (err) {
189
+ if (!(err instanceof Error) || !err.message.includes('EpochNotStable')) {
190
+ throw err;
191
+ }
192
+ await cheatCodes.advanceToNextEpoch();
193
+ const newCurrentSlot = Number(await cheatCodes.getSlot());
194
+ // Keep the lead after the warp: never return a slot we could no longer warp ahead of.
195
+ candidate = Math.max(candidate, newCurrentSlot + minLeadSlots);
196
+ continue;
197
+ }
198
+
199
+ if (proposer && proposer.equals(targetProposer)) {
200
+ logger.warn(`Found target proposer ${targetProposer} at slot ${candidate}`);
201
+ return SlotNumber(candidate);
202
+ }
203
+ candidate++;
204
+ }
205
+
206
+ throw new Error(`Target proposer ${targetProposer} not found within ${maxSlotsToScan} slots`);
207
+ }
208
+
150
209
  /**
151
210
  * Advance epochs until we find one where the target proposer is selected for a slot at least
152
211
  * `warmupSlots` into the epoch, then stop one epoch before it. This leaves time for the caller to
@@ -1,5 +1,5 @@
1
1
  import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing';
2
- import { AztecNodeService } from '@aztec/aztec-node';
2
+ import { AztecNodeService, createAztecNodeService } from '@aztec/aztec-node';
3
3
  import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses';
4
4
  import { type Logger, createLogger } from '@aztec/aztec.js/log';
5
5
  import type { AztecNode } from '@aztec/aztec.js/node';
@@ -177,6 +177,7 @@ export class FullProverTest {
177
177
  this.logger.verbose(`Main setup completed, initializing full prover PXE, Node, and Prover Node`);
178
178
  const { wallet: provenWallet, teardown: provenTeardown } = await setupPXEAndGetWallet(
179
179
  this.aztecNode,
180
+ this.context.aztecNode,
180
181
  { proverEnabled: this.realProofs },
181
182
  undefined,
182
183
  'pxe-proven',
@@ -225,7 +226,7 @@ export class FullProverTest {
225
226
  this.context.genesis!.genesisTimestamp,
226
227
  );
227
228
 
228
- const proverNodeConfig: Parameters<typeof AztecNodeService.createAndSync>[0] = {
229
+ const proverNodeConfig: Parameters<typeof createAztecNodeService>[0] = {
229
230
  ...config,
230
231
  enableProverNode: true,
231
232
  disableValidator: true,
@@ -250,7 +251,7 @@ export class FullProverTest {
250
251
  validatorPrivateKeys: new SecretValue([]),
251
252
  };
252
253
 
253
- this.proverAztecNode = await AztecNodeService.createAndSync(
254
+ this.proverAztecNode = await createAztecNodeService(
254
255
  proverNodeConfig,
255
256
  { dateProvider: this.context.dateProvider, p2pClientDeps: { rpcTxProviders: [this.aztecNode] } },
256
257
  { genesis },
@@ -109,6 +109,14 @@ export const TEST_PEER_CHECK_INTERVAL_MS = 1000;
109
109
  export const TEST_MAX_PENDING_TX_POOL_COUNT = 10_000; // Number of max pending TXs ~ 1.56GB
110
110
 
111
111
  export const MNEMONIC = 'test test test test test test test test test test test junk';
112
+
113
+ // Mnemonic account index for tests that issue direct L1 writes (e.g. bridging fee juice) while a
114
+ // sequencer is running. The deployer/sequencer publisher uses index 0, the prover index 2, and
115
+ // validators index 3+. Test-side viem writes and the publisher's l1-tx-utils track nonces
116
+ // independently, so sharing an account causes "replacement transaction underpriced" races; index 1
117
+ // is otherwise unused, so issuing those writes from it keeps them off the publisher's nonce stream.
118
+ export const L1_DIRECT_WRITE_ACCOUNT_INDEX = 1;
119
+
112
120
  export const privateKey = Buffer.from('ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', 'hex');
113
121
  export const privateKey2 = Buffer.from('59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', 'hex');
114
122
 
@@ -1,5 +1,5 @@
1
1
  import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing';
2
- import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node';
2
+ import { type AztecNodeConfig, AztecNodeService, createAztecNodeService, getConfigEnvVars } from '@aztec/aztec-node';
3
3
  import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses';
4
4
  import type { ContractMethod } from '@aztec/aztec.js/contracts';
5
5
  import { publishContractClass, publishInstance } from '@aztec/aztec.js/deployment';
@@ -109,6 +109,8 @@ export async function setupSharedBlobStorage(config: { dataDirectory?: string }
109
109
  /**
110
110
  * Sets up Private eXecution Environment (PXE) and returns the corresponding test wallet.
111
111
  * @param aztecNode - An instance of Aztec Node.
112
+ * @param nodeDebug - The node's debug API, used to register public function signatures for named traces; pass
113
+ * `undefined` when the node does not expose it.
112
114
  * @param opts - Partial configuration for the PXE.
113
115
  * @param logger - The logger to be used.
114
116
  * @param actor - Actor label to include in log output (e.g., 'pxe-test').
@@ -116,6 +118,7 @@ export async function setupSharedBlobStorage(config: { dataDirectory?: string }
116
118
  */
117
119
  export async function setupPXEAndGetWallet(
118
120
  aztecNode: AztecNode,
121
+ nodeDebug: AztecNodeDebug | undefined,
119
122
  opts: Partial<PXEConfig> = {},
120
123
  logger = getLogger(),
121
124
  actor?: string,
@@ -136,7 +139,10 @@ export async function setupPXEAndGetWallet(
136
139
 
137
140
  const teardown = configuredDataDirectory ? () => Promise.resolve() : () => tryRmDir(PXEConfig.dataDirectory!);
138
141
 
139
- const wallet = await TestWallet.create(aztecNode, PXEConfig, { loggerActorLabel: actor });
142
+ const wallet = await TestWallet.create(aztecNode, PXEConfig, {
143
+ loggerActorLabel: actor,
144
+ nodeDebug,
145
+ });
140
146
 
141
147
  return {
142
148
  wallet,
@@ -294,6 +300,17 @@ function assertContractArtifactsVersion() {
294
300
  }
295
301
  }
296
302
 
303
+ /**
304
+ * Records a function-level timing span into the shared collector installed by the e2e timing
305
+ * environment. No-op unless TEST_TIMING_FILE is set (the env only installs the collector then). The
306
+ * span is tagged with the name of the currently running test so the env can attribute it to the
307
+ * right line; `null` during beforeAll/afterAll lands it on the suite-scoped line.
308
+ */
309
+ function recordFnSpan(kind: 'setup' | 'teardown', ms: number) {
310
+ const collector = (globalThis as { __e2eTimings?: { current: string | null; fnSpans: unknown[] } }).__e2eTimings;
311
+ collector?.fnSpans.push({ name: collector.current, kind, ms });
312
+ }
313
+
297
314
  /**
298
315
  * Sets up the environment for the end-to-end tests.
299
316
  * @param numberOfAccounts - The number of new accounts to be created once the PXE is initiated.
@@ -305,6 +322,20 @@ export async function setup(
305
322
  opts: SetupOptions = {},
306
323
  pxeOpts: Partial<PXEConfig> = {},
307
324
  chain: Chain = foundry,
325
+ ): Promise<EndToEndContext> {
326
+ const setupStart = performance.now();
327
+ try {
328
+ return await setupInner(numberOfAccounts, opts, pxeOpts, chain);
329
+ } finally {
330
+ recordFnSpan('setup', performance.now() - setupStart);
331
+ }
332
+ }
333
+
334
+ async function setupInner(
335
+ numberOfAccounts: number,
336
+ opts: SetupOptions,
337
+ pxeOpts: Partial<PXEConfig>,
338
+ chain: Chain,
308
339
  ): Promise<EndToEndContext> {
309
340
  assertContractArtifactsVersion();
310
341
  let anvil: Anvil | undefined;
@@ -556,7 +587,7 @@ export async function setup(
556
587
  : config;
557
588
 
558
589
  const aztecNodeService = await withLoggerBindings({ actor: 'node-0' }, () =>
559
- AztecNodeService.createAndSync(
590
+ createAztecNodeService(
560
591
  initialNodeConfig,
561
592
  { dateProvider, telemetry: telemetryClient, p2pClientDeps },
562
593
  { genesis, dontStartSequencer: opts.skipInitialSequencer },
@@ -598,6 +629,8 @@ export async function setup(
598
629
  pxeConfig.proverEnabled = !!pxeOpts.proverEnabled;
599
630
  const wallet = await TestWallet.create(aztecNodeService, pxeConfig, {
600
631
  loggerActorLabel: 'pxe-0',
632
+ // In-process node implements the debug API, so register public function signatures for named traces.
633
+ nodeDebug: aztecNodeService,
601
634
  ...opts.pxeCreationOptions,
602
635
  });
603
636
 
@@ -605,12 +638,7 @@ export async function setup(
605
638
  wallet.setMinFeePadding(opts.walletMinFeePadding);
606
639
  }
607
640
 
608
- const cheatCodes = await CheatCodes.create(
609
- config.l1RpcUrls,
610
- aztecNodeService,
611
- dateProvider,
612
- aztecNodeService.getAutomineSequencer(),
613
- );
641
+ const cheatCodes = await CheatCodes.create(config.l1RpcUrls, aztecNodeService, dateProvider);
614
642
 
615
643
  if (
616
644
  (opts.aztecTargetCommitteeSize && opts.aztecTargetCommitteeSize > 0) ||
@@ -664,6 +692,7 @@ export async function setup(
664
692
  }
665
693
 
666
694
  const teardown = async () => {
695
+ const teardownStart = performance.now();
667
696
  try {
668
697
  await tryStop(wallet, logger);
669
698
  await tryStop(aztecNodeService, logger);
@@ -688,6 +717,7 @@ export async function setup(
688
717
  } catch (err) {
689
718
  logger.error(`Error during telemetry client stop`, err);
690
719
  }
720
+ recordFnSpan('teardown', performance.now() - teardownStart);
691
721
  }
692
722
  };
693
723
 
@@ -807,7 +837,7 @@ export function createAndSyncProverNode(
807
837
  options: { genesis?: GenesisData; dontStart?: boolean },
808
838
  ): Promise<{ proverNode: AztecNodeService }> {
809
839
  return withLoggerBindings({ actor: 'prover-0' }, async () => {
810
- const proverNode = await AztecNodeService.createAndSync(
840
+ const proverNode = await createAztecNodeService(
811
841
  {
812
842
  ...baseConfig,
813
843
  ...configOverrides,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Test fixtures and utilities to set up and run a test using multiple validators
3
3
  */
4
- import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node';
4
+ import { type AztecNodeConfig, AztecNodeService, createAztecNodeService } from '@aztec/aztec-node';
5
5
  import { range } from '@aztec/foundation/array';
6
6
  import { SecretValue } from '@aztec/foundation/config';
7
7
  import { withLoggerBindings } from '@aztec/foundation/log/server';
@@ -109,7 +109,7 @@ export async function createNode(
109
109
  return await withLoggerBindings({ actor: `validator-${actorIndex}` }, async () => {
110
110
  const validatorConfig = await createValidatorConfig(config, bootstrapNode, tcpPort, addressIndex, dataDirectory);
111
111
  const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
112
- return await AztecNodeService.createAndSync(
112
+ return await createAztecNodeService(
113
113
  validatorConfig,
114
114
  { telemetry, dateProvider },
115
115
  { genesis, dontStartSequencer: config.dontStartSequencer },
@@ -137,7 +137,7 @@ export async function createNonValidatorNode(
137
137
  sequencerPublisherPrivateKeys: [],
138
138
  };
139
139
  const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
140
- return await AztecNodeService.createAndSync(config, { telemetry, dateProvider }, { genesis });
140
+ return await createAztecNodeService(config, { telemetry, dateProvider }, { genesis });
141
141
  });
142
142
  }
143
143
 
@@ -3,6 +3,15 @@ import { BatchCall } from '@aztec/aztec.js/contracts';
3
3
  import type { Logger } from '@aztec/aztec.js/log';
4
4
  import type { Wallet } from '@aztec/aztec.js/wallet';
5
5
  import { TokenContract } from '@aztec/noir-contracts.js/Token';
6
+ import { TestTokenContract } from '@aztec/noir-test-contracts.js/TestToken';
7
+
8
+ /**
9
+ * Either token flavour. Canonical `TokenContract` uses constrained message delivery (production / docs
10
+ * source of truth); `TestTokenContract` is its codegen'd unconstrained-delivery sibling, used by tests
11
+ * where a token is just a unit-of-account vehicle. The two share an identical ABI, so helpers that only
12
+ * read balances or mint accept either.
13
+ */
14
+ export type AnyTokenContract = TokenContract | TestTokenContract;
6
15
 
7
16
  export async function deployToken(wallet: Wallet, admin: AztecAddress, initialAdminBalance: bigint, logger: Logger) {
8
17
  logger.info(`Deploying Token contract...`);
@@ -19,8 +28,34 @@ export async function deployToken(wallet: Wallet, admin: AztecAddress, initialAd
19
28
  return { contract, instance };
20
29
  }
21
30
 
31
+ /**
32
+ * Deploys the unconstrained-delivery `TestTokenContract`. Use this in tests where the token is a
33
+ * unit-of-account vehicle rather than the subject, so they don't pay constrained delivery's first-send
34
+ * handshake cost (which distorts step/log counts that benches assert on). Use {@link deployToken} when a
35
+ * test exercises canonical Token semantics or note discovery.
36
+ */
37
+ export async function deployTestToken(
38
+ wallet: Wallet,
39
+ admin: AztecAddress,
40
+ initialAdminBalance: bigint,
41
+ logger: Logger,
42
+ ) {
43
+ logger.info(`Deploying TestToken contract...`);
44
+ const { contract, instance } = await TestTokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({
45
+ from: admin,
46
+ });
47
+
48
+ if (initialAdminBalance > 0n) {
49
+ await mintTokensToPrivate(contract, admin, admin, initialAdminBalance);
50
+ }
51
+
52
+ logger.info('L2 contract deployed');
53
+
54
+ return { contract, instance };
55
+ }
56
+
22
57
  export async function mintTokensToPrivate(
23
- token: TokenContract,
58
+ token: AnyTokenContract,
24
59
  minter: AztecAddress,
25
60
  recipient: AztecAddress,
26
61
  amount: bigint,
@@ -31,13 +66,13 @@ export async function mintTokensToPrivate(
31
66
 
32
67
  export async function expectTokenBalance(
33
68
  wallet: Wallet,
34
- token: TokenContract,
69
+ token: AnyTokenContract,
35
70
  owner: AztecAddress,
36
71
  expectedBalance: bigint,
37
72
  logger: Logger,
38
73
  ) {
39
74
  // Then check the balance
40
- const contractWithWallet = TokenContract.at(token.address, wallet);
75
+ const contractWithWallet = token.withWallet(wallet);
41
76
  const { result: balance } = await contractWithWallet.methods.balance_of_private(owner).simulate({ from: owner });
42
77
  logger.info(`Account ${owner} balance: ${balance}`);
43
78
  expect(balance).toBe(expectedBalance);
@@ -47,7 +82,7 @@ export async function mintNotes(
47
82
  wallet: Wallet,
48
83
  minter: AztecAddress,
49
84
  recipient: AztecAddress,
50
- asset: TokenContract,
85
+ asset: AnyTokenContract,
51
86
  noteAmounts: bigint[],
52
87
  ): Promise<bigint> {
53
88
  // We can only mint 5 notes at a time, since that's the maximum number of calls our entrypoints allow
@@ -38,6 +38,11 @@ fi
38
38
 
39
39
  TRANSFER_AMOUNT=42
40
40
 
41
+ # Alice's bridged fee-juice claim is only consumable inboxLag (2) checkpoints after the L1->L2 message
42
+ # is inserted. The token deploy and mint above produced two blocks; force one more here (a public mint
43
+ # from test0, which leaves the alice/bob private balances untouched) so the claim is available below.
44
+ aztec-wallet send mint_to_public -ca last --args accounts:test0 1 -f test0
45
+
41
46
  aztec-wallet send transfer_in_private -ca last --args accounts:alice accounts:bob $TRANSFER_AMOUNT 0 -f alice --payment method=fee_juice,claim
42
47
 
43
48
  # Test end result
@@ -0,0 +1,259 @@
1
+ import { appendFileSync, writeFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+
4
+ import CustomEnvironment from '../../../foundation/src/jest/env.mjs';
5
+
6
+ // Per-test e2e timing environment. Gated entirely on the TEST_TIMING_FILE env var: when unset, this
7
+ // behaves exactly like the base CustomEnvironment (it only delegates). When set, it records, per test
8
+ // worker process, the time spent in jest before/after hooks and the test body, and merges in the
9
+ // function-level time captured by setup.ts/teardown.ts via a collector shared on `this.global`.
10
+ //
11
+ // Output is one JSONL file per worker process (the env runs once per worker). Each line carries a
12
+ // `type` discriminator and is one of:
13
+ // - `type: 'test'` (`name` set): beforeEach hooks, the it() body, afterEach hooks for one test; or
14
+ // - `type: 'suite'` (`name: null`): the suite-scoped beforeAll/afterAll hooks for the whole file.
15
+ export default class TimingEnvironment extends CustomEnvironment {
16
+ constructor(config, context) {
17
+ super(config, context);
18
+
19
+ this.timingFile = process.env.TEST_TIMING_FILE;
20
+ if (!this.timingFile) {
21
+ return;
22
+ }
23
+
24
+ this.suite = basename(context?.testPath ?? config?.testPath ?? 'unknown').replace(/\.test\.[cm]?[jt]s$/, '');
25
+ this.meta = {
26
+ commit: process.env.COMMIT_HASH ?? null,
27
+ branch: process.env.TARGET_BRANCH ?? process.env.REF_NAME ?? null,
28
+ runId: process.env.RUN_ID ?? null,
29
+ };
30
+
31
+ // Shared collector. setup.ts (running in the sandbox realm) reads `globalThis.__e2eTimings`; this
32
+ // env (host realm) reads `this.global.__e2eTimings` — they are the same object. `current` is the
33
+ // full name of the test currently running (null during beforeAll/afterAll), used to tag fn spans.
34
+ this.collector = { current: null, fnSpans: [] };
35
+ this.global.__e2eTimings = this.collector;
36
+
37
+ // Records to flush as JSONL on teardown. One per test plus one suite-scoped record.
38
+ this.records = [];
39
+ this.recordsByName = new Map();
40
+ // The suite-scoped record (beforeAll/afterAll); bodyMs/totalMs are computed at flush time.
41
+ this.suiteRecord = { name: null, status: 'passed', beforeHooksMs: 0, afterHooksMs: 0 };
42
+ // Start times keyed by hook type (beforeAll/beforeEach/afterAll/afterEach).
43
+ this.hookStarts = {};
44
+ this.testStarts = new Map();
45
+ // Guards against double-flushing (we flush on both the teardown event and the teardown method).
46
+ this.flushed = false;
47
+ }
48
+
49
+ // Flush on the teardown method too: the jest lifecycle always calls this, whereas the 'teardown'
50
+ // event is not reliably delivered when a test run is interrupted. Whichever fires first wins.
51
+ async teardown() {
52
+ this.finalizeAndFlush();
53
+ await super.teardown();
54
+ }
55
+
56
+ async handleTestEvent(event, state) {
57
+ // Run the base env first so its unhandledRejection patching for after-hooks stays intact.
58
+ await super.handleTestEvent(event, state);
59
+
60
+ if (!this.timingFile) {
61
+ return;
62
+ }
63
+
64
+ const now = Date.now();
65
+ switch (event.name) {
66
+ case 'hook_start': {
67
+ this.hookStarts[event.hook.type] = now;
68
+ break;
69
+ }
70
+ case 'hook_success':
71
+ case 'hook_failure': {
72
+ const start = this.hookStarts[event.hook.type];
73
+ if (start === undefined) {
74
+ break;
75
+ }
76
+ delete this.hookStarts[event.hook.type];
77
+ const ms = now - start;
78
+ if (event.hook.type === 'beforeEach') {
79
+ this.addToCurrent('beforeHooksMs', ms);
80
+ } else if (event.hook.type === 'afterEach') {
81
+ this.addToCurrent('afterHooksMs', ms);
82
+ } else if (event.hook.type === 'beforeAll') {
83
+ this.suiteRecord.beforeHooksMs += ms;
84
+ } else if (event.hook.type === 'afterAll') {
85
+ this.suiteRecord.afterHooksMs += ms;
86
+ }
87
+ break;
88
+ }
89
+ case 'test_start': {
90
+ const name = this.fullTestName(event.test);
91
+ const record = {
92
+ type: 'test',
93
+ name,
94
+ status: 'passed',
95
+ setupFnMs: 0,
96
+ beforeHooksMs: 0,
97
+ bodyMs: 0,
98
+ teardownFnMs: 0,
99
+ afterHooksMs: 0,
100
+ totalMs: 0,
101
+ startedAt: new Date(now).toISOString(),
102
+ };
103
+ this.records.push(record);
104
+ this.recordsByName.set(name, record);
105
+ this.testStarts.set(name, now);
106
+ this.collector.current = name;
107
+ break;
108
+ }
109
+ case 'test_fn_start': {
110
+ this.testStarts.set(`fn:${this.fullTestName(event.test)}`, now);
111
+ break;
112
+ }
113
+ case 'test_fn_success':
114
+ case 'test_fn_failure': {
115
+ const name = this.fullTestName(event.test);
116
+ const start = this.testStarts.get(`fn:${name}`);
117
+ const record = this.recordsByName.get(name);
118
+ if (record && start !== undefined) {
119
+ record.bodyMs = now - start;
120
+ }
121
+ if (record && event.name === 'test_fn_failure') {
122
+ record.status = 'failed';
123
+ }
124
+ break;
125
+ }
126
+ case 'test_done': {
127
+ const name = this.fullTestName(event.test);
128
+ const record = this.recordsByName.get(name);
129
+ const start = this.testStarts.get(name);
130
+ if (record && start !== undefined) {
131
+ record.totalMs = now - start;
132
+ }
133
+ if (record && event.test?.errors?.length) {
134
+ record.status = 'failed';
135
+ }
136
+ this.mergeFnSpans(name, record);
137
+ this.collector.current = null;
138
+ break;
139
+ }
140
+ case 'teardown': {
141
+ this.finalizeAndFlush();
142
+ break;
143
+ }
144
+ default:
145
+ break;
146
+ }
147
+ }
148
+
149
+ /** The record for the test currently running, if any. */
150
+ currentRecord() {
151
+ return this.collector.current ? this.recordsByName.get(this.collector.current) : undefined;
152
+ }
153
+
154
+ /** Adds `ms` to a hook bucket on the current per-test record, or the suite record during beforeAll/afterAll. */
155
+ addToCurrent(field, ms) {
156
+ const record = this.currentRecord();
157
+ if (record) {
158
+ record[field] += ms;
159
+ }
160
+ }
161
+
162
+ /** Builds the full test name by walking up the describe blocks, matching jest's currentTestName. */
163
+ fullTestName(test) {
164
+ if (!test) {
165
+ return null;
166
+ }
167
+ const parts = [];
168
+ let node = test;
169
+ while (node && node.name && node.name !== 'ROOT_DESCRIBE_BLOCK') {
170
+ parts.unshift(node.name);
171
+ node = node.parent;
172
+ }
173
+ return parts.join(' ');
174
+ }
175
+
176
+ /** Moves fn spans tagged with `name` into the matching record's setup/teardown buckets. */
177
+ mergeFnSpans(name, record) {
178
+ if (!record) {
179
+ return;
180
+ }
181
+ const remaining = [];
182
+ for (const span of this.collector.fnSpans) {
183
+ if (span.name === name) {
184
+ if (span.kind === 'setup') {
185
+ record.setupFnMs += span.ms;
186
+ } else if (span.kind === 'teardown') {
187
+ record.teardownFnMs += span.ms;
188
+ }
189
+ } else {
190
+ remaining.push(span);
191
+ }
192
+ }
193
+ this.collector.fnSpans = remaining;
194
+ }
195
+
196
+ /** Finalizes the suite-scoped record (untagged fn spans + beforeAll/afterAll) and writes all JSONL. */
197
+ finalizeAndFlush() {
198
+ if (this.flushed || !this.timingFile) {
199
+ return;
200
+ }
201
+ this.flushed = true;
202
+ const suite = {
203
+ setupFnMs: 0,
204
+ beforeHooksMs: this.suiteRecord.beforeHooksMs,
205
+ teardownFnMs: 0,
206
+ afterHooksMs: this.suiteRecord.afterHooksMs,
207
+ };
208
+ for (const span of this.collector.fnSpans) {
209
+ if (span.name === null || span.name === undefined) {
210
+ if (span.kind === 'setup') {
211
+ suite.setupFnMs += span.ms;
212
+ } else if (span.kind === 'teardown') {
213
+ suite.teardownFnMs += span.ms;
214
+ }
215
+ }
216
+ }
217
+ this.collector.fnSpans = [];
218
+
219
+ const lines = [];
220
+ for (const record of this.records) {
221
+ lines.push(this.toLine({ ...record }));
222
+ }
223
+ lines.push(
224
+ this.toLine({
225
+ type: 'suite',
226
+ name: null,
227
+ status: 'passed',
228
+ setupFnMs: suite.setupFnMs,
229
+ beforeHooksMs: suite.beforeHooksMs,
230
+ teardownFnMs: suite.teardownFnMs,
231
+ afterHooksMs: suite.afterHooksMs,
232
+ totalMs: suite.beforeHooksMs + suite.afterHooksMs,
233
+ }),
234
+ );
235
+
236
+ const payload = lines.join('\n') + '\n';
237
+ try {
238
+ // One file per worker process, but a worker may run several suites: append so we keep them all.
239
+ appendFileSync(this.timingFile, payload);
240
+ } catch {
241
+ try {
242
+ writeFileSync(this.timingFile, payload);
243
+ } catch {
244
+ // Timing is best-effort; never fail a test because we couldn't write the file.
245
+ }
246
+ }
247
+ }
248
+
249
+ /** Flattens metadata onto a record and serializes to a single JSON line. */
250
+ toLine(record) {
251
+ const obj = { suite: this.suite, ...record, ...this.meta };
252
+ for (const key of Object.keys(obj)) {
253
+ if (key.endsWith('Ms') && typeof obj[key] === 'number') {
254
+ obj[key] = Math.round(obj[key]);
255
+ }
256
+ }
257
+ return JSON.stringify(obj);
258
+ }
259
+ }
@@ -208,7 +208,7 @@ export class LendingSimulator {
208
208
  expect(interestAccumulator).toEqual(this.accumulator);
209
209
  expect(asset['last_updated_ts']).toEqual(BigInt(this.time));
210
210
 
211
- for (const key of [this.account.address, AztecAddress.fromField(await this.account.key())]) {
211
+ for (const key of [this.account.address, AztecAddress.fromFieldUnsafe(await this.account.key())]) {
212
212
  const { result: privatePos } = await this.lendingContract.methods
213
213
  .get_position(key)
214
214
  .simulate({ from: this.account.address });