@aztec/end-to-end 0.0.1-commit.3f296a7d2 → 0.0.1-commit.3f5453c7b

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 (62) hide show
  1. package/README.md +27 -0
  2. package/dest/bench/client_flows/client_flows_benchmark.d.ts +1 -1
  3. package/dest/bench/client_flows/client_flows_benchmark.d.ts.map +1 -1
  4. package/dest/bench/client_flows/client_flows_benchmark.js +11 -26
  5. package/dest/e2e_blacklist_token_contract/blacklist_token_contract_test.d.ts +3 -2
  6. package/dest/e2e_blacklist_token_contract/blacklist_token_contract_test.d.ts.map +1 -1
  7. package/dest/e2e_blacklist_token_contract/blacklist_token_contract_test.js +1 -1
  8. package/dest/e2e_epochs/epochs_test.js +3 -3
  9. package/dest/e2e_fees/fees_test.js +1 -1
  10. package/dest/e2e_p2p/inactivity_slash_test.js +3 -3
  11. package/dest/e2e_p2p/p2p_network.d.ts +5 -7
  12. package/dest/e2e_p2p/p2p_network.d.ts.map +1 -1
  13. package/dest/e2e_p2p/p2p_network.js +14 -13
  14. package/dest/e2e_p2p/reqresp/utils.js +1 -1
  15. package/dest/e2e_p2p/shared.d.ts +5 -7
  16. package/dest/e2e_p2p/shared.d.ts.map +1 -1
  17. package/dest/e2e_p2p/shared.js +16 -42
  18. package/dest/fixtures/authwit_proxy.d.ts +1 -1
  19. package/dest/fixtures/authwit_proxy.d.ts.map +1 -1
  20. package/dest/fixtures/authwit_proxy.js +4 -0
  21. package/dest/fixtures/e2e_prover_test.d.ts +1 -1
  22. package/dest/fixtures/e2e_prover_test.d.ts.map +1 -1
  23. package/dest/fixtures/e2e_prover_test.js +4 -7
  24. package/dest/fixtures/setup.d.ts +11 -8
  25. package/dest/fixtures/setup.d.ts.map +1 -1
  26. package/dest/fixtures/setup.js +11 -9
  27. package/dest/fixtures/setup_p2p_test.d.ts +6 -6
  28. package/dest/fixtures/setup_p2p_test.d.ts.map +1 -1
  29. package/dest/fixtures/setup_p2p_test.js +8 -8
  30. package/dest/fixtures/token_utils.d.ts +1 -1
  31. package/dest/fixtures/token_utils.d.ts.map +1 -1
  32. package/dest/fixtures/token_utils.js +2 -5
  33. package/dest/legacy-jest-resolver.d.cts +3 -0
  34. package/dest/legacy-jest-resolver.d.cts.map +1 -0
  35. package/dest/spartan/setup_test_wallets.d.ts +1 -1
  36. package/dest/spartan/setup_test_wallets.d.ts.map +1 -1
  37. package/dest/spartan/setup_test_wallets.js +58 -40
  38. package/dest/test-wallet/test_wallet.d.ts +16 -8
  39. package/dest/test-wallet/test_wallet.d.ts.map +1 -1
  40. package/dest/test-wallet/test_wallet.js +82 -46
  41. package/dest/test-wallet/worker_wallet.d.ts +4 -4
  42. package/dest/test-wallet/worker_wallet.d.ts.map +1 -1
  43. package/dest/test-wallet/worker_wallet_schema.d.ts +7 -2
  44. package/dest/test-wallet/worker_wallet_schema.d.ts.map +1 -1
  45. package/package.json +40 -39
  46. package/src/bench/client_flows/client_flows_benchmark.ts +29 -21
  47. package/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts +3 -6
  48. package/src/e2e_epochs/epochs_test.ts +3 -3
  49. package/src/e2e_fees/fees_test.ts +1 -1
  50. package/src/e2e_p2p/inactivity_slash_test.ts +3 -3
  51. package/src/e2e_p2p/p2p_network.ts +22 -27
  52. package/src/e2e_p2p/reqresp/utils.ts +1 -1
  53. package/src/e2e_p2p/shared.ts +16 -57
  54. package/src/fixtures/authwit_proxy.ts +4 -0
  55. package/src/fixtures/e2e_prover_test.ts +7 -6
  56. package/src/fixtures/setup.ts +21 -17
  57. package/src/fixtures/setup_p2p_test.ts +9 -9
  58. package/src/fixtures/token_utils.ts +1 -4
  59. package/src/legacy-jest-resolver.cjs +135 -0
  60. package/src/spartan/setup_test_wallets.ts +59 -36
  61. package/src/test-wallet/test_wallet.ts +100 -50
  62. package/src/test-wallet/worker_wallet.ts +3 -2
@@ -7,6 +7,8 @@ import {
7
7
  BatchCall,
8
8
  type ContractFunctionInteraction,
9
9
  type ContractMethod,
10
+ type DeployInteractionWaitOptions,
11
+ type DeployOptions,
10
12
  getContractClassFromArtifact,
11
13
  waitForProven,
12
14
  } from '@aztec/aztec.js/contracts';
@@ -49,9 +51,10 @@ import type { ProverNodeConfig } from '@aztec/prover-node';
49
51
  import { type PXEConfig, getPXEConfig } from '@aztec/pxe/server';
50
52
  import type { SequencerClient } from '@aztec/sequencer-client';
51
53
  import { type ContractInstanceWithAddress, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
52
- import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
54
+ import type { AztecNodeAdmin, AztecNodeDebug } from '@aztec/stdlib/interfaces/client';
53
55
  import { tryStop } from '@aztec/stdlib/interfaces/server';
54
56
  import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
57
+ import type { GenesisData } from '@aztec/stdlib/world-state';
55
58
  import {
56
59
  type TelemetryClient,
57
60
  type TelemetryClientConfig,
@@ -178,6 +181,8 @@ export type SetupOptions = {
178
181
  proverNodeConfig?: Partial<ProverNodeConfig>;
179
182
  /** Whether to use a mock gossip sub network for p2p clients. */
180
183
  mockGossipSubNetwork?: boolean;
184
+ /** Whether to add simulated latency to the mock gossipsub network (in ms) */
185
+ mockGossipSubNetworkLatency?: number;
181
186
  /** Whether to disable the anvil test watcher (can still be manually started) */
182
187
  disableAnvilTestWatcher?: boolean;
183
188
  /** Whether to enable anvil automine during deployment of L1 contracts (consider defaulting this to true). */
@@ -210,7 +215,7 @@ export type EndToEndContext = {
210
215
  /** The Anvil instance (only set if anvil was started locally). */
211
216
  anvil: Anvil | undefined;
212
217
  /** The Aztec Node service or client a connected to it. */
213
- aztecNode: AztecNode;
218
+ aztecNode: AztecNode & AztecNodeDebug;
214
219
  /** The Aztec Node as a service. */
215
220
  aztecNodeService: AztecNodeService;
216
221
  /** Client to the Aztec Node admin interface. */
@@ -249,8 +254,8 @@ export type EndToEndContext = {
249
254
  sequencerDelayer: Delayer | undefined;
250
255
  /** Delayer for prover node L1 txs (only when enableDelayer and startProverNode are true). */
251
256
  proverDelayer: Delayer | undefined;
252
- /** Prefilled public data used for setting up nodes. */
253
- prefilledPublicData: PublicDataTreeLeaf[] | undefined;
257
+ /** Genesis data used for setting up nodes. */
258
+ genesis: GenesisData | undefined;
254
259
  /** ACVM config (only set if running locally). */
255
260
  acvmConfig: Awaited<ReturnType<typeof getACVMConfig>>;
256
261
  /** BB config (only set if running locally). */
@@ -276,7 +281,7 @@ export async function setup(
276
281
  let anvil: Anvil | undefined;
277
282
  try {
278
283
  opts.aztecTargetCommitteeSize ??= 0;
279
- opts.slasherFlavor ??= 'none';
284
+ opts.slasherEnabled ??= false;
280
285
 
281
286
  const config: AztecNodeConfig & SetupOptions = { ...getConfigEnvVars(), ...opts };
282
287
  // use initialValidators for the node config
@@ -375,10 +380,12 @@ export async function setup(
375
380
  addressesToFund.push(sponsoredFPCAddress);
376
381
  }
377
382
 
378
- const { genesisArchiveRoot, prefilledPublicData, fundingNeeded } = await getGenesisValues(
383
+ const genesisTimestamp = BigInt(Math.floor(Date.now() / 1000));
384
+ const { genesisArchiveRoot, genesis, fundingNeeded } = await getGenesisValues(
379
385
  addressesToFund,
380
386
  opts.initialAccountFeeJuice,
381
387
  opts.genesisPublicData,
388
+ genesisTimestamp,
382
389
  );
383
390
 
384
391
  const wasAutomining = await ethCheatCodes.isAutoMining();
@@ -467,7 +474,7 @@ export async function setup(
467
474
  let p2pClientDeps: P2PClientDeps | undefined = undefined;
468
475
 
469
476
  if (opts.mockGossipSubNetwork) {
470
- mockGossipSubNetwork = new MockGossipSubNetwork();
477
+ mockGossipSubNetwork = new MockGossipSubNetwork(opts.mockGossipSubNetworkLatency);
471
478
  p2pClientDeps = { p2pServiceFactory: getMockPubSubP2PServiceFactory(mockGossipSubNetwork) };
472
479
  }
473
480
 
@@ -496,11 +503,7 @@ export async function setup(
496
503
  }
497
504
 
498
505
  const aztecNodeService = await withLoggerBindings({ actor: 'node-0' }, () =>
499
- AztecNodeService.createAndSync(
500
- config,
501
- { dateProvider, telemetry: telemetryClient, p2pClientDeps },
502
- { prefilledPublicData },
503
- ),
506
+ AztecNodeService.createAndSync(config, { dateProvider, telemetry: telemetryClient, p2pClientDeps }, { genesis }),
504
507
  );
505
508
  const sequencerClient = aztecNodeService.getSequencer();
506
509
 
@@ -524,7 +527,7 @@ export async function setup(
524
527
  dataDirectory: proverNodeDataDirectory,
525
528
  },
526
529
  { dateProvider, p2pClientDeps, telemetry: telemetryClient },
527
- { prefilledPublicData },
530
+ { genesis },
528
531
  ));
529
532
  }
530
533
 
@@ -629,7 +632,7 @@ export async function setup(
629
632
  initialFundedAccounts,
630
633
  logger,
631
634
  mockGossipSubNetwork,
632
- prefilledPublicData,
635
+ genesis,
633
636
  proverNode,
634
637
  sequencerDelayer,
635
638
  proverDelayer,
@@ -729,7 +732,7 @@ export function createAndSyncProverNode(
729
732
  dateProvider: DateProvider;
730
733
  p2pClientDeps?: P2PClientDeps;
731
734
  },
732
- options: { prefilledPublicData: PublicDataTreeLeaf[]; dontStart?: boolean },
735
+ options: { genesis?: GenesisData; dontStart?: boolean },
733
736
  ): Promise<{ proverNode: AztecNodeService }> {
734
737
  return withLoggerBindings({ actor: 'prover-0' }, async () => {
735
738
  const proverNode = await AztecNodeService.createAndSync(
@@ -742,7 +745,7 @@ export function createAndSyncProverNode(
742
745
  proverPublisherPrivateKeys: [new SecretValue(proverNodePrivateKey)],
743
746
  },
744
747
  deps,
745
- { ...options, dontStartProverNode: options.dontStart },
748
+ { genesis: options.genesis, dontStartProverNode: options.dontStart },
746
749
  );
747
750
 
748
751
  if (!proverNode.getProverNode()) {
@@ -833,7 +836,7 @@ export async function ensureAccountContractsPublished(wallet: Wallet, accountsTo
833
836
  * Returns deployed account data that can be used by tests.
834
837
  */
835
838
  export const deployAccounts =
836
- (numberOfAccounts: number, logger: Logger) =>
839
+ (numberOfAccounts: number, logger: Logger, deployOptions?: Partial<DeployOptions<DeployInteractionWaitOptions>>) =>
837
840
  async ({ wallet, initialFundedAccounts }: { wallet: TestWallet; initialFundedAccounts: InitialAccountData[] }) => {
838
841
  if (initialFundedAccounts.length < numberOfAccounts) {
839
842
  throw new Error(`Cannot deploy more than ${initialFundedAccounts.length} initial accounts.`);
@@ -852,6 +855,7 @@ export const deployAccounts =
852
855
  await deployMethod.send({
853
856
  from: NO_FROM,
854
857
  skipClassPublication: i !== 0, // Publish the contract class at most once.
858
+ ...deployOptions,
855
859
  });
856
860
  }
857
861
 
@@ -7,7 +7,7 @@ import { SecretValue } from '@aztec/foundation/config';
7
7
  import { withLoggerBindings } from '@aztec/foundation/log/server';
8
8
  import { bufferToHex } from '@aztec/foundation/string';
9
9
  import type { DateProvider } from '@aztec/foundation/timer';
10
- import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
10
+ import type { GenesisData } from '@aztec/stdlib/world-state';
11
11
 
12
12
  import getPort from 'get-port';
13
13
 
@@ -40,7 +40,7 @@ export async function createNodes(
40
40
  bootstrapNodeEnr: string,
41
41
  numNodes: number,
42
42
  bootNodePort: number,
43
- prefilledPublicData?: PublicDataTreeLeaf[],
43
+ genesis?: GenesisData,
44
44
  dataDirectory?: string,
45
45
  metricsPort?: number,
46
46
  indexOffset = 0,
@@ -65,7 +65,7 @@ export async function createNodes(
65
65
  port,
66
66
  bootstrapNodeEnr,
67
67
  validatorIndices,
68
- prefilledPublicData,
68
+ genesis,
69
69
  dataDir,
70
70
  metricsPort,
71
71
  );
@@ -97,7 +97,7 @@ export async function createNode(
97
97
  tcpPort: number,
98
98
  bootstrapNode: string | undefined,
99
99
  addressIndex: number | number[],
100
- prefilledPublicData?: PublicDataTreeLeaf[],
100
+ genesis?: GenesisData,
101
101
  dataDirectory?: string,
102
102
  metricsPort?: number,
103
103
  ) {
@@ -108,7 +108,7 @@ export async function createNode(
108
108
  return await AztecNodeService.createAndSync(
109
109
  validatorConfig,
110
110
  { telemetry, dateProvider },
111
- { prefilledPublicData, dontStartSequencer: config.dontStartSequencer },
111
+ { genesis, dontStartSequencer: config.dontStartSequencer },
112
112
  );
113
113
  });
114
114
  }
@@ -119,7 +119,7 @@ export async function createNonValidatorNode(
119
119
  dateProvider: DateProvider,
120
120
  tcpPort: number,
121
121
  bootstrapNode: string | undefined,
122
- prefilledPublicData?: PublicDataTreeLeaf[],
122
+ genesis?: GenesisData,
123
123
  dataDirectory?: string,
124
124
  metricsPort?: number,
125
125
  ) {
@@ -133,7 +133,7 @@ export async function createNonValidatorNode(
133
133
  sequencerPublisherPrivateKeys: [],
134
134
  };
135
135
  const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
136
- return await AztecNodeService.createAndSync(config, { telemetry, dateProvider }, { prefilledPublicData });
136
+ return await AztecNodeService.createAndSync(config, { telemetry, dateProvider }, { genesis });
137
137
  });
138
138
  }
139
139
 
@@ -143,7 +143,7 @@ export async function createProverNode(
143
143
  bootstrapNode: string | undefined,
144
144
  addressIndex: number,
145
145
  deps: { dateProvider: DateProvider },
146
- prefilledPublicData?: PublicDataTreeLeaf[],
146
+ genesis?: GenesisData,
147
147
  dataDirectory?: string,
148
148
  metricsPort?: number,
149
149
  ): Promise<{ proverNode: AztecNodeService }> {
@@ -159,7 +159,7 @@ export async function createProverNode(
159
159
  { ...config, ...p2pConfig },
160
160
  { dataDirectory },
161
161
  { ...deps, telemetry },
162
- { prefilledPublicData: prefilledPublicData ?? [] },
162
+ { genesis },
163
163
  );
164
164
  });
165
165
  }
@@ -6,11 +6,8 @@ import { TokenContract } from '@aztec/noir-contracts.js/Token';
6
6
 
7
7
  export async function deployToken(wallet: Wallet, admin: AztecAddress, initialAdminBalance: bigint, logger: Logger) {
8
8
  logger.info(`Deploying Token contract...`);
9
- const {
10
- receipt: { contract, instance },
11
- } = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({
9
+ const { contract, instance } = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({
12
10
  from: admin,
13
- wait: { returnReceipt: true },
14
11
  });
15
12
 
16
13
  if (initialAdminBalance > 0n) {
@@ -0,0 +1,135 @@
1
+ // Custom Jest resolver. When CONTRACT_ARTIFACTS_VERSION is set, redirects *only* JSON artifact files under
2
+ // @aztec/noir-contracts.js/artifacts/, @aztec/noir-test-contracts.js/artifacts/, and @aztec/accounts/artifacts/ to a local cache of the pinned
3
+ // legacy versions. TypeScript wrapper classes (e.g. Token.ts) continue to load from the current workspace and use the
4
+ // current @aztec/aztec.js — only the artifact JSON (the deployed-contract ABI / bytecode / notes surface) is swapped.
5
+ //
6
+ // Why JSON-only: the JSON artifact is the actual interchange surface a "deployed contract" exposes. The TS wrapper is
7
+ // generated client-side ergonomics that's tightly coupled to the current @aztec/aztec.js API. Redirecting the wrapper
8
+ // would couple this test to a moving aztec.js surface and break at import time on unrelated breaking changes; we want
9
+ // to fail only on actual artifact-compat regressions.
10
+ //
11
+ // The cache is populated on demand by running `npm install` into .legacy-contracts/<version>/.
12
+ //
13
+ // Activated by env var; passthrough otherwise.
14
+ /* eslint-disable @typescript-eslint/no-require-imports */
15
+
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const { execSync } = require('child_process');
19
+
20
+ const version = process.env.CONTRACT_ARTIFACTS_VERSION;
21
+ const REDIRECTED = ['@aztec/noir-contracts.js', '@aztec/noir-test-contracts.js', '@aztec/accounts'];
22
+
23
+ // Jest sets rootDir to <e2e>/src; this file lives there too.
24
+ const e2eRoot = path.resolve(__dirname, '..');
25
+ const cacheRoot = version ? path.join(e2eRoot, '.legacy-contracts', version) : null;
26
+
27
+ function pkgJsonPath(name) {
28
+ return path.join(cacheRoot, 'node_modules', name, 'package.json');
29
+ }
30
+
31
+ function ensureCache() {
32
+ const missing = REDIRECTED.some(p => !fs.existsSync(pkgJsonPath(p)));
33
+ if (!missing) {
34
+ return;
35
+ }
36
+ fs.mkdirSync(cacheRoot, { recursive: true });
37
+ // Seed a standalone package.json so `npm install --prefix` treats cacheRoot as its own project. Without this, npm
38
+ // walks up and finds the yarn-project workspace root, which breaks on `workspace:` protocol deps and risks
39
+ // clobbering the monorepo's node_modules.
40
+ const seed = path.join(cacheRoot, 'package.json');
41
+ if (!fs.existsSync(seed)) {
42
+ fs.writeFileSync(seed, JSON.stringify({ name: 'legacy-contracts-cache', private: true }));
43
+ }
44
+
45
+ const specs = REDIRECTED.map(p => `${p}@${version}`).join(' ');
46
+ process.stderr.write(`[legacy-contracts] installing ${specs} into ${cacheRoot}\n`);
47
+ // --prefix: install into cacheRoot instead of cwd, so the cache is isolated from the monorepo.
48
+ // --no-save: don't write the installed packages back to the seeded package.json.
49
+ // --ignore-scripts: skip lifecycle scripts (preinstall/postinstall) of the legacy packages and their transitive
50
+ // deps; we only want the files on disk, not to run any build steps.
51
+ // --legacy-peer-deps: tolerate peer-dependency mismatches between the pinned legacy @aztec/* graph and whatever
52
+ // current versions npm would otherwise try to reconcile.
53
+ execSync(`npm install --prefix "${cacheRoot}" --no-save --ignore-scripts --legacy-peer-deps ${specs}`, {
54
+ stdio: 'inherit',
55
+ });
56
+
57
+ // Verify versions on disk match the requested version.
58
+ for (const p of REDIRECTED) {
59
+ const onDisk = JSON.parse(fs.readFileSync(pkgJsonPath(p), 'utf8')).version;
60
+ if (onDisk !== version) {
61
+ throw new Error(`[legacy-contracts] ${p} on disk is ${onDisk}, expected ${version}`);
62
+ }
63
+ }
64
+ }
65
+
66
+ if (version) {
67
+ ensureCache();
68
+ }
69
+
70
+ let bannerPrinted = false;
71
+ const seen = new Set();
72
+
73
+ function printBannerOnce() {
74
+ if (bannerPrinted || !version) {
75
+ return;
76
+ }
77
+ bannerPrinted = true;
78
+ const lines = ['='.repeat(60), `[legacy-contracts][jest] CONTRACT_ARTIFACTS_VERSION=${version}`];
79
+ for (const p of REDIRECTED) {
80
+ const v = JSON.parse(fs.readFileSync(pkgJsonPath(p), 'utf8')).version;
81
+ if (v !== version) {
82
+ throw new Error(`[legacy-contracts] ${p} on disk is ${v}, expected ${version}`);
83
+ }
84
+ lines.push(`[legacy-contracts][jest] redirecting ${p}/artifacts/*.json -> .legacy-contracts/${version}/...`);
85
+ }
86
+ lines.push('='.repeat(60));
87
+ process.stderr.write(lines.join('\n') + '\n');
88
+ }
89
+
90
+ // Match a resolved absolute path against the workspace artifacts dirs and return the legacy cache equivalent, or null
91
+ // if it's not an artifact path we should redirect.
92
+ function legacyArtifactPath(resolved) {
93
+ if (!resolved.endsWith('.json')) {
94
+ return null;
95
+ }
96
+ for (const pkg of REDIRECTED) {
97
+ // pkg = '@aztec/noir-contracts.js' -> match '/noir-contracts.js/artifacts/'
98
+ const dirName = pkg.split('/')[1];
99
+ const marker = `/${dirName}/artifacts/`;
100
+ const idx = resolved.indexOf(marker);
101
+ if (idx === -1) {
102
+ continue;
103
+ }
104
+ const basename = resolved.slice(idx + marker.length);
105
+ return path.join(cacheRoot, 'node_modules', pkg, 'artifacts', basename);
106
+ }
107
+ return null;
108
+ }
109
+
110
+ module.exports = function legacyResolver(request, options) {
111
+ // Always run the default resolver first. We only inspect (and possibly rewrite) the *result*; this catches both
112
+ // bare-specifier imports of `@aztec/noir-contracts.js/artifacts/foo.json` and the relative `../artifacts/foo.json`
113
+ // imports inside the workspace TS wrapper classes — both resolve to the same workspace artifact path that we then
114
+ // redirect.
115
+ const resolved = options.defaultResolver(request, options);
116
+ if (!version) {
117
+ return resolved;
118
+ }
119
+ printBannerOnce();
120
+ const legacy = legacyArtifactPath(resolved);
121
+ if (!legacy) {
122
+ return resolved;
123
+ }
124
+ if (!fs.existsSync(legacy)) {
125
+ throw new Error(
126
+ `[legacy-contracts] artifact ${path.basename(legacy)} not present in legacy cache @${version}; ` +
127
+ `the contract may have been added after that release. Pin a newer CONTRACT_ARTIFACTS_VERSION or skip this test.`,
128
+ );
129
+ }
130
+ if (!seen.has(resolved)) {
131
+ seen.add(resolved);
132
+ process.stderr.write(`[legacy-contracts][jest] redirected ${path.basename(legacy)} -> ${legacy}\n`);
133
+ }
134
+ return legacy;
135
+ };
@@ -11,7 +11,7 @@ import type { Wallet } from '@aztec/aztec.js/wallet';
11
11
  import { createEthereumChain } from '@aztec/ethereum/chain';
12
12
  import { createExtendedL1Client } from '@aztec/ethereum/client';
13
13
  import type { Logger } from '@aztec/foundation/log';
14
- import { retryUntil } from '@aztec/foundation/retry';
14
+ import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry';
15
15
  import { TokenContract } from '@aztec/noir-contracts.js/Token';
16
16
  import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client';
17
17
  import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing';
@@ -138,38 +138,57 @@ async function deployAccountWithDiagnostics(
138
138
  estimateGas?: boolean,
139
139
  ): Promise<void> {
140
140
  const deployMethod = await account.getDeployMethod();
141
- let txHash;
142
- try {
143
- let gasSettings;
144
- if (estimateGas) {
145
- const sim = await deployMethod.simulate({ from: NO_FROM, fee: { paymentMethod } });
146
- gasSettings = sim.estimatedGas;
147
- logger.info(`${accountLabel} estimated gas: DA=${gasSettings.gasLimits.daGas} L2=${gasSettings.gasLimits.l2Gas}`);
148
- }
149
- const deployResult = await deployMethod.send({
150
- from: NO_FROM,
151
- fee: { paymentMethod, gasSettings },
152
- wait: NO_WAIT,
153
- });
154
- txHash = deployResult.txHash;
155
- await waitForTx(aztecNode, txHash, { timeout: 2400 });
156
- logger.info(`${accountLabel} deployed at ${account.address}`);
157
- } catch (error) {
158
- const blockNumber = await aztecNode.getBlockNumber();
159
- let receipt;
160
- try {
161
- receipt = await aztecNode.getTxReceipt(txHash);
162
- } catch {
163
- receipt = 'unavailable';
164
- }
165
- logger.error(`${accountLabel} deployment failed`, {
166
- txHash: txHash.toString(),
167
- receipt: JSON.stringify(receipt),
168
- currentBlockNumber: blockNumber,
169
- error: String(error),
170
- });
171
- throw error;
141
+ let gasSettings: any;
142
+ if (estimateGas) {
143
+ const sim = await deployMethod.simulate({ from: NO_FROM, fee: { paymentMethod } });
144
+ gasSettings = sim.estimatedGas;
145
+ logger.info(`${accountLabel} estimated gas: DA=${gasSettings.gasLimits.daGas} L2=${gasSettings.gasLimits.l2Gas}`);
172
146
  }
147
+
148
+ // Track the tx hash across retries so we don't re-send when the previous tx is still pending.
149
+ let sentTxHash: { txHash: any } | undefined;
150
+
151
+ await retry(
152
+ async () => {
153
+ // Check if already deployed (handles case where previous attempt succeeded but waitForTx timed out)
154
+ const existing = await aztecNode.getContract(account.address);
155
+ if (existing) {
156
+ logger.info(`${accountLabel} already deployed at ${account.address}, skipping`);
157
+ return;
158
+ }
159
+
160
+ // If we already sent a tx, check if it was dropped before deciding to re-send.
161
+ if (sentTxHash) {
162
+ const prevReceipt = await aztecNode.getTxReceipt(sentTxHash.txHash);
163
+ if (prevReceipt.isDropped()) {
164
+ logger.info(`${accountLabel} previous tx ${sentTxHash.txHash} was dropped, re-sending`);
165
+ sentTxHash = undefined;
166
+ } else {
167
+ logger.info(`${accountLabel} previous tx ${sentTxHash.txHash} still pending, waiting again...`);
168
+ }
169
+ }
170
+
171
+ if (!sentTxHash) {
172
+ const deployResult = await deployMethod.send({
173
+ from: NO_FROM,
174
+ fee: { paymentMethod, gasSettings },
175
+ wait: NO_WAIT,
176
+ });
177
+ sentTxHash = { txHash: deployResult.txHash };
178
+ logger.info(`${accountLabel} tx sent`, { txHash: sentTxHash.txHash.toString() });
179
+ }
180
+
181
+ const receipt = await waitForTx(aztecNode, sentTxHash.txHash, { timeout: 600 });
182
+ if (receipt.isDropped()) {
183
+ sentTxHash = undefined;
184
+ throw new Error(`${accountLabel} tx was dropped, retrying...`);
185
+ }
186
+ logger.info(`${accountLabel} deployed at ${account.address}`);
187
+ },
188
+ `deploy ${accountLabel}`,
189
+ makeBackoff([1, 2, 4, 8, 16]),
190
+ logger,
191
+ );
173
192
  }
174
193
 
175
194
  async function deployAccountsInBatches(
@@ -346,14 +365,18 @@ async function deployTokenAndMint(
346
365
  logger: Logger,
347
366
  ) {
348
367
  logger.verbose(`Deploying TokenContract...`);
349
- const {
350
- receipt: { contract: tokenContract },
351
- } = await TokenContract.deploy(wallet, admin, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS).send({
368
+ const { contract: tokenContract } = await TokenContract.deploy(
369
+ wallet,
370
+ admin,
371
+ TOKEN_NAME,
372
+ TOKEN_SYMBOL,
373
+ TOKEN_DECIMALS,
374
+ ).send({
352
375
  from: admin,
353
376
  fee: {
354
377
  paymentMethod,
355
378
  },
356
- wait: { timeout: 600, returnReceipt: true },
379
+ wait: { timeout: 600 },
357
380
  });
358
381
 
359
382
  const tokenAddress = tokenContract.address;