@aztec/ethereum 3.0.0-nightly.20251003 → 3.0.0-nightly.20251004

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 (37) hide show
  1. package/dest/config.js +1 -1
  2. package/dest/contracts/multicall.d.ts +2 -2
  3. package/dest/contracts/multicall.d.ts.map +1 -1
  4. package/dest/contracts/rollup.d.ts +1 -1
  5. package/dest/contracts/rollup.d.ts.map +1 -1
  6. package/dest/contracts/rollup.js +1 -1
  7. package/dest/deploy_l1_contracts.d.ts +2 -2
  8. package/dest/deploy_l1_contracts.d.ts.map +1 -1
  9. package/dest/l1_tx_utils/config.d.ts +10 -7
  10. package/dest/l1_tx_utils/config.d.ts.map +1 -1
  11. package/dest/l1_tx_utils/config.js +12 -6
  12. package/dest/l1_tx_utils/l1_tx_utils.d.ts +16 -4
  13. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  14. package/dest/l1_tx_utils/l1_tx_utils.js +259 -129
  15. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +2 -2
  16. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  17. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +10 -20
  18. package/dest/l1_tx_utils/types.d.ts +11 -2
  19. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  20. package/dest/l1_tx_utils/types.js +17 -0
  21. package/dest/publisher_manager.d.ts.map +1 -1
  22. package/dest/publisher_manager.js +16 -6
  23. package/dest/test/eth_cheat_codes.d.ts +18 -1
  24. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  25. package/dest/test/eth_cheat_codes.js +101 -22
  26. package/package.json +5 -5
  27. package/src/config.ts +1 -1
  28. package/src/contracts/multicall.ts +3 -6
  29. package/src/contracts/rollup.ts +2 -2
  30. package/src/deploy_l1_contracts.ts +2 -2
  31. package/src/l1_tx_utils/README.md +177 -0
  32. package/src/l1_tx_utils/config.ts +24 -13
  33. package/src/l1_tx_utils/l1_tx_utils.ts +282 -158
  34. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +23 -19
  35. package/src/l1_tx_utils/types.ts +20 -2
  36. package/src/publisher_manager.ts +24 -5
  37. package/src/test/eth_cheat_codes.ts +120 -20
@@ -1,8 +1,8 @@
1
1
  import { maxBigint } from '@aztec/foundation/bigint';
2
- import { times } from '@aztec/foundation/collection';
3
- import { TimeoutError } from '@aztec/foundation/error';
2
+ import { merge, pick } from '@aztec/foundation/collection';
3
+ import { InterruptError, TimeoutError } from '@aztec/foundation/error';
4
4
  import { createLogger } from '@aztec/foundation/log';
5
- import { makeBackoff, retry } from '@aztec/foundation/retry';
5
+ import { retryUntil } from '@aztec/foundation/retry';
6
6
  import { sleep } from '@aztec/foundation/sleep';
7
7
  import { DateProvider } from '@aztec/foundation/timer';
8
8
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
@@ -13,7 +13,7 @@ import { formatViemError } from '../utils.js';
13
13
  import { l1TxUtilsConfigMappings } from './config.js';
14
14
  import { LARGE_GAS_LIMIT } from './constants.js';
15
15
  import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
16
- import { TxUtilsState } from './types.js';
16
+ import { DroppedTransactionError, TerminalTxUtilsState, TxUtilsState, UnknownMinedTxError } from './types.js';
17
17
  const MAX_L1_TX_STATES = 32;
18
18
  export class L1TxUtils extends ReadOnlyL1TxUtils {
19
19
  client;
@@ -39,13 +39,10 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
39
39
  const oldState = l1TxState.status;
40
40
  l1TxState.status = newState;
41
41
  const sender = this.getSenderAddress().toString();
42
- this.logger.debug(`State changed from ${TxUtilsState[oldState]} to ${TxUtilsState[newState]} for nonce ${l1TxState.nonce} account ${sender}`);
42
+ this.logger.debug(`Tx state changed from ${TxUtilsState[oldState]} to ${TxUtilsState[newState]} for nonce ${l1TxState.nonce} account ${sender}`);
43
43
  }
44
44
  updateConfig(newConfig) {
45
- this.config = {
46
- ...this.config,
47
- ...newConfig
48
- };
45
+ this.config = merge(this.config, newConfig);
49
46
  this.logger.info('Updated L1TxUtils config', pickBy(newConfig, (_, key)=>key in l1TxUtilsConfigMappings));
50
47
  }
51
48
  getSenderAddress() {
@@ -70,11 +67,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
70
67
  * @param gasConfig - Optional gas configuration
71
68
  * @returns The transaction hash and parameters used
72
69
  */ async sendTransaction(request, gasConfigOverrides, blobInputs, stateChange = TxUtilsState.SENT) {
70
+ if (this.interrupted) {
71
+ throw new InterruptError(`Transaction sending is interrupted`);
72
+ }
73
73
  try {
74
- const gasConfig = {
75
- ...this.config,
76
- ...gasConfigOverrides
77
- };
74
+ const gasConfig = merge(this.config, gasConfigOverrides);
78
75
  const account = this.getSenderAddress().toString();
79
76
  let gasLimit;
80
77
  if (this.debugMaxGasLimit) {
@@ -84,31 +81,16 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
84
81
  } else {
85
82
  gasLimit = await this.estimateGas(account, request, gasConfig);
86
83
  }
87
- this.logger?.debug(`Gas limit for request is ${gasLimit}`, {
84
+ this.logger.trace(`Computed gas limit ${gasLimit}`, {
88
85
  gasLimit,
89
86
  ...request
90
87
  });
91
88
  const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
92
- if (gasConfig.txTimeoutAt && this.dateProvider.now() > gasConfig.txTimeoutAt.getTime()) {
93
- throw new Error('Transaction timed out before sending');
94
- }
95
89
  const nonce = await this.nonceManager.consume({
96
90
  client: this.client,
97
91
  address: account,
98
92
  chainId: this.client.chain.id
99
93
  });
100
- const l1TxState = {
101
- txHashes: [],
102
- cancelTxHashes: [],
103
- gasPrice,
104
- request,
105
- status: TxUtilsState.IDLE,
106
- nonce,
107
- gasLimit,
108
- txConfig: gasConfig,
109
- blobInputs
110
- };
111
- this.updateState(l1TxState, stateChange);
112
94
  const baseTxData = {
113
95
  ...request,
114
96
  gas: gasLimit,
@@ -121,24 +103,51 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
121
103
  ...blobInputs,
122
104
  maxFeePerBlobGas: gasPrice.maxFeePerBlobGas
123
105
  } : baseTxData;
106
+ const now = new Date(await this.getL1Timestamp());
107
+ if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
108
+ throw new TimeoutError(`Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`);
109
+ }
110
+ if (this.interrupted) {
111
+ throw new InterruptError(`Transaction sending is interrupted`);
112
+ }
124
113
  const signedRequest = await this.prepareSignedTransaction(txData);
125
114
  const txHash = await this.client.sendRawTransaction({
126
115
  serializedTransaction: signedRequest
127
116
  });
128
- l1TxState.txHashes.push(txHash);
117
+ const l1TxState = {
118
+ txHashes: [
119
+ txHash
120
+ ],
121
+ cancelTxHashes: [],
122
+ gasPrice,
123
+ request,
124
+ status: TxUtilsState.IDLE,
125
+ nonce,
126
+ gasLimit,
127
+ txConfigOverrides: gasConfigOverrides ?? {},
128
+ blobInputs,
129
+ sentAtL1Ts: now,
130
+ lastSentAtL1Ts: now
131
+ };
132
+ this.updateState(l1TxState, stateChange);
129
133
  this.txs.push(l1TxState);
130
134
  if (this.txs.length > MAX_L1_TX_STATES) {
131
135
  this.txs.shift();
132
136
  }
133
- const cleanGasConfig = pickBy(gasConfig, (_, key)=>key in l1TxUtilsConfigMappings);
134
- this.logger?.info(`Sent L1 transaction ${txHash}`, {
137
+ this.logger.info(`Sent L1 transaction ${txHash}`, {
138
+ to: request.to,
139
+ value: request.value,
140
+ nonce,
141
+ account,
142
+ sentAt: now,
135
143
  gasLimit,
136
144
  maxFeePerGas: formatGwei(gasPrice.maxFeePerGas),
137
145
  maxPriorityFeePerGas: formatGwei(gasPrice.maxPriorityFeePerGas),
138
- gasConfig: cleanGasConfig,
139
146
  ...gasPrice.maxFeePerBlobGas && {
140
147
  maxFeePerBlobGas: formatGwei(gasPrice.maxFeePerBlobGas)
141
- }
148
+ },
149
+ isBlobTx: !!blobInputs,
150
+ txConfig: gasConfigOverrides
142
151
  });
143
152
  return {
144
153
  txHash,
@@ -146,8 +155,8 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
146
155
  };
147
156
  } catch (err) {
148
157
  const viemError = formatViemError(err, request.abi);
149
- this.logger?.error(`Failed to send L1 transaction`, viemError.message, {
150
- metaMessages: viemError.metaMessages
158
+ this.logger.error(`Failed to send L1 transaction`, viemError, {
159
+ request: pick(request, 'to', 'value')
151
160
  });
152
161
  throw viemError;
153
162
  }
@@ -159,11 +168,20 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
159
168
  hash
160
169
  });
161
170
  if (receipt) {
171
+ const account = this.getSenderAddress().toString();
162
172
  const what = isCancelTx ? 'Cancellation L1 transaction' : 'L1 transaction';
163
173
  if (receipt.status === 'reverted') {
164
- this.logger?.warn(`${what} ${hash} with nonce ${nonce} reverted`, receipt);
174
+ this.logger.warn(`${what} ${hash} with nonce ${nonce} reverted`, {
175
+ receipt,
176
+ nonce,
177
+ account
178
+ });
165
179
  } else {
166
- this.logger?.verbose(`${what} ${hash} with nonce ${nonce} mined`, receipt);
180
+ this.logger.info(`${what} ${hash} with nonce ${nonce} mined`, {
181
+ receipt,
182
+ nonce,
183
+ account
184
+ });
167
185
  }
168
186
  return receipt;
169
187
  }
@@ -178,35 +196,74 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
178
196
  }
179
197
  }
180
198
  /**
199
+ * Returns whether a given tx should be considered as timed out and no longer monitored.
200
+ * Relies on the txTimeout setting for user txs, and on the txCancellationTimeout for cancel txs.
201
+ * @remarks We check against the latestBlockTimestamp as opposed to the current time to avoid a race condition where
202
+ * the tx is mined in a block with the same timestamp as txTimeoutAt, but our execution node has not yet processed it,
203
+ * or the loop here has not yet checked the tx before that timeout.
204
+ */ isTxTimedOut(state, l1Timestamp) {
205
+ const account = this.getSenderAddress().toString();
206
+ const { nonce } = state;
207
+ const txConfig = merge(this.config, state.txConfigOverrides);
208
+ const { txTimeoutAt, txTimeoutMs, maxSpeedUpAttempts } = txConfig;
209
+ const isCancelTx = state.cancelTxHashes.length > 0;
210
+ this.logger.trace(`Tx timeout check for ${account} with nonce ${nonce}`, {
211
+ nonce,
212
+ account,
213
+ l1Timestamp,
214
+ lastAttemptSent: state.lastSentAtL1Ts.getTime(),
215
+ initialTxTime: state.sentAtL1Ts.getTime(),
216
+ now: this.dateProvider.now(),
217
+ txTimeoutAt,
218
+ txTimeoutMs,
219
+ interrupted: this.interrupted
220
+ });
221
+ if (this.interrupted) {
222
+ this.logger.warn(`Tx monitoring interrupted during nonce ${nonce} for ${account} check`, {
223
+ nonce,
224
+ account
225
+ });
226
+ return true;
227
+ }
228
+ if (isCancelTx) {
229
+ // Note that we check against the lastSentAt time for cancellations, since we time them out
230
+ // after the last attempt to submit them, not the initial time.
231
+ const attempts = state.cancelTxHashes.length;
232
+ return attempts > maxSpeedUpAttempts && state.lastSentAtL1Ts.getTime() + txConfig.txCancellationFinalTimeoutMs <= l1Timestamp;
233
+ }
234
+ return txTimeoutAt !== undefined && l1Timestamp >= txTimeoutAt.getTime() || txTimeoutMs !== undefined && txTimeoutMs > 0 && l1Timestamp - state.sentAtL1Ts.getTime() >= txTimeoutMs;
235
+ }
236
+ /**
181
237
  * Monitors a transaction until completion, handling speed-ups if needed
182
238
  */ async monitorTransaction(state) {
183
- const { request, nonce, txHashes, cancelTxHashes, gasLimit, blobInputs, txConfig: gasConfig } = state;
184
- const isCancelTx = cancelTxHashes.length > 0;
239
+ const { request, nonce, gasLimit, blobInputs, txConfigOverrides: gasConfigOverrides } = state;
240
+ const gasConfig = merge(this.config, gasConfigOverrides);
241
+ const { maxSpeedUpAttempts, stallTimeMs } = gasConfig;
242
+ const isCancelTx = state.cancelTxHashes.length > 0;
243
+ const txHashes = isCancelTx ? state.cancelTxHashes : state.txHashes;
185
244
  const isBlobTx = !!blobInputs;
186
245
  const account = this.getSenderAddress().toString();
187
- const makeGetTransactionBackoff = ()=>makeBackoff(times(gasConfig.txPropagationMaxQueryAttempts ?? 3, (i)=>i + 1));
188
- let currentTxHash = isCancelTx ? cancelTxHashes[0] : txHashes[0];
189
- let attempts = 0;
190
- let lastAttemptSent = this.dateProvider.now();
191
- const initialTxTime = lastAttemptSent;
192
- let txTimedOut = false;
193
- let latestBlockTimestamp;
194
- // We check against the latestBlockTimestamp as opposed to the current time to avoid a race condition where
195
- // the tx is mined in a block with the same timestamp as txTimeoutAt, but our execution node has not yet processed it,
196
- // or the loop here has not yet checked the tx before that timeout.
197
- const isTimedOut = ()=>gasConfig.txTimeoutAt && latestBlockTimestamp !== undefined && Number(latestBlockTimestamp) * 1000 >= gasConfig.txTimeoutAt.getTime() || gasConfig.txTimeoutMs !== undefined && this.dateProvider.now() - initialTxTime > gasConfig.txTimeoutMs || this.interrupted || false;
198
- while(!txTimedOut){
246
+ const initialTxHash = txHashes[0];
247
+ let currentTxHash = initialTxHash;
248
+ let l1Timestamp;
249
+ while(true){
250
+ l1Timestamp = await this.getL1Timestamp();
199
251
  try {
200
- ({ timestamp: latestBlockTimestamp } = await this.client.getBlock({
201
- blockTag: 'latest',
202
- includeTransactions: false
203
- }));
204
- const currentNonce = await this.client.getTransactionCount({
205
- address: account
206
- });
252
+ const timePassed = l1Timestamp - state.lastSentAtL1Ts.getTime();
253
+ const [currentNonce, pendingNonce] = await Promise.all([
254
+ this.client.getTransactionCount({
255
+ address: account,
256
+ blockTag: 'latest'
257
+ }),
258
+ this.client.getTransactionCount({
259
+ address: account,
260
+ blockTag: 'pending'
261
+ })
262
+ ]);
207
263
  // If the current nonce on our account is greater than our transaction's nonce then a tx with the same nonce has been mined.
208
264
  if (currentNonce > nonce) {
209
- const receipt = await this.tryGetTxReceipt(cancelTxHashes, nonce, true) ?? await this.tryGetTxReceipt(txHashes, nonce, false);
265
+ // We try getting the receipt twice, since sometimes anvil fails to return it if the tx has just been mined
266
+ const receipt = await this.tryGetTxReceipt(state.cancelTxHashes, nonce, true) ?? await this.tryGetTxReceipt(state.txHashes, nonce, false) ?? await sleep(500) ?? await this.tryGetTxReceipt(state.cancelTxHashes, nonce, true) ?? await this.tryGetTxReceipt(state.txHashes, nonce, false);
210
267
  if (receipt) {
211
268
  this.updateState(state, TxUtilsState.MINED);
212
269
  state.receipt = receipt;
@@ -215,41 +272,36 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
215
272
  // If we get here then we have checked all of our tx versions and not found anything.
216
273
  // We should consider the nonce as MINED
217
274
  this.updateState(state, TxUtilsState.MINED);
218
- throw new Error(`Nonce ${nonce} is MINED but not by one of our expected transactions`);
275
+ throw new UnknownMinedTxError(nonce, account);
219
276
  }
220
- this.logger?.trace(`Tx timeout check for ${currentTxHash}: ${isTimedOut()}`, {
221
- latestBlockTimestamp: Number(latestBlockTimestamp) * 1000,
222
- lastAttemptSent,
223
- initialTxTime,
224
- now: this.dateProvider.now(),
225
- txTimeoutAt: gasConfig.txTimeoutAt?.getTime(),
226
- txTimeoutMs: gasConfig.txTimeoutMs,
227
- txStallTime: gasConfig.stallTimeMs
228
- });
229
- // Retry a few times, in case the tx is not yet propagated.
230
- const tx = await retry(()=>this.client.getTransaction({
231
- hash: currentTxHash
232
- }), `Getting L1 transaction ${currentTxHash}`, makeGetTransactionBackoff(), this.logger, true);
233
- const timePassed = this.dateProvider.now() - lastAttemptSent;
234
- if (tx && timePassed < gasConfig.stallTimeMs) {
235
- this.logger?.debug(`L1 transaction ${currentTxHash} pending. Time passed: ${timePassed}ms.`);
236
- // Check timeout before continuing
237
- txTimedOut = isTimedOut();
238
- if (txTimedOut) {
239
- break;
240
- }
241
- await sleep(gasConfig.checkIntervalMs);
242
- continue;
277
+ // If this is a cancel tx and its nonce is no longer on the mempool, we consider it dropped and stop monitoring
278
+ // If it is a regular tx, we let the loop speed it up after the stall time
279
+ if (isCancelTx && pendingNonce <= nonce && timePassed >= gasConfig.txUnseenConsideredDroppedMs) {
280
+ this.logger.warn(`Cancellation tx with nonce ${nonce} for account ${account} has been dropped from the visible mempool`, {
281
+ nonce,
282
+ account,
283
+ pendingNonce,
284
+ timePassed
285
+ });
286
+ this.updateState(state, TxUtilsState.NOT_MINED);
287
+ this.nonceManager.reset({
288
+ address: account,
289
+ chainId: this.client.chain.id
290
+ });
291
+ throw new DroppedTransactionError(nonce, account);
243
292
  }
244
- if (timePassed > gasConfig.stallTimeMs && attempts < gasConfig.maxAttempts) {
245
- attempts++;
246
- const newGasPrice = await this.getGasPrice(gasConfig, isBlobTx, attempts, tx.maxFeePerGas && tx.maxPriorityFeePerGas ? {
247
- maxFeePerGas: tx.maxFeePerGas,
248
- maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
249
- maxFeePerBlobGas: tx.maxFeePerBlobGas
250
- } : undefined);
293
+ // Break if the tx has timed out (ie expired)
294
+ if (this.isTxTimedOut(state, l1Timestamp)) {
295
+ break;
296
+ }
297
+ // Speed up the transaction if it appears to be stuck (exceeded stall time and still have retry attempts)
298
+ const attempts = txHashes.length;
299
+ if (timePassed >= stallTimeMs && attempts <= maxSpeedUpAttempts) {
300
+ const newGasPrice = await this.getGasPrice(gasConfig, isBlobTx, attempts, state.gasPrice);
251
301
  state.gasPrice = newGasPrice;
252
- this.logger?.debug(`L1 transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` + `with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei`, {
302
+ this.logger.debug(`Tx ${currentTxHash} with nonce ${nonce} from ${account} appears stuck. ` + `Attempting speed-up ${attempts}/${maxSpeedUpAttempts} ` + `with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei.`, {
303
+ account,
304
+ nonce,
253
305
  maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
254
306
  maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
255
307
  ...newGasPrice.maxFeePerBlobGas && {
@@ -275,55 +327,89 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
275
327
  if (!isCancelTx) {
276
328
  this.updateState(state, TxUtilsState.SPEED_UP);
277
329
  }
278
- const cleanGasConfig = pickBy(gasConfig, (_, key)=>key in l1TxUtilsConfigMappings);
279
- this.logger?.verbose(`Sent L1 speed-up tx ${newHash}, replacing ${currentTxHash}`, {
330
+ this.logger.verbose(`Sent L1 speed-up tx ${newHash} replacing ${currentTxHash} for nonce ${nonce} from ${account}`, {
331
+ nonce,
332
+ account,
280
333
  gasLimit,
281
334
  maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
282
335
  maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
283
- gasConfig: cleanGasConfig,
336
+ txConfig: state.txConfigOverrides,
284
337
  ...newGasPrice.maxFeePerBlobGas && {
285
338
  maxFeePerBlobGas: formatGwei(newGasPrice.maxFeePerBlobGas)
286
339
  }
287
340
  });
288
341
  currentTxHash = newHash;
289
- (isCancelTx ? cancelTxHashes : txHashes).push(currentTxHash);
290
- lastAttemptSent = this.dateProvider.now();
342
+ txHashes.push(currentTxHash);
343
+ state.lastSentAtL1Ts = new Date(l1Timestamp);
344
+ await sleep(gasConfig.checkIntervalMs);
345
+ continue;
291
346
  }
347
+ this.logger.debug(`Tx ${currentTxHash} from ${account} with nonce ${nonce} still pending after ${timePassed}ms`, {
348
+ account,
349
+ nonce,
350
+ pendingNonce,
351
+ attempts,
352
+ timePassed,
353
+ isBlobTx,
354
+ isCancelTx,
355
+ ...pick(state.gasPrice, 'maxFeePerGas', 'maxPriorityFeePerGas', 'maxFeePerBlobGas'),
356
+ ...pick(gasConfig, 'txUnseenConsideredDroppedMs', 'txCancellationFinalTimeoutMs', 'maxSpeedUpAttempts', 'stallTimeMs', 'txTimeoutAt', 'txTimeoutMs')
357
+ });
292
358
  await sleep(gasConfig.checkIntervalMs);
293
359
  } catch (err) {
294
- const viemError = formatViemError(err);
295
- this.logger?.warn(`Error monitoring L1 transaction ${currentTxHash}:`, viemError.message);
296
- if (viemError.message?.includes('reverted')) {
297
- throw viemError;
360
+ if (err instanceof DroppedTransactionError || err instanceof UnknownMinedTxError) {
361
+ throw err;
298
362
  }
363
+ const viemError = formatViemError(err);
364
+ this.logger.error(`Error while monitoring L1 tx ${currentTxHash}`, viemError, {
365
+ nonce,
366
+ account
367
+ });
299
368
  await sleep(gasConfig.checkIntervalMs);
300
369
  }
301
- // Check if tx has timed out.
302
- txTimedOut = isTimedOut();
303
370
  }
304
- // The transaction has timed out. If it's a cancellation then we are giving up on it.
305
- // Otherwise we may attempt to cancel it if configured to do so.
306
- if (isCancelTx) {
371
+ // Oh no, the transaction has timed out!
372
+ if (isCancelTx || !gasConfig.cancelTxOnTimeout) {
373
+ // If this was already a cancellation tx, or we are configured to not cancel txs, we just mark it as NOT_MINED
374
+ // and reset the nonce manager, so the next tx that comes along can reuse the nonce if/when this tx gets dropped.
375
+ // This is the nastiest scenario for us, since the new tx could acquire the next nonce, but then this tx is dropped,
376
+ // and the new tx would never get mined. Eventually, the new tx would also drop.
307
377
  this.updateState(state, TxUtilsState.NOT_MINED);
308
- } else if (gasConfig.cancelTxOnTimeout) {
309
- // Fire cancellation without awaiting to avoid blocking the main thread
310
- this.attemptTxCancellation(state, attempts).catch((err)=>{
311
- const viemError = formatViemError(err);
312
- this.logger?.error(`Failed to send cancellation for timed out tx ${currentTxHash}:`, viemError.message, {
313
- metaMessages: viemError.metaMessages
378
+ this.nonceManager.reset({
379
+ address: account,
380
+ chainId: this.client.chain.id
381
+ });
382
+ } else {
383
+ // Otherwise we fire the cancellation without awaiting to avoid blocking the caller,
384
+ // and monitor it in the background so we can speed it up as needed.
385
+ void this.attemptTxCancellation(state).catch((err)=>{
386
+ this.updateState(state, TxUtilsState.NOT_MINED);
387
+ this.logger.error(`Failed to send cancellation for timed out tx ${initialTxHash} with nonce ${nonce}`, err, {
388
+ account,
389
+ nonce,
390
+ initialTxHash
314
391
  });
315
392
  });
316
393
  }
317
- this.logger?.warn(`L1 transaction ${currentTxHash} timed out`, {
318
- txHash: currentTxHash,
319
- txTimeoutAt: gasConfig.txTimeoutAt,
394
+ const what = isCancelTx ? 'Cancellation L1' : 'L1';
395
+ this.logger.warn(`${what} transaction ${initialTxHash} with nonce ${nonce} from ${account} timed out`, {
396
+ initialTxHash,
397
+ currentTxHash,
398
+ nonce,
399
+ account,
400
+ txTimeoutAt: gasConfig.txTimeoutAt?.getTime(),
320
401
  txTimeoutMs: gasConfig.txTimeoutMs,
321
- txInitialTime: initialTxTime,
402
+ txInitialTime: state.sentAtL1Ts.getTime(),
403
+ l1Timestamp,
322
404
  now: this.dateProvider.now(),
323
- attempts,
405
+ attempts: txHashes.length - 1,
324
406
  isInterrupted: this.interrupted
325
407
  });
326
- throw new TimeoutError(`L1 transaction ${currentTxHash} timed out`);
408
+ throw new TimeoutError(`L1 transaction ${initialTxHash} timed out`);
409
+ }
410
+ /** Returns when all monitor loops have stopped. */ async waitMonitoringStopped(timeoutSeconds = 10) {
411
+ const account = this.getSenderAddress().toString();
412
+ await retryUntil(()=>this.txs.every((tx)=>TerminalTxUtilsState.includes(tx.status)), `monitoring stopped for ${account}`, timeoutSeconds, 0.1).catch(()=>this.logger.warn(`Timeout waiting for monitoring loops to stop for ${account}`));
327
413
  }
328
414
  /**
329
415
  * Sends a transaction and monitors it until completion
@@ -342,10 +428,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
342
428
  const blockOverrides = {
343
429
  ..._blockOverrides
344
430
  };
345
- const gasConfig = {
346
- ...this.config,
347
- ..._gasConfig
348
- };
431
+ const gasConfig = merge(this.config, _gasConfig);
349
432
  const gasPrice = await this.getGasPrice(gasConfig, false);
350
433
  const call = {
351
434
  to: request.to,
@@ -363,18 +446,51 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
363
446
  }
364
447
  /**
365
448
  * Attempts to cancel a transaction by sending a 0-value tx to self with same nonce but higher gas prices
449
+ * Only sends the cancellation if the original tx is still pending, not if it was dropped
366
450
  * @returns The hash of the cancellation transaction
367
- */ async attemptTxCancellation(state, attempts) {
451
+ */ async attemptTxCancellation(state) {
368
452
  const isBlobTx = state.blobInputs !== undefined;
369
453
  const { nonce, gasPrice: previousGasPrice } = state;
454
+ const account = this.getSenderAddress().toString();
455
+ // Do not send cancellation if interrupted
456
+ if (this.interrupted) {
457
+ this.logger.warn(`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as interrupted`, {
458
+ nonce,
459
+ account
460
+ });
461
+ this.updateState(state, TxUtilsState.NOT_MINED);
462
+ this.nonceManager.reset({
463
+ address: account,
464
+ chainId: this.client.chain.id
465
+ });
466
+ return;
467
+ }
468
+ // Check if the original tx is still pending
469
+ const currentNonce = await this.client.getTransactionCount({
470
+ address: account,
471
+ blockTag: 'pending'
472
+ });
473
+ if (currentNonce < nonce) {
474
+ this.logger.verbose(`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`, {
475
+ nonce,
476
+ account,
477
+ currentNonce
478
+ });
479
+ this.updateState(state, TxUtilsState.NOT_MINED);
480
+ this.nonceManager.reset({
481
+ address: account,
482
+ chainId: this.client.chain.id
483
+ });
484
+ return;
485
+ }
370
486
  // Get gas price with higher priority fee for cancellation
371
487
  const cancelGasPrice = await this.getGasPrice({
372
488
  ...this.config,
373
489
  // Use high bump for cancellation to ensure it replaces the original tx
374
490
  priorityFeeRetryBumpPercentage: 150
375
- }, isBlobTx, attempts + 1, previousGasPrice);
491
+ }, isBlobTx, state.txHashes.length, previousGasPrice);
376
492
  const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelGasPrice;
377
- this.logger?.info(`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction ${state.txHashes[0]} with nonce ${nonce}`, {
493
+ this.logger.verbose(`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction from account ${account} with nonce ${nonce} after time out`, {
378
494
  maxFeePerGas: formatGwei(maxFeePerGas),
379
495
  maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
380
496
  ...maxFeePerBlobGas && {
@@ -404,15 +520,29 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
404
520
  state.gasPrice = cancelGasPrice;
405
521
  state.gasLimit = 21_000n;
406
522
  state.cancelTxHashes.push(cancelTxHash);
523
+ state.lastSentAtL1Ts = new Date(await this.getL1Timestamp());
407
524
  this.updateState(state, TxUtilsState.CANCELLED);
408
- this.logger?.info(`Sent cancellation tx ${cancelTxHash} for timed out tx with nonce ${nonce}`, {
525
+ this.logger.warn(`Sent cancellation tx ${cancelTxHash} for timed out tx from ${account} with nonce ${nonce}`, {
409
526
  nonce,
410
- txData,
527
+ txData: baseTxData,
411
528
  isBlobTx,
412
529
  txHashes: state.txHashes
413
530
  });
414
- const { transactionHash } = await this.monitorTransaction(state);
415
- return transactionHash;
531
+ // Do not await the cancel tx to be mined
532
+ void this.monitorTransaction(state).catch((err)=>{
533
+ this.logger.error(`Failed to mine cancellation tx ${cancelTxHash} for nonce ${nonce} account ${account}`, err, {
534
+ nonce,
535
+ account,
536
+ cancelTxHash
537
+ });
538
+ });
539
+ }
540
+ async getL1Timestamp() {
541
+ const { timestamp } = await this.client.getBlock({
542
+ blockTag: 'latest',
543
+ includeTransactions: false
544
+ });
545
+ return Number(timestamp) * 1000;
416
546
  }
417
547
  /** Makes empty blob inputs for the cancellation tx. To be overridden in L1TxUtilsWithBlobs. */ makeEmptyBlobInputs(_maxFeePerBlobGas) {
418
548
  throw new Error('Cannot make empty blob inputs for cancellation');
@@ -9,7 +9,7 @@ export declare class ReadOnlyL1TxUtils {
9
9
  protected logger: Logger;
10
10
  readonly dateProvider: DateProvider;
11
11
  protected debugMaxGasLimit: boolean;
12
- config: L1TxUtilsConfig;
12
+ config: Required<L1TxUtilsConfig>;
13
13
  protected interrupted: boolean;
14
14
  constructor(client: ViemClient, logger: Logger | undefined, dateProvider: DateProvider, config?: Partial<L1TxUtilsConfig>, debugMaxGasLimit?: boolean);
15
15
  interrupt(): void;
@@ -47,7 +47,7 @@ export declare class ReadOnlyL1TxUtils {
47
47
  /**
48
48
  * Gets the current gas price with bounds checking
49
49
  */
50
- getGasPrice(_gasConfig?: L1TxUtilsConfig, isBlobTx?: boolean, attempt?: number, previousGasPrice?: typeof attempt extends 0 ? never : GasPrice): Promise<GasPrice>;
50
+ getGasPrice(gasConfigOverrides?: L1TxUtilsConfig, isBlobTx?: boolean, attempt?: number, previousGasPrice?: typeof attempt extends 0 ? never : GasPrice): Promise<GasPrice>;
51
51
  /**
52
52
  * Estimates gas and adds buffer
53
53
  */
@@ -1 +1 @@
1
- {"version":3,"file":"readonly_l1_tx_utils.d.ts","sourceRoot":"","sources":["../../src/l1_tx_utils/readonly_l1_tx_utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"readonly_l1_tx_utils.d.ts","sourceRoot":"","sources":["../../src/l1_tx_utils/readonly_l1_tx_utils.ts"],"names":[],"mappings":"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"}