@aztec/ethereum 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a

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 (72) hide show
  1. package/dest/client.d.ts +11 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +35 -3
  4. package/dest/contracts/chain_state_override.d.ts +47 -7
  5. package/dest/contracts/chain_state_override.d.ts.map +1 -1
  6. package/dest/contracts/chain_state_override.js +57 -20
  7. package/dest/contracts/empire_base.d.ts +1 -3
  8. package/dest/contracts/empire_base.d.ts.map +1 -1
  9. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  10. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  11. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  12. package/dest/contracts/governance.d.ts +108 -33
  13. package/dest/contracts/governance.d.ts.map +1 -1
  14. package/dest/contracts/governance.js +193 -12
  15. package/dest/contracts/governance_proposer.d.ts +33 -3
  16. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  17. package/dest/contracts/governance_proposer.js +51 -9
  18. package/dest/contracts/inbox.js +1 -1
  19. package/dest/contracts/multicall.d.ts +79 -13
  20. package/dest/contracts/multicall.d.ts.map +1 -1
  21. package/dest/contracts/multicall.js +117 -95
  22. package/dest/contracts/outbox.d.ts +27 -4
  23. package/dest/contracts/outbox.d.ts.map +1 -1
  24. package/dest/contracts/outbox.js +34 -7
  25. package/dest/contracts/rollup.d.ts +327 -57
  26. package/dest/contracts/rollup.d.ts.map +1 -1
  27. package/dest/contracts/rollup.js +102 -40
  28. package/dest/l1_artifacts.d.ts +5108 -155
  29. package/dest/l1_artifacts.d.ts.map +1 -1
  30. package/dest/l1_contract_addresses.d.ts +61 -61
  31. package/dest/l1_contract_addresses.d.ts.map +1 -1
  32. package/dest/l1_contract_addresses.js +101 -76
  33. package/dest/l1_reader.d.ts +3 -5
  34. package/dest/l1_reader.d.ts.map +1 -1
  35. package/dest/l1_reader.js +3 -6
  36. package/dest/l1_tx_utils/l1_tx_utils.d.ts +7 -1
  37. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  38. package/dest/l1_tx_utils/l1_tx_utils.js +21 -10
  39. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  40. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  41. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
  42. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  43. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  44. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  45. package/dest/test/eth_cheat_codes.d.ts +7 -1
  46. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  47. package/dest/test/eth_cheat_codes.js +11 -0
  48. package/dest/test/rollup_cheat_codes.d.ts +27 -3
  49. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  50. package/dest/test/rollup_cheat_codes.js +50 -6
  51. package/dest/utils.d.ts +9 -2
  52. package/dest/utils.d.ts.map +1 -1
  53. package/dest/utils.js +34 -6
  54. package/package.json +6 -6
  55. package/src/client.ts +39 -1
  56. package/src/contracts/chain_state_override.ts +77 -24
  57. package/src/contracts/empire_base.ts +0 -2
  58. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  59. package/src/contracts/governance.ts +266 -9
  60. package/src/contracts/governance_proposer.ts +52 -5
  61. package/src/contracts/inbox.ts +1 -1
  62. package/src/contracts/multicall.ts +179 -105
  63. package/src/contracts/outbox.ts +42 -8
  64. package/src/contracts/rollup.ts +124 -42
  65. package/src/l1_contract_addresses.ts +120 -87
  66. package/src/l1_reader.ts +4 -9
  67. package/src/l1_tx_utils/l1_tx_utils.ts +27 -14
  68. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
  69. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  70. package/src/test/eth_cheat_codes.ts +10 -0
  71. package/src/test/rollup_cheat_codes.ts +53 -5
  72. package/src/utils.ts +30 -6
@@ -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,95 +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
- feeAssetHandlerAddress?: EthAddress | undefined;
31
- stakingAssetHandlerAddress?: EthAddress | undefined;
32
- zkPassportVerifierAddress?: EthAddress | undefined;
33
- gseAddress?: EthAddress | undefined;
34
- dateGatedRelayerAddress?: EthAddress | undefined;
42
+ [K in OptionalContractKey]?: EthAddress;
35
43
  };
36
44
 
37
- export const L1ContractAddressesSchema = zodFor<L1ContractAddresses>()(
38
- z.object({
39
- rollupAddress: schemas.EthAddress,
40
- registryAddress: schemas.EthAddress,
41
- inboxAddress: schemas.EthAddress,
42
- outboxAddress: schemas.EthAddress,
43
- feeJuiceAddress: schemas.EthAddress,
44
- stakingAssetAddress: schemas.EthAddress,
45
- feeJuicePortalAddress: schemas.EthAddress,
46
- coinIssuerAddress: schemas.EthAddress,
47
- rewardDistributorAddress: schemas.EthAddress,
48
- governanceProposerAddress: schemas.EthAddress,
49
- governanceAddress: schemas.EthAddress,
50
- feeAssetHandlerAddress: schemas.EthAddress.optional(),
51
- stakingAssetHandlerAddress: schemas.EthAddress.optional(),
52
- zkPassportVerifierAddress: schemas.EthAddress.optional(),
53
- gseAddress: schemas.EthAddress.optional(),
54
- dateGatedRelayerAddress: schemas.EthAddress.optional(),
55
- }),
56
- );
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;
57
51
 
58
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
+ });
59
58
 
60
- export const l1ContractAddressesMapping: ConfigMappingsType<
61
- Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress' | 'dateGatedRelayerAddress'>
62
- > = {
63
- registryAddress: {
64
- env: 'REGISTRY_CONTRACT_ADDRESS',
65
- description: 'The deployed L1 registry contract address.',
66
- parseEnv,
67
- },
68
- feeAssetHandlerAddress: {
69
- env: 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS',
70
- description: 'The deployed L1 feeAssetHandler contract address',
71
- parseEnv,
72
- },
73
- rollupAddress: {
74
- description: 'The deployed L1 rollup contract address.',
75
- parseEnv,
76
- },
77
- inboxAddress: {
78
- description: 'The deployed L1 inbox contract address.',
79
- parseEnv,
80
- },
81
- outboxAddress: {
82
- description: 'The deployed L1 outbox contract address.',
83
- parseEnv,
84
- },
85
- feeJuiceAddress: {
86
- description: 'The deployed L1 Fee Juice contract address.',
87
- parseEnv,
88
- },
89
- stakingAssetAddress: {
90
- description: 'The deployed L1 staking asset contract address.',
91
- parseEnv,
92
- },
93
- feeJuicePortalAddress: {
94
- description: 'The deployed L1 Fee Juice portal contract address.',
95
- parseEnv,
96
- },
97
- coinIssuerAddress: {
98
- description: 'The deployed L1 coinIssuer contract address',
99
- parseEnv,
100
- },
101
- rewardDistributorAddress: {
102
- description: 'The deployed L1 rewardDistributor contract address',
103
- parseEnv,
104
- },
105
- governanceProposerAddress: {
106
- description: 'The deployed L1 governanceProposer contract address',
107
- parseEnv,
108
- },
109
- governanceAddress: {
110
- description: 'The deployed L1 governance contract address',
111
- parseEnv,
112
- },
113
- stakingAssetHandlerAddress: {
114
- description: 'The deployed L1 stakingAssetHandler contract address',
115
- parseEnv,
116
- },
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'),
117
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 {
@@ -131,6 +131,14 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
131
131
  );
132
132
  }
133
133
 
134
+ /**
135
+ * Clears the cached `lastSentNonce` so the next `sendTransaction` call fetches the real
136
+ * nonce from the chain. Call this after an L1 reorg to prevent stale nonce references.
137
+ */
138
+ public resetNonce(): void {
139
+ this.lastSentNonce = undefined;
140
+ }
141
+
134
142
  public getSenderAddress() {
135
143
  return this.address;
136
144
  }
@@ -213,6 +221,19 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
213
221
  return await this.signTransaction(txRequest as TransactionSerializable);
214
222
  }
215
223
 
224
+ private async checkInterruptedOrTimedOut(gasConfig: Pick<L1TxConfig, 'txTimeoutAt'>): Promise<Date> {
225
+ if (this.interrupted) {
226
+ throw new InterruptError(`Transaction sending is interrupted`);
227
+ }
228
+ const now = new Date(await this.getL1Timestamp());
229
+ if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
230
+ throw new TimeoutError(
231
+ `Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
232
+ );
233
+ }
234
+ return now;
235
+ }
236
+
216
237
  /**
217
238
  * Sends a transaction with gas estimation and pricing
218
239
  * @param request - The transaction request (to, data, value)
@@ -225,14 +246,15 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
225
246
  blobInputs?: L1BlobInputs,
226
247
  stateChange: TxUtilsState = TxUtilsState.SENT,
227
248
  ): Promise<{ txHash: Hex; state: L1TxState }> {
228
- if (this.interrupted) {
229
- throw new InterruptError(`Transaction sending is interrupted`);
230
- }
231
-
232
249
  try {
233
250
  const gasConfig = merge(this.config, gasConfigOverrides);
234
251
  const account = this.getSenderAddress().toString();
235
252
 
253
+ // Fail fast before doing any work (gas estimation, balance check) if we've been interrupted
254
+ // or if the caller's deadline has already passed. The same check is repeated after gas
255
+ // estimation in case it took long enough to push us past the deadline.
256
+ await this.checkInterruptedOrTimedOut(gasConfig);
257
+
236
258
  let gasLimit: bigint;
237
259
  if (this.debugMaxGasLimit) {
238
260
  gasLimit = MAX_L1_TX_LIMIT;
@@ -245,16 +267,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
245
267
 
246
268
  const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
247
269
 
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
- }
270
+ const now = await this.checkInterruptedOrTimedOut(gasConfig);
258
271
 
259
272
  let txHash: Hex;
260
273
  let nonce: number;
@@ -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';
@@ -395,7 +396,7 @@ export class ReadOnlyL1TxUtils {
395
396
  this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
396
397
  return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
397
398
  } catch (err) {
398
- if (err instanceof MethodNotFoundRpcError || err instanceof MethodNotSupportedRpcError) {
399
+ if (getErrorCause(err, MethodNotFoundRpcError) || getErrorCause(err, MethodNotSupportedRpcError)) {
399
400
  if (gasConfig.fallbackGasEstimate) {
400
401
  this.logger?.warn(
401
402
  `Node does not support eth_simulateV1 API. Using fallback gas estimate: ${gasConfig.fallbackGasEstimate}`,
@@ -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)}`, {
@@ -95,6 +95,16 @@ export class EthCheatCodes {
95
95
  return parseInt(res.timestamp, 16);
96
96
  }
97
97
 
98
+ /**
99
+ * Get the timestamp of the pending L1 block — the block that anvil will mine next.
100
+ * Reflects any prior `setNextBlockTimestamp` call or the configured block interval.
101
+ * @returns The pending block timestamp in seconds.
102
+ */
103
+ public async nextBlockTimestamp(): Promise<number> {
104
+ const res = await this.doRpcCall('eth_getBlockByNumber', ['pending', false]);
105
+ return parseInt(res.timestamp, 16);
106
+ }
107
+
98
108
  /**
99
109
  * Advance the chain by a number of blocks
100
110
  * @param numberOfBlocks - The number of blocks to mine
@@ -15,6 +15,7 @@ import {
15
15
  getContract,
16
16
  hexToBigInt,
17
17
  http,
18
+ keccak256,
18
19
  } from 'viem';
19
20
 
20
21
  import { EthCheatCodes } from './eth_cheat_codes.js';
@@ -178,6 +179,21 @@ export class RollupCheatCodes {
178
179
  return [timestamp, nextSlot];
179
180
  }
180
181
 
182
+ /**
183
+ * Warps L1 time to the start of an explicit absolute slot. Unlike `advanceSlots(N)` which is
184
+ * relative to the current L1 timestamp at call time and therefore races with real-time
185
+ * progression between query and warp, this lands at the slot regardless of latency.
186
+ *
187
+ * Throws if the requested slot is in the past relative to current L1 time (anvil cannot
188
+ * rewind).
189
+ */
190
+ public async advanceToSlot(slot: SlotNumber) {
191
+ const timestamp = await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
192
+ await this.ethCheatCodes.warp(Number(timestamp), { silent: true, resetBlockInterval: true });
193
+ this.logger.warn(`Advanced to slot ${slot}`, { timestamp });
194
+ return timestamp;
195
+ }
196
+
181
197
  /**
182
198
  * Warps time in L1 equivalent to however many slots.
183
199
  * @param howMany - The number of slots to advance.
@@ -269,12 +285,14 @@ export class RollupCheatCodes {
269
285
  });
270
286
  }
271
287
 
272
- public insertOutbox(epoch: EpochNumber, outHash: bigint) {
288
+ public insertOutbox(epoch: EpochNumber, numCheckpointsInEpoch: number, outHash: bigint) {
273
289
  return this.ethCheatCodes.execWithPausedAnvil(async () => {
274
290
  const outboxAddress = await this.rollup.read.getOutbox();
275
- const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch);
291
+ const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch, numCheckpointsInEpoch);
276
292
  await this.ethCheatCodes.store(EthAddress.fromString(outboxAddress), epochRootSlot, outHash);
277
- this.logger.warn(`Advanced outbox to epoch ${epoch} with out hash ${outHash}`);
293
+ this.logger.warn(
294
+ `Advanced outbox to epoch ${epoch} numCheckpointsInEpoch ${numCheckpointsInEpoch} with out hash ${outHash}`,
295
+ );
278
296
  });
279
297
  }
280
298
 
@@ -323,7 +341,8 @@ export class RollupCheatCodes {
323
341
  }
324
342
 
325
343
  /**
326
- * Directly updates proving cost per mana.
344
+ * Directly updates proving cost per mana. Throws if the on-chain tx reverts
345
+ * (e.g. rate-limit cooldown, step cap, or floor) instead of silently succeeding.
327
346
  * @param ethValue - The new proving cost per mana in ETH
328
347
  */
329
348
  public async setProvingCostPerMana(ethValue: bigint) {
@@ -333,8 +352,37 @@ export class RollupCheatCodes {
333
352
  chain: this.client.chain,
334
353
  gasLimit: 1000000n,
335
354
  });
336
- await this.client.waitForTransactionReceipt({ hash });
355
+ const receipt = await this.client.waitForTransactionReceipt({ hash });
356
+ if (receipt.status !== 'success') {
357
+ throw new Error(
358
+ `setProvingCostPerMana(${ethValue}) reverted on L1 (tx ${hash}). ` +
359
+ `Likely FeeLib rate-limit (30-day cooldown or 1.5x step cap); ` +
360
+ `use clearProvingCostCooldown() between successive updates.`,
361
+ );
362
+ }
337
363
  this.logger.warn(`Updated proving cost per mana to ${ethValue}`);
338
364
  });
339
365
  }
366
+
367
+ /**
368
+ * Resets the 30-day proving-cost update cooldown enforced by FeeLib.updateProvingCostPerMana
369
+ * by zeroing `FeeStore.provingCostLastUpdate` directly in contract storage. Use between
370
+ * successive setProvingCostPerMana / bumpProvingCostPerMana calls so the later update can
371
+ * land instead of reverting. Does not touch L1 time, so PXE/tx-expiration state stays intact.
372
+ *
373
+ * @note This is tightly coupled to the `FeeStore` layout in
374
+ * l1-contracts/src/core/libraries/rollup/FeeLib.sol:
375
+ * slot + 0: CompressedFeeConfig config (uint256)
376
+ * slot + 1: L1GasOracleValues l1GasOracleValues (14+14+4 bytes, packed)
377
+ * slot + 2: uint64 provingCostLastUpdate (only member — zeroing the slot is safe)
378
+ * If the struct layout changes, update the offset below.
379
+ */
380
+ public async clearProvingCostCooldown() {
381
+ const feeStoreBaseSlot = hexToBigInt(keccak256(Buffer.from('aztec.fee.storage', 'utf-8')));
382
+ const provingCostLastUpdateSlot = feeStoreBaseSlot + 2n;
383
+ await this.ethCheatCodes.store(EthAddress.fromString(this.rollup.address), provingCostLastUpdateSlot, 0n, {
384
+ silent: true,
385
+ });
386
+ this.logger.warn(`Cleared proving-cost update cooldown`);
387
+ }
340
388
  }
package/src/utils.ts CHANGED
@@ -27,8 +27,8 @@ export interface L2Claim {
27
27
  export class FormattedViemError extends Error {
28
28
  metaMessages?: any[];
29
29
 
30
- constructor(message: string, metaMessages?: any[]) {
31
- super(message);
30
+ constructor(message: string, metaMessages?: any[], options?: ErrorOptions) {
31
+ super(message, options);
32
32
  this.name = 'FormattedViemError';
33
33
  this.metaMessages = metaMessages;
34
34
  }
@@ -197,6 +197,8 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
197
197
  return error;
198
198
  }
199
199
 
200
+ const originalError = error;
201
+
200
202
  // First try to decode as a custom error using the ABI
201
203
  try {
202
204
  const data = getNestedErrorData(error);
@@ -207,7 +209,9 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
207
209
  data: data as Hex,
208
210
  });
209
211
  if (decoded) {
210
- return new FormattedViemError(`${decoded.errorName}(${decoded.args?.join(', ') ?? ''})`, error?.metaMessages);
212
+ return new FormattedViemError(`${decoded.errorName}(${decoded.args?.join(', ') ?? ''})`, error?.metaMessages, {
213
+ cause: originalError,
214
+ });
211
215
  }
212
216
  }
213
217
 
@@ -224,7 +228,7 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
224
228
  revertError.metaMessages && revertError.metaMessages?.length > 1
225
229
  ? revertError.metaMessages[1].trimStart()
226
230
  : '';
227
- return new FormattedViemError(`${errorName}${args}`, error?.metaMessages);
231
+ return new FormattedViemError(`${errorName}${args}`, error?.metaMessages, { cause: originalError });
228
232
  }
229
233
  }
230
234
  } catch {
@@ -247,10 +251,12 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
247
251
 
248
252
  // If it's a regular Error instance, return it with its message
249
253
  if (error instanceof Error) {
250
- return new FormattedViemError(truncateErrorMessage(error.message), (error as any)?.metaMessages);
254
+ return new FormattedViemError(truncateErrorMessage(error.message), (error as any)?.metaMessages, {
255
+ cause: originalError,
256
+ });
251
257
  }
252
258
 
253
- return new FormattedViemError(truncateErrorMessage(String(error)));
259
+ return new FormattedViemError(truncateErrorMessage(String(error)), undefined, { cause: originalError });
254
260
  }
255
261
 
256
262
  function stripAbis(obj: any) {
@@ -276,6 +282,24 @@ function stripAbis(obj: any) {
276
282
  });
277
283
  }
278
284
 
285
+ /**
286
+ * Best-effort decode of a raw revert payload (`0x...`) against an ABI.
287
+ * Returns a human-readable `ErrorName(arg1, arg2, ...)` string, or `undefined` if the selector
288
+ * is unknown or the payload is empty. Use to surface decoded error names alongside the raw
289
+ * payload in log lines for operators.
290
+ */
291
+ export function tryDecodeRevertReason(data: Hex | undefined, abi: Abi): string | undefined {
292
+ if (!data || data === '0x') {
293
+ return undefined;
294
+ }
295
+ try {
296
+ const decoded = decodeErrorResult({ abi, data });
297
+ return `${decoded.errorName}(${decoded.args?.join(', ') ?? ''})`;
298
+ } catch {
299
+ return undefined;
300
+ }
301
+ }
302
+
279
303
  export function tryGetCustomErrorName(err: any) {
280
304
  try {
281
305
  // See https://viem.sh/docs/contract/simulateContract#handling-custom-errors