@aztec/ethereum 3.0.0-nightly.20250925 → 3.0.0-nightly.20250927

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 (89) hide show
  1. package/dest/config.d.ts +1 -1
  2. package/dest/config.d.ts.map +1 -1
  3. package/dest/config.js +1 -1
  4. package/dest/contracts/empire_base.d.ts +1 -1
  5. package/dest/contracts/empire_base.d.ts.map +1 -1
  6. package/dest/contracts/empire_slashing_proposer.d.ts +1 -1
  7. package/dest/contracts/empire_slashing_proposer.d.ts.map +1 -1
  8. package/dest/contracts/fee_asset_handler.d.ts +3 -3
  9. package/dest/contracts/fee_asset_handler.d.ts.map +1 -1
  10. package/dest/contracts/governance.js +1 -1
  11. package/dest/contracts/governance_proposer.d.ts +1 -1
  12. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  13. package/dest/contracts/multicall.d.ts +4 -4
  14. package/dest/contracts/multicall.d.ts.map +1 -1
  15. package/dest/contracts/rollup.d.ts +3 -3
  16. package/dest/contracts/rollup.d.ts.map +1 -1
  17. package/dest/deploy_l1_contracts.d.ts +1 -1
  18. package/dest/deploy_l1_contracts.d.ts.map +1 -1
  19. package/dest/deploy_l1_contracts.js +1 -1
  20. package/dest/index.d.ts +1 -1
  21. package/dest/index.d.ts.map +1 -1
  22. package/dest/index.js +1 -1
  23. package/dest/l1_artifacts.d.ts +978 -2218
  24. package/dest/l1_artifacts.d.ts.map +1 -1
  25. package/dest/l1_tx_utils/config.d.ts +56 -0
  26. package/dest/l1_tx_utils/config.d.ts.map +1 -0
  27. package/dest/l1_tx_utils/config.js +67 -0
  28. package/dest/l1_tx_utils/constants.d.ts +6 -0
  29. package/dest/l1_tx_utils/constants.d.ts.map +1 -0
  30. package/dest/l1_tx_utils/constants.js +14 -0
  31. package/dest/l1_tx_utils/factory.d.ts +9 -0
  32. package/dest/l1_tx_utils/factory.d.ts.map +1 -0
  33. package/dest/l1_tx_utils/factory.js +14 -0
  34. package/dest/l1_tx_utils/index.d.ts +9 -0
  35. package/dest/l1_tx_utils/index.d.ts.map +1 -0
  36. package/dest/l1_tx_utils/index.js +9 -0
  37. package/dest/l1_tx_utils/l1_tx_utils.d.ts +80 -0
  38. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -0
  39. package/dest/{l1_tx_utils.js → l1_tx_utils/l1_tx_utils.js} +14 -433
  40. package/dest/{l1_tx_utils_with_blobs.d.ts → l1_tx_utils/l1_tx_utils_with_blobs.d.ts} +6 -4
  41. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +1 -0
  42. package/dest/{l1_tx_utils_with_blobs.js → l1_tx_utils/l1_tx_utils_with_blobs.js} +2 -1
  43. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +81 -0
  44. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -0
  45. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +304 -0
  46. package/dest/l1_tx_utils/signer.d.ts +4 -0
  47. package/dest/l1_tx_utils/signer.d.ts.map +1 -0
  48. package/dest/l1_tx_utils/signer.js +16 -0
  49. package/dest/l1_tx_utils/types.d.ts +44 -0
  50. package/dest/l1_tx_utils/types.d.ts.map +1 -0
  51. package/dest/l1_tx_utils/types.js +9 -0
  52. package/dest/l1_tx_utils/utils.d.ts +4 -0
  53. package/dest/l1_tx_utils/utils.d.ts.map +1 -0
  54. package/dest/l1_tx_utils/utils.js +14 -0
  55. package/dest/publisher_manager.d.ts +1 -1
  56. package/dest/publisher_manager.d.ts.map +1 -1
  57. package/dest/publisher_manager.js +1 -1
  58. package/dest/test/delayed_tx_utils.d.ts +2 -2
  59. package/dest/test/delayed_tx_utils.d.ts.map +1 -1
  60. package/dest/test/delayed_tx_utils.js +2 -2
  61. package/dest/test/upgrade_utils.js +1 -1
  62. package/package.json +6 -6
  63. package/src/config.ts +1 -1
  64. package/src/contracts/empire_base.ts +1 -1
  65. package/src/contracts/empire_slashing_proposer.ts +1 -1
  66. package/src/contracts/fee_asset_handler.ts +1 -1
  67. package/src/contracts/governance.ts +1 -1
  68. package/src/contracts/governance_proposer.ts +1 -1
  69. package/src/contracts/multicall.ts +1 -1
  70. package/src/contracts/rollup.ts +1 -1
  71. package/src/deploy_l1_contracts.ts +1 -1
  72. package/src/index.ts +1 -1
  73. package/src/l1_tx_utils/config.ts +129 -0
  74. package/src/l1_tx_utils/constants.ts +18 -0
  75. package/src/l1_tx_utils/factory.ts +44 -0
  76. package/src/l1_tx_utils/index.ts +11 -0
  77. package/src/l1_tx_utils/l1_tx_utils.ts +527 -0
  78. package/src/{l1_tx_utils_with_blobs.ts → l1_tx_utils/l1_tx_utils_with_blobs.ts} +7 -10
  79. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +368 -0
  80. package/src/l1_tx_utils/signer.ts +28 -0
  81. package/src/l1_tx_utils/types.ts +52 -0
  82. package/src/l1_tx_utils/utils.ts +16 -0
  83. package/src/publisher_manager.ts +1 -1
  84. package/src/test/delayed_tx_utils.ts +2 -2
  85. package/src/test/upgrade_utils.ts +1 -1
  86. package/dest/l1_tx_utils.d.ts +0 -252
  87. package/dest/l1_tx_utils.d.ts.map +0 -1
  88. package/dest/l1_tx_utils_with_blobs.d.ts.map +0 -1
  89. package/src/l1_tx_utils.ts +0 -1125
@@ -0,0 +1,527 @@
1
+ import { times } from '@aztec/foundation/collection';
2
+ import { TimeoutError } from '@aztec/foundation/error';
3
+ import { EthAddress } from '@aztec/foundation/eth-address';
4
+ import { type Logger, createLogger } from '@aztec/foundation/log';
5
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
6
+ import { sleep } from '@aztec/foundation/sleep';
7
+ import { DateProvider } from '@aztec/foundation/timer';
8
+ import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
9
+
10
+ import pickBy from 'lodash.pickby';
11
+ import {
12
+ type Abi,
13
+ type BlockOverrides,
14
+ type GetTransactionReturnType,
15
+ type Hex,
16
+ type NonceManager,
17
+ type PrepareTransactionRequestRequest,
18
+ type StateOverride,
19
+ type TransactionReceipt,
20
+ type TransactionSerializable,
21
+ createNonceManager,
22
+ formatGwei,
23
+ serializeTransaction,
24
+ } from 'viem';
25
+ import { jsonRpc } from 'viem/nonce';
26
+
27
+ import type { ViemClient } from '../types.js';
28
+ import { formatViemError } from '../utils.js';
29
+ import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
30
+ import { LARGE_GAS_LIMIT } from './constants.js';
31
+ import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
32
+ import {
33
+ type GasPrice,
34
+ type L1BlobInputs,
35
+ type L1GasConfig,
36
+ type L1TxRequest,
37
+ type SigningCallback,
38
+ TxUtilsState,
39
+ } from './types.js';
40
+
41
+ export class L1TxUtils extends ReadOnlyL1TxUtils {
42
+ private txUtilsState: TxUtilsState = TxUtilsState.IDLE;
43
+ private lastMinedBlockNumber: bigint | undefined = undefined;
44
+ private nonceManager: NonceManager;
45
+
46
+ constructor(
47
+ public override client: ViemClient,
48
+ public address: EthAddress,
49
+ protected signer: SigningCallback,
50
+ protected override logger: Logger = createLogger('L1TxUtils'),
51
+ dateProvider: DateProvider = new DateProvider(),
52
+ config?: Partial<L1TxUtilsConfig>,
53
+ debugMaxGasLimit: boolean = false,
54
+ ) {
55
+ super(client, logger, dateProvider, config, debugMaxGasLimit);
56
+ this.nonceManager = createNonceManager({ source: jsonRpc() });
57
+ }
58
+
59
+ public get state() {
60
+ return this.txUtilsState;
61
+ }
62
+
63
+ public get lastMinedAtBlockNumber() {
64
+ return this.lastMinedBlockNumber;
65
+ }
66
+
67
+ private set lastMinedAtBlockNumber(blockNumber: bigint | undefined) {
68
+ this.lastMinedBlockNumber = blockNumber;
69
+ }
70
+
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()}`,
75
+ );
76
+ }
77
+
78
+ public getSenderAddress() {
79
+ return this.address;
80
+ }
81
+
82
+ public getSenderBalance(): Promise<bigint> {
83
+ return this.client.getBalance({
84
+ address: this.getSenderAddress().toString(),
85
+ });
86
+ }
87
+
88
+ private async signTransaction(txRequest: TransactionSerializable): Promise<`0x${string}`> {
89
+ const signature = await this.signer(txRequest, this.getSenderAddress());
90
+ return serializeTransaction(txRequest, signature);
91
+ }
92
+
93
+ protected async prepareSignedTransaction(txData: PrepareTransactionRequestRequest) {
94
+ const txRequest = await this.client.prepareTransactionRequest(txData);
95
+ return await this.signTransaction(txRequest as TransactionSerializable);
96
+ }
97
+
98
+ /**
99
+ * Sends a transaction with gas estimation and pricing
100
+ * @param request - The transaction request (to, data, value)
101
+ * @param gasConfig - Optional gas configuration
102
+ * @returns The transaction hash and parameters used
103
+ */
104
+ public async sendTransaction(
105
+ request: L1TxRequest,
106
+ _gasConfig?: L1GasConfig,
107
+ blobInputs?: L1BlobInputs,
108
+ stateChange: TxUtilsState = TxUtilsState.SENT,
109
+ ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> {
110
+ try {
111
+ const gasConfig = { ...this.config, ..._gasConfig };
112
+ const account = this.getSenderAddress().toString();
113
+
114
+ let gasLimit: bigint;
115
+ if (this.debugMaxGasLimit) {
116
+ gasLimit = LARGE_GAS_LIMIT;
117
+ } else if (gasConfig.gasLimit) {
118
+ gasLimit = gasConfig.gasLimit;
119
+ } else {
120
+ gasLimit = await this.estimateGas(account, request, gasConfig);
121
+ }
122
+ this.logger?.debug(`Gas limit for request is ${gasLimit}`, { gasLimit, ...request });
123
+
124
+ const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
125
+
126
+ if (gasConfig.txTimeoutAt && this.dateProvider.now() > gasConfig.txTimeoutAt.getTime()) {
127
+ throw new Error('Transaction timed out before sending');
128
+ }
129
+
130
+ const nonce = await this.nonceManager.consume({
131
+ client: this.client,
132
+ address: account,
133
+ chainId: this.client.chain.id,
134
+ });
135
+
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 });
160
+ }
161
+ this.state = stateChange;
162
+ const cleanGasConfig = pickBy(gasConfig, (_, key) => key in l1TxUtilsConfigMappings);
163
+ this.logger?.info(`Sent L1 transaction ${txHash}`, {
164
+ gasLimit,
165
+ maxFeePerGas: formatGwei(gasPrice.maxFeePerGas),
166
+ maxPriorityFeePerGas: formatGwei(gasPrice.maxPriorityFeePerGas),
167
+ gasConfig: cleanGasConfig,
168
+ ...(gasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(gasPrice.maxFeePerBlobGas) }),
169
+ });
170
+
171
+ return { txHash, gasLimit, gasPrice };
172
+ } catch (err: any) {
173
+ const viemError = formatViemError(err, request.abi);
174
+ this.logger?.error(`Failed to send L1 transaction`, viemError.message, {
175
+ metaMessages: viemError.metaMessages,
176
+ });
177
+ throw viemError;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 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
+ */
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 };
200
+ const account = this.getSenderAddress().toString();
201
+
202
+ const blobInputs = _blobInputs || {};
203
+ const makeGetTransactionBackoff = () =>
204
+ makeBackoff(times(gasConfig.txPropagationMaxQueryAttempts ?? 3, i => i + 1));
205
+
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;
226
+ let attempts = 0;
227
+ 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
+
235
+ let txTimedOut = false;
236
+ let latestBlockTimestamp: bigint | undefined;
237
+
238
+ // We check against the latestBlockTimestamp as opposed to the current time to avoid a race condition where
239
+ // the tx is mined in a block with the same timestamp as txTimeoutAt, but our execution node has not yet processed it,
240
+ // or the loop here has not yet checked the tx before that timeout.
241
+ const isTimedOut = () =>
242
+ (gasConfig.txTimeoutAt &&
243
+ latestBlockTimestamp !== undefined &&
244
+ Number(latestBlockTimestamp) * 1000 >= gasConfig.txTimeoutAt.getTime()) ||
245
+ (gasConfig.txTimeoutMs !== undefined && this.dateProvider.now() - initialTxTime > gasConfig.txTimeoutMs) ||
246
+ this.interrupted ||
247
+ false;
248
+
249
+ while (!txTimedOut) {
250
+ try {
251
+ ({ timestamp: latestBlockTimestamp } = await this.client.getBlock({
252
+ blockTag: 'latest',
253
+ includeTransactions: false,
254
+ }));
255
+
256
+ const currentNonce = await this.client.getTransactionCount({ address: account });
257
+ // 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
+ 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
+ }
277
+ }
278
+ // If we get here then we have checked all of our tx versions and not found anything.
279
+ // We should consider the nonce as MINED
280
+ this.state = TxUtilsState.MINED;
281
+ throw new Error(`Nonce ${nonce} is MINED but not by one of our expected transactions`);
282
+ }
283
+
284
+ this.logger?.trace(`Tx timeout check for ${currentTxHash}: ${isTimedOut()}`, {
285
+ latestBlockTimestamp: Number(latestBlockTimestamp) * 1000,
286
+ lastAttemptSent,
287
+ initialTxTime,
288
+ now: this.dateProvider.now(),
289
+ txTimeoutAt: gasConfig.txTimeoutAt?.getTime(),
290
+ txTimeoutMs: gasConfig.txTimeoutMs,
291
+ txStallTime: gasConfig.stallTimeMs,
292
+ });
293
+
294
+ // Retry a few times, in case the tx is not yet propagated.
295
+ const tx = await retry<GetTransactionReturnType>(
296
+ () => this.client.getTransaction({ hash: currentTxHash }),
297
+ `Getting L1 transaction ${currentTxHash}`,
298
+ makeGetTransactionBackoff(),
299
+ this.logger,
300
+ true,
301
+ );
302
+ const timePassed = this.dateProvider.now() - lastAttemptSent;
303
+
304
+ if (tx && timePassed < gasConfig.stallTimeMs!) {
305
+ this.logger?.debug(`L1 transaction ${currentTxHash} pending. Time passed: ${timePassed}ms.`);
306
+
307
+ // Check timeout before continuing
308
+ txTimedOut = isTimedOut();
309
+ if (txTimedOut) {
310
+ break;
311
+ }
312
+
313
+ await sleep(gasConfig.checkIntervalMs!);
314
+ continue;
315
+ }
316
+
317
+ if (timePassed > gasConfig.stallTimeMs! && attempts < gasConfig.maxAttempts!) {
318
+ attempts++;
319
+ const newGasPrice = await this.getGasPrice(
320
+ gasConfig,
321
+ isBlobTx,
322
+ attempts,
323
+ tx.maxFeePerGas && tx.maxPriorityFeePerGas
324
+ ? {
325
+ maxFeePerGas: tx.maxFeePerGas,
326
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
327
+ maxFeePerBlobGas: tx.maxFeePerBlobGas,
328
+ }
329
+ : undefined,
330
+ );
331
+ lastGasPrice = newGasPrice;
332
+
333
+ this.logger?.debug(
334
+ `L1 transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` +
335
+ `with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei`,
336
+ {
337
+ maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
338
+ maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
339
+ ...(newGasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(newGasPrice.maxFeePerBlobGas) }),
340
+ },
341
+ );
342
+
343
+ const txData: PrepareTransactionRequestRequest = {
344
+ ...request,
345
+ ...blobInputs,
346
+ nonce,
347
+ gas: params.gasLimit,
348
+ maxFeePerGas: newGasPrice.maxFeePerGas,
349
+ maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas,
350
+ };
351
+ if (isBlobTx && newGasPrice.maxFeePerBlobGas) {
352
+ (txData as any).maxFeePerBlobGas = newGasPrice.maxFeePerBlobGas;
353
+ }
354
+ const signedRequest = await this.prepareSignedTransaction(txData);
355
+ const newHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
356
+ if (!isCancelTx) {
357
+ this.state = TxUtilsState.SPEED_UP;
358
+ }
359
+
360
+ const cleanGasConfig = pickBy(gasConfig, (_, key) => key in l1TxUtilsConfigMappings);
361
+ this.logger?.verbose(`Sent L1 speed-up tx ${newHash}, replacing ${currentTxHash}`, {
362
+ gasLimit: params.gasLimit,
363
+ maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
364
+ maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
365
+ gasConfig: cleanGasConfig,
366
+ ...(newGasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(newGasPrice.maxFeePerBlobGas) }),
367
+ });
368
+
369
+ currentTxHash = newHash;
370
+
371
+ allVersions.add(currentTxHash);
372
+ lastAttemptSent = this.dateProvider.now();
373
+ }
374
+ await sleep(gasConfig.checkIntervalMs!);
375
+ } catch (err: any) {
376
+ const viemError = formatViemError(err);
377
+ this.logger?.warn(`Error monitoring L1 transaction ${currentTxHash}:`, viemError.message);
378
+ if (viemError.message?.includes('reverted')) {
379
+ throw viemError;
380
+ }
381
+ await sleep(gasConfig.checkIntervalMs!);
382
+ }
383
+ // Check if tx has timed out.
384
+ txTimedOut = isTimedOut();
385
+ }
386
+
387
+ // The transaction has timed out. If it's a cancellation then we are giving up on it.
388
+ // Otherwise we may attempt to cancel it if configured to do so.
389
+ if (isCancelTx) {
390
+ this.state = TxUtilsState.NOT_MINED;
391
+ } else if (gasConfig.cancelTxOnTimeout) {
392
+ // Fire cancellation without awaiting to avoid blocking the main thread
393
+ this.attemptTxCancellation(currentTxHash, nonce, allVersions, isBlobTx, lastGasPrice, attempts).catch(err => {
394
+ const viemError = formatViemError(err);
395
+ this.logger?.error(`Failed to send cancellation for timed out tx ${currentTxHash}:`, viemError.message, {
396
+ metaMessages: viemError.metaMessages,
397
+ });
398
+ });
399
+ }
400
+
401
+ this.logger?.error(`L1 transaction ${currentTxHash} timed out`, undefined, {
402
+ txHash: currentTxHash,
403
+ txTimeoutAt: gasConfig.txTimeoutAt,
404
+ txTimeoutMs: gasConfig.txTimeoutMs,
405
+ txInitialTime: initialTxTime,
406
+ now: this.dateProvider.now(),
407
+ attempts,
408
+ isInterrupted: this.interrupted,
409
+ ...tx,
410
+ });
411
+
412
+ throw new TimeoutError(`L1 transaction ${currentTxHash} timed out`);
413
+ }
414
+
415
+ /**
416
+ * Sends a transaction and monitors it until completion
417
+ * @param request - The transaction request (to, data, value)
418
+ * @param gasConfig - Optional gas configuration
419
+ * @returns The receipt of the successful transaction
420
+ */
421
+ public async sendAndMonitorTransaction(
422
+ request: L1TxRequest,
423
+ gasConfig?: L1GasConfig,
424
+ 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 };
429
+ }
430
+
431
+ public override async simulate(
432
+ request: L1TxRequest & { gas?: bigint; from?: Hex },
433
+ _blockOverrides: BlockOverrides<bigint, number> = {},
434
+ stateOverrides: StateOverride = [],
435
+ abi: Abi = RollupAbi,
436
+ _gasConfig?: L1TxUtilsConfig & { fallbackGasEstimate?: bigint; ignoreBlockGasLimit?: boolean },
437
+ ): Promise<{ gasUsed: bigint; result: `0x${string}` }> {
438
+ const blockOverrides = { ..._blockOverrides };
439
+ const gasConfig = { ...this.config, ..._gasConfig };
440
+ const gasPrice = await this.getGasPrice(gasConfig, false);
441
+
442
+ const call: any = {
443
+ to: request.to!,
444
+ data: request.data,
445
+ from: request.from ?? this.getSenderAddress().toString(),
446
+ maxFeePerGas: gasPrice.maxFeePerGas,
447
+ maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
448
+ gas: request.gas ?? LARGE_GAS_LIMIT,
449
+ };
450
+
451
+ if (!request.gas && !gasConfig.ignoreBlockGasLimit) {
452
+ // LARGE_GAS_LIMIT is set as call.gas, increase block gasLimit
453
+ blockOverrides.gasLimit = LARGE_GAS_LIMIT * 2n;
454
+ }
455
+
456
+ return this._simulate(call, blockOverrides, stateOverrides, gasConfig, abi);
457
+ }
458
+
459
+ /**
460
+ * 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
+ * @returns The hash of the cancellation transaction
466
+ */
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
+ }
478
+
479
+ // Get gas price with higher priority fee for cancellation
480
+ const cancelGasPrice = await this.getGasPrice(
481
+ {
482
+ ...this.config,
483
+ // Use high bump for cancellation to ensure it replaces the original tx
484
+ priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
485
+ },
486
+ isBlobTx,
487
+ attempts + 1,
488
+ previousGasPrice,
489
+ );
490
+
491
+ this.logger?.info(`Attempting to cancel L1 transaction ${currentTxHash} with nonce ${nonce}`, {
492
+ maxFeePerGas: formatGwei(cancelGasPrice.maxFeePerGas),
493
+ maxPriorityFeePerGas: formatGwei(cancelGasPrice.maxPriorityFeePerGas),
494
+ });
495
+ const request = {
496
+ to: this.getSenderAddress().toString(),
497
+ value: 0n,
498
+ };
499
+
500
+ // Send 0-value tx to self with higher gas price
501
+ const txData = {
502
+ ...request,
503
+ nonce,
504
+ gas: 21_000n, // Standard ETH transfer gas
505
+ maxFeePerGas: cancelGasPrice.maxFeePerGas,
506
+ maxPriorityFeePerGas: cancelGasPrice.maxPriorityFeePerGas,
507
+ };
508
+ const signedRequest = await this.prepareSignedTransaction(txData);
509
+ const cancelTxHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
510
+
511
+ this.state = TxUtilsState.CANCELLED;
512
+
513
+ this.logger?.info(`Sent cancellation tx ${cancelTxHash} for timed out tx ${currentTxHash}`, { nonce });
514
+
515
+ const receipt = await this.monitorTransaction(
516
+ request,
517
+ cancelTxHash,
518
+ allVersions,
519
+ { gasLimit: 21_000n },
520
+ undefined,
521
+ undefined,
522
+ true,
523
+ );
524
+
525
+ return receipt.transactionHash;
526
+ }
527
+ }
@@ -5,15 +5,12 @@ import { DateProvider } from '@aztec/foundation/timer';
5
5
 
6
6
  import { type Hex, type TransactionSerializable, formatGwei } from 'viem';
7
7
 
8
- import type { EthSigner } from './eth-signer/eth-signer.js';
9
- import {
10
- type GasPrice,
11
- L1TxUtils,
12
- type L1TxUtilsConfig,
13
- type SigningCallback,
14
- createViemSigner,
15
- } from './l1_tx_utils.js';
16
- import type { ExtendedViemWalletClient, ViemClient } from './types.js';
8
+ import type { EthSigner } from '../eth-signer/eth-signer.js';
9
+ import type { ExtendedViemWalletClient, ViemClient } from '../types.js';
10
+ import type { L1TxUtilsConfig } from './config.js';
11
+ import { L1TxUtils } from './l1_tx_utils.js';
12
+ import { createViemSigner } from './signer.js';
13
+ import type { GasPrice, SigningCallback } from './types.js';
17
14
 
18
15
  export class L1TxUtilsWithBlobs extends L1TxUtils {
19
16
  /**
@@ -24,7 +21,7 @@ export class L1TxUtilsWithBlobs extends L1TxUtils {
24
21
  * @param attempts - The number of attempts to cancel the transaction
25
22
  * @returns The hash of the cancellation transaction
26
23
  */
27
- override async attemptTxCancellation(
24
+ protected override async attemptTxCancellation(
28
25
  currentTxHash: Hex,
29
26
  nonce: number,
30
27
  allVersions: Set<Hex>,