@aztec/ethereum 3.0.0-nightly.20250930 → 3.0.0-nightly.20251002

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 (42) hide show
  1. package/dest/config.js +2 -2
  2. package/dest/contracts/fee_asset_handler.d.ts +2 -2
  3. package/dest/contracts/governance_proposer.d.ts +1 -2
  4. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  5. package/dest/contracts/governance_proposer.js +1 -2
  6. package/dest/contracts/multicall.d.ts +0 -2
  7. package/dest/contracts/multicall.d.ts.map +1 -1
  8. package/dest/contracts/multicall.js +2 -4
  9. package/dest/contracts/rollup.d.ts +2 -2
  10. package/dest/deploy_l1_contracts.d.ts +11 -1
  11. package/dest/deploy_l1_contracts.d.ts.map +1 -1
  12. package/dest/deploy_l1_contracts.js +174 -54
  13. package/dest/l1_artifacts.d.ts +1978 -0
  14. package/dest/l1_artifacts.d.ts.map +1 -1
  15. package/dest/l1_artifacts.js +6 -1
  16. package/dest/l1_contract_addresses.d.ts +5 -1
  17. package/dest/l1_contract_addresses.d.ts.map +1 -1
  18. package/dest/l1_contract_addresses.js +2 -1
  19. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  20. package/dest/l1_tx_utils/factory.js +2 -2
  21. package/dest/l1_tx_utils/l1_tx_utils.d.ts +14 -26
  22. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  23. package/dest/l1_tx_utils/l1_tx_utils.js +140 -136
  24. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +4 -11
  25. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +1 -1
  26. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +10 -70
  27. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  28. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  29. package/dest/l1_tx_utils/types.d.ts +15 -2
  30. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  31. package/package.json +5 -5
  32. package/src/config.ts +2 -2
  33. package/src/contracts/governance_proposer.ts +3 -4
  34. package/src/contracts/multicall.ts +4 -4
  35. package/src/deploy_l1_contracts.ts +161 -51
  36. package/src/l1_artifacts.ts +8 -0
  37. package/src/l1_contract_addresses.ts +3 -1
  38. package/src/l1_tx_utils/factory.ts +2 -2
  39. package/src/l1_tx_utils/l1_tx_utils.ts +159 -157
  40. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +8 -99
  41. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +1 -1
  42. package/src/l1_tx_utils/types.ts +16 -2
@@ -1,3 +1,4 @@
1
+ import { maxBigint } from '@aztec/foundation/bigint';
1
2
  import { times } from '@aztec/foundation/collection';
2
3
  import { TimeoutError } from '@aztec/foundation/error';
3
4
  import { EthAddress } from '@aztec/foundation/eth-address';
@@ -30,18 +31,19 @@ import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
30
31
  import { LARGE_GAS_LIMIT } from './constants.js';
31
32
  import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
32
33
  import {
33
- type GasPrice,
34
34
  type L1BlobInputs,
35
35
  type L1GasConfig,
36
36
  type L1TxRequest,
37
+ type L1TxState,
37
38
  type SigningCallback,
38
39
  TxUtilsState,
39
40
  } from './types.js';
40
41
 
42
+ const MAX_L1_TX_STATES = 32;
43
+
41
44
  export class L1TxUtils extends ReadOnlyL1TxUtils {
42
- private txUtilsState: TxUtilsState = TxUtilsState.IDLE;
43
- private lastMinedBlockNumber: bigint | undefined = undefined;
44
- private nonceManager: NonceManager;
45
+ protected nonceManager: NonceManager;
46
+ protected txs: L1TxState[] = [];
45
47
 
46
48
  constructor(
47
49
  public override client: ViemClient,
@@ -57,21 +59,28 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
57
59
  }
58
60
 
59
61
  public get state() {
60
- return this.txUtilsState;
62
+ return this.txs.at(-1)?.status ?? TxUtilsState.IDLE;
61
63
  }
62
64
 
63
65
  public get lastMinedAtBlockNumber() {
64
- return this.lastMinedBlockNumber;
66
+ const minedBlockNumbers = this.txs.map(tx => tx.receipt?.blockNumber).filter(bn => bn !== undefined);
67
+ return minedBlockNumbers.length === 0 ? undefined : maxBigint(...minedBlockNumbers);
65
68
  }
66
69
 
67
- private set lastMinedAtBlockNumber(blockNumber: bigint | undefined) {
68
- this.lastMinedBlockNumber = blockNumber;
70
+ protected updateState(l1TxState: L1TxState, newState: TxUtilsState) {
71
+ const oldState = l1TxState.status;
72
+ l1TxState.status = newState;
73
+ const sender = this.getSenderAddress().toString();
74
+ this.logger.debug(
75
+ `State changed from ${TxUtilsState[oldState]} to ${TxUtilsState[newState]} for nonce ${l1TxState.nonce} account ${sender}`,
76
+ );
69
77
  }
70
78
 
71
- private set state(state: TxUtilsState) {
72
- this.txUtilsState = state;
73
- this.logger?.debug(
74
- `L1TxUtils state changed to ${TxUtilsState[state]} for sender: ${this.getSenderAddress().toString()}`,
79
+ public updateConfig(newConfig: Partial<L1TxUtilsConfig>) {
80
+ this.config = { ...this.config, ...newConfig };
81
+ this.logger.info(
82
+ 'Updated L1TxUtils config',
83
+ pickBy(newConfig, (_, key) => key in l1TxUtilsConfigMappings),
75
84
  );
76
85
  }
77
86
 
@@ -103,12 +112,12 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
103
112
  */
104
113
  public async sendTransaction(
105
114
  request: L1TxRequest,
106
- _gasConfig?: L1GasConfig,
115
+ gasConfigOverrides?: L1GasConfig,
107
116
  blobInputs?: L1BlobInputs,
108
117
  stateChange: TxUtilsState = TxUtilsState.SENT,
109
- ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> {
118
+ ): Promise<{ txHash: Hex; state: L1TxState }> {
110
119
  try {
111
- const gasConfig = { ...this.config, ..._gasConfig };
120
+ const gasConfig = { ...this.config, ...gasConfigOverrides };
112
121
  const account = this.getSenderAddress().toString();
113
122
 
114
123
  let gasLimit: bigint;
@@ -133,32 +142,41 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
133
142
  chainId: this.client.chain.id,
134
143
  });
135
144
 
136
- let txHash: Hex;
137
- if (blobInputs) {
138
- const txData = {
139
- ...request,
140
- ...blobInputs,
141
- gas: gasLimit,
142
- maxFeePerGas: gasPrice.maxFeePerGas,
143
- maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
144
- maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!,
145
- nonce,
146
- };
147
-
148
- const signedRequest = await this.prepareSignedTransaction(txData);
149
- txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
150
- } else {
151
- const txData = {
152
- ...request,
153
- gas: gasLimit,
154
- maxFeePerGas: gasPrice.maxFeePerGas,
155
- maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
156
- nonce,
157
- };
158
- const signedRequest = await this.prepareSignedTransaction(txData);
159
- txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
145
+ const l1TxState: L1TxState = {
146
+ txHashes: [],
147
+ cancelTxHashes: [],
148
+ gasPrice,
149
+ request,
150
+ status: TxUtilsState.IDLE,
151
+ nonce,
152
+ gasLimit,
153
+ txConfig: gasConfig,
154
+ blobInputs,
155
+ };
156
+
157
+ this.updateState(l1TxState, stateChange);
158
+
159
+ const baseTxData = {
160
+ ...request,
161
+ gas: gasLimit,
162
+ maxFeePerGas: gasPrice.maxFeePerGas,
163
+ maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
164
+ nonce,
165
+ };
166
+
167
+ const txData = blobInputs
168
+ ? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! }
169
+ : baseTxData;
170
+
171
+ const signedRequest = await this.prepareSignedTransaction(txData);
172
+ const txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
173
+
174
+ l1TxState.txHashes.push(txHash);
175
+ this.txs.push(l1TxState);
176
+ if (this.txs.length > MAX_L1_TX_STATES) {
177
+ this.txs.shift();
160
178
  }
161
- this.state = stateChange;
179
+
162
180
  const cleanGasConfig = pickBy(gasConfig, (_, key) => key in l1TxUtilsConfigMappings);
163
181
  this.logger?.info(`Sent L1 transaction ${txHash}`, {
164
182
  gasLimit,
@@ -168,7 +186,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
168
186
  ...(gasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(gasPrice.maxFeePerBlobGas) }),
169
187
  });
170
188
 
171
- return { txHash, gasLimit, gasPrice };
189
+ return { txHash, state: l1TxState };
172
190
  } catch (err: any) {
173
191
  const viemError = formatViemError(err, request.abi);
174
192
  this.logger?.error(`Failed to send L1 transaction`, viemError.message, {
@@ -178,60 +196,51 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
178
196
  }
179
197
  }
180
198
 
199
+ private async tryGetTxReceipt(
200
+ txHashes: Hex[],
201
+ nonce: number,
202
+ isCancelTx: boolean,
203
+ ): Promise<TransactionReceipt | undefined> {
204
+ for (const hash of txHashes) {
205
+ try {
206
+ const receipt = await this.client.getTransactionReceipt({ hash });
207
+ if (receipt) {
208
+ const what = isCancelTx ? 'Cancellation L1 transaction' : 'L1 transaction';
209
+ if (receipt.status === 'reverted') {
210
+ this.logger?.warn(`${what} ${hash} with nonce ${nonce} reverted`, receipt);
211
+ } else {
212
+ this.logger?.verbose(`${what} ${hash} with nonce ${nonce} mined`, receipt);
213
+ }
214
+ return receipt;
215
+ }
216
+ } catch (err) {
217
+ if (err instanceof Error && err.name === 'TransactionReceiptNotFoundError') {
218
+ continue;
219
+ } else {
220
+ this.logger.error(`Error getting receipt for tx ${hash}`, err);
221
+ continue;
222
+ }
223
+ }
224
+ }
225
+ }
226
+
181
227
  /**
182
228
  * Monitors a transaction until completion, handling speed-ups if needed
183
- * @param request - Original transaction request (needed for speed-ups)
184
- * @param initialTxHash - Hash of the initial transaction
185
- * @param allVersions - Hashes of all transactions submitted under the same nonce (any of them could mine)
186
- * @param params - Parameters used in the initial transaction
187
- * @param gasConfig - Optional gas configuration
188
229
  */
189
- public async monitorTransaction(
190
- request: L1TxRequest,
191
- initialTxHash: Hex,
192
- allVersions: Set<Hex>,
193
- params: { gasLimit: bigint },
194
- _gasConfig?: Partial<L1TxUtilsConfig> & { txTimeoutAt?: Date },
195
- _blobInputs?: L1BlobInputs,
196
- isCancelTx: boolean = false,
197
- ): Promise<TransactionReceipt> {
198
- const isBlobTx = !!_blobInputs;
199
- const gasConfig = { ...this.config, ..._gasConfig };
230
+ protected async monitorTransaction(state: L1TxState): Promise<TransactionReceipt> {
231
+ const { request, nonce, txHashes, cancelTxHashes, gasLimit, blobInputs, txConfig: gasConfig } = state;
232
+ const isCancelTx = cancelTxHashes.length > 0;
233
+ const isBlobTx = !!blobInputs;
200
234
  const account = this.getSenderAddress().toString();
201
235
 
202
- const blobInputs = _blobInputs || {};
203
236
  const makeGetTransactionBackoff = () =>
204
237
  makeBackoff(times(gasConfig.txPropagationMaxQueryAttempts ?? 3, i => i + 1));
205
238
 
206
- // Retry a few times, in case the tx is not yet propagated.
207
- const tx = await retry<GetTransactionReturnType>(
208
- () => this.client.getTransaction({ hash: initialTxHash }),
209
- `Getting L1 transaction ${initialTxHash}`,
210
- makeGetTransactionBackoff(),
211
- this.logger,
212
- true,
213
- );
214
-
215
- if (!tx) {
216
- throw new Error(`Failed to get L1 transaction ${initialTxHash} to monitor`);
217
- }
218
-
219
- if (tx?.nonce === undefined || tx?.nonce === null) {
220
- throw new Error(`Failed to get L1 transaction ${initialTxHash} nonce`);
221
- }
222
- const nonce = tx.nonce;
223
-
224
- allVersions.add(initialTxHash);
225
- let currentTxHash = initialTxHash;
239
+ let currentTxHash = isCancelTx ? cancelTxHashes[0] : txHashes[0];
226
240
  let attempts = 0;
227
241
  let lastAttemptSent = this.dateProvider.now();
228
- let lastGasPrice: GasPrice = {
229
- maxFeePerGas: tx.maxFeePerGas!,
230
- maxPriorityFeePerGas: tx.maxPriorityFeePerGas!,
231
- maxFeePerBlobGas: tx.maxFeePerBlobGas!,
232
- };
233
- const initialTxTime = lastAttemptSent;
234
242
 
243
+ const initialTxTime = lastAttemptSent;
235
244
  let txTimedOut = false;
236
245
  let latestBlockTimestamp: bigint | undefined;
237
246
 
@@ -256,28 +265,19 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
256
265
  const currentNonce = await this.client.getTransactionCount({ address: account });
257
266
  // If the current nonce on our account is greater than our transaction's nonce then a tx with the same nonce has been mined.
258
267
  if (currentNonce > nonce) {
259
- for (const hash of allVersions) {
260
- try {
261
- const receipt = await this.client.getTransactionReceipt({ hash });
262
- if (receipt) {
263
- if (receipt.status === 'reverted') {
264
- this.logger?.error(`L1 transaction ${hash} reverted`, receipt);
265
- } else {
266
- this.logger?.debug(`L1 transaction ${hash} mined`);
267
- }
268
- this.state = TxUtilsState.MINED;
269
- this.lastMinedAtBlockNumber = receipt.blockNumber;
270
- return receipt;
271
- }
272
- } catch (err) {
273
- if (err instanceof Error && err.message.includes('reverted')) {
274
- throw formatViemError(err);
275
- }
276
- }
268
+ const receipt =
269
+ (await this.tryGetTxReceipt(cancelTxHashes, nonce, true)) ??
270
+ (await this.tryGetTxReceipt(txHashes, nonce, false));
271
+
272
+ if (receipt) {
273
+ this.updateState(state, TxUtilsState.MINED);
274
+ state.receipt = receipt;
275
+ return receipt;
277
276
  }
277
+
278
278
  // If we get here then we have checked all of our tx versions and not found anything.
279
279
  // We should consider the nonce as MINED
280
- this.state = TxUtilsState.MINED;
280
+ this.updateState(state, TxUtilsState.MINED);
281
281
  throw new Error(`Nonce ${nonce} is MINED but not by one of our expected transactions`);
282
282
  }
283
283
 
@@ -328,7 +328,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
328
328
  }
329
329
  : undefined,
330
330
  );
331
- lastGasPrice = newGasPrice;
331
+ state.gasPrice = newGasPrice;
332
332
 
333
333
  this.logger?.debug(
334
334
  `L1 transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` +
@@ -340,26 +340,28 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
340
340
  },
341
341
  );
342
342
 
343
- const txData: PrepareTransactionRequestRequest = {
343
+ const baseTxData = {
344
344
  ...request,
345
- ...blobInputs,
346
- nonce,
347
- gas: params.gasLimit,
345
+ gas: gasLimit,
348
346
  maxFeePerGas: newGasPrice.maxFeePerGas,
349
347
  maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas,
348
+ nonce,
350
349
  };
351
- if (isBlobTx && newGasPrice.maxFeePerBlobGas) {
352
- (txData as any).maxFeePerBlobGas = newGasPrice.maxFeePerBlobGas;
353
- }
350
+
351
+ const txData = blobInputs
352
+ ? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: newGasPrice.maxFeePerBlobGas! }
353
+ : baseTxData;
354
+
354
355
  const signedRequest = await this.prepareSignedTransaction(txData);
355
356
  const newHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
357
+
356
358
  if (!isCancelTx) {
357
- this.state = TxUtilsState.SPEED_UP;
359
+ this.updateState(state, TxUtilsState.SPEED_UP);
358
360
  }
359
361
 
360
362
  const cleanGasConfig = pickBy(gasConfig, (_, key) => key in l1TxUtilsConfigMappings);
361
363
  this.logger?.verbose(`Sent L1 speed-up tx ${newHash}, replacing ${currentTxHash}`, {
362
- gasLimit: params.gasLimit,
364
+ gasLimit,
363
365
  maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
364
366
  maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
365
367
  gasConfig: cleanGasConfig,
@@ -368,7 +370,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
368
370
 
369
371
  currentTxHash = newHash;
370
372
 
371
- allVersions.add(currentTxHash);
373
+ (isCancelTx ? cancelTxHashes : txHashes).push(currentTxHash);
372
374
  lastAttemptSent = this.dateProvider.now();
373
375
  }
374
376
  await sleep(gasConfig.checkIntervalMs!);
@@ -387,10 +389,10 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
387
389
  // The transaction has timed out. If it's a cancellation then we are giving up on it.
388
390
  // Otherwise we may attempt to cancel it if configured to do so.
389
391
  if (isCancelTx) {
390
- this.state = TxUtilsState.NOT_MINED;
392
+ this.updateState(state, TxUtilsState.NOT_MINED);
391
393
  } else if (gasConfig.cancelTxOnTimeout) {
392
394
  // Fire cancellation without awaiting to avoid blocking the main thread
393
- this.attemptTxCancellation(currentTxHash, nonce, allVersions, isBlobTx, lastGasPrice, attempts).catch(err => {
395
+ this.attemptTxCancellation(state, attempts).catch(err => {
394
396
  const viemError = formatViemError(err);
395
397
  this.logger?.error(`Failed to send cancellation for timed out tx ${currentTxHash}:`, viemError.message, {
396
398
  metaMessages: viemError.metaMessages,
@@ -398,7 +400,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
398
400
  });
399
401
  }
400
402
 
401
- this.logger?.error(`L1 transaction ${currentTxHash} timed out`, undefined, {
403
+ this.logger?.warn(`L1 transaction ${currentTxHash} timed out`, {
402
404
  txHash: currentTxHash,
403
405
  txTimeoutAt: gasConfig.txTimeoutAt,
404
406
  txTimeoutMs: gasConfig.txTimeoutMs,
@@ -406,7 +408,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
406
408
  now: this.dateProvider.now(),
407
409
  attempts,
408
410
  isInterrupted: this.interrupted,
409
- ...tx,
410
411
  });
411
412
 
412
413
  throw new TimeoutError(`L1 transaction ${currentTxHash} timed out`);
@@ -422,10 +423,10 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
422
423
  request: L1TxRequest,
423
424
  gasConfig?: L1GasConfig,
424
425
  blobInputs?: L1BlobInputs,
425
- ): Promise<{ receipt: TransactionReceipt; gasPrice: GasPrice }> {
426
- const { txHash, gasLimit, gasPrice } = await this.sendTransaction(request, gasConfig, blobInputs);
427
- const receipt = await this.monitorTransaction(request, txHash, new Set(), { gasLimit }, gasConfig, blobInputs);
428
- return { receipt, gasPrice };
426
+ ): Promise<{ receipt: TransactionReceipt; state: L1TxState }> {
427
+ const { state } = await this.sendTransaction(request, gasConfig, blobInputs);
428
+ const receipt = await this.monitorTransaction(state);
429
+ return { receipt, state };
429
430
  }
430
431
 
431
432
  public override async simulate(
@@ -458,23 +459,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
458
459
 
459
460
  /**
460
461
  * Attempts to cancel a transaction by sending a 0-value tx to self with same nonce but higher gas prices
461
- * @param nonce - The nonce of the transaction to cancel
462
- * @param allVersions - Hashes of all transactions submitted under the same nonce (any of them could mine)
463
- * @param previousGasPrice - The gas price of the previous transaction
464
- * @param attempts - The number of attempts to cancel the transaction
465
462
  * @returns The hash of the cancellation transaction
466
463
  */
467
- protected async attemptTxCancellation(
468
- currentTxHash: Hex,
469
- nonce: number,
470
- allVersions: Set<Hex>,
471
- isBlobTx = false,
472
- previousGasPrice?: GasPrice,
473
- attempts = 0,
474
- ) {
475
- if (isBlobTx) {
476
- throw new Error('Cannot cancel blob transactions, please use L1TxUtilsWithBlobsClass');
477
- }
464
+ protected async attemptTxCancellation(state: L1TxState, attempts: number) {
465
+ const isBlobTx = state.blobInputs !== undefined;
466
+ const { nonce, gasPrice: previousGasPrice } = state;
478
467
 
479
468
  // Get gas price with higher priority fee for cancellation
480
469
  const cancelGasPrice = await this.getGasPrice(
@@ -488,40 +477,53 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
488
477
  previousGasPrice,
489
478
  );
490
479
 
491
- this.logger?.info(`Attempting to cancel L1 transaction ${currentTxHash} with nonce ${nonce}`, {
492
- maxFeePerGas: formatGwei(cancelGasPrice.maxFeePerGas),
493
- maxPriorityFeePerGas: formatGwei(cancelGasPrice.maxPriorityFeePerGas),
494
- });
480
+ const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelGasPrice;
481
+ this.logger?.info(
482
+ `Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction ${state.txHashes[0]} with nonce ${nonce}`,
483
+ {
484
+ maxFeePerGas: formatGwei(maxFeePerGas),
485
+ maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
486
+ ...(maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(maxFeePerBlobGas) }),
487
+ },
488
+ );
489
+
495
490
  const request = {
496
491
  to: this.getSenderAddress().toString(),
497
492
  value: 0n,
498
493
  };
499
494
 
500
495
  // Send 0-value tx to self with higher gas price
501
- const txData = {
496
+ const baseTxData = {
502
497
  ...request,
503
498
  nonce,
504
- gas: 21_000n, // Standard ETH transfer gas
505
- maxFeePerGas: cancelGasPrice.maxFeePerGas,
506
- maxPriorityFeePerGas: cancelGasPrice.maxPriorityFeePerGas,
499
+ gas: 21_000n,
500
+ maxFeePerGas,
501
+ maxPriorityFeePerGas,
507
502
  };
503
+
504
+ const txData = isBlobTx ? { ...baseTxData, ...this.makeEmptyBlobInputs(maxFeePerBlobGas!) } : baseTxData;
508
505
  const signedRequest = await this.prepareSignedTransaction(txData);
509
506
  const cancelTxHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
510
507
 
511
- this.state = TxUtilsState.CANCELLED;
508
+ state.gasPrice = cancelGasPrice;
509
+ state.gasLimit = 21_000n;
510
+ state.cancelTxHashes.push(cancelTxHash);
512
511
 
513
- this.logger?.info(`Sent cancellation tx ${cancelTxHash} for timed out tx ${currentTxHash}`, { nonce });
512
+ this.updateState(state, TxUtilsState.CANCELLED);
514
513
 
515
- const receipt = await this.monitorTransaction(
516
- request,
517
- cancelTxHash,
518
- allVersions,
519
- { gasLimit: 21_000n },
520
- undefined,
521
- undefined,
522
- true,
523
- );
514
+ this.logger?.info(`Sent cancellation tx ${cancelTxHash} for timed out tx with nonce ${nonce}`, {
515
+ nonce,
516
+ txData,
517
+ isBlobTx,
518
+ txHashes: state.txHashes,
519
+ });
520
+
521
+ const { transactionHash } = await this.monitorTransaction(state);
522
+ return transactionHash;
523
+ }
524
524
 
525
- return receipt.transactionHash;
525
+ /** Makes empty blob inputs for the cancellation tx. To be overridden in L1TxUtilsWithBlobs. */
526
+ protected makeEmptyBlobInputs(_maxFeePerBlobGas: bigint): Required<L1BlobInputs> {
527
+ throw new Error('Cannot make empty blob inputs for cancellation');
526
528
  }
527
529
  }
@@ -3,113 +3,22 @@ import { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
4
  import { DateProvider } from '@aztec/foundation/timer';
5
5
 
6
- import { type Hex, type TransactionSerializable, formatGwei } from 'viem';
6
+ import type { TransactionSerializable } from 'viem';
7
7
 
8
8
  import type { EthSigner } from '../eth-signer/eth-signer.js';
9
9
  import type { ExtendedViemWalletClient, ViemClient } from '../types.js';
10
10
  import type { L1TxUtilsConfig } from './config.js';
11
11
  import { L1TxUtils } from './l1_tx_utils.js';
12
12
  import { createViemSigner } from './signer.js';
13
- import type { GasPrice, SigningCallback } from './types.js';
13
+ import type { L1BlobInputs, SigningCallback } from './types.js';
14
14
 
15
+ /** Extends L1TxUtils with the capability to cancel blobs. This needs to be a separate class so we don't require a dependency on blob-lib unnecessarily. */
15
16
  export class L1TxUtilsWithBlobs extends L1TxUtils {
16
- /**
17
- * Attempts to cancel a transaction by sending a 0-value tx to self with same nonce but higher gas prices
18
- * @param nonce - The nonce of the transaction to cancel
19
- * @param allVersions - Hashes of all transactions submitted under the same nonce (any of them could mine)
20
- * @param previousGasPrice - The gas price of the previous transaction
21
- * @param attempts - The number of attempts to cancel the transaction
22
- * @returns The hash of the cancellation transaction
23
- */
24
- protected override async attemptTxCancellation(
25
- currentTxHash: Hex,
26
- nonce: number,
27
- allVersions: Set<Hex>,
28
- isBlobTx = false,
29
- previousGasPrice?: GasPrice,
30
- attempts = 0,
31
- ) {
32
- // Get gas price with higher priority fee for cancellation
33
- const cancelGasPrice = await this.getGasPrice(
34
- {
35
- ...this.config,
36
- // Use high bump for cancellation to ensure it replaces the original tx
37
- priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
38
- },
39
- isBlobTx,
40
- attempts + 1,
41
- previousGasPrice,
42
- );
43
-
44
- this.logger?.info(`Attempting to cancel blob L1 transaction ${currentTxHash} with nonce ${nonce}`, {
45
- maxFeePerGas: formatGwei(cancelGasPrice.maxFeePerGas),
46
- maxPriorityFeePerGas: formatGwei(cancelGasPrice.maxPriorityFeePerGas),
47
- maxFeePerBlobGas:
48
- cancelGasPrice.maxFeePerBlobGas === undefined ? undefined : formatGwei(cancelGasPrice.maxFeePerBlobGas),
49
- });
50
- const request = {
51
- to: this.getSenderAddress().toString(),
52
- value: 0n,
53
- };
54
-
55
- // Send 0-value tx to self with higher gas price
56
- if (!isBlobTx) {
57
- const txData = {
58
- ...request,
59
- nonce,
60
- gas: 21_000n, // Standard ETH transfer gas
61
- maxFeePerGas: cancelGasPrice.maxFeePerGas,
62
- maxPriorityFeePerGas: cancelGasPrice.maxPriorityFeePerGas,
63
- };
64
- const signedRequest = await this.prepareSignedTransaction(txData);
65
- const cancelTxHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
66
-
67
- this.logger?.info(`Sent cancellation tx ${cancelTxHash} for timed out tx ${currentTxHash}`);
68
-
69
- const receipt = await this.monitorTransaction(
70
- request,
71
- cancelTxHash,
72
- allVersions,
73
- { gasLimit: 21_000n },
74
- undefined,
75
- undefined,
76
- true,
77
- );
78
-
79
- return receipt.transactionHash;
80
- } else {
81
- const blobData = new Uint8Array(131072).fill(0);
82
- const kzg = Blob.getViemKzgInstance();
83
- const blobInputs = {
84
- blobs: [blobData],
85
- kzg,
86
- maxFeePerBlobGas: cancelGasPrice.maxFeePerBlobGas!,
87
- };
88
- const txData = {
89
- ...request,
90
- ...blobInputs,
91
- nonce,
92
- gas: 21_000n,
93
- maxFeePerGas: cancelGasPrice.maxFeePerGas,
94
- maxPriorityFeePerGas: cancelGasPrice.maxPriorityFeePerGas,
95
- };
96
- const signedRequest = await this.prepareSignedTransaction(txData);
97
- const cancelTxHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
98
-
99
- this.logger?.info(`Sent cancellation tx ${cancelTxHash} for timed out tx ${currentTxHash}`);
100
-
101
- const receipt = await this.monitorTransaction(
102
- request,
103
- cancelTxHash,
104
- allVersions,
105
- { gasLimit: 21_000n },
106
- undefined,
107
- blobInputs,
108
- true,
109
- );
110
-
111
- return receipt.transactionHash;
112
- }
17
+ /** Makes empty blob inputs for the cancellation tx. */
18
+ protected override makeEmptyBlobInputs(maxFeePerBlobGas: bigint): Required<L1BlobInputs> {
19
+ const blobData = new Uint8Array(131072).fill(0);
20
+ const kzg = Blob.getViemKzgInstance();
21
+ return { blobs: [blobData], kzg, maxFeePerBlobGas };
113
22
  }
114
23
  }
115
24
 
@@ -34,7 +34,7 @@ import type { GasPrice, L1BlobInputs, L1TxRequest, TransactionStats } from './ty
34
34
  import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './utils.js';
35
35
 
36
36
  export class ReadOnlyL1TxUtils {
37
- public readonly config: L1TxUtilsConfig;
37
+ public config: L1TxUtilsConfig;
38
38
  protected interrupted = false;
39
39
 
40
40
  constructor(
@@ -1,7 +1,8 @@
1
+ import type { BlobKzgInstance } from '@aztec/blob-lib/types';
1
2
  import { EthAddress } from '@aztec/foundation/eth-address';
2
3
  import type { ViemTransactionSignature } from '@aztec/foundation/eth-signature';
3
4
 
4
- import type { Abi, Address, Hex, TransactionSerializable } from 'viem';
5
+ import type { Abi, Address, Hex, TransactionReceipt, TransactionSerializable } from 'viem';
5
6
 
6
7
  import type { L1TxUtilsConfig } from './config.js';
7
8
 
@@ -16,7 +17,7 @@ export type L1GasConfig = Partial<L1TxUtilsConfig> & { gasLimit?: bigint; txTime
16
17
 
17
18
  export interface L1BlobInputs {
18
19
  blobs: Uint8Array[];
19
- kzg: any;
20
+ kzg: BlobKzgInstance;
20
21
  maxFeePerBlobGas?: bigint;
21
22
  }
22
23
 
@@ -46,6 +47,19 @@ export enum TxUtilsState {
46
47
  MINED,
47
48
  }
48
49
 
50
+ export type L1TxState = {
51
+ txHashes: Hex[];
52
+ cancelTxHashes: Hex[];
53
+ gasLimit: bigint;
54
+ gasPrice: GasPrice;
55
+ txConfig: L1GasConfig;
56
+ request: L1TxRequest;
57
+ status: TxUtilsState;
58
+ nonce: number;
59
+ receipt?: TransactionReceipt;
60
+ blobInputs: L1BlobInputs | undefined;
61
+ };
62
+
49
63
  export type SigningCallback = (
50
64
  transaction: TransactionSerializable,
51
65
  signingAddress: EthAddress,