@aztec/ethereum 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d

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 (144) hide show
  1. package/dest/client.d.ts +19 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +52 -3
  4. package/dest/config.d.ts +15 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +29 -9
  7. package/dest/contracts/chain_state_override.d.ts +78 -0
  8. package/dest/contracts/chain_state_override.d.ts.map +1 -0
  9. package/dest/contracts/chain_state_override.js +137 -0
  10. package/dest/contracts/empire_base.d.ts +1 -3
  11. package/dest/contracts/empire_base.d.ts.map +1 -1
  12. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  13. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  14. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  15. package/dest/contracts/governance.d.ts +135 -35
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +226 -15
  18. package/dest/contracts/governance_proposer.d.ts +33 -4
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +52 -13
  21. package/dest/contracts/gse.d.ts +4 -2
  22. package/dest/contracts/gse.d.ts.map +1 -1
  23. package/dest/contracts/gse.js +2 -2
  24. package/dest/contracts/inbox.d.ts +3 -3
  25. package/dest/contracts/inbox.d.ts.map +1 -1
  26. package/dest/contracts/inbox.js +9 -3
  27. package/dest/contracts/index.d.ts +3 -3
  28. package/dest/contracts/index.d.ts.map +1 -1
  29. package/dest/contracts/index.js +2 -2
  30. package/dest/contracts/multicall.d.ts +79 -13
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +117 -95
  33. package/dest/contracts/outbox.d.ts +27 -4
  34. package/dest/contracts/outbox.d.ts.map +1 -1
  35. package/dest/contracts/outbox.js +34 -7
  36. package/dest/contracts/rollup.d.ts +5401 -34
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +271 -119
  39. package/dest/contracts/slashing_proposer.d.ts +171 -0
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/slashing_proposer.js +753 -0
  42. package/dest/contracts/watch_event.d.ts +36 -0
  43. package/dest/contracts/watch_event.d.ts.map +1 -0
  44. package/dest/contracts/watch_event.js +66 -0
  45. package/dest/deploy_aztec_l1_contracts.d.ts +4 -7
  46. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  47. package/dest/deploy_aztec_l1_contracts.js +15 -8
  48. package/dest/foundry_binary.d.ts +13 -0
  49. package/dest/foundry_binary.d.ts.map +1 -0
  50. package/dest/foundry_binary.js +52 -0
  51. package/dest/generated/l1-contracts-defaults.d.ts +2 -2
  52. package/dest/generated/l1-contracts-defaults.js +2 -2
  53. package/dest/l1_artifacts.d.ts +14941 -16273
  54. package/dest/l1_artifacts.d.ts.map +1 -1
  55. package/dest/l1_artifacts.js +9 -24
  56. package/dest/l1_contract_addresses.d.ts +61 -65
  57. package/dest/l1_contract_addresses.d.ts.map +1 -1
  58. package/dest/l1_contract_addresses.js +101 -82
  59. package/dest/l1_reader.d.ts +3 -5
  60. package/dest/l1_reader.d.ts.map +1 -1
  61. package/dest/l1_reader.js +3 -6
  62. package/dest/l1_tx_utils/config.js +3 -3
  63. package/dest/l1_tx_utils/l1_tx_utils.d.ts +7 -1
  64. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  65. package/dest/l1_tx_utils/l1_tx_utils.js +82 -29
  66. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  67. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  68. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +49 -19
  69. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  70. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  71. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  72. package/dest/publisher_manager.d.ts +18 -6
  73. package/dest/publisher_manager.d.ts.map +1 -1
  74. package/dest/publisher_manager.js +44 -7
  75. package/dest/queries.d.ts +13 -2
  76. package/dest/queries.d.ts.map +1 -1
  77. package/dest/queries.js +62 -3
  78. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  79. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  80. package/dest/test/blob_kzg_warmup.js +64 -0
  81. package/dest/test/chain_monitor.d.ts +34 -2
  82. package/dest/test/chain_monitor.d.ts.map +1 -1
  83. package/dest/test/chain_monitor.js +76 -6
  84. package/dest/test/eth_cheat_codes.d.ts +26 -3
  85. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  86. package/dest/test/eth_cheat_codes.js +61 -40
  87. package/dest/test/index.d.ts +2 -1
  88. package/dest/test/index.d.ts.map +1 -1
  89. package/dest/test/index.js +1 -0
  90. package/dest/test/recording_rpc_server.d.ts +18 -0
  91. package/dest/test/recording_rpc_server.d.ts.map +1 -0
  92. package/dest/test/recording_rpc_server.js +68 -0
  93. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  94. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  95. package/dest/test/rollup_cheat_codes.js +86 -7
  96. package/dest/test/start_anvil.d.ts +1 -1
  97. package/dest/test/start_anvil.d.ts.map +1 -1
  98. package/dest/test/start_anvil.js +41 -5
  99. package/dest/utils.d.ts +9 -2
  100. package/dest/utils.d.ts.map +1 -1
  101. package/dest/utils.js +45 -14
  102. package/package.json +6 -6
  103. package/src/client.ts +63 -1
  104. package/src/config.ts +44 -13
  105. package/src/contracts/chain_state_override.ts +200 -0
  106. package/src/contracts/empire_base.ts +0 -2
  107. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  108. package/src/contracts/governance.ts +310 -10
  109. package/src/contracts/governance_proposer.ts +56 -13
  110. package/src/contracts/gse.ts +7 -2
  111. package/src/contracts/inbox.ts +12 -4
  112. package/src/contracts/index.ts +2 -2
  113. package/src/contracts/multicall.ts +179 -105
  114. package/src/contracts/outbox.ts +42 -8
  115. package/src/contracts/rollup.ts +302 -119
  116. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +119 -45
  117. package/src/contracts/watch_event.ts +110 -0
  118. package/src/deploy_aztec_l1_contracts.ts +19 -10
  119. package/src/foundry_binary.ts +57 -0
  120. package/src/generated/l1-contracts-defaults.ts +2 -2
  121. package/src/l1_artifacts.ts +12 -35
  122. package/src/l1_contract_addresses.ts +120 -94
  123. package/src/l1_reader.ts +4 -9
  124. package/src/l1_tx_utils/config.ts +3 -3
  125. package/src/l1_tx_utils/l1_tx_utils.ts +83 -36
  126. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +49 -13
  127. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  128. package/src/publisher_manager.ts +54 -18
  129. package/src/queries.ts +70 -4
  130. package/src/test/blob_kzg_warmup.ts +65 -0
  131. package/src/test/chain_monitor.ts +93 -5
  132. package/src/test/eth_cheat_codes.ts +57 -43
  133. package/src/test/index.ts +1 -0
  134. package/src/test/recording_rpc_server.ts +69 -0
  135. package/src/test/rollup_cheat_codes.ts +115 -6
  136. package/src/test/start_anvil.ts +38 -6
  137. package/src/utils.ts +41 -17
  138. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  139. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  140. package/dest/contracts/empire_slashing_proposer.js +0 -216
  141. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  142. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  143. package/dest/contracts/tally_slashing_proposer.js +0 -320
  144. package/src/contracts/empire_slashing_proposer.ts +0 -265
@@ -1,15 +1,15 @@
1
- import type { ConfigMappingsType } from '@aztec/foundation/config';
1
+ import type { ConfigMapping, ConfigMappingsType } from '@aztec/foundation/config';
2
2
  import { EthAddress } from '@aztec/foundation/eth-address';
3
- import { schemas, zodFor } from '@aztec/foundation/schemas';
3
+ import { schemas } from '@aztec/foundation/schemas';
4
4
 
5
5
  import { z } from 'zod';
6
6
 
7
7
  /**
8
- * The names of the current L1 contract addresses.
8
+ * Required L1 contract address keys.
9
9
  * NOTE: When changing this list, make sure to update CLI & CI scripts accordingly.
10
10
  * For reference: https://github.com/AztecProtocol/aztec-packages/pull/5553
11
11
  */
12
- export const L1ContractsNames = [
12
+ const REQUIRED_L1_CONTRACT_ADDRESS_KEYS = [
13
13
  'rollupAddress',
14
14
  'registryAddress',
15
15
  'inboxAddress',
@@ -23,102 +23,128 @@ export const L1ContractsNames = [
23
23
  'stakingAssetAddress',
24
24
  ] as const;
25
25
 
26
+ /** Optional L1 contract address keys — present only in some deployments. */
27
+ export const OPTIONAL_L1_CONTRACT_ADDRESS_KEYS = [
28
+ 'feeAssetHandlerAddress',
29
+ 'stakingAssetHandlerAddress',
30
+ 'zkPassportVerifierAddress',
31
+ 'gseAddress',
32
+ 'dateGatedRelayerAddress',
33
+ ] as const;
34
+
35
+ type RequiredContractKey = (typeof REQUIRED_L1_CONTRACT_ADDRESS_KEYS)[number];
36
+ type OptionalContractKey = (typeof OPTIONAL_L1_CONTRACT_ADDRESS_KEYS)[number];
37
+
26
38
  /** Provides the directory of current L1 contract addresses */
27
39
  export type L1ContractAddresses = {
28
- [K in (typeof L1ContractsNames)[number]]: EthAddress;
40
+ [K in RequiredContractKey]: EthAddress;
29
41
  } & {
30
- slashFactoryAddress?: EthAddress | undefined;
31
- feeAssetHandlerAddress?: EthAddress | undefined;
32
- stakingAssetHandlerAddress?: EthAddress | undefined;
33
- zkPassportVerifierAddress?: EthAddress | undefined;
34
- gseAddress?: EthAddress | undefined;
35
- dateGatedRelayerAddress?: EthAddress | undefined;
42
+ [K in OptionalContractKey]?: EthAddress;
36
43
  };
37
44
 
38
- export const L1ContractAddressesSchema = zodFor<L1ContractAddresses>()(
39
- z.object({
40
- rollupAddress: schemas.EthAddress,
41
- registryAddress: schemas.EthAddress,
42
- inboxAddress: schemas.EthAddress,
43
- outboxAddress: schemas.EthAddress,
44
- feeJuiceAddress: schemas.EthAddress,
45
- stakingAssetAddress: schemas.EthAddress,
46
- feeJuicePortalAddress: schemas.EthAddress,
47
- coinIssuerAddress: schemas.EthAddress,
48
- rewardDistributorAddress: schemas.EthAddress,
49
- governanceProposerAddress: schemas.EthAddress,
50
- governanceAddress: schemas.EthAddress,
51
- slashFactoryAddress: schemas.EthAddress.optional(),
52
- feeAssetHandlerAddress: schemas.EthAddress.optional(),
53
- stakingAssetHandlerAddress: schemas.EthAddress.optional(),
54
- zkPassportVerifierAddress: schemas.EthAddress.optional(),
55
- gseAddress: schemas.EthAddress.optional(),
56
- dateGatedRelayerAddress: schemas.EthAddress.optional(),
57
- }),
58
- );
45
+ /**
46
+ * Names of required L1 contract addresses.
47
+ * NOTE: When changing the set of contracts, update CLI & CI scripts accordingly.
48
+ * For reference: https://github.com/AztecProtocol/aztec-packages/pull/5553
49
+ */
50
+ export const L1ContractsNames = REQUIRED_L1_CONTRACT_ADDRESS_KEYS;
59
51
 
60
52
  const parseEnv = (val: string) => EthAddress.fromString(val);
53
+ const addrEnv = (description: string, env?: ConfigMapping<EthAddress>['env']): ConfigMapping<EthAddress> => ({
54
+ description,
55
+ parseEnv,
56
+ ...(env && { env }),
57
+ });
61
58
 
62
- export const l1ContractAddressesMapping: ConfigMappingsType<
63
- Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress' | 'dateGatedRelayerAddress'>
64
- > = {
65
- registryAddress: {
66
- env: 'REGISTRY_CONTRACT_ADDRESS',
67
- description: 'The deployed L1 registry contract address.',
68
- parseEnv,
69
- },
70
- slashFactoryAddress: {
71
- env: 'SLASH_FACTORY_CONTRACT_ADDRESS',
72
- description: 'The deployed L1 slashFactory contract address',
73
- parseEnv,
74
- },
75
- feeAssetHandlerAddress: {
76
- env: 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS',
77
- description: 'The deployed L1 feeAssetHandler contract address',
78
- parseEnv,
79
- },
80
- rollupAddress: {
81
- description: 'The deployed L1 rollup contract address.',
82
- parseEnv,
83
- },
84
- inboxAddress: {
85
- description: 'The deployed L1 inbox contract address.',
86
- parseEnv,
87
- },
88
- outboxAddress: {
89
- description: 'The deployed L1 outbox contract address.',
90
- parseEnv,
91
- },
92
- feeJuiceAddress: {
93
- description: 'The deployed L1 Fee Juice contract address.',
94
- parseEnv,
95
- },
96
- stakingAssetAddress: {
97
- description: 'The deployed L1 staking asset contract address.',
98
- parseEnv,
99
- },
100
- feeJuicePortalAddress: {
101
- description: 'The deployed L1 Fee Juice portal contract address.',
102
- parseEnv,
103
- },
104
- coinIssuerAddress: {
105
- description: 'The deployed L1 coinIssuer contract address',
106
- parseEnv,
107
- },
108
- rewardDistributorAddress: {
109
- description: 'The deployed L1 rewardDistributor contract address',
110
- parseEnv,
111
- },
112
- governanceProposerAddress: {
113
- description: 'The deployed L1 governanceProposer contract address',
114
- parseEnv,
115
- },
116
- governanceAddress: {
117
- description: 'The deployed L1 governance contract address',
118
- parseEnv,
119
- },
120
- stakingAssetHandlerAddress: {
121
- description: 'The deployed L1 stakingAssetHandler contract address',
122
- parseEnv,
123
- },
59
+ /**
60
+ * Config mapping for all L1 contract addresses.
61
+ * This is the single source of truth for which contract addresses exist and how to parse them.
62
+ */
63
+ export const l1ContractAddressesMapping: ConfigMappingsType<L1ContractAddresses> = {
64
+ rollupAddress: addrEnv('The deployed L1 rollup contract address.'),
65
+ registryAddress: addrEnv('The deployed L1 registry contract address.', 'REGISTRY_CONTRACT_ADDRESS'),
66
+ inboxAddress: addrEnv('The deployed L1 inbox contract address.'),
67
+ outboxAddress: addrEnv('The deployed L1 outbox contract address.'),
68
+ feeJuiceAddress: addrEnv('The deployed L1 Fee Juice contract address.'),
69
+ feeJuicePortalAddress: addrEnv('The deployed L1 Fee Juice portal contract address.'),
70
+ coinIssuerAddress: addrEnv('The deployed L1 coinIssuer contract address'),
71
+ rewardDistributorAddress: addrEnv('The deployed L1 rewardDistributor contract address'),
72
+ governanceProposerAddress: addrEnv('The deployed L1 governanceProposer contract address'),
73
+ governanceAddress: addrEnv('The deployed L1 governance contract address'),
74
+ stakingAssetAddress: addrEnv('The deployed L1 staking asset contract address.'),
75
+ feeAssetHandlerAddress: addrEnv(
76
+ 'The deployed L1 feeAssetHandler contract address',
77
+ 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS',
78
+ ),
79
+ stakingAssetHandlerAddress: addrEnv('The deployed L1 stakingAssetHandler contract address'),
80
+ zkPassportVerifierAddress: addrEnv('The deployed L1 ZK passport verifier contract address'),
81
+ gseAddress: addrEnv('The deployed L1 GSE contract address'),
82
+ dateGatedRelayerAddress: addrEnv('The deployed L1 date-gated relayer contract address'),
124
83
  };
84
+
85
+ /** Keys present in {@link l1ContractAddressesMapping}. */
86
+ export type L1ContractAddressMappingKey = keyof typeof l1ContractAddressesMapping;
87
+
88
+ type RequiredAddressShape = { [K in RequiredContractKey]: typeof schemas.EthAddress };
89
+ type OptionalAddressShape = { [K in OptionalContractKey]: ReturnType<typeof schemas.EthAddress.optional> };
90
+
91
+ export const L1ContractAddressesSchema = z.object({
92
+ ...(Object.fromEntries(REQUIRED_L1_CONTRACT_ADDRESS_KEYS.map(k => [k, schemas.EthAddress])) as RequiredAddressShape),
93
+ ...(Object.fromEntries(
94
+ OPTIONAL_L1_CONTRACT_ADDRESS_KEYS.map(k => [k, schemas.EthAddress.optional()]),
95
+ ) as OptionalAddressShape),
96
+ });
97
+
98
+ /**
99
+ * Selects entries from {@link l1ContractAddressesMapping} so composed configs can reuse the same
100
+ * env vars, descriptions, and parsers without duplicating definitions.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const mappings = {
105
+ * ...pickL1ContractAddressMappings('rollupAddress'),
106
+ * nodeId: { ... },
107
+ * };
108
+ * ```
109
+ */
110
+ export function pickL1ContractAddressMappings<const K extends readonly L1ContractAddressMappingKey[]>(
111
+ ...keys: K
112
+ ): Pick<typeof l1ContractAddressesMapping, K[number]> {
113
+ return Object.fromEntries(keys.map(key => [key, l1ContractAddressesMapping[key]] as const)) as Pick<
114
+ typeof l1ContractAddressesMapping,
115
+ K[number]
116
+ >;
117
+ }
118
+
119
+ /**
120
+ * Like {@link pickL1ContractAddressMappings}, but returns a fragment of {@link L1ContractAddressesSchema}'s shape for
121
+ * spreading into `z.object({ ...pickL1ContractAddressesSchemaShape('rollupAddress'), localField: z... })`.
122
+ */
123
+ export function pickL1ContractAddressesSchema<const K extends readonly L1ContractAddressMappingKey[]>(
124
+ ...keys: K
125
+ ): Pick<typeof L1ContractAddressesSchema.shape, K[number]> {
126
+ return Object.fromEntries(keys.map(key => [key, L1ContractAddressesSchema.shape[key]])) as Pick<
127
+ typeof L1ContractAddressesSchema.shape,
128
+ K[number]
129
+ >;
130
+ }
131
+
132
+ /** Builds an {@link L1ContractAddresses} object from config that stores those fields at the top level. */
133
+ export function pickL1ContractAddresses<T extends L1ContractAddresses>(config: T): L1ContractAddresses {
134
+ const result: Partial<L1ContractAddresses> = {};
135
+ for (const key of REQUIRED_L1_CONTRACT_ADDRESS_KEYS) {
136
+ result[key] = config[key];
137
+ }
138
+ for (const key of OPTIONAL_L1_CONTRACT_ADDRESS_KEYS) {
139
+ const v = config[key];
140
+ if (v !== undefined) {
141
+ result[key] = v;
142
+ }
143
+ }
144
+ return result as L1ContractAddresses;
145
+ }
146
+
147
+ export function randomL1ContractAddresses(includeOptional: boolean = false): L1ContractAddresses {
148
+ const keys = includeOptional ? [...L1ContractsNames, ...OPTIONAL_L1_CONTRACT_ADDRESS_KEYS] : L1ContractsNames;
149
+ return Object.fromEntries(keys.map(name => [name, EthAddress.random()])) as L1ContractAddresses;
150
+ }
package/src/l1_reader.ts CHANGED
@@ -8,29 +8,23 @@ import {
8
8
  import { type L1ContractAddresses, l1ContractAddressesMapping } from './l1_contract_addresses.js';
9
9
 
10
10
  /** Configuration of the L1GlobalReader. */
11
- export interface L1ReaderConfig {
11
+ export type L1ReaderConfig = {
12
12
  /** List of URLs of Ethereum RPC nodes that services will connect to (comma separated). */
13
13
  l1RpcUrls: string[];
14
14
  /** The RPC Url of the ethereum debug host for trace and debug methods. */
15
15
  l1DebugRpcUrls: string[];
16
16
  /** The chain ID of the ethereum host. */
17
17
  l1ChainId: number;
18
- /** The deployed l1 contract addresses */
19
- l1Contracts: L1ContractAddresses;
20
18
  /** The polling interval viem uses in ms */
21
19
  viemPollingIntervalMS: number;
22
20
  /** Timeout for HTTP requests to the L1 RPC node in ms. */
23
21
  l1HttpTimeoutMS?: number;
24
- }
22
+ } & L1ContractAddresses;
25
23
 
26
24
  export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
27
- l1Contracts: {
28
- description: 'The deployed L1 contract addresses',
29
- nested: l1ContractAddressesMapping,
30
- },
31
25
  l1ChainId: {
32
26
  env: 'L1_CHAIN_ID',
33
- parseEnv: (val: string) => +val,
27
+ ...optionalNumberConfigHelper(),
34
28
  description: 'The chain ID of the ethereum host.',
35
29
  },
36
30
  l1RpcUrls: {
@@ -55,6 +49,7 @@ export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
55
49
  description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
56
50
  ...optionalNumberConfigHelper(),
57
51
  },
52
+ ...l1ContractAddressesMapping,
58
53
  };
59
54
 
60
55
  export function getL1ReaderConfigFromEnv(): L1ReaderConfig {
@@ -73,7 +73,7 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
73
73
  gasLimitBufferPercentage: {
74
74
  description: 'How much to increase calculated gas limit by (percentage)',
75
75
  env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE',
76
- ...numberConfigHelper(20),
76
+ ...floatConfigHelper(20),
77
77
  },
78
78
  maxGwei: {
79
79
  description: 'Maximum gas price in gwei to be used for transactions.',
@@ -90,12 +90,12 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
90
90
  priorityFeeBumpPercentage: {
91
91
  description: 'How much to increase priority fee by each attempt (percentage)',
92
92
  env: 'L1_PRIORITY_FEE_BUMP_PERCENTAGE',
93
- ...numberConfigHelper(20),
93
+ ...floatConfigHelper(20),
94
94
  },
95
95
  priorityFeeRetryBumpPercentage: {
96
96
  description: 'How much to increase priority fee by each retry attempt (percentage)',
97
97
  env: 'L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE',
98
- ...numberConfigHelper(50),
98
+ ...floatConfigHelper(50),
99
99
  },
100
100
  minimumPriorityFeePerGas: {
101
101
  description:
@@ -5,7 +5,7 @@ import { InterruptError, TimeoutError } from '@aztec/foundation/error';
5
5
  import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
7
7
  import { Semaphore } from '@aztec/foundation/queue';
8
- import { retryUntil } from '@aztec/foundation/retry';
8
+ import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry';
9
9
  import { sleep } from '@aztec/foundation/sleep';
10
10
  import { DateProvider } from '@aztec/foundation/timer';
11
11
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
@@ -44,6 +44,11 @@ import {
44
44
 
45
45
  const MAX_L1_TX_STATES = 32;
46
46
 
47
+ // Backoff (in seconds) for retrying the read-only RPC calls that prepare a tx cancellation. A
48
+ // cancellation is fired in the background after a tx times out and is important (it frees the stuck
49
+ // nonce), so a transient RPC failure while reading the pending nonce or gas price must not abandon it.
50
+ const CANCELLATION_PREP_RETRY_BACKOFF_S = [1, 2, 4, 8];
51
+
47
52
  export class L1TxUtils extends ReadOnlyL1TxUtils {
48
53
  protected txs: L1TxState[] = [];
49
54
  /** Last nonce successfully sent to the chain. Used as a lower bound when a fallback RPC node returns a stale count. */
@@ -131,6 +136,14 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
131
136
  );
132
137
  }
133
138
 
139
+ /**
140
+ * Clears the cached `lastSentNonce` so the next `sendTransaction` call fetches the real
141
+ * nonce from the chain. Call this after an L1 reorg to prevent stale nonce references.
142
+ */
143
+ public resetNonce(): void {
144
+ this.lastSentNonce = undefined;
145
+ }
146
+
134
147
  public getSenderAddress() {
135
148
  return this.address;
136
149
  }
@@ -213,6 +226,19 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
213
226
  return await this.signTransaction(txRequest as TransactionSerializable);
214
227
  }
215
228
 
229
+ private async checkInterruptedOrTimedOut(gasConfig: Pick<L1TxConfig, 'txTimeoutAt'>): Promise<Date> {
230
+ if (this.interrupted) {
231
+ throw new InterruptError(`Transaction sending is interrupted`);
232
+ }
233
+ const now = new Date(await this.getL1Timestamp());
234
+ if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
235
+ throw new TimeoutError(
236
+ `Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
237
+ );
238
+ }
239
+ return now;
240
+ }
241
+
216
242
  /**
217
243
  * Sends a transaction with gas estimation and pricing
218
244
  * @param request - The transaction request (to, data, value)
@@ -225,14 +251,15 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
225
251
  blobInputs?: L1BlobInputs,
226
252
  stateChange: TxUtilsState = TxUtilsState.SENT,
227
253
  ): Promise<{ txHash: Hex; state: L1TxState }> {
228
- if (this.interrupted) {
229
- throw new InterruptError(`Transaction sending is interrupted`);
230
- }
231
-
232
254
  try {
233
255
  const gasConfig = merge(this.config, gasConfigOverrides);
234
256
  const account = this.getSenderAddress().toString();
235
257
 
258
+ // Fail fast before doing any work (gas estimation, balance check) if we've been interrupted
259
+ // or if the caller's deadline has already passed. The same check is repeated after gas
260
+ // estimation in case it took long enough to push us past the deadline.
261
+ await this.checkInterruptedOrTimedOut(gasConfig);
262
+
236
263
  let gasLimit: bigint;
237
264
  if (this.debugMaxGasLimit) {
238
265
  gasLimit = MAX_L1_TX_LIMIT;
@@ -245,16 +272,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
245
272
 
246
273
  const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
247
274
 
248
- if (this.interrupted) {
249
- throw new InterruptError(`Transaction sending is interrupted`);
250
- }
251
-
252
- const now = new Date(await this.getL1Timestamp());
253
- if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
254
- throw new TimeoutError(
255
- `Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
256
- );
257
- }
275
+ const now = await this.checkInterruptedOrTimedOut(gasConfig);
258
276
 
259
277
  let txHash: Hex;
260
278
  let nonce: number;
@@ -418,10 +436,23 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
418
436
 
419
437
  const initialTxHash = txHashes[0];
420
438
  let currentTxHash = txHashes.at(-1)!;
421
- let l1Timestamp: number;
439
+ let l1Timestamp = 0;
422
440
 
423
441
  while (true) {
424
- l1Timestamp = await this.getL1Timestamp();
442
+ try {
443
+ l1Timestamp = await this.getL1Timestamp();
444
+ } catch (err) {
445
+ // A transient RPC failure here must not abort monitoring with a rejection: callers that
446
+ // fire this loop in the background would surface it as an unhandled rejection (e.g. when a
447
+ // test tears down its L1 node while a monitor iteration is in flight). Exit quietly if we
448
+ // are shutting down, otherwise retry on the next interval.
449
+ if (this.interrupted) {
450
+ break;
451
+ }
452
+ this.logger.error(`Error fetching L1 timestamp while monitoring tx ${currentTxHash}`, err, { nonce, account });
453
+ await sleep(gasConfig.checkIntervalMs!);
454
+ continue;
455
+ }
425
456
 
426
457
  try {
427
458
  const timePassed = l1Timestamp - state.lastSentAtL1Ts.getTime();
@@ -654,14 +685,14 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
654
685
  ): Promise<{ gasUsed: bigint; result: `0x${string}` }> {
655
686
  const blockOverrides = { ..._blockOverrides };
656
687
  const gasConfig = merge(this.config, _gasConfig);
657
- const gasPrice = await this.getGasPrice(gasConfig, false);
658
688
 
689
+ // Fee fields are deliberately omitted. Supplying them makes the node apply the EIP-1559 upfront funds
690
+ // check (sender balance >= gas * maxFeePerGas) against the worst-case gas cap below, which rejects the
691
+ // whole simulation request before any execution. Omitted fee fields default to a zero gas price.
659
692
  const call: any = {
660
693
  to: request.to!,
661
694
  data: request.data,
662
695
  from: request.from ?? this.getSenderAddress().toString(),
663
- maxFeePerGas: gasPrice.maxFeePerGas,
664
- maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
665
696
  gas: request.gas ?? MAX_L1_TX_LIMIT,
666
697
  };
667
698
 
@@ -670,7 +701,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
670
701
  blockOverrides.gasLimit = MAX_L1_TX_LIMIT;
671
702
  }
672
703
 
673
- return this._simulate(call, blockOverrides, stateOverrides, gasConfig, abi);
704
+ return await this._simulate(call, blockOverrides, stateOverrides, gasConfig, abi);
674
705
  }
675
706
 
676
707
  /**
@@ -693,9 +724,37 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
693
724
  return;
694
725
  }
695
726
 
696
- // Check if the original tx is still pending
697
- const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
698
- if (currentNonce < nonce) {
727
+ // Resolve the pending nonce and cancellation gas price up front. These are read-only RPC calls, so
728
+ // they are safe to retry; a transient RPC failure here must not permanently abandon the cancellation
729
+ // (which would leave the nonce stuck and the tx state stranded short of CANCELLED). We retry with a
730
+ // real backoff before giving up, unlike getGasPrice's tight internal retry which can be exhausted by
731
+ // a brief node hiccup. The retried block performs no state-changing send, so retrying cannot
732
+ // double-send the cancellation tx.
733
+ const { currentNonce, cancelGasPrice } = await retry(
734
+ async () => {
735
+ const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
736
+ if (currentNonce >= nonce) {
737
+ // Get gas price with higher priority fee for cancellation
738
+ const cancelGasPrice = await this.getGasPrice(
739
+ {
740
+ ...this.config,
741
+ // Use high bump for cancellation to ensure it replaces the original tx
742
+ priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
743
+ },
744
+ isBlobTx,
745
+ state.txHashes.length,
746
+ previousGasPrice,
747
+ );
748
+ return { currentNonce, cancelGasPrice };
749
+ }
750
+ return { currentNonce, cancelGasPrice: undefined };
751
+ },
752
+ `Preparing cancellation for L1 tx from account ${account} with nonce ${nonce}`,
753
+ makeBackoff(CANCELLATION_PREP_RETRY_BACKOFF_S),
754
+ this.logger,
755
+ );
756
+
757
+ if (cancelGasPrice === undefined) {
699
758
  this.logger.verbose(
700
759
  `Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`,
701
760
  { nonce, account, currentNonce },
@@ -704,18 +763,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
704
763
  return;
705
764
  }
706
765
 
707
- // Get gas price with higher priority fee for cancellation
708
- const cancelGasPrice = await this.getGasPrice(
709
- {
710
- ...this.config,
711
- // Use high bump for cancellation to ensure it replaces the original tx
712
- priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
713
- },
714
- isBlobTx,
715
- state.txHashes.length,
716
- previousGasPrice,
717
- );
718
-
719
766
  const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelGasPrice;
720
767
  this.logger.verbose(
721
768
  `Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction from account ${account} with nonce ${nonce} after time out`,
@@ -3,6 +3,7 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
4
  import { makeBackoff, retry } from '@aztec/foundation/retry';
5
5
  import { DateProvider } from '@aztec/foundation/timer';
6
+ import { getErrorCause } from '@aztec/foundation/types';
6
7
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
7
8
 
8
9
  import pickBy from 'lodash.pickby';
@@ -16,6 +17,7 @@ import {
16
17
  type Hex,
17
18
  MethodNotFoundRpcError,
18
19
  MethodNotSupportedRpcError,
20
+ RpcRequestError,
19
21
  type StateOverride,
20
22
  decodeErrorResult,
21
23
  formatGwei,
@@ -23,6 +25,7 @@ import {
23
25
  hexToBytes,
24
26
  } from 'viem';
25
27
 
28
+ import { getL1RpcErrorCode } from '../client.js';
26
29
  import type { ViemClient } from '../types.js';
27
30
  import { type L1TxUtilsConfig, defaultL1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
28
31
  import {
@@ -39,6 +42,23 @@ import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './ut
39
42
  // Change this to the current strategy we want to use
40
43
  const CurrentStrategy: PriorityFeeStrategy = P75AllTxsPriorityFeeStrategy;
41
44
 
45
+ /** Error code returned by eth_simulateV1 for `insufficient funds for gas * price + value`. */
46
+ const INSUFFICIENT_FUNDS_RPC_ERROR_CODE = -38014;
47
+
48
+ /**
49
+ * Returns true when an RPC transport error reports the node's upfront funds check
50
+ * (sender balance >= gasLimit * maxFeePerGas) rejecting the request. Matches on the message as well as on the
51
+ * error code, since RPC gateways differ in how they wrap node errors. Apply this only to transport errors: an
52
+ * execution revert string can mention insufficient funds without being this kind of rejection.
53
+ */
54
+ function isInsufficientFundsRpcError(err: unknown): boolean {
55
+ if (getL1RpcErrorCode(err) === INSUFFICIENT_FUNDS_RPC_ERROR_CODE) {
56
+ return true;
57
+ }
58
+ const messages = [err, getErrorCause(err, RpcRequestError)].map(e => (e instanceof Error ? e.message : '')).join(' ');
59
+ return /insufficient funds/i.test(messages);
60
+ }
61
+
42
62
  export class ReadOnlyL1TxUtils {
43
63
  public config: Required<L1TxUtilsConfig>;
44
64
  protected interrupted = false;
@@ -372,8 +392,8 @@ export class ReadOnlyL1TxUtils {
372
392
  gasConfig: L1TxUtilsConfig & { fallbackGasEstimate?: bigint },
373
393
  abi: Abi,
374
394
  ) {
375
- try {
376
- const result = await this.client.simulateBlocks({
395
+ const simulateBlocks = () =>
396
+ this.client.simulateBlocks({
377
397
  validation: false,
378
398
  blocks: [
379
399
  {
@@ -384,18 +404,13 @@ export class ReadOnlyL1TxUtils {
384
404
  ],
385
405
  });
386
406
 
387
- if (result[0].calls[0].status === 'failure') {
388
- this.logger?.error('L1 transaction simulation failed', result[0].calls[0].error);
389
- const decodedError = decodeErrorResult({ abi, data: result[0].calls[0].data });
390
-
391
- throw new Error(
392
- `L1 transaction simulation failed with error ${decodedError.errorName}(${decodedError.args?.join(',')})`,
393
- );
394
- }
395
- this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
396
- return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
407
+ // Only the request itself is wrapped, so that transport-level rejections are never confused with an
408
+ // execution revert (whose decoded revert string could well mention insufficient funds too).
409
+ let result: Awaited<ReturnType<typeof simulateBlocks>>;
410
+ try {
411
+ result = await simulateBlocks();
397
412
  } catch (err) {
398
- if (err instanceof MethodNotFoundRpcError || err instanceof MethodNotSupportedRpcError) {
413
+ if (getErrorCause(err, MethodNotFoundRpcError) || getErrorCause(err, MethodNotSupportedRpcError)) {
399
414
  if (gasConfig.fallbackGasEstimate) {
400
415
  this.logger?.warn(
401
416
  `Node does not support eth_simulateV1 API. Using fallback gas estimate: ${gasConfig.fallbackGasEstimate}`,
@@ -404,8 +419,29 @@ export class ReadOnlyL1TxUtils {
404
419
  }
405
420
  this.logger?.error('Node does not support eth_simulateV1 API');
406
421
  }
422
+ if (isInsufficientFundsRpcError(err)) {
423
+ const rpcError = getErrorCause(err, RpcRequestError);
424
+ const providerMessage = rpcError?.details ?? (err instanceof Error ? err.message : String(err));
425
+ const gas = call?.gas !== undefined ? `, gas ${call.gas}` : '';
426
+ throw new Error(
427
+ `L1 node rejected the eth_simulateV1 request with an insufficient funds error before executing it ` +
428
+ `(sender ${call?.from ?? 'unknown'}${gas}): ${providerMessage}`,
429
+ { cause: err },
430
+ );
431
+ }
407
432
  throw err;
408
433
  }
434
+
435
+ if (result[0].calls[0].status === 'failure') {
436
+ this.logger?.error('L1 transaction simulation failed', result[0].calls[0].error);
437
+ const decodedError = decodeErrorResult({ abi, data: result[0].calls[0].data });
438
+
439
+ throw new Error(
440
+ `L1 transaction simulation failed with error ${decodedError.errorName}(${decodedError.args?.join(',')})`,
441
+ );
442
+ }
443
+ this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
444
+ return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
409
445
  }
410
446
 
411
447
  public bumpGasLimit(gasLimit: bigint, _gasConfig?: L1TxUtilsConfig): bigint {
@@ -81,7 +81,11 @@ export class Delayer {
81
81
 
82
82
  public maxInclusionTimeIntoSlot: number | undefined = undefined;
83
83
  public ethereumSlotDuration: bigint;
84
- public nextWait: { l1Timestamp: bigint } | { l1BlockNumber: bigint } | { indefinitely: true } | undefined = undefined;
84
+ public nextWait:
85
+ | { l1Timestamp: bigint; timeoutSeconds?: number }
86
+ | { l1BlockNumber: bigint; timeoutSeconds?: number }
87
+ | { indefinitely: true }
88
+ | undefined = undefined;
85
89
  public sentTxHashes: Hex[] = [];
86
90
  public cancelledTxs: Hex[] = [];
87
91
 
@@ -110,13 +114,13 @@ export class Delayer {
110
114
  }
111
115
 
112
116
  /** Delays the next tx to be sent so it lands on the given L1 block number. */
113
- pauseNextTxUntilBlock(l1BlockNumber: number | bigint) {
114
- this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber) };
117
+ pauseNextTxUntilBlock(l1BlockNumber: number | bigint, timeoutSeconds?: number) {
118
+ this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber), timeoutSeconds };
115
119
  }
116
120
 
117
121
  /** Delays the next tx to be sent so it lands on the given timestamp. */
118
- pauseNextTxUntilTimestamp(l1Timestamp: number | bigint) {
119
- this.nextWait = { l1Timestamp: BigInt(l1Timestamp) };
122
+ pauseNextTxUntilTimestamp(l1Timestamp: number | bigint, timeoutSeconds?: number) {
123
+ this.nextWait = { l1Timestamp: BigInt(l1Timestamp), timeoutSeconds };
120
124
  }
121
125
 
122
126
  /** Delays the next tx to be sent indefinitely. */
@@ -198,9 +202,14 @@ export function wrapClientWithDelayer<T extends ViemClient>(client: T, delayer:
198
202
  // Or wait until the desired block number or timestamp
199
203
  wait =
200
204
  'l1BlockNumber' in waitUntil
201
- ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger)
205
+ ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger, waitUntil.timeoutSeconds)
202
206
  : 'l1Timestamp' in waitUntil
203
- ? waitUntilL1Timestamp(publicClient, waitUntil.l1Timestamp - delayer.ethereumSlotDuration, logger)
207
+ ? waitUntilL1Timestamp(
208
+ publicClient,
209
+ waitUntil.l1Timestamp - delayer.ethereumSlotDuration,
210
+ logger,
211
+ waitUntil.timeoutSeconds,
212
+ )
204
213
  : undefined;
205
214
 
206
215
  logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`, {