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

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 (36) hide show
  1. package/dest/contracts/governance.js +6 -2
  2. package/dest/deploy_l1_contracts.d.ts.map +1 -1
  3. package/dest/deploy_l1_contracts.js +13 -2
  4. package/dest/l1_tx_utils/factory.d.ts +18 -3
  5. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  6. package/dest/l1_tx_utils/factory.js +4 -6
  7. package/dest/l1_tx_utils/index.d.ts +1 -0
  8. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  9. package/dest/l1_tx_utils/index.js +1 -0
  10. package/dest/l1_tx_utils/interfaces.d.ts +76 -0
  11. package/dest/l1_tx_utils/interfaces.d.ts.map +1 -0
  12. package/dest/l1_tx_utils/interfaces.js +4 -0
  13. package/dest/l1_tx_utils/l1_tx_utils.d.ts +18 -3
  14. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  15. package/dest/l1_tx_utils/l1_tx_utils.js +130 -70
  16. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +14 -3
  17. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +1 -1
  18. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +4 -6
  19. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  20. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +1 -1
  21. package/dest/l1_tx_utils/types.d.ts +1 -0
  22. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  23. package/dest/publisher_manager.d.ts +2 -0
  24. package/dest/publisher_manager.d.ts.map +1 -1
  25. package/dest/publisher_manager.js +3 -0
  26. package/package.json +5 -5
  27. package/src/contracts/governance.ts +2 -2
  28. package/src/deploy_l1_contracts.ts +7 -5
  29. package/src/l1_tx_utils/factory.ts +35 -15
  30. package/src/l1_tx_utils/index.ts +1 -0
  31. package/src/l1_tx_utils/interfaces.ts +86 -0
  32. package/src/l1_tx_utils/l1_tx_utils.ts +135 -70
  33. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +31 -10
  34. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +1 -1
  35. package/src/l1_tx_utils/types.ts +1 -0
  36. package/src/publisher_manager.ts +5 -0
@@ -1,6 +1,7 @@
1
1
  export * from './config.js';
2
2
  export * from './constants.js';
3
3
  export * from './factory.js';
4
+ export * from './interfaces.js';
4
5
  export * from './l1_tx_utils.js';
5
6
  export * from './readonly_l1_tx_utils.js';
6
7
  export * from './signer.js';
@@ -0,0 +1,86 @@
1
+ import type { L1BlobInputs, L1TxState } from './types.js';
2
+
3
+ /**
4
+ * Interface for L1 transaction state storage.
5
+ * Implementations handle persistence of transaction states across restarts.
6
+ */
7
+ export interface IL1TxStore {
8
+ /**
9
+ * Gets the next available state ID for an account.
10
+ */
11
+ consumeNextStateId(account: string): Promise<number>;
12
+
13
+ /**
14
+ * Saves a single transaction state for a specific account.
15
+ * Does not save blob data (see saveBlobs).
16
+ * @param account - The sender account address
17
+ * @param state - Transaction state to save
18
+ */
19
+ saveState(account: string, state: L1TxState): Promise<L1TxState>;
20
+
21
+ /**
22
+ * Saves blobs for a given state.
23
+ * @param account - The sender account address
24
+ * @param stateId - The state ID
25
+ * @param blobInputs - Blob inputs to save
26
+ */
27
+ saveBlobs(account: string, stateId: number, blobInputs: L1BlobInputs | undefined): Promise<void>;
28
+
29
+ /**
30
+ * Loads all transaction states for a specific account.
31
+ * @param account - The sender account address
32
+ * @returns Array of transaction states with their IDs
33
+ */
34
+ loadStates(account: string): Promise<L1TxState[]>;
35
+
36
+ /**
37
+ * Loads a single state by ID.
38
+ * @param account - The sender account address
39
+ * @param stateId - The state ID
40
+ * @returns The transaction state or undefined if not found
41
+ */
42
+ loadState(account: string, stateId: number): Promise<L1TxState | undefined>;
43
+
44
+ /**
45
+ * Deletes a specific state and its associated blobs.
46
+ * @param account - The sender account address
47
+ * @param stateId - The state ID to delete
48
+ */
49
+ deleteState(account: string, stateId: number): Promise<void>;
50
+
51
+ /**
52
+ * Clears all transaction states for a specific account.
53
+ * @param account - The sender account address
54
+ */
55
+ clearStates(account: string): Promise<void>;
56
+
57
+ /**
58
+ * Gets all accounts that have stored states.
59
+ * @returns Array of account addresses
60
+ */
61
+ getAllAccounts(): Promise<string[]>;
62
+
63
+ /**
64
+ * Closes the store.
65
+ */
66
+ close(): Promise<void>;
67
+ }
68
+
69
+ /**
70
+ * Interface for L1 transaction metrics recording.
71
+ * Implementations handle tracking of transaction lifecycle and gas costs.
72
+ */
73
+ export interface IL1TxMetrics {
74
+ /**
75
+ * Records metrics when a transaction is mined
76
+ * @param state - The L1 transaction state
77
+ * @param l1Timestamp - The current L1 timestamp
78
+ */
79
+ recordMinedTx(state: L1TxState, l1Timestamp: Date): void;
80
+
81
+ /**
82
+ * Records metrics when a transaction is dropped
83
+ * @param state - The L1 transaction state
84
+ */
85
+ recordDroppedTx(state: L1TxState): void;
86
+ }
@@ -28,6 +28,7 @@ import type { ViemClient } from '../types.js';
28
28
  import { formatViemError } from '../utils.js';
29
29
  import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
30
30
  import { LARGE_GAS_LIMIT } from './constants.js';
31
+ import type { IL1TxMetrics, IL1TxStore } from './interfaces.js';
31
32
  import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
32
33
  import {
33
34
  DroppedTransactionError,
@@ -51,10 +52,12 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
51
52
  public override client: ViemClient,
52
53
  public address: EthAddress,
53
54
  protected signer: SigningCallback,
54
- protected override logger: Logger = createLogger('L1TxUtils'),
55
+ logger: Logger = createLogger('ethereum:publisher'),
55
56
  dateProvider: DateProvider = new DateProvider(),
56
57
  config?: Partial<L1TxUtilsConfig>,
57
58
  debugMaxGasLimit: boolean = false,
59
+ protected store?: IL1TxStore,
60
+ protected metrics?: IL1TxMetrics,
58
61
  ) {
59
62
  super(client, logger, dateProvider, config, debugMaxGasLimit);
60
63
  this.nonceManager = createNonceManager({ source: jsonRpc() });
@@ -69,13 +72,27 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
69
72
  return minedBlockNumbers.length === 0 ? undefined : maxBigint(...minedBlockNumbers);
70
73
  }
71
74
 
72
- protected updateState(l1TxState: L1TxState, newState: TxUtilsState) {
75
+ protected async updateState(l1TxState: L1TxState, newState: TxUtilsState.MINED, l1Timestamp: number): Promise<void>;
76
+ protected async updateState(l1TxState: L1TxState, newState: TxUtilsState, l1Timestamp?: undefined): Promise<void>;
77
+ protected async updateState(l1TxState: L1TxState, newState: TxUtilsState, l1Timestamp?: number) {
73
78
  const oldState = l1TxState.status;
74
79
  l1TxState.status = newState;
75
80
  const sender = this.getSenderAddress().toString();
76
81
  this.logger.debug(
77
82
  `Tx state changed from ${TxUtilsState[oldState]} to ${TxUtilsState[newState]} for nonce ${l1TxState.nonce} account ${sender}`,
78
83
  );
84
+
85
+ // Record metrics
86
+ if (newState === TxUtilsState.MINED && l1Timestamp !== undefined) {
87
+ this.metrics?.recordMinedTx(l1TxState, new Date(l1Timestamp));
88
+ } else if (newState === TxUtilsState.NOT_MINED) {
89
+ this.metrics?.recordDroppedTx(l1TxState);
90
+ }
91
+
92
+ // Update state in the store
93
+ await this.store
94
+ ?.saveState(sender, l1TxState)
95
+ .catch(err => this.logger.error('Failed to persist L1 tx state', err));
79
96
  }
80
97
 
81
98
  public updateConfig(newConfig: Partial<L1TxUtilsConfig>) {
@@ -96,6 +113,48 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
96
113
  });
97
114
  }
98
115
 
116
+ /**
117
+ * Rehydrates transaction states from the store and resumes monitoring for pending transactions.
118
+ * This should be called on startup to restore state and resume monitoring of any in-flight transactions.
119
+ */
120
+ public async loadStateAndResumeMonitoring(): Promise<void> {
121
+ if (!this.store) {
122
+ return;
123
+ }
124
+
125
+ const account = this.getSenderAddress().toString();
126
+ const loadedStates = await this.store.loadStates(account);
127
+
128
+ if (loadedStates.length === 0) {
129
+ this.logger.debug(`No states to rehydrate for account ${account}`);
130
+ return;
131
+ }
132
+
133
+ // Convert loaded states (which have id) to the txs format
134
+ this.txs = loadedStates;
135
+ this.logger.info(`Rehydrated ${loadedStates.length} tx states for account ${account}`);
136
+
137
+ // Find all pending states and resume monitoring
138
+ const pendingStates = loadedStates.filter(state => !TerminalTxUtilsState.includes(state.status));
139
+ if (pendingStates.length === 0) {
140
+ return;
141
+ }
142
+
143
+ this.logger.info(`Resuming monitoring for ${pendingStates.length} pending transactions for account ${account}`, {
144
+ txs: pendingStates.map(s => ({ id: s.id, nonce: s.nonce, status: TxUtilsState[s.status] })),
145
+ });
146
+
147
+ for (const state of pendingStates) {
148
+ void this.monitorTransaction(state).catch(err => {
149
+ this.logger.error(
150
+ `Error monitoring rehydrated tx with nonce ${state.nonce} for account ${account}`,
151
+ formatViemError(err),
152
+ { nonce: state.nonce, account },
153
+ );
154
+ });
155
+ }
156
+ }
157
+
99
158
  private async signTransaction(txRequest: TransactionSerializable): Promise<`0x${string}`> {
100
159
  const signature = await this.signer(txRequest, this.getSenderAddress());
101
160
  return serializeTransaction(txRequest, signature);
@@ -138,23 +197,18 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
138
197
 
139
198
  const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
140
199
 
200
+ if (this.interrupted) {
201
+ throw new InterruptError(`Transaction sending is interrupted`);
202
+ }
203
+
141
204
  const nonce = await this.nonceManager.consume({
142
205
  client: this.client,
143
206
  address: account,
144
207
  chainId: this.client.chain.id,
145
208
  });
146
209
 
147
- const baseTxData = {
148
- ...request,
149
- gas: gasLimit,
150
- maxFeePerGas: gasPrice.maxFeePerGas,
151
- maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
152
- nonce,
153
- };
154
-
155
- const txData = blobInputs
156
- ? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! }
157
- : baseTxData;
210
+ const baseState = { request, gasLimit, blobInputs, gasPrice, nonce };
211
+ const txData = this.makeTxData(baseState, { isCancelTx: false });
158
212
 
159
213
  const now = new Date(await this.getL1Timestamp());
160
214
  if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
@@ -163,32 +217,33 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
163
217
  );
164
218
  }
165
219
 
166
- if (this.interrupted) {
167
- throw new InterruptError(`Transaction sending is interrupted`);
168
- }
169
-
220
+ // Send the new tx
170
221
  const signedRequest = await this.prepareSignedTransaction(txData);
171
222
  const txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
172
223
 
224
+ // Create the new state for monitoring
173
225
  const l1TxState: L1TxState = {
226
+ ...baseState,
227
+ id: (await this.store?.consumeNextStateId(account)) ?? Math.max(...this.txs.map(tx => tx.id), 0),
174
228
  txHashes: [txHash],
175
229
  cancelTxHashes: [],
176
- gasPrice,
177
- request,
178
230
  status: TxUtilsState.IDLE,
179
- nonce,
180
- gasLimit,
181
231
  txConfigOverrides: gasConfigOverrides ?? {},
182
- blobInputs,
183
232
  sentAtL1Ts: now,
184
233
  lastSentAtL1Ts: now,
185
234
  };
186
235
 
187
- this.updateState(l1TxState, stateChange);
188
-
236
+ // And persist it
237
+ await this.updateState(l1TxState, stateChange);
238
+ await this.store?.saveBlobs(account, l1TxState.id, blobInputs);
189
239
  this.txs.push(l1TxState);
240
+
241
+ // Clean up stale states
190
242
  if (this.txs.length > MAX_L1_TX_STATES) {
191
- this.txs.shift();
243
+ const removed = this.txs.shift();
244
+ if (removed && this.store) {
245
+ await this.store.deleteState(account, removed.id);
246
+ }
192
247
  }
193
248
 
194
249
  this.logger.info(`Sent L1 transaction ${txHash}`, {
@@ -296,7 +351,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
296
351
  * Monitors a transaction until completion, handling speed-ups if needed
297
352
  */
298
353
  protected async monitorTransaction(state: L1TxState): Promise<TransactionReceipt> {
299
- const { request, nonce, gasLimit, blobInputs, txConfigOverrides: gasConfigOverrides } = state;
354
+ const { nonce, gasLimit, blobInputs, txConfigOverrides: gasConfigOverrides } = state;
300
355
  const gasConfig = merge(this.config, gasConfigOverrides);
301
356
  const { maxSpeedUpAttempts, stallTimeMs } = gasConfig;
302
357
  const isCancelTx = state.cancelTxHashes.length > 0;
@@ -305,7 +360,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
305
360
  const account = this.getSenderAddress().toString();
306
361
 
307
362
  const initialTxHash = txHashes[0];
308
- let currentTxHash = initialTxHash;
363
+ let currentTxHash = txHashes.at(-1)!;
309
364
  let l1Timestamp: number;
310
365
 
311
366
  while (true) {
@@ -329,14 +384,14 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
329
384
  (await this.tryGetTxReceipt(state.txHashes, nonce, false));
330
385
 
331
386
  if (receipt) {
332
- this.updateState(state, TxUtilsState.MINED);
333
387
  state.receipt = receipt;
388
+ await this.updateState(state, TxUtilsState.MINED, l1Timestamp);
334
389
  return receipt;
335
390
  }
336
391
 
337
392
  // If we get here then we have checked all of our tx versions and not found anything.
338
393
  // We should consider the nonce as MINED
339
- this.updateState(state, TxUtilsState.MINED);
394
+ await this.updateState(state, TxUtilsState.MINED, l1Timestamp);
340
395
  throw new UnknownMinedTxError(nonce, account);
341
396
  }
342
397
 
@@ -347,7 +402,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
347
402
  `Cancellation tx with nonce ${nonce} for account ${account} has been dropped from the visible mempool`,
348
403
  { nonce, account, pendingNonce, timePassed },
349
404
  );
350
- this.updateState(state, TxUtilsState.NOT_MINED);
405
+ await this.updateState(state, TxUtilsState.NOT_MINED);
351
406
  this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
352
407
  throw new DroppedTransactionError(nonce, account);
353
408
  }
@@ -376,25 +431,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
376
431
  },
377
432
  );
378
433
 
379
- const baseTxData = {
380
- ...request,
381
- gas: gasLimit,
382
- maxFeePerGas: newGasPrice.maxFeePerGas,
383
- maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas,
384
- nonce,
385
- };
386
-
387
- const txData = blobInputs
388
- ? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: newGasPrice.maxFeePerBlobGas! }
389
- : baseTxData;
434
+ const txData = this.makeTxData(state, { isCancelTx });
390
435
 
391
436
  const signedRequest = await this.prepareSignedTransaction(txData);
392
437
  const newHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
393
438
 
394
- if (!isCancelTx) {
395
- this.updateState(state, TxUtilsState.SPEED_UP);
396
- }
397
-
398
439
  this.logger.verbose(
399
440
  `Sent L1 speed-up tx ${newHash} replacing ${currentTxHash} for nonce ${nonce} from ${account}`,
400
441
  {
@@ -411,6 +452,8 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
411
452
  currentTxHash = newHash;
412
453
  txHashes.push(currentTxHash);
413
454
  state.lastSentAtL1Ts = new Date(l1Timestamp);
455
+ await this.updateState(state, isCancelTx ? TxUtilsState.CANCELLED : TxUtilsState.SPEED_UP);
456
+
414
457
  await sleep(gasConfig.checkIntervalMs!);
415
458
  continue;
416
459
  }
@@ -456,13 +499,13 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
456
499
  // and reset the nonce manager, so the next tx that comes along can reuse the nonce if/when this tx gets dropped.
457
500
  // This is the nastiest scenario for us, since the new tx could acquire the next nonce, but then this tx is dropped,
458
501
  // and the new tx would never get mined. Eventually, the new tx would also drop.
459
- this.updateState(state, TxUtilsState.NOT_MINED);
502
+ await this.updateState(state, TxUtilsState.NOT_MINED);
460
503
  this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
461
504
  } else {
462
505
  // Otherwise we fire the cancellation without awaiting to avoid blocking the caller,
463
506
  // and monitor it in the background so we can speed it up as needed.
464
- void this.attemptTxCancellation(state).catch(err => {
465
- this.updateState(state, TxUtilsState.NOT_MINED);
507
+ void this.attemptTxCancellation(state).catch(async err => {
508
+ await this.updateState(state, TxUtilsState.NOT_MINED);
466
509
  this.logger.error(`Failed to send cancellation for timed out tx ${initialTxHash} with nonce ${nonce}`, err, {
467
510
  account,
468
511
  nonce,
@@ -489,6 +532,41 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
489
532
  throw new TimeoutError(`L1 transaction ${initialTxHash} timed out`);
490
533
  }
491
534
 
535
+ /**
536
+ * Creates tx data to be signed by viem signTransaction method, using the state as input.
537
+ * If isCancelTx is true, creates a 0-value tx to self with 21k gas and no data instead,
538
+ * and an empty blob input if the original tx also had blobs.
539
+ */
540
+ private makeTxData(
541
+ state: Pick<L1TxState, 'request' | 'gasLimit' | 'blobInputs' | 'gasPrice' | 'nonce'>,
542
+ opts: { isCancelTx: boolean },
543
+ ): PrepareTransactionRequestRequest {
544
+ const { request, gasLimit, blobInputs, gasPrice, nonce } = state;
545
+ const isBlobTx = blobInputs !== undefined;
546
+
547
+ const baseTxOpts = { nonce, ...pick(gasPrice, 'maxFeePerGas', 'maxPriorityFeePerGas') };
548
+
549
+ if (opts.isCancelTx) {
550
+ const baseTxData = {
551
+ to: this.getSenderAddress().toString(),
552
+ value: 0n,
553
+ data: '0x' as const,
554
+ gas: 21_000n,
555
+ ...baseTxOpts,
556
+ };
557
+
558
+ return isBlobTx ? { ...baseTxData, ...this.makeEmptyBlobInputs(gasPrice.maxFeePerBlobGas!) } : baseTxData;
559
+ }
560
+
561
+ const baseTxData = {
562
+ ...request,
563
+ ...baseTxOpts,
564
+ gas: gasLimit,
565
+ };
566
+
567
+ return blobInputs ? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! } : baseTxData;
568
+ }
569
+
492
570
  /** Returns when all monitor loops have stopped. */
493
571
  public async waitMonitoringStopped(timeoutSeconds = 10) {
494
572
  const account = this.getSenderAddress().toString();
@@ -549,7 +627,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
549
627
  * Only sends the cancellation if the original tx is still pending, not if it was dropped
550
628
  * @returns The hash of the cancellation transaction
551
629
  */
552
- protected async attemptTxCancellation(state: L1TxState) {
630
+ protected async attemptTxCancellation(state: L1TxState): Promise<void> {
553
631
  const isBlobTx = state.blobInputs !== undefined;
554
632
  const { nonce, gasPrice: previousGasPrice } = state;
555
633
  const account = this.getSenderAddress().toString();
@@ -560,7 +638,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
560
638
  `Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as interrupted`,
561
639
  { nonce, account },
562
640
  );
563
- this.updateState(state, TxUtilsState.NOT_MINED);
641
+ await this.updateState(state, TxUtilsState.NOT_MINED);
564
642
  this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
565
643
  return;
566
644
  }
@@ -572,7 +650,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
572
650
  `Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`,
573
651
  { nonce, account, currentNonce },
574
652
  );
575
- this.updateState(state, TxUtilsState.NOT_MINED);
653
+ await this.updateState(state, TxUtilsState.NOT_MINED);
576
654
  this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
577
655
  return;
578
656
  }
@@ -599,34 +677,20 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
599
677
  },
600
678
  );
601
679
 
602
- const request = {
603
- to: this.getSenderAddress().toString(),
604
- value: 0n,
605
- };
606
-
607
680
  // Send 0-value tx to self with higher gas price
608
- const baseTxData = {
609
- ...request,
610
- nonce,
611
- gas: 21_000n,
612
- maxFeePerGas,
613
- maxPriorityFeePerGas,
614
- };
681
+ state.gasPrice = cancelGasPrice;
682
+ state.lastSentAtL1Ts = new Date(await this.getL1Timestamp());
615
683
 
616
- const txData = isBlobTx ? { ...baseTxData, ...this.makeEmptyBlobInputs(maxFeePerBlobGas!) } : baseTxData;
684
+ const txData = this.makeTxData(state, { isCancelTx: true });
617
685
  const signedRequest = await this.prepareSignedTransaction(txData);
618
686
  const cancelTxHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
619
687
 
620
- state.gasPrice = cancelGasPrice;
621
- state.gasLimit = 21_000n;
622
688
  state.cancelTxHashes.push(cancelTxHash);
623
- state.lastSentAtL1Ts = new Date(await this.getL1Timestamp());
624
-
625
- this.updateState(state, TxUtilsState.CANCELLED);
689
+ await this.updateState(state, TxUtilsState.CANCELLED);
626
690
 
627
691
  this.logger.warn(`Sent cancellation tx ${cancelTxHash} for timed out tx from ${account} with nonce ${nonce}`, {
628
692
  nonce,
629
- txData: baseTxData,
693
+ cancelGasPrice,
630
694
  isBlobTx,
631
695
  txHashes: state.txHashes,
632
696
  });
@@ -641,6 +705,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
641
705
  });
642
706
  }
643
707
 
708
+ /** Returns L1 timestamps in milliseconds */
644
709
  private async getL1Timestamp() {
645
710
  const { timestamp } = await this.client.getBlock({ blockTag: 'latest', includeTransactions: false });
646
711
  return Number(timestamp) * 1000;
@@ -1,6 +1,6 @@
1
1
  import { Blob } from '@aztec/blob-lib';
2
2
  import { EthAddress } from '@aztec/foundation/eth-address';
3
- import { type Logger, createLogger } from '@aztec/foundation/log';
3
+ import type { Logger } from '@aztec/foundation/log';
4
4
  import { DateProvider } from '@aztec/foundation/timer';
5
5
 
6
6
  import type { TransactionSerializable } from 'viem';
@@ -8,6 +8,7 @@ import type { TransactionSerializable } from 'viem';
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
+ import type { IL1TxMetrics, IL1TxStore } from './interfaces.js';
11
12
  import { L1TxUtils } from './l1_tx_utils.js';
12
13
  import { createViemSigner } from './signer.js';
13
14
  import type { L1BlobInputs, SigningCallback } from './types.js';
@@ -24,33 +25,53 @@ export class L1TxUtilsWithBlobs extends L1TxUtils {
24
25
 
25
26
  export function createL1TxUtilsWithBlobsFromViemWallet(
26
27
  client: ExtendedViemWalletClient,
27
- logger: Logger = createLogger('L1TxUtils'),
28
- dateProvider: DateProvider = new DateProvider(),
29
- config?: Partial<L1TxUtilsConfig>,
28
+ deps: {
29
+ logger?: Logger;
30
+ dateProvider?: DateProvider;
31
+ store?: IL1TxStore;
32
+ metrics?: IL1TxMetrics;
33
+ } = {},
34
+ config: Partial<L1TxUtilsConfig> = {},
30
35
  debugMaxGasLimit: boolean = false,
31
36
  ) {
32
37
  return new L1TxUtilsWithBlobs(
33
38
  client,
34
39
  EthAddress.fromString(client.account.address),
35
40
  createViemSigner(client),
36
- logger,
37
- dateProvider,
41
+ deps.logger,
42
+ deps.dateProvider,
38
43
  config,
39
44
  debugMaxGasLimit,
45
+ deps.store,
46
+ deps.metrics,
40
47
  );
41
48
  }
42
49
 
43
50
  export function createL1TxUtilsWithBlobsFromEthSigner(
44
51
  client: ViemClient,
45
52
  signer: EthSigner,
46
- logger: Logger = createLogger('L1TxUtils'),
47
- dateProvider: DateProvider = new DateProvider(),
48
- config?: Partial<L1TxUtilsConfig>,
53
+ deps: {
54
+ logger?: Logger;
55
+ dateProvider?: DateProvider;
56
+ store?: IL1TxStore;
57
+ metrics?: IL1TxMetrics;
58
+ } = {},
59
+ config: Partial<L1TxUtilsConfig> = {},
49
60
  debugMaxGasLimit: boolean = false,
50
61
  ) {
51
62
  const callback: SigningCallback = async (transaction: TransactionSerializable, _signingAddress) => {
52
63
  return (await signer.signTransaction(transaction)).toViemTransactionSignature();
53
64
  };
54
65
 
55
- return new L1TxUtilsWithBlobs(client, signer.address, callback, logger, dateProvider, config, debugMaxGasLimit);
66
+ return new L1TxUtilsWithBlobs(
67
+ client,
68
+ signer.address,
69
+ callback,
70
+ deps.logger,
71
+ deps.dateProvider,
72
+ config,
73
+ debugMaxGasLimit,
74
+ deps.store,
75
+ deps.metrics,
76
+ );
56
77
  }
@@ -39,7 +39,7 @@ export class ReadOnlyL1TxUtils {
39
39
 
40
40
  constructor(
41
41
  public client: ViemClient,
42
- protected logger: Logger = createLogger('ReadOnlyL1TxUtils'),
42
+ protected logger: Logger = createLogger('ethereum:readonly-l1-utils'),
43
43
  public readonly dateProvider: DateProvider,
44
44
  config?: Partial<L1TxUtilsConfig>,
45
45
  protected debugMaxGasLimit: boolean = false,
@@ -50,6 +50,7 @@ export enum TxUtilsState {
50
50
  export const TerminalTxUtilsState = [TxUtilsState.IDLE, TxUtilsState.MINED, TxUtilsState.NOT_MINED];
51
51
 
52
52
  export type L1TxState = {
53
+ id: number;
53
54
  txHashes: Hex[];
54
55
  cancelTxHashes: Hex[];
55
56
  gasLimit: bigint;
@@ -40,6 +40,11 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
40
40
  this.config = pick(config, 'publisherAllowInvalidStates');
41
41
  }
42
42
 
43
+ /** Loads the state of all publishers and resumes monitoring any pending txs */
44
+ public async loadState(): Promise<void> {
45
+ await Promise.all(this.publishers.map(pub => pub.loadStateAndResumeMonitoring()));
46
+ }
47
+
43
48
  // Finds and prioritises available publishers based on
44
49
  // 1. Validity as per the provided filter function
45
50
  // 2. Validity based on the state the publisher is in