@aztec/ethereum 0.70.0 → 0.72.1

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.
@@ -109,121 +109,67 @@ export interface ContractArtifacts {
109
109
  libraries?: Libraries;
110
110
  }
111
111
 
112
- /**
113
- * All L1 Contract Artifacts for deployment
114
- */
115
- export interface L1ContractArtifactsForDeployment {
116
- /**
117
- * Inbox contract artifacts
118
- */
119
- inbox: ContractArtifacts;
120
- /**
121
- * Outbox contract artifacts
122
- */
123
- outbox: ContractArtifacts;
124
- /**
125
- * Registry contract artifacts
126
- */
127
- registry: ContractArtifacts;
128
- /**
129
- * Rollup contract artifacts
130
- */
131
- rollup: ContractArtifacts;
132
- /**
133
- * The token to stake.
134
- */
135
- stakingAsset: ContractArtifacts;
136
- /**
137
- * The token to pay for gas. This will be bridged to L2 via the feeJuicePortal below
138
- */
139
- feeAsset: ContractArtifacts;
140
- /**
141
- * Fee juice portal contract artifacts. Optional for now as gas is not strictly enforced
142
- */
143
- feeJuicePortal: ContractArtifacts;
144
- /**
145
- * CoinIssuer contract artifacts.
146
- */
147
- coinIssuer: ContractArtifacts;
148
- /**
149
- * RewardDistributor contract artifacts.
150
- */
151
- rewardDistributor: ContractArtifacts;
152
- /**
153
- * GovernanceProposer contract artifacts.
154
- */
155
- governanceProposer: ContractArtifacts;
156
- /**
157
- * Governance contract artifacts.
158
- */
159
- governance: ContractArtifacts;
160
- /**
161
- * SlashFactory contract artifacts.
162
- */
163
- slashFactory: ContractArtifacts;
164
- }
165
-
166
- export const l1Artifacts: L1ContractArtifactsForDeployment = {
112
+ export const l1Artifacts = {
167
113
  registry: {
168
114
  contractAbi: RegistryAbi,
169
- contractBytecode: RegistryBytecode,
115
+ contractBytecode: RegistryBytecode as Hex,
170
116
  },
171
117
  inbox: {
172
118
  contractAbi: InboxAbi,
173
- contractBytecode: InboxBytecode,
119
+ contractBytecode: InboxBytecode as Hex,
174
120
  },
175
121
  outbox: {
176
122
  contractAbi: OutboxAbi,
177
- contractBytecode: OutboxBytecode,
123
+ contractBytecode: OutboxBytecode as Hex,
178
124
  },
179
125
  rollup: {
180
126
  contractAbi: RollupAbi,
181
- contractBytecode: RollupBytecode,
127
+ contractBytecode: RollupBytecode as Hex,
182
128
  libraries: {
183
129
  linkReferences: RollupLinkReferences,
184
130
  libraryCode: {
185
131
  LeonidasLib: {
186
132
  contractAbi: LeonidasLibAbi,
187
- contractBytecode: LeonidasLibBytecode,
133
+ contractBytecode: LeonidasLibBytecode as Hex,
188
134
  },
189
135
  ExtRollupLib: {
190
136
  contractAbi: ExtRollupLibAbi,
191
- contractBytecode: ExtRollupLibBytecode,
137
+ contractBytecode: ExtRollupLibBytecode as Hex,
192
138
  },
193
139
  },
194
140
  },
195
141
  },
196
142
  stakingAsset: {
197
143
  contractAbi: TestERC20Abi,
198
- contractBytecode: TestERC20Bytecode,
144
+ contractBytecode: TestERC20Bytecode as Hex,
199
145
  },
200
146
  feeAsset: {
201
147
  contractAbi: TestERC20Abi,
202
- contractBytecode: TestERC20Bytecode,
148
+ contractBytecode: TestERC20Bytecode as Hex,
203
149
  },
204
150
  feeJuicePortal: {
205
151
  contractAbi: FeeJuicePortalAbi,
206
- contractBytecode: FeeJuicePortalBytecode,
152
+ contractBytecode: FeeJuicePortalBytecode as Hex,
207
153
  },
208
154
  rewardDistributor: {
209
155
  contractAbi: RewardDistributorAbi,
210
- contractBytecode: RewardDistributorBytecode,
156
+ contractBytecode: RewardDistributorBytecode as Hex,
211
157
  },
212
158
  coinIssuer: {
213
159
  contractAbi: CoinIssuerAbi,
214
- contractBytecode: CoinIssuerBytecode,
160
+ contractBytecode: CoinIssuerBytecode as Hex,
215
161
  },
216
162
  governanceProposer: {
217
163
  contractAbi: GovernanceProposerAbi,
218
- contractBytecode: GovernanceProposerBytecode,
164
+ contractBytecode: GovernanceProposerBytecode as Hex,
219
165
  },
220
166
  governance: {
221
167
  contractAbi: GovernanceAbi,
222
- contractBytecode: GovernanceBytecode,
168
+ contractBytecode: GovernanceBytecode as Hex,
223
169
  },
224
170
  slashFactory: {
225
171
  contractAbi: SlashFactoryAbi,
226
- contractBytecode: SlashFactoryBytecode,
172
+ contractBytecode: SlashFactoryBytecode as Hex,
227
173
  },
228
174
  };
229
175
 
@@ -400,6 +346,8 @@ export const deployL1Contracts = async (
400
346
  account.address.toString(),
401
347
  rollupConfigArgs,
402
348
  ];
349
+ await deployer.waitForDeployments();
350
+
403
351
  const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs);
404
352
  logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs);
405
353
 
@@ -443,32 +391,49 @@ export const deployL1Contracts = async (
443
391
  }
444
392
 
445
393
  if (args.initialValidators && args.initialValidators.length > 0) {
446
- // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch.
447
- const stakeNeeded = args.minimumStake * BigInt(args.initialValidators.length);
448
- await Promise.all(
449
- [
450
- await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any),
451
- await stakingAsset.write.approve([rollupAddress.toString(), stakeNeeded], {} as any),
452
- ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })),
394
+ // Check if some of the initial validators are already registered, so we support idempotent deployments
395
+ const validatorsInfo = await Promise.all(
396
+ args.initialValidators.map(async address => ({ address, ...(await rollup.read.getInfo([address.toString()])) })),
453
397
  );
398
+ const existingValidators = validatorsInfo.filter(v => v.status !== 0);
399
+ if (existingValidators.length > 0) {
400
+ logger.warn(
401
+ `Validators ${existingValidators.map(v => v.address).join(', ')} already exist. Skipping from initialization.`,
402
+ );
403
+ }
404
+
405
+ const newValidatorsAddresses = validatorsInfo.filter(v => v.status === 0).map(v => v.address.toString());
454
406
 
455
- const initiateValidatorSetTxHash = await rollup.write.cheat__InitialiseValidatorSet([
456
- args.initialValidators.map(v => ({
457
- attester: v.toString(),
458
- proposer: v.toString(),
459
- withdrawer: v.toString(),
460
- amount: args.minimumStake,
461
- })),
462
- ]);
463
- txHashes.push(initiateValidatorSetTxHash);
464
- logger.info(`Initialized validator set (${args.initialValidators.join(', ')}) in tx ${initiateValidatorSetTxHash}`);
407
+ if (newValidatorsAddresses.length > 0) {
408
+ // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch.
409
+ const stakeNeeded = args.minimumStake * BigInt(newValidatorsAddresses.length);
410
+ await Promise.all(
411
+ [
412
+ await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any),
413
+ await stakingAsset.write.approve([rollupAddress.toString(), stakeNeeded], {} as any),
414
+ ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })),
415
+ );
416
+
417
+ const initiateValidatorSetTxHash = await rollup.write.cheat__InitialiseValidatorSet([
418
+ newValidatorsAddresses.map(v => ({
419
+ attester: v,
420
+ proposer: v,
421
+ withdrawer: v,
422
+ amount: args.minimumStake,
423
+ })),
424
+ ]);
425
+ txHashes.push(initiateValidatorSetTxHash);
426
+ logger.info(
427
+ `Initialized validator set (${newValidatorsAddresses.join(', ')}) in tx ${initiateValidatorSetTxHash}`,
428
+ );
429
+ }
465
430
  }
466
431
 
467
432
  // @note This value MUST match what is in `constants.nr`. It is currently specified here instead of just importing
468
433
  // because there is circular dependency hell. This is a temporary solution. #3342
469
434
  // @todo #8084
470
435
  // fund the portal contract with Fee Juice
471
- const FEE_JUICE_INITIAL_MINT = 200000000000000000000n;
436
+ const FEE_JUICE_INITIAL_MINT = 200000000000000000000000n;
472
437
  const mintTxHash = await feeAsset.write.mint([feeJuicePortalAddress.toString(), FEE_JUICE_INITIAL_MINT], {} as any);
473
438
 
474
439
  // @note This is used to ensure we fully wait for the transaction when running against a real chain
@@ -476,8 +441,8 @@ export const deployL1Contracts = async (
476
441
  await publicClient.waitForTransactionReceipt({ hash: mintTxHash });
477
442
  logger.verbose(`Funding fee juice portal contract with fee juice in ${mintTxHash}`);
478
443
 
479
- if (!(await feeJuicePortal.read.initialized([]))) {
480
- const initPortalTxHash = await feeJuicePortal.write.initialize([]);
444
+ if (!(await feeJuicePortal.read.initialized())) {
445
+ const initPortalTxHash = await feeJuicePortal.write.initialize();
481
446
  txHashes.push(initPortalTxHash);
482
447
  logger.verbose(`Fee juice portal initializing in tx ${initPortalTxHash}`);
483
448
  } else {
@@ -493,13 +458,13 @@ export const deployL1Contracts = async (
493
458
  // The edge case being that the genesis block is already occupying slot 0, so we cannot have another block.
494
459
  try {
495
460
  // Need to get the time
496
- const currentSlot = (await rollup.read.getCurrentSlot([])) as bigint;
461
+ const currentSlot = (await rollup.read.getCurrentSlot()) as bigint;
497
462
 
498
463
  if (BigInt(currentSlot) === 0n) {
499
- const ts = Number(await rollup.read.getTimestampForSlot([1]));
464
+ const ts = Number(await rollup.read.getTimestampForSlot([1n]));
500
465
  await rpcCall('evm_setNextBlockTimestamp', [ts]);
501
466
  await rpcCall('hardhat_mine', [1]);
502
- const currentSlot = (await rollup.read.getCurrentSlot([])) as bigint;
467
+ const currentSlot = (await rollup.read.getCurrentSlot()) as bigint;
503
468
 
504
469
  if (BigInt(currentSlot) !== 1n) {
505
470
  throw new Error(`Error jumping time: current slot is ${currentSlot}`);
@@ -518,10 +483,10 @@ export const deployL1Contracts = async (
518
483
  }
519
484
 
520
485
  // Inbox and Outbox are immutable and are deployed from Rollup's constructor so we just fetch them from the contract.
521
- const inboxAddress = EthAddress.fromString((await rollup.read.INBOX([])) as any);
486
+ const inboxAddress = EthAddress.fromString((await rollup.read.INBOX()) as any);
522
487
  logger.verbose(`Inbox available at ${inboxAddress}`);
523
488
 
524
- const outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX([])) as any);
489
+ const outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX()) as any);
525
490
  logger.verbose(`Outbox available at ${outboxAddress}`);
526
491
 
527
492
  // We need to call a function on the registry to set the various contract addresses.
@@ -541,7 +506,7 @@ export const deployL1Contracts = async (
541
506
  }
542
507
 
543
508
  // If the owner is not the Governance contract, transfer ownership to the Governance contract
544
- if ((await registryContract.read.owner([])) !== getAddress(governanceAddress.toString())) {
509
+ if ((await registryContract.read.owner()) !== getAddress(governanceAddress.toString())) {
545
510
  const transferOwnershipTxHash = await registryContract.write.transferOwnership(
546
511
  [getAddress(governanceAddress.toString())],
547
512
  {
@@ -616,50 +581,6 @@ class L1Deployer {
616
581
  }
617
582
  }
618
583
 
619
- /**
620
- * Compiles a contract source code using the provided solc compiler.
621
- * @param fileName - Contract file name (eg UltraHonkVerifier.sol)
622
- * @param contractName - Contract name within the file (eg HonkVerifier)
623
- * @param source - Source code to compile
624
- * @param solc - Solc instance
625
- * @returns ABI and bytecode of the compiled contract
626
- */
627
- export function compileContract(
628
- fileName: string,
629
- contractName: string,
630
- source: string,
631
- solc: { compile: (source: string) => string },
632
- ): { abi: Narrow<Abi | readonly unknown[]>; bytecode: Hex } {
633
- const input = {
634
- language: 'Solidity',
635
- sources: {
636
- [fileName]: {
637
- content: source,
638
- },
639
- },
640
- settings: {
641
- // we require the optimizer
642
- optimizer: {
643
- enabled: true,
644
- runs: 200,
645
- },
646
- evmVersion: 'cancun',
647
- outputSelection: {
648
- '*': {
649
- '*': ['evm.bytecode.object', 'abi'],
650
- },
651
- },
652
- },
653
- };
654
-
655
- const output = JSON.parse(solc.compile(JSON.stringify(input)));
656
-
657
- const abi = output.contracts[fileName][contractName].abi;
658
- const bytecode: `0x${string}` = `0x${output.contracts[fileName][contractName].evm.bytecode.object}`;
659
-
660
- return { abi, bytecode };
661
- }
662
-
663
584
  // docs:start:deployL1Contract
664
585
  /**
665
586
  * Helper function to deploy ETH contracts.
@@ -126,7 +126,7 @@ export class EthCheatCodes {
126
126
  * Set the next block base fee per gas
127
127
  * @param baseFee - The base fee to set
128
128
  */
129
- public async setNextBlockBaseFeePerGas(baseFee: bigint): Promise<void> {
129
+ public async setNextBlockBaseFeePerGas(baseFee: bigint | number): Promise<void> {
130
130
  const res = await this.rpcCall('anvil_setNextBlockBaseFeePerGas', [baseFee.toString()]);
131
131
  if (res.error) {
132
132
  throw new Error(`Error setting next block base fee per gas: ${res.error.message}`);
@@ -12,11 +12,15 @@ import { sleep } from '@aztec/foundation/sleep';
12
12
  import {
13
13
  type Account,
14
14
  type Address,
15
+ type BlockOverrides,
15
16
  type Chain,
16
17
  type GetTransactionReturnType,
17
18
  type Hex,
18
19
  type HttpTransport,
20
+ MethodNotFoundRpcError,
21
+ MethodNotSupportedRpcError,
19
22
  type PublicClient,
23
+ type StateOverride,
20
24
  type TransactionReceipt,
21
25
  type WalletClient,
22
26
  formatGwei,
@@ -95,9 +99,9 @@ export interface L1TxUtilsConfig {
95
99
 
96
100
  export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
97
101
  gasLimitBufferPercentage: {
98
- description: 'How much to increase gas price by each attempt (percentage)',
102
+ description: 'How much to increase calculated gas limit by (percentage)',
99
103
  env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE',
100
- ...numberConfigHelper(10),
104
+ ...numberConfigHelper(20),
101
105
  },
102
106
  minGwei: {
103
107
  description: 'Minimum gas price in gwei',
@@ -199,7 +203,7 @@ export class L1TxUtils {
199
203
  */
200
204
  public async sendTransaction(
201
205
  request: L1TxRequest,
202
- _gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint; txTimeoutAt?: Date },
206
+ _gasConfig?: Partial<L1TxUtilsConfig> & { gasLimit?: bigint; txTimeoutAt?: Date },
203
207
  blobInputs?: L1BlobInputs,
204
208
  ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> {
205
209
  try {
@@ -207,8 +211,8 @@ export class L1TxUtils {
207
211
  const account = this.walletClient.account;
208
212
  let gasLimit: bigint;
209
213
 
210
- if (gasConfig.fixedGas) {
211
- gasLimit = gasConfig.fixedGas;
214
+ if (gasConfig.gasLimit) {
215
+ gasLimit = gasConfig.gasLimit;
212
216
  } else {
213
217
  gasLimit = await this.estimateGas(account, request);
214
218
  }
@@ -246,9 +250,9 @@ export class L1TxUtils {
246
250
 
247
251
  return { txHash, gasLimit, gasPrice };
248
252
  } catch (err: any) {
249
- const formattedErr = formatViemError(err);
250
- this.logger?.error(`Failed to send transaction`, formattedErr);
251
- throw formattedErr;
253
+ const viemError = formatViemError(err);
254
+ this.logger?.error(`Failed to send L1 transaction`, viemError.message, { metaMessages: viemError.metaMessages });
255
+ throw viemError;
252
256
  }
253
257
  }
254
258
 
@@ -306,15 +310,16 @@ export class L1TxUtils {
306
310
  try {
307
311
  const receipt = await this.publicClient.getTransactionReceipt({ hash });
308
312
  if (receipt) {
309
- this.logger?.debug(`L1 transaction ${hash} mined`);
310
313
  if (receipt.status === 'reverted') {
311
- this.logger?.error(`L1 transaction ${hash} reverted`);
314
+ this.logger?.error(`L1 transaction ${hash} reverted`, receipt);
315
+ } else {
316
+ this.logger?.debug(`L1 transaction ${hash} mined`);
312
317
  }
313
318
  return receipt;
314
319
  }
315
320
  } catch (err) {
316
321
  if (err instanceof Error && err.message.includes('reverted')) {
317
- throw err;
322
+ throw formatViemError(err);
318
323
  }
319
324
  }
320
325
  }
@@ -382,16 +387,20 @@ export class L1TxUtils {
382
387
  }
383
388
  await sleep(gasConfig.checkIntervalMs!);
384
389
  } catch (err: any) {
385
- const formattedErr = formatViemError(err);
386
- this.logger?.warn(`Error monitoring tx ${currentTxHash}:`, formattedErr);
387
- if (err.message?.includes('reverted')) {
388
- throw formattedErr;
390
+ const viemError = formatViemError(err);
391
+ this.logger?.warn(`Error monitoring L1 transaction ${currentTxHash}:`, viemError.message);
392
+ if (viemError.message?.includes('reverted')) {
393
+ throw viemError;
389
394
  }
390
395
  await sleep(gasConfig.checkIntervalMs!);
391
396
  }
392
397
  // Check if tx has timed out.
393
398
  txTimedOut = isTimedOut();
394
399
  }
400
+ this.logger?.error(`L1 transaction ${currentTxHash} timed out`, {
401
+ txHash: currentTxHash,
402
+ ...tx,
403
+ });
395
404
  throw new Error(`L1 transaction ${currentTxHash} timed out`);
396
405
  }
397
406
 
@@ -403,7 +412,7 @@ export class L1TxUtils {
403
412
  */
404
413
  public async sendAndMonitorTransaction(
405
414
  request: L1TxRequest,
406
- gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint; txTimeoutAt?: Date },
415
+ gasConfig?: Partial<L1TxUtilsConfig> & { gasLimit?: bigint; txTimeoutAt?: Date },
407
416
  blobInputs?: L1BlobInputs,
408
417
  ): Promise<{ receipt: TransactionReceipt; gasPrice: GasPrice }> {
409
418
  const { txHash, gasLimit, gasPrice } = await this.sendTransaction(request, gasConfig, blobInputs);
@@ -429,14 +438,14 @@ export class L1TxUtils {
429
438
  try {
430
439
  const blobBaseFeeHex = await this.publicClient.request({ method: 'eth_blobBaseFee' });
431
440
  blobBaseFee = BigInt(blobBaseFeeHex);
432
- this.logger?.debug('Blob base fee:', { blobBaseFee: formatGwei(blobBaseFee) });
441
+ this.logger?.debug('L1 Blob base fee:', { blobBaseFee: formatGwei(blobBaseFee) });
433
442
  } catch {
434
- this.logger?.warn('Failed to get blob base fee', attempt);
443
+ this.logger?.warn('Failed to get L1 blob base fee', attempt);
435
444
  }
436
445
 
437
446
  let priorityFee: bigint;
438
447
  if (gasConfig.fixedPriorityFeePerGas) {
439
- this.logger?.debug('Using fixed priority fee per gas', {
448
+ this.logger?.debug('Using fixed priority fee per L1 gas', {
440
449
  fixedPriorityFeePerGas: gasConfig.fixedPriorityFeePerGas,
441
450
  });
442
451
  // try to maintain precision up to 1000000 wei
@@ -514,7 +523,7 @@ export class L1TxUtils {
514
523
  maxFeePerBlobGas = maxFeePerBlobGas > minBlobFee ? maxFeePerBlobGas : minBlobFee;
515
524
  }
516
525
 
517
- this.logger?.debug(`Computed gas price`, {
526
+ this.logger?.debug(`Computed L1 gas price`, {
518
527
  attempt,
519
528
  baseFee: formatGwei(baseFee),
520
529
  maxFeePerGas: formatGwei(maxFeePerGas),
@@ -553,14 +562,74 @@ export class L1TxUtils {
553
562
  maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!,
554
563
  })
555
564
  )?.gas;
565
+ this.logger?.debug('L1 gas used in estimateGas by blob tx', { gas: initialEstimate });
556
566
  } else {
557
567
  initialEstimate = await this.publicClient.estimateGas({ account, ...request });
568
+ this.logger?.debug('L1 gas used in estimateGas by non-blob tx', { gas: initialEstimate });
558
569
  }
559
570
 
560
571
  // Add buffer based on either fixed amount or percentage
561
- const withBuffer =
562
- initialEstimate + (initialEstimate * BigInt((gasConfig.gasLimitBufferPercentage || 0) * 1_00)) / 100_00n;
572
+ const withBuffer = this.bumpGasLimit(initialEstimate, gasConfig);
563
573
 
564
574
  return withBuffer;
565
575
  }
576
+
577
+ public async simulateGasUsed(
578
+ request: L1TxRequest & { gas?: bigint },
579
+ blockOverrides: BlockOverrides<bigint, number> = {},
580
+ stateOverrides: StateOverride = [],
581
+ _gasConfig?: L1TxUtilsConfig & { fallbackGasEstimate?: bigint },
582
+ ): Promise<bigint> {
583
+ const gasConfig = { ...this.config, ..._gasConfig };
584
+ const gasPrice = await this.getGasPrice(gasConfig, false);
585
+
586
+ const nonce = await this.publicClient.getTransactionCount({ address: this.walletClient.account.address });
587
+
588
+ try {
589
+ const result = await this.publicClient.simulate({
590
+ validation: true,
591
+ blocks: [
592
+ {
593
+ blockOverrides,
594
+ stateOverrides,
595
+ calls: [
596
+ {
597
+ from: this.walletClient.account.address,
598
+ to: request.to!,
599
+ data: request.data,
600
+ maxFeePerGas: gasPrice.maxFeePerGas,
601
+ maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
602
+ gas: request.gas ?? 10_000_000n,
603
+ nonce,
604
+ },
605
+ ],
606
+ },
607
+ ],
608
+ });
609
+ this.logger?.debug(`L1 gas used in simulation: ${result[0].calls[0].gasUsed}`, {
610
+ result,
611
+ });
612
+ if (result[0].calls[0].status === 'failure') {
613
+ this.logger?.error('L1 transaction Simulation failed', {
614
+ error: result[0].calls[0].error,
615
+ });
616
+ throw new Error(`L1 transaction simulation failed with error: ${result[0].calls[0].error.message}`);
617
+ }
618
+ return result[0].gasUsed;
619
+ } catch (err) {
620
+ if (err instanceof MethodNotFoundRpcError || err instanceof MethodNotSupportedRpcError) {
621
+ this.logger?.error('Node does not support eth_simulateV1 API');
622
+ if (gasConfig.fallbackGasEstimate) {
623
+ this.logger?.debug(`Using fallback gas estimate: ${gasConfig.fallbackGasEstimate}`);
624
+ return gasConfig.fallbackGasEstimate;
625
+ }
626
+ }
627
+ throw err;
628
+ }
629
+ }
630
+
631
+ public bumpGasLimit(gasLimit: bigint, _gasConfig?: L1TxUtilsConfig): bigint {
632
+ const gasConfig = { ...this.config, ..._gasConfig };
633
+ return gasLimit + (gasLimit * BigInt((gasConfig?.gasLimitBufferPercentage || 0) * 1_00)) / 100_00n;
634
+ }
566
635
  }
package/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Fr } from '@aztec/foundation/fields';
2
2
  import { type Logger } from '@aztec/foundation/log';
3
+ import { ErrorsAbi } from '@aztec/l1-artifacts';
3
4
 
4
5
  import {
5
6
  type Abi,
@@ -9,6 +10,7 @@ import {
9
10
  type DecodeEventLogReturnType,
10
11
  type Hex,
11
12
  type Log,
13
+ decodeErrorResult,
12
14
  decodeEventLog,
13
15
  } from 'viem';
14
16
 
@@ -19,6 +21,16 @@ export interface L2Claim {
19
21
  messageLeafIndex: bigint;
20
22
  }
21
23
 
24
+ export class FormattedViemError extends Error {
25
+ metaMessages?: any[];
26
+
27
+ constructor(message: string, metaMessages?: any[]) {
28
+ super(message);
29
+ this.name = 'FormattedViemError';
30
+ this.metaMessages = metaMessages;
31
+ }
32
+ }
33
+
22
34
  export function extractEvent<
23
35
  const TAbi extends Abi | readonly unknown[],
24
36
  TEventName extends ContractEventName<TAbi>,
@@ -80,7 +92,56 @@ export function prettyLogViemErrorMsg(err: any) {
80
92
  return err?.message ?? err;
81
93
  }
82
94
 
83
- export function formatViemError(error: any): string {
95
+ /**
96
+ * Formats a Viem error into a FormattedViemError instance.
97
+ * @param error - The error to format.
98
+ * @param abi - The ABI to use for decoding.
99
+ * @returns A FormattedViemError instance.
100
+ */
101
+ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViemError {
102
+ // If error is already a FormattedViemError, return it as is
103
+ if (error instanceof FormattedViemError) {
104
+ return error;
105
+ }
106
+
107
+ // First try to decode as a custom error using the ABI
108
+ try {
109
+ if (error?.data) {
110
+ // Try to decode the error data using the ABI
111
+ const decoded = decodeErrorResult({
112
+ abi,
113
+ data: error.data as Hex,
114
+ });
115
+ if (decoded) {
116
+ return new FormattedViemError(`${decoded.errorName}(${decoded.args?.join(', ') ?? ''})`, error?.metaMessages);
117
+ }
118
+ }
119
+
120
+ // If it's a BaseError, try to get the custom error through ContractFunctionRevertedError
121
+ if (error instanceof BaseError) {
122
+ const revertError = error.walk(err => err instanceof ContractFunctionRevertedError);
123
+ if (revertError instanceof ContractFunctionRevertedError) {
124
+ let errorName = revertError.data?.errorName;
125
+ if (!errorName) {
126
+ errorName = revertError.signature ?? '';
127
+ }
128
+ const args =
129
+ revertError.metaMessages && revertError.metaMessages?.length > 1
130
+ ? revertError.metaMessages[1].trimStart()
131
+ : '';
132
+ return new FormattedViemError(`${errorName}${args}`, error?.metaMessages);
133
+ }
134
+ }
135
+ } catch (decodeErr) {
136
+ // If decoding fails, we fall back to the original formatting
137
+ }
138
+
139
+ // If it's a regular Error instance, return it with its message
140
+ if (error instanceof Error) {
141
+ return new FormattedViemError(error.message);
142
+ }
143
+
144
+ // Original formatting logic for non-custom errors
84
145
  const truncateHex = (hex: string, length = 100) => {
85
146
  if (!hex || typeof hex !== 'string') {
86
147
  return hex;
@@ -168,8 +229,7 @@ export function formatViemError(error: any): string {
168
229
  return result;
169
230
  };
170
231
 
171
- return JSON.stringify({ error: extractAndFormatRequestBody(error?.message || String(error)) }, null, 2).replace(
172
- /\\n/g,
173
- '\n',
174
- );
232
+ const formattedRes = extractAndFormatRequestBody(error?.message || String(error));
233
+
234
+ return new FormattedViemError(formattedRes.replace(/\\n/g, '\n'), error?.metaMessages);
175
235
  }