@aztec/ethereum 3.0.0-nightly.20250929 → 3.0.0-nightly.20251001

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 (40) hide show
  1. package/dest/client.d.ts +1 -1
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/config.js +2 -2
  4. package/dest/contracts/fee_asset_handler.d.ts +2 -2
  5. package/dest/contracts/governance_proposer.d.ts +1 -2
  6. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  7. package/dest/contracts/governance_proposer.js +1 -2
  8. package/dest/contracts/multicall.d.ts +0 -2
  9. package/dest/contracts/multicall.d.ts.map +1 -1
  10. package/dest/contracts/multicall.js +2 -4
  11. package/dest/contracts/rollup.d.ts +2 -2
  12. package/dest/deploy_l1_contracts.d.ts.map +1 -1
  13. package/dest/deploy_l1_contracts.js +6 -3
  14. package/dest/l1_reader.d.ts +1 -1
  15. package/dest/l1_reader.d.ts.map +1 -1
  16. package/dest/l1_reader.js +1 -1
  17. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  18. package/dest/l1_tx_utils/factory.js +2 -2
  19. package/dest/l1_tx_utils/l1_tx_utils.d.ts +14 -26
  20. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  21. package/dest/l1_tx_utils/l1_tx_utils.js +140 -136
  22. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +4 -11
  23. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +1 -1
  24. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +10 -70
  25. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  26. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  27. package/dest/l1_tx_utils/types.d.ts +15 -2
  28. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  29. package/package.json +5 -5
  30. package/src/client.ts +1 -1
  31. package/src/config.ts +2 -2
  32. package/src/contracts/governance_proposer.ts +3 -4
  33. package/src/contracts/multicall.ts +4 -4
  34. package/src/deploy_l1_contracts.ts +5 -4
  35. package/src/l1_reader.ts +2 -2
  36. package/src/l1_tx_utils/factory.ts +2 -2
  37. package/src/l1_tx_utils/l1_tx_utils.ts +159 -157
  38. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +8 -99
  39. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +1 -1
  40. package/src/l1_tx_utils/types.ts +16 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/ethereum",
3
- "version": "3.0.0-nightly.20250929",
3
+ "version": "3.0.0-nightly.20251001",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -31,10 +31,10 @@
31
31
  "../package.common.json"
32
32
  ],
33
33
  "dependencies": {
34
- "@aztec/blob-lib": "3.0.0-nightly.20250929",
35
- "@aztec/constants": "3.0.0-nightly.20250929",
36
- "@aztec/foundation": "3.0.0-nightly.20250929",
37
- "@aztec/l1-artifacts": "3.0.0-nightly.20250929",
34
+ "@aztec/blob-lib": "3.0.0-nightly.20251001",
35
+ "@aztec/constants": "3.0.0-nightly.20251001",
36
+ "@aztec/foundation": "3.0.0-nightly.20251001",
37
+ "@aztec/l1-artifacts": "3.0.0-nightly.20251001",
38
38
  "@viem/anvil": "^0.0.10",
39
39
  "dotenv": "^16.0.3",
40
40
  "lodash.chunk": "^4.2.0",
package/src/client.ts CHANGED
@@ -19,7 +19,7 @@ import { createEthereumChain } from './chain.js';
19
19
  import type { ExtendedViemWalletClient, ViemPublicClient } from './types.js';
20
20
 
21
21
  type Config = {
22
- /** The RPC Url of the ethereum host. */
22
+ /** List of URLs of Ethereum RPC nodes that services will connect to (comma separated). */
23
23
  l1RpcUrls: string[];
24
24
  /** The chain ID of the ethereum host. */
25
25
  l1ChainId: number;
package/src/config.ts CHANGED
@@ -232,8 +232,8 @@ const TestnetEntryQueueConfig = {
232
232
  };
233
233
 
234
234
  const StagingIgnitionEntryQueueConfig = {
235
- bootstrapValidatorSetSize: 24n,
236
- bootstrapFlushSize: 24n,
235
+ bootstrapValidatorSetSize: 48n,
236
+ bootstrapFlushSize: 48n,
237
237
  normalFlushSizeMin: 1n,
238
238
  normalFlushSizeQuotient: 2048n,
239
239
  maxQueueFlushSize: 24n,
@@ -11,7 +11,7 @@ import {
11
11
  getContract,
12
12
  } from 'viem';
13
13
 
14
- import type { GasPrice, L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
14
+ import type { L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
15
15
  import type { ViemClient } from '../types.js';
16
16
  import { type IEmpireBase, encodeSignal, encodeSignalWithSignature, signSignalWithSig } from './empire_base.js';
17
17
  import { extractProposalIdFromLogs } from './governance.js';
@@ -102,10 +102,9 @@ export class GovernanceProposerContract implements IEmpireBase {
102
102
  l1TxUtils: L1TxUtils,
103
103
  ): Promise<{
104
104
  receipt: TransactionReceipt;
105
- gasPrice: GasPrice;
106
105
  proposalId: bigint;
107
106
  }> {
108
- const { receipt, gasPrice } = await l1TxUtils.sendAndMonitorTransaction({
107
+ const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
109
108
  to: this.address.toString(),
110
109
  data: encodeFunctionData({
111
110
  abi: this.proposer.abi,
@@ -114,6 +113,6 @@ export class GovernanceProposerContract implements IEmpireBase {
114
113
  }),
115
114
  });
116
115
  const proposalId = extractProposalIdFromLogs(receipt.logs);
117
- return { receipt, gasPrice, proposalId };
116
+ return { receipt, proposalId };
118
117
  }
119
118
  }
@@ -36,7 +36,7 @@ export class Multicall3 {
36
36
  const encodedForwarderData = encodeFunctionData(forwarderFunctionData);
37
37
 
38
38
  try {
39
- const { receipt, gasPrice } = await l1TxUtils.sendAndMonitorTransaction(
39
+ const { receipt, state } = await l1TxUtils.sendAndMonitorTransaction(
40
40
  {
41
41
  to: MULTI_CALL_3_ADDRESS,
42
42
  data: encodedForwarderData,
@@ -47,7 +47,7 @@ export class Multicall3 {
47
47
 
48
48
  if (receipt.status === 'success') {
49
49
  const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
50
- return { receipt, gasPrice, stats };
50
+ return { receipt, stats };
51
51
  } else {
52
52
  logger.error('Forwarder transaction failed', undefined, { receipt });
53
53
 
@@ -59,7 +59,7 @@ export class Multicall3 {
59
59
  let errorMsg: string | undefined;
60
60
 
61
61
  if (blobConfig) {
62
- const maxFeePerBlobGas = blobConfig.maxFeePerBlobGas ?? gasPrice.maxFeePerBlobGas;
62
+ const maxFeePerBlobGas = blobConfig.maxFeePerBlobGas ?? state.gasPrice.maxFeePerBlobGas;
63
63
  if (maxFeePerBlobGas === undefined) {
64
64
  errorMsg = 'maxFeePerBlobGas is required to get the error message';
65
65
  } else {
@@ -90,7 +90,7 @@ export class Multicall3 {
90
90
  errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(encodedForwarderData, args, undefined, []);
91
91
  }
92
92
 
93
- return { receipt, gasPrice, errorMsg };
93
+ return { receipt, errorMsg };
94
94
  }
95
95
  } catch (err) {
96
96
  if (err instanceof TimeoutError) {
@@ -1449,11 +1449,14 @@ export class L1Deployer {
1449
1449
  tx: L1TxRequest,
1450
1450
  options?: L1GasConfig,
1451
1451
  ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> {
1452
- return this.l1TxUtils.sendTransaction(tx, options);
1452
+ return this.l1TxUtils.sendTransaction(tx, options).then(({ txHash, state }) => ({
1453
+ txHash,
1454
+ gasLimit: state.gasLimit,
1455
+ gasPrice: state.gasPrice,
1456
+ }));
1453
1457
  }
1454
1458
  }
1455
1459
 
1456
- // docs:start:deployL1Contract
1457
1460
  /**
1458
1461
  * Helper function to deploy ETH contracts.
1459
1462
  * @param walletClient - A viem WalletClient.
@@ -1650,5 +1653,3 @@ export function getExpectedAddress(
1650
1653
  calldata,
1651
1654
  };
1652
1655
  }
1653
-
1654
- // docs:end:deployL1Contract
package/src/l1_reader.ts CHANGED
@@ -4,7 +4,7 @@ import { type L1ContractAddresses, l1ContractAddressesMapping } from './l1_contr
4
4
 
5
5
  /** Configuration of the L1GlobalReader. */
6
6
  export interface L1ReaderConfig {
7
- /** The RPC Url of the ethereum host. */
7
+ /** List of URLs of Ethereum RPC nodes that services will connect to (comma separated). */
8
8
  l1RpcUrls: string[];
9
9
  /** The chain ID of the ethereum host. */
10
10
  l1ChainId: number;
@@ -26,7 +26,7 @@ export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
26
26
  },
27
27
  l1RpcUrls: {
28
28
  env: 'ETHEREUM_HOSTS',
29
- description: 'The RPC Url of the ethereum host.',
29
+ description: 'List of URLs of Ethereum RPC nodes that services will connect to (comma separated).',
30
30
  parseEnv: (val: string) => val.split(',').map(url => url.trim()),
31
31
  defaultValue: [],
32
32
  },
@@ -13,7 +13,7 @@ import type { SigningCallback } from './types.js';
13
13
 
14
14
  export function createL1TxUtilsFromViemWallet(
15
15
  client: ExtendedViemWalletClient,
16
- logger: Logger = createLogger('L1TxUtils'),
16
+ logger: Logger = createLogger('l1-tx-utils'),
17
17
  dateProvider: DateProvider = new DateProvider(),
18
18
  config?: Partial<L1TxUtilsConfig>,
19
19
  debugMaxGasLimit: boolean = false,
@@ -32,7 +32,7 @@ export function createL1TxUtilsFromViemWallet(
32
32
  export function createL1TxUtilsFromEthSigner(
33
33
  client: ViemClient,
34
34
  signer: EthSigner,
35
- logger: Logger = createLogger('L1TxUtils'),
35
+ logger: Logger = createLogger('l1-tx-utils'),
36
36
  dateProvider: DateProvider = new DateProvider(),
37
37
  config?: Partial<L1TxUtilsConfig>,
38
38
  debugMaxGasLimit: boolean = false,
@@ -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
  }