@aztec-labs/ethereum 6.0.0-nightly.20260829
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.
- package/README.md +3 -0
- package/dest/account.d.ts +2 -0
- package/dest/account.d.ts.map +1 -0
- package/dest/account.js +4 -0
- package/dest/chain.d.ts +25 -0
- package/dest/chain.d.ts.map +1 -0
- package/dest/chain.js +53 -0
- package/dest/client.d.ts +42 -0
- package/dest/client.d.ts.map +1 -0
- package/dest/client.js +104 -0
- package/dest/config.d.ts +103 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +233 -0
- package/dest/constants.d.ts +4 -0
- package/dest/constants.d.ts.map +1 -0
- package/dest/constants.js +2 -0
- package/dest/contracts/chain_state_override.d.ts +81 -0
- package/dest/contracts/chain_state_override.d.ts.map +1 -0
- package/dest/contracts/chain_state_override.js +139 -0
- package/dest/contracts/empire_base.d.ts +30 -0
- package/dest/contracts/empire_base.d.ts.map +1 -0
- package/dest/contracts/empire_base.js +84 -0
- package/dest/contracts/errors.d.ts +7 -0
- package/dest/contracts/errors.d.ts.map +1 -0
- package/dest/contracts/errors.js +12 -0
- package/dest/contracts/fee_asset_handler.d.ts +20 -0
- package/dest/contracts/fee_asset_handler.d.ts.map +1 -0
- package/dest/contracts/fee_asset_handler.js +59 -0
- package/dest/contracts/fee_asset_price_oracle.d.ts +105 -0
- package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
- package/dest/contracts/fee_asset_price_oracle.js +656 -0
- package/dest/contracts/fee_juice.d.ts +14 -0
- package/dest/contracts/fee_juice.d.ts.map +1 -0
- package/dest/contracts/fee_juice.js +59 -0
- package/dest/contracts/governance.d.ts +192 -0
- package/dest/contracts/governance.d.ts.map +1 -0
- package/dest/contracts/governance.js +471 -0
- package/dest/contracts/governance_proposer.d.ts +63 -0
- package/dest/contracts/governance_proposer.d.ts.map +1 -0
- package/dest/contracts/governance_proposer.js +536 -0
- package/dest/contracts/gse.d.ts +38 -0
- package/dest/contracts/gse.d.ts.map +1 -0
- package/dest/contracts/gse.js +81 -0
- package/dest/contracts/inbox.d.ts +94 -0
- package/dest/contracts/inbox.d.ts.map +1 -0
- package/dest/contracts/inbox.js +141 -0
- package/dest/contracts/index.d.ts +18 -0
- package/dest/contracts/index.d.ts.map +1 -0
- package/dest/contracts/index.js +17 -0
- package/dest/contracts/log.d.ts +13 -0
- package/dest/contracts/log.d.ts.map +1 -0
- package/dest/contracts/log.js +1 -0
- package/dest/contracts/multicall.d.ts +138 -0
- package/dest/contracts/multicall.d.ts.map +1 -0
- package/dest/contracts/multicall.js +266 -0
- package/dest/contracts/outbox.d.ts +64 -0
- package/dest/contracts/outbox.d.ts.map +1 -0
- package/dest/contracts/outbox.js +113 -0
- package/dest/contracts/registry.d.ts +31 -0
- package/dest/contracts/registry.d.ts.map +1 -0
- package/dest/contracts/registry.js +142 -0
- package/dest/contracts/rollup.d.ts +5836 -0
- package/dest/contracts/rollup.d.ts.map +1 -0
- package/dest/contracts/rollup.js +1612 -0
- package/dest/contracts/slasher_contract.d.ts +44 -0
- package/dest/contracts/slasher_contract.d.ts.map +1 -0
- package/dest/contracts/slasher_contract.js +75 -0
- package/dest/contracts/slashing_proposer.d.ts +174 -0
- package/dest/contracts/slashing_proposer.d.ts.map +1 -0
- package/dest/contracts/slashing_proposer.js +755 -0
- package/dest/contracts/utils.d.ts +3 -0
- package/dest/contracts/utils.d.ts.map +1 -0
- package/dest/contracts/utils.js +11 -0
- package/dest/contracts/watch_event.d.ts +36 -0
- package/dest/contracts/watch_event.d.ts.map +1 -0
- package/dest/contracts/watch_event.js +66 -0
- package/dest/deploy_aztec_l1_contracts.d.ts +267 -0
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -0
- package/dest/deploy_aztec_l1_contracts.js +424 -0
- package/dest/deploy_l1_contract.d.ts +68 -0
- package/dest/deploy_l1_contract.d.ts.map +1 -0
- package/dest/deploy_l1_contract.js +312 -0
- package/dest/eth-signer/eth-signer.d.ts +21 -0
- package/dest/eth-signer/eth-signer.d.ts.map +1 -0
- package/dest/eth-signer/eth-signer.js +5 -0
- package/dest/eth-signer/index.d.ts +2 -0
- package/dest/eth-signer/index.d.ts.map +1 -0
- package/dest/eth-signer/index.js +1 -0
- package/dest/forwarder_proxy.d.ts +32 -0
- package/dest/forwarder_proxy.d.ts.map +1 -0
- package/dest/forwarder_proxy.js +93 -0
- package/dest/foundry_binary.d.ts +13 -0
- package/dest/foundry_binary.d.ts.map +1 -0
- package/dest/foundry_binary.js +52 -0
- package/dest/l1_artifacts.d.ts +81896 -0
- package/dest/l1_artifacts.d.ts.map +1 -0
- package/dest/l1_artifacts.js +151 -0
- package/dest/l1_contract_addresses.d.ts +73 -0
- package/dest/l1_contract_addresses.d.ts.map +1 -0
- package/dest/l1_contract_addresses.js +120 -0
- package/dest/l1_reader.d.ts +18 -0
- package/dest/l1_reader.d.ts.map +1 -0
- package/dest/l1_reader.js +35 -0
- package/dest/l1_tx_utils/config.d.ts +65 -0
- package/dest/l1_tx_utils/config.d.ts.map +1 -0
- package/dest/l1_tx_utils/config.js +109 -0
- package/dest/l1_tx_utils/constants.d.ts +12 -0
- package/dest/l1_tx_utils/constants.d.ts.map +1 -0
- package/dest/l1_tx_utils/constants.js +39 -0
- package/dest/l1_tx_utils/factory.d.ts +32 -0
- package/dest/l1_tx_utils/factory.d.ts.map +1 -0
- package/dest/l1_tx_utils/factory.js +22 -0
- package/dest/l1_tx_utils/fee-strategies/index.d.ts +10 -0
- package/dest/l1_tx_utils/fee-strategies/index.d.ts.map +1 -0
- package/dest/l1_tx_utils/fee-strategies/index.js +12 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts +8 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts.map +1 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +129 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.d.ts +23 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.d.ts.map +1 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +191 -0
- package/dest/l1_tx_utils/fee-strategies/types.d.ts +51 -0
- package/dest/l1_tx_utils/fee-strategies/types.d.ts.map +1 -0
- package/dest/l1_tx_utils/fee-strategies/types.js +3 -0
- package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts +41 -0
- package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts.map +1 -0
- package/dest/l1_tx_utils/forwarder_l1_tx_utils.js +42 -0
- package/dest/l1_tx_utils/index-blobs.d.ts +3 -0
- package/dest/l1_tx_utils/index-blobs.d.ts.map +1 -0
- package/dest/l1_tx_utils/index-blobs.js +2 -0
- package/dest/l1_tx_utils/index.d.ts +13 -0
- package/dest/l1_tx_utils/index.d.ts.map +1 -0
- package/dest/l1_tx_utils/index.js +13 -0
- package/dest/l1_tx_utils/interfaces.d.ts +76 -0
- package/dest/l1_tx_utils/interfaces.d.ts.map +1 -0
- package/dest/l1_tx_utils/interfaces.js +4 -0
- package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +252 -0
- package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -0
- package/dest/l1_tx_utils/l1_fee_analyzer.js +568 -0
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +113 -0
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -0
- package/dest/l1_tx_utils/l1_tx_utils.js +720 -0
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +83 -0
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -0
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +357 -0
- package/dest/l1_tx_utils/signer.d.ts +4 -0
- package/dest/l1_tx_utils/signer.d.ts.map +1 -0
- package/dest/l1_tx_utils/signer.js +16 -0
- package/dest/l1_tx_utils/tx_delayer.d.ts +58 -0
- package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -0
- package/dest/l1_tx_utils/tx_delayer.js +223 -0
- package/dest/l1_tx_utils/types.d.ts +93 -0
- package/dest/l1_tx_utils/types.d.ts.map +1 -0
- package/dest/l1_tx_utils/types.js +36 -0
- package/dest/l1_tx_utils/utils.d.ts +4 -0
- package/dest/l1_tx_utils/utils.d.ts.map +1 -0
- package/dest/l1_tx_utils/utils.js +14 -0
- package/dest/l1_types.d.ts +6 -0
- package/dest/l1_types.d.ts.map +1 -0
- package/dest/l1_types.js +1 -0
- package/dest/publisher_manager.d.ts +42 -0
- package/dest/publisher_manager.d.ts.map +1 -0
- package/dest/publisher_manager.js +199 -0
- package/dest/queries.d.ts +25 -0
- package/dest/queries.d.ts.map +1 -0
- package/dest/queries.js +144 -0
- package/dest/test/blob_kzg_warmup.d.ts +19 -0
- package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
- package/dest/test/blob_kzg_warmup.js +64 -0
- package/dest/test/chain_monitor.d.ts +126 -0
- package/dest/test/chain_monitor.d.ts.map +1 -0
- package/dest/test/chain_monitor.js +314 -0
- package/dest/test/eth_cheat_codes.d.ts +260 -0
- package/dest/test/eth_cheat_codes.d.ts.map +1 -0
- package/dest/test/eth_cheat_codes.js +608 -0
- package/dest/test/eth_cheat_codes_with_state.d.ts +18 -0
- package/dest/test/eth_cheat_codes_with_state.d.ts.map +1 -0
- package/dest/test/eth_cheat_codes_with_state.js +34 -0
- package/dest/test/index.d.ts +8 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +7 -0
- package/dest/test/recording_rpc_server.d.ts +18 -0
- package/dest/test/recording_rpc_server.d.ts.map +1 -0
- package/dest/test/recording_rpc_server.js +68 -0
- package/dest/test/rollup_cheat_codes.d.ts +142 -0
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -0
- package/dest/test/rollup_cheat_codes.js +338 -0
- package/dest/test/start_anvil.d.ts +40 -0
- package/dest/test/start_anvil.d.ts.map +1 -0
- package/dest/test/start_anvil.js +210 -0
- package/dest/test/upgrade_utils.d.ts +12 -0
- package/dest/test/upgrade_utils.d.ts.map +1 -0
- package/dest/test/upgrade_utils.js +111 -0
- package/dest/types.d.ts +68 -0
- package/dest/types.d.ts.map +1 -0
- package/dest/types.js +3 -0
- package/dest/utils.d.ts +45 -0
- package/dest/utils.d.ts.map +1 -0
- package/dest/utils.js +260 -0
- package/dest/zkPassportVerifierAddress.d.ts +15 -0
- package/dest/zkPassportVerifierAddress.d.ts.map +1 -0
- package/dest/zkPassportVerifierAddress.js +11 -0
- package/package.json +122 -0
- package/src/account.ts +5 -0
- package/src/chain.ts +71 -0
- package/src/client.ts +165 -0
- package/src/config.ts +342 -0
- package/src/constants.ts +4 -0
- package/src/contracts/README.md +157 -0
- package/src/contracts/chain_state_override.ts +203 -0
- package/src/contracts/empire_base.ts +89 -0
- package/src/contracts/errors.ts +13 -0
- package/src/contracts/fee_asset_handler.ts +66 -0
- package/src/contracts/fee_asset_price_oracle.ts +284 -0
- package/src/contracts/fee_juice.ts +57 -0
- package/src/contracts/governance.ts +597 -0
- package/src/contracts/governance_proposer.ts +181 -0
- package/src/contracts/gse.ts +105 -0
- package/src/contracts/inbox.ts +223 -0
- package/src/contracts/index.ts +17 -0
- package/src/contracts/log.ts +13 -0
- package/src/contracts/multicall.ts +298 -0
- package/src/contracts/outbox.ts +132 -0
- package/src/contracts/registry.ts +161 -0
- package/src/contracts/rollup.ts +1502 -0
- package/src/contracts/slasher_contract.ts +89 -0
- package/src/contracts/slashing_proposer.ts +397 -0
- package/src/contracts/utils.ts +14 -0
- package/src/contracts/watch_event.ts +109 -0
- package/src/deploy_aztec_l1_contracts.ts +663 -0
- package/src/deploy_l1_contract.ts +361 -0
- package/src/eth-signer/eth-signer.ts +24 -0
- package/src/eth-signer/index.ts +1 -0
- package/src/forwarder_proxy.ts +107 -0
- package/src/foundry_binary.ts +57 -0
- package/src/l1_artifacts.ts +231 -0
- package/src/l1_contract_addresses.ts +149 -0
- package/src/l1_reader.ts +57 -0
- package/src/l1_tx_utils/README.md +177 -0
- package/src/l1_tx_utils/config.ts +181 -0
- package/src/l1_tx_utils/constants.ts +29 -0
- package/src/l1_tx_utils/factory.ts +64 -0
- package/src/l1_tx_utils/fee-strategies/index.ts +22 -0
- package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +162 -0
- package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +244 -0
- package/src/l1_tx_utils/fee-strategies/types.ts +55 -0
- package/src/l1_tx_utils/forwarder_l1_tx_utils.ts +107 -0
- package/src/l1_tx_utils/index-blobs.ts +2 -0
- package/src/l1_tx_utils/index.ts +15 -0
- package/src/l1_tx_utils/interfaces.ts +86 -0
- package/src/l1_tx_utils/l1_fee_analyzer.ts +874 -0
- package/src/l1_tx_utils/l1_tx_utils.ts +840 -0
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +459 -0
- package/src/l1_tx_utils/signer.ts +27 -0
- package/src/l1_tx_utils/tx_delayer.ts +302 -0
- package/src/l1_tx_utils/types.ts +116 -0
- package/src/l1_tx_utils/utils.ts +15 -0
- package/src/l1_types.ts +6 -0
- package/src/publisher_manager.ts +239 -0
- package/src/queries.ts +189 -0
- package/src/test/blob_kzg_warmup.ts +65 -0
- package/src/test/chain_monitor.ts +389 -0
- package/src/test/eth_cheat_codes.ts +624 -0
- package/src/test/eth_cheat_codes_with_state.ts +38 -0
- package/src/test/index.ts +7 -0
- package/src/test/recording_rpc_server.ts +69 -0
- package/src/test/rollup_cheat_codes.ts +398 -0
- package/src/test/start_anvil.ts +255 -0
- package/src/test/upgrade_utils.ts +109 -0
- package/src/types.ts +97 -0
- package/src/utils.ts +344 -0
- package/src/zkPassportVerifierAddress.ts +15 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
|
|
2
|
+
|
|
3
|
+
import type { BlobKzgInstance } from '@aztec-labs/blob-lib/types';
|
|
4
|
+
import { maxBigint } from '@aztec-labs/foundation/bigint';
|
|
5
|
+
import { merge, pick } from '@aztec-labs/foundation/collection';
|
|
6
|
+
import { InterruptError, TimeoutError } from '@aztec-labs/foundation/error';
|
|
7
|
+
import { EthAddress } from '@aztec-labs/foundation/eth-address';
|
|
8
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec-labs/foundation/log';
|
|
9
|
+
import { Semaphore } from '@aztec-labs/foundation/queue';
|
|
10
|
+
import { makeBackoff, retry, retryUntil } from '@aztec-labs/foundation/retry';
|
|
11
|
+
import { sleep } from '@aztec-labs/foundation/sleep';
|
|
12
|
+
import { DateProvider } from '@aztec-labs/foundation/timer';
|
|
13
|
+
import pickBy from 'lodash.pickby';
|
|
14
|
+
import {
|
|
15
|
+
type Abi,
|
|
16
|
+
type BlockOverrides,
|
|
17
|
+
type Hex,
|
|
18
|
+
type PrepareTransactionRequestRequest,
|
|
19
|
+
type StateOverride,
|
|
20
|
+
type TransactionReceipt,
|
|
21
|
+
type TransactionSerializable,
|
|
22
|
+
formatGwei,
|
|
23
|
+
serializeTransaction,
|
|
24
|
+
} from 'viem';
|
|
25
|
+
|
|
26
|
+
import type { ViemClient } from '../types.js';
|
|
27
|
+
import { formatViemError } from '../utils.js';
|
|
28
|
+
import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
|
|
29
|
+
import { MAX_L1_TX_LIMIT } from './constants.js';
|
|
30
|
+
import type { IL1TxMetrics, IL1TxStore } from './interfaces.js';
|
|
31
|
+
import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
|
|
32
|
+
import { Delayer, createDelayer, wrapClientWithDelayer } from './tx_delayer.js';
|
|
33
|
+
import {
|
|
34
|
+
DroppedTransactionError,
|
|
35
|
+
type L1BlobInputs,
|
|
36
|
+
type L1TxConfig,
|
|
37
|
+
type L1TxRequest,
|
|
38
|
+
type L1TxState,
|
|
39
|
+
L1TxTimeoutError,
|
|
40
|
+
type SigningCallback,
|
|
41
|
+
TerminalTxUtilsState,
|
|
42
|
+
TxUtilsState,
|
|
43
|
+
UnknownMinedTxError,
|
|
44
|
+
} from './types.js';
|
|
45
|
+
|
|
46
|
+
const MAX_L1_TX_STATES = 32;
|
|
47
|
+
|
|
48
|
+
// Backoff (in seconds) for retrying the read-only RPC calls that prepare a tx cancellation. A
|
|
49
|
+
// cancellation is fired in the background after a tx times out and is important (it frees the stuck
|
|
50
|
+
// nonce), so a transient RPC failure while reading the pending nonce or gas price must not abandon it.
|
|
51
|
+
const CANCELLATION_PREP_RETRY_BACKOFF_S = [1, 2, 4, 8];
|
|
52
|
+
|
|
53
|
+
export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
54
|
+
protected txs: L1TxState[] = [];
|
|
55
|
+
/** Last nonce successfully sent to the chain. Used as a lower bound when a fallback RPC node returns a stale count. */
|
|
56
|
+
private lastSentNonce: number | undefined;
|
|
57
|
+
/** Mutex to prevent concurrent sendTransaction calls from racing on the same nonce. */
|
|
58
|
+
private readonly sendMutex = new Semaphore(1);
|
|
59
|
+
/** Tx delayer for testing. Only set when enableDelayer config is true. */
|
|
60
|
+
public delayer?: Delayer;
|
|
61
|
+
/** KZG instance for blob operations. */
|
|
62
|
+
protected kzg?: BlobKzgInstance;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
public override client: ViemClient,
|
|
66
|
+
public address: EthAddress,
|
|
67
|
+
protected signer: SigningCallback,
|
|
68
|
+
logger: Logger = createLogger('ethereum:publisher'),
|
|
69
|
+
dateProvider: DateProvider = new DateProvider(),
|
|
70
|
+
config?: Partial<L1TxUtilsConfig>,
|
|
71
|
+
debugMaxGasLimit: boolean = false,
|
|
72
|
+
protected store?: IL1TxStore,
|
|
73
|
+
protected metrics?: IL1TxMetrics,
|
|
74
|
+
kzg?: BlobKzgInstance,
|
|
75
|
+
delayer?: Delayer,
|
|
76
|
+
) {
|
|
77
|
+
super(client, logger, dateProvider, config, debugMaxGasLimit);
|
|
78
|
+
this.kzg = kzg;
|
|
79
|
+
|
|
80
|
+
// Set up delayer: use provided one or create new
|
|
81
|
+
if (config?.enableDelayer && config?.ethereumSlotDuration) {
|
|
82
|
+
this.delayer =
|
|
83
|
+
delayer ?? this.createDelayer({ ethereumSlotDuration: config.ethereumSlotDuration }, logger.getBindings());
|
|
84
|
+
this.client = wrapClientWithDelayer(this.client, this.delayer);
|
|
85
|
+
if (config.txDelayerMaxInclusionTimeIntoSlot !== undefined) {
|
|
86
|
+
this.delayer.setMaxInclusionTimeIntoSlot(config.txDelayerMaxInclusionTimeIntoSlot);
|
|
87
|
+
}
|
|
88
|
+
} else if (delayer) {
|
|
89
|
+
// Delayer provided but enableDelayer not set — just store it without wrapping
|
|
90
|
+
logger.warn('Delayer provided but enableDelayer config is not set; delayer will not be used');
|
|
91
|
+
this.delayer = delayer;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public get state() {
|
|
96
|
+
return this.txs.at(-1)?.status ?? TxUtilsState.IDLE;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public get lastMinedAtBlockNumber() {
|
|
100
|
+
const minedBlockNumbers = this.txs.map(tx => tx.receipt?.blockNumber).filter(bn => bn !== undefined);
|
|
101
|
+
return minedBlockNumbers.length === 0 ? undefined : maxBigint(...minedBlockNumbers);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected async updateState(l1TxState: L1TxState, newState: TxUtilsState.MINED, l1Timestamp: number): Promise<void>;
|
|
105
|
+
protected async updateState(l1TxState: L1TxState, newState: TxUtilsState, l1Timestamp?: undefined): Promise<void>;
|
|
106
|
+
protected async updateState(l1TxState: L1TxState, newState: TxUtilsState, l1Timestamp?: number) {
|
|
107
|
+
const oldState = l1TxState.status;
|
|
108
|
+
l1TxState.status = newState;
|
|
109
|
+
const sender = this.getSenderAddress().toString();
|
|
110
|
+
this.logger.debug(
|
|
111
|
+
`Tx state changed from ${TxUtilsState[oldState]} to ${TxUtilsState[newState]} for nonce ${l1TxState.nonce} account ${sender}`,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Record metrics
|
|
115
|
+
if (newState === TxUtilsState.MINED && l1Timestamp !== undefined) {
|
|
116
|
+
this.metrics?.recordMinedTx(l1TxState, new Date(l1Timestamp));
|
|
117
|
+
} else if (newState === TxUtilsState.NOT_MINED) {
|
|
118
|
+
this.metrics?.recordDroppedTx(l1TxState);
|
|
119
|
+
// The tx was dropped: the chain nonce reverted to l1TxState.nonce, so our lower bound is
|
|
120
|
+
// no longer valid. Clear it so the next send fetches the real nonce from the chain.
|
|
121
|
+
if (this.lastSentNonce === l1TxState.nonce) {
|
|
122
|
+
this.lastSentNonce = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Update state in the store
|
|
127
|
+
await this.store
|
|
128
|
+
?.saveState(sender, l1TxState)
|
|
129
|
+
.catch(err => this.logger.error('Failed to persist L1 tx state', err));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public updateConfig(newConfig: Partial<L1TxUtilsConfig>) {
|
|
133
|
+
this.config = merge(this.config, newConfig);
|
|
134
|
+
this.logger.info(
|
|
135
|
+
'Updated L1TxUtils config',
|
|
136
|
+
pickBy(newConfig, (_, key) => key in l1TxUtilsConfigMappings),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clears the cached `lastSentNonce` so the next `sendTransaction` call fetches the real
|
|
142
|
+
* nonce from the chain. Call this after an L1 reorg to prevent stale nonce references.
|
|
143
|
+
*/
|
|
144
|
+
public resetNonce(): void {
|
|
145
|
+
this.lastSentNonce = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public getSenderAddress() {
|
|
149
|
+
return this.address;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public getSenderBalance(): Promise<bigint> {
|
|
153
|
+
return this.client.getBalance({
|
|
154
|
+
address: this.getSenderAddress().toString(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Rehydrates transaction states from the store and resumes monitoring for pending transactions.
|
|
160
|
+
* This should be called on startup to restore state and resume monitoring of any in-flight transactions.
|
|
161
|
+
*/
|
|
162
|
+
public async loadStateAndResumeMonitoring(): Promise<void> {
|
|
163
|
+
if (!this.store) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const account = this.getSenderAddress().toString();
|
|
168
|
+
const loadedStates = await this.store.loadStates(account);
|
|
169
|
+
|
|
170
|
+
if (loadedStates.length === 0) {
|
|
171
|
+
this.logger.debug(`No states to rehydrate for account ${account}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Clean up excess states if we have more than MAX_L1_TX_STATES
|
|
176
|
+
if (loadedStates.length > MAX_L1_TX_STATES) {
|
|
177
|
+
this.logger.warn(
|
|
178
|
+
`Found ${loadedStates.length} tx states for account ${account}, pruning to most recent ${MAX_L1_TX_STATES}`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Keep only the most recent MAX_L1_TX_STATES
|
|
182
|
+
const statesToKeep = loadedStates.slice(-MAX_L1_TX_STATES);
|
|
183
|
+
const statesToDelete = loadedStates.slice(0, -MAX_L1_TX_STATES);
|
|
184
|
+
|
|
185
|
+
// Batch delete old states in a transaction for efficiency
|
|
186
|
+
const idsToDelete = statesToDelete.map(s => s.id);
|
|
187
|
+
await this.store.deleteState(account, ...idsToDelete);
|
|
188
|
+
|
|
189
|
+
this.txs = statesToKeep;
|
|
190
|
+
this.logger.info(
|
|
191
|
+
`Cleaned up ${statesToDelete.length} old tx states, kept ${statesToKeep.length} for account ${account}`,
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
// Convert loaded states (which have id) to the txs format
|
|
195
|
+
this.txs = loadedStates;
|
|
196
|
+
this.logger.info(`Rehydrated ${loadedStates.length} tx states for account ${account}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Find all pending states and resume monitoring
|
|
200
|
+
const pendingStates = this.txs.filter(state => !TerminalTxUtilsState.includes(state.status));
|
|
201
|
+
if (pendingStates.length === 0) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.logger.info(`Resuming monitoring for ${pendingStates.length} pending transactions for account ${account}`, {
|
|
206
|
+
txs: pendingStates.map(s => ({ id: s.id, nonce: s.nonce, status: TxUtilsState[s.status] })),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
for (const state of pendingStates) {
|
|
210
|
+
void this.monitorTransaction(state).catch(err => {
|
|
211
|
+
this.logger.error(
|
|
212
|
+
`Error monitoring rehydrated tx with nonce ${state.nonce} for account ${account}`,
|
|
213
|
+
formatViemError(err),
|
|
214
|
+
{ nonce: state.nonce, account },
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async signTransaction(txRequest: TransactionSerializable): Promise<`0x${string}`> {
|
|
221
|
+
const signature = await this.signer(txRequest, this.getSenderAddress());
|
|
222
|
+
return serializeTransaction(txRequest, signature);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
protected async prepareSignedTransaction(txData: PrepareTransactionRequestRequest) {
|
|
226
|
+
const txRequest = await this.client.prepareTransactionRequest(txData);
|
|
227
|
+
return await this.signTransaction(txRequest as TransactionSerializable);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async checkInterruptedOrTimedOut(gasConfig: Pick<L1TxConfig, 'txTimeoutAt'>): Promise<Date> {
|
|
231
|
+
if (this.interrupted) {
|
|
232
|
+
throw new InterruptError(`Transaction sending is interrupted`);
|
|
233
|
+
}
|
|
234
|
+
const now = new Date(await this.getL1Timestamp());
|
|
235
|
+
if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
|
|
236
|
+
throw new TimeoutError(
|
|
237
|
+
`Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return now;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Sends a transaction with gas estimation and pricing
|
|
245
|
+
* @param request - The transaction request (to, data, value)
|
|
246
|
+
* @param gasConfig - Optional gas configuration
|
|
247
|
+
* @returns The transaction hash and parameters used
|
|
248
|
+
*/
|
|
249
|
+
public async sendTransaction(
|
|
250
|
+
request: L1TxRequest,
|
|
251
|
+
gasConfigOverrides?: L1TxConfig,
|
|
252
|
+
blobInputs?: L1BlobInputs,
|
|
253
|
+
stateChange: TxUtilsState = TxUtilsState.SENT,
|
|
254
|
+
): Promise<{ txHash: Hex; state: L1TxState }> {
|
|
255
|
+
try {
|
|
256
|
+
const gasConfig = merge(this.config, gasConfigOverrides);
|
|
257
|
+
const account = this.getSenderAddress().toString();
|
|
258
|
+
|
|
259
|
+
// Fail fast before doing any work (gas estimation, balance check) if we've been interrupted
|
|
260
|
+
// or if the caller's deadline has already passed. The same check is repeated after gas
|
|
261
|
+
// estimation in case it took long enough to push us past the deadline.
|
|
262
|
+
await this.checkInterruptedOrTimedOut(gasConfig);
|
|
263
|
+
|
|
264
|
+
let gasLimit: bigint;
|
|
265
|
+
if (this.debugMaxGasLimit) {
|
|
266
|
+
gasLimit = MAX_L1_TX_LIMIT;
|
|
267
|
+
} else if (gasConfig.gasLimit) {
|
|
268
|
+
gasLimit = gasConfig.gasLimit;
|
|
269
|
+
} else {
|
|
270
|
+
gasLimit = await this.estimateGas(account, request, gasConfig);
|
|
271
|
+
}
|
|
272
|
+
this.logger.trace(`Computed gas limit ${gasLimit}`, { gasLimit, ...request });
|
|
273
|
+
|
|
274
|
+
const feesPerGas = await this.getFeesPerGas(gasConfig, !!blobInputs);
|
|
275
|
+
|
|
276
|
+
const now = await this.checkInterruptedOrTimedOut(gasConfig);
|
|
277
|
+
|
|
278
|
+
let txHash: Hex;
|
|
279
|
+
let nonce: number;
|
|
280
|
+
let baseState: Pick<L1TxState, 'request' | 'gasLimit' | 'blobInputs' | 'feesPerGas' | 'nonce'>;
|
|
281
|
+
|
|
282
|
+
await this.sendMutex.acquire();
|
|
283
|
+
try {
|
|
284
|
+
const chainNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
285
|
+
// If a fallback RPC node returns a stale count (lower than what we last sent), use our
|
|
286
|
+
// local lower bound to avoid sending a duplicate of an already-pending transaction.
|
|
287
|
+
nonce =
|
|
288
|
+
this.lastSentNonce !== undefined && chainNonce <= this.lastSentNonce ? this.lastSentNonce + 1 : chainNonce;
|
|
289
|
+
|
|
290
|
+
baseState = { request, gasLimit, blobInputs, feesPerGas, nonce };
|
|
291
|
+
const txData = this.makeTxData(baseState, { isCancelTx: false });
|
|
292
|
+
|
|
293
|
+
const signedRequest = await this.prepareSignedTransaction(txData);
|
|
294
|
+
txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
|
|
295
|
+
this.lastSentNonce = nonce;
|
|
296
|
+
} finally {
|
|
297
|
+
this.sendMutex.release();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Create the new state for monitoring
|
|
301
|
+
const l1TxState: L1TxState = {
|
|
302
|
+
...baseState,
|
|
303
|
+
id: (await this.store?.consumeNextStateId(account)) ?? Math.max(...this.txs.map(tx => tx.id), -1) + 1,
|
|
304
|
+
txHashes: [txHash],
|
|
305
|
+
cancelTxHashes: [],
|
|
306
|
+
status: TxUtilsState.IDLE,
|
|
307
|
+
txConfigOverrides: gasConfigOverrides ?? {},
|
|
308
|
+
sentAtL1Ts: now,
|
|
309
|
+
lastSentAtL1Ts: now,
|
|
310
|
+
feesPerGasHistory: [baseState.feesPerGas],
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// And persist it
|
|
314
|
+
await this.updateState(l1TxState, stateChange);
|
|
315
|
+
await this.store?.saveBlobs(account, l1TxState.id, blobInputs);
|
|
316
|
+
this.txs.push(l1TxState);
|
|
317
|
+
|
|
318
|
+
// Clean up stale states
|
|
319
|
+
if (this.txs.length > MAX_L1_TX_STATES) {
|
|
320
|
+
const removed = this.txs.shift();
|
|
321
|
+
if (removed && this.store) {
|
|
322
|
+
await this.store.deleteState(account, removed.id);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.logger.info(`Sent L1 transaction ${txHash}`, {
|
|
327
|
+
to: request.to,
|
|
328
|
+
value: request.value,
|
|
329
|
+
nonce,
|
|
330
|
+
account,
|
|
331
|
+
sentAt: now,
|
|
332
|
+
gasLimit,
|
|
333
|
+
maxFeePerGas: formatGwei(feesPerGas.maxFeePerGas),
|
|
334
|
+
maxPriorityFeePerGas: formatGwei(feesPerGas.maxPriorityFeePerGas),
|
|
335
|
+
...(feesPerGas.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(feesPerGas.maxFeePerBlobGas) }),
|
|
336
|
+
isBlobTx: !!blobInputs,
|
|
337
|
+
txConfig: gasConfigOverrides,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return { txHash, state: l1TxState };
|
|
341
|
+
} catch (err: any) {
|
|
342
|
+
const viemError = formatViemError(err, request.abi);
|
|
343
|
+
this.logger.error(`Failed to send L1 transaction: ${viemError.message}`, viemError, {
|
|
344
|
+
request: pick(request, 'to', 'value'),
|
|
345
|
+
});
|
|
346
|
+
throw viemError;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private async tryGetTxReceipt(
|
|
351
|
+
txHashes: Hex[],
|
|
352
|
+
nonce: number,
|
|
353
|
+
isCancelTx: boolean,
|
|
354
|
+
): Promise<TransactionReceipt | undefined> {
|
|
355
|
+
for (const hash of txHashes) {
|
|
356
|
+
try {
|
|
357
|
+
const receipt = await this.client.getTransactionReceipt({ hash });
|
|
358
|
+
if (receipt) {
|
|
359
|
+
const account = this.getSenderAddress().toString();
|
|
360
|
+
const what = isCancelTx ? 'Cancellation L1 transaction' : 'L1 transaction';
|
|
361
|
+
if (receipt.status === 'reverted') {
|
|
362
|
+
this.logger.warn(`${what} ${hash} with nonce ${nonce} reverted`, { receipt, nonce, account });
|
|
363
|
+
} else {
|
|
364
|
+
this.logger.info(`${what} ${hash} with nonce ${nonce} mined`, { receipt, nonce, account });
|
|
365
|
+
}
|
|
366
|
+
return receipt;
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (err instanceof Error && err.name === 'TransactionReceiptNotFoundError') {
|
|
370
|
+
continue;
|
|
371
|
+
} else {
|
|
372
|
+
this.logger.error(`Error getting receipt for tx ${hash}`, err);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Returns whether a given tx should be considered as timed out and no longer monitored.
|
|
381
|
+
* Relies on the txTimeout setting for user txs, and on the txCancellationTimeout for cancel txs.
|
|
382
|
+
* @remarks We check against the latestBlockTimestamp as opposed to the current time to avoid a race condition where
|
|
383
|
+
* the tx is mined in a block with the same timestamp as txTimeoutAt, but our execution node has not yet processed it,
|
|
384
|
+
* or the loop here has not yet checked the tx before that timeout.
|
|
385
|
+
*/
|
|
386
|
+
private isTxTimedOut(state: L1TxState, l1Timestamp: number) {
|
|
387
|
+
const account = this.getSenderAddress().toString();
|
|
388
|
+
const { nonce } = state;
|
|
389
|
+
|
|
390
|
+
const txConfig = merge(this.config, state.txConfigOverrides);
|
|
391
|
+
const { txTimeoutAt, txTimeoutMs, maxSpeedUpAttempts } = txConfig;
|
|
392
|
+
const isCancelTx = state.cancelTxHashes.length > 0;
|
|
393
|
+
|
|
394
|
+
this.logger.trace(`Tx timeout check for ${account} with nonce ${nonce}`, {
|
|
395
|
+
nonce,
|
|
396
|
+
account,
|
|
397
|
+
l1Timestamp,
|
|
398
|
+
lastAttemptSent: state.lastSentAtL1Ts.getTime(),
|
|
399
|
+
initialTxTime: state.sentAtL1Ts.getTime(),
|
|
400
|
+
now: this.dateProvider.now(),
|
|
401
|
+
txTimeoutAt,
|
|
402
|
+
txTimeoutMs,
|
|
403
|
+
interrupted: this.interrupted,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (this.interrupted) {
|
|
407
|
+
this.logger.warn(`Tx monitoring interrupted during nonce ${nonce} for ${account} check`, { nonce, account });
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (isCancelTx) {
|
|
412
|
+
// Note that we check against the lastSentAt time for cancellations, since we time them out
|
|
413
|
+
// after the last attempt to submit them, not the initial time.
|
|
414
|
+
const attempts = state.cancelTxHashes.length;
|
|
415
|
+
return (
|
|
416
|
+
attempts > maxSpeedUpAttempts &&
|
|
417
|
+
state.lastSentAtL1Ts.getTime() + txConfig.txCancellationFinalTimeoutMs! <= l1Timestamp
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
(txTimeoutAt !== undefined && l1Timestamp >= txTimeoutAt.getTime()) ||
|
|
423
|
+
(txTimeoutMs !== undefined && txTimeoutMs > 0 && l1Timestamp - state.sentAtL1Ts.getTime() >= txTimeoutMs)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Monitors a transaction until completion, handling speed-ups if needed
|
|
429
|
+
*/
|
|
430
|
+
protected async monitorTransaction(state: L1TxState): Promise<TransactionReceipt> {
|
|
431
|
+
const { nonce, gasLimit, blobInputs, txConfigOverrides: gasConfigOverrides } = state;
|
|
432
|
+
const gasConfig = merge(this.config, gasConfigOverrides);
|
|
433
|
+
const { maxSpeedUpAttempts, stallTimeMs } = gasConfig;
|
|
434
|
+
const isCancelTx = state.cancelTxHashes.length > 0;
|
|
435
|
+
const txHashes = isCancelTx ? state.cancelTxHashes : state.txHashes;
|
|
436
|
+
const isBlobTx = !!blobInputs;
|
|
437
|
+
const account = this.getSenderAddress().toString();
|
|
438
|
+
|
|
439
|
+
const initialTxHash = txHashes[0];
|
|
440
|
+
let currentTxHash = txHashes.at(-1)!;
|
|
441
|
+
let l1Timestamp = 0;
|
|
442
|
+
|
|
443
|
+
while (true) {
|
|
444
|
+
try {
|
|
445
|
+
l1Timestamp = await this.getL1Timestamp();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
// A transient RPC failure here must not abort monitoring with a rejection: callers that
|
|
448
|
+
// fire this loop in the background would surface it as an unhandled rejection (e.g. when a
|
|
449
|
+
// test tears down its L1 node while a monitor iteration is in flight). Exit quietly if we
|
|
450
|
+
// are shutting down, otherwise retry on the next interval.
|
|
451
|
+
if (this.interrupted) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
this.logger.error(`Error fetching L1 timestamp while monitoring tx ${currentTxHash}`, err, { nonce, account });
|
|
455
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const timePassed = l1Timestamp - state.lastSentAtL1Ts.getTime();
|
|
461
|
+
const [currentNonce, pendingNonce] = await Promise.all([
|
|
462
|
+
this.client.getTransactionCount({ address: account, blockTag: 'latest' }),
|
|
463
|
+
this.client.getTransactionCount({ address: account, blockTag: 'pending' }),
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
// If the current nonce on our account is greater than our transaction's nonce then a tx with the same nonce has been mined.
|
|
467
|
+
if (currentNonce > nonce) {
|
|
468
|
+
// We try getting the receipt twice, since sometimes anvil fails to return it if the tx has just been mined
|
|
469
|
+
const receipt =
|
|
470
|
+
(await this.tryGetTxReceipt(state.cancelTxHashes, nonce, true)) ??
|
|
471
|
+
(await this.tryGetTxReceipt(state.txHashes, nonce, false)) ??
|
|
472
|
+
(await sleep(500)) ??
|
|
473
|
+
(await this.tryGetTxReceipt(state.cancelTxHashes, nonce, true)) ??
|
|
474
|
+
(await this.tryGetTxReceipt(state.txHashes, nonce, false));
|
|
475
|
+
|
|
476
|
+
if (receipt) {
|
|
477
|
+
state.receipt = receipt;
|
|
478
|
+
await this.updateState(state, TxUtilsState.MINED, l1Timestamp);
|
|
479
|
+
return receipt;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// If we get here then we have checked all of our tx versions and not found anything.
|
|
483
|
+
// We should consider the nonce as MINED
|
|
484
|
+
await this.updateState(state, TxUtilsState.MINED, l1Timestamp);
|
|
485
|
+
throw new UnknownMinedTxError(nonce, account);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// If this is a cancel tx and its nonce is no longer on the mempool, we consider it dropped and stop monitoring
|
|
489
|
+
// If it is a regular tx, we let the loop speed it up after the stall time
|
|
490
|
+
if (isCancelTx && pendingNonce <= nonce && timePassed >= gasConfig.txUnseenConsideredDroppedMs) {
|
|
491
|
+
this.logger.warn(
|
|
492
|
+
`Cancellation tx with nonce ${nonce} for account ${account} has been dropped from the visible mempool`,
|
|
493
|
+
{ nonce, account, pendingNonce, timePassed },
|
|
494
|
+
);
|
|
495
|
+
await this.updateState(state, TxUtilsState.NOT_MINED);
|
|
496
|
+
throw new DroppedTransactionError(nonce, account);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Break if the tx has timed out (ie expired)
|
|
500
|
+
if (this.isTxTimedOut(state, l1Timestamp)) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Speed up the transaction if it appears to be stuck (exceeded stall time and still have retry attempts)
|
|
505
|
+
const attempts = txHashes.length;
|
|
506
|
+
if (timePassed >= stallTimeMs && attempts <= maxSpeedUpAttempts) {
|
|
507
|
+
const newFeesPerGas = await this.getFeesPerGas(gasConfig, isBlobTx, attempts, state.feesPerGas);
|
|
508
|
+
state.feesPerGas = newFeesPerGas;
|
|
509
|
+
state.feesPerGasHistory?.push(newFeesPerGas);
|
|
510
|
+
|
|
511
|
+
this.logger.debug(
|
|
512
|
+
`Tx ${currentTxHash} with nonce ${nonce} from ${account} appears stuck. ` +
|
|
513
|
+
`Attempting speed-up ${attempts}/${maxSpeedUpAttempts} ` +
|
|
514
|
+
`with new priority fee ${formatGwei(newFeesPerGas.maxPriorityFeePerGas)} gwei.`,
|
|
515
|
+
{
|
|
516
|
+
account,
|
|
517
|
+
nonce,
|
|
518
|
+
maxFeePerGas: formatGwei(newFeesPerGas.maxFeePerGas),
|
|
519
|
+
maxPriorityFeePerGas: formatGwei(newFeesPerGas.maxPriorityFeePerGas),
|
|
520
|
+
...(newFeesPerGas.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(newFeesPerGas.maxFeePerBlobGas) }),
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const txData = this.makeTxData(state, { isCancelTx });
|
|
525
|
+
|
|
526
|
+
const signedRequest = await this.prepareSignedTransaction(txData);
|
|
527
|
+
const newHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
|
|
528
|
+
|
|
529
|
+
this.logger.verbose(
|
|
530
|
+
`Sent L1 speed-up tx ${newHash} replacing ${currentTxHash} for nonce ${nonce} from ${account}`,
|
|
531
|
+
{
|
|
532
|
+
nonce,
|
|
533
|
+
account,
|
|
534
|
+
gasLimit,
|
|
535
|
+
maxFeePerGas: formatGwei(newFeesPerGas.maxFeePerGas),
|
|
536
|
+
maxPriorityFeePerGas: formatGwei(newFeesPerGas.maxPriorityFeePerGas),
|
|
537
|
+
txConfig: state.txConfigOverrides,
|
|
538
|
+
...(newFeesPerGas.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(newFeesPerGas.maxFeePerBlobGas) }),
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
currentTxHash = newHash;
|
|
543
|
+
txHashes.push(currentTxHash);
|
|
544
|
+
state.lastSentAtL1Ts = new Date(l1Timestamp);
|
|
545
|
+
await this.updateState(state, isCancelTx ? TxUtilsState.CANCELLED : TxUtilsState.SPEED_UP);
|
|
546
|
+
|
|
547
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this.logger.debug(
|
|
552
|
+
`Tx ${currentTxHash} from ${account} with nonce ${nonce} still pending after ${timePassed}ms`,
|
|
553
|
+
{
|
|
554
|
+
account,
|
|
555
|
+
nonce,
|
|
556
|
+
pendingNonce,
|
|
557
|
+
attempts,
|
|
558
|
+
timePassed,
|
|
559
|
+
isBlobTx,
|
|
560
|
+
isCancelTx,
|
|
561
|
+
...pick(state.feesPerGas, 'maxFeePerGas', 'maxPriorityFeePerGas', 'maxFeePerBlobGas'),
|
|
562
|
+
...pick(
|
|
563
|
+
gasConfig,
|
|
564
|
+
'txUnseenConsideredDroppedMs',
|
|
565
|
+
'txCancellationFinalTimeoutMs',
|
|
566
|
+
'maxSpeedUpAttempts',
|
|
567
|
+
'stallTimeMs',
|
|
568
|
+
'txTimeoutAt',
|
|
569
|
+
'txTimeoutMs',
|
|
570
|
+
),
|
|
571
|
+
},
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
575
|
+
} catch (err: any) {
|
|
576
|
+
if (err instanceof DroppedTransactionError || err instanceof UnknownMinedTxError) {
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const viemError = formatViemError(err);
|
|
581
|
+
this.logger.error(`Error while monitoring L1 tx ${currentTxHash}`, viemError, { nonce, account });
|
|
582
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Oh no, the transaction has timed out!
|
|
587
|
+
if (isCancelTx || !gasConfig.cancelTxOnTimeout) {
|
|
588
|
+
await this.updateState(state, TxUtilsState.NOT_MINED);
|
|
589
|
+
} else {
|
|
590
|
+
// Otherwise we fire the cancellation without awaiting to avoid blocking the caller,
|
|
591
|
+
// and monitor it in the background so we can speed it up as needed.
|
|
592
|
+
void this.attemptTxCancellation(state).catch(async err => {
|
|
593
|
+
await this.updateState(state, TxUtilsState.NOT_MINED);
|
|
594
|
+
this.logger.error(`Failed to send cancellation for timed out tx ${initialTxHash} with nonce ${nonce}`, err, {
|
|
595
|
+
account,
|
|
596
|
+
nonce,
|
|
597
|
+
initialTxHash,
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const what = isCancelTx ? 'Cancellation L1' : 'L1';
|
|
603
|
+
this.logger.warn(`${what} transaction ${initialTxHash} with nonce ${nonce} from ${account} timed out`, {
|
|
604
|
+
initialTxHash,
|
|
605
|
+
currentTxHash,
|
|
606
|
+
nonce,
|
|
607
|
+
account,
|
|
608
|
+
txTimeoutAt: gasConfig.txTimeoutAt?.getTime(),
|
|
609
|
+
txTimeoutMs: gasConfig.txTimeoutMs,
|
|
610
|
+
txInitialTime: state.sentAtL1Ts.getTime(),
|
|
611
|
+
l1Timestamp,
|
|
612
|
+
now: this.dateProvider.now(),
|
|
613
|
+
attempts: txHashes.length - 1,
|
|
614
|
+
isInterrupted: this.interrupted,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
throw new TimeoutError(`L1 transaction ${initialTxHash} timed out`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Creates tx data to be signed by viem signTransaction method, using the state as input.
|
|
622
|
+
* If isCancelTx is true, creates a 0-value tx to self with 21k gas and no data instead,
|
|
623
|
+
* and an empty blob input if the original tx also had blobs.
|
|
624
|
+
*/
|
|
625
|
+
private makeTxData(
|
|
626
|
+
state: Pick<L1TxState, 'request' | 'gasLimit' | 'blobInputs' | 'feesPerGas' | 'nonce'>,
|
|
627
|
+
opts: { isCancelTx: boolean },
|
|
628
|
+
): PrepareTransactionRequestRequest {
|
|
629
|
+
const { request, gasLimit, blobInputs, feesPerGas, nonce } = state;
|
|
630
|
+
const isBlobTx = blobInputs !== undefined;
|
|
631
|
+
|
|
632
|
+
const baseTxOpts = { nonce, ...pick(feesPerGas, 'maxFeePerGas', 'maxPriorityFeePerGas') };
|
|
633
|
+
|
|
634
|
+
if (opts.isCancelTx) {
|
|
635
|
+
const baseTxData = {
|
|
636
|
+
to: this.getSenderAddress().toString(),
|
|
637
|
+
value: 0n,
|
|
638
|
+
data: '0x' as const,
|
|
639
|
+
gas: 21_000n,
|
|
640
|
+
...baseTxOpts,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
return isBlobTx ? { ...baseTxData, ...this.makeEmptyBlobInputs(feesPerGas.maxFeePerBlobGas!) } : baseTxData;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const baseTxData = {
|
|
647
|
+
...request,
|
|
648
|
+
...baseTxOpts,
|
|
649
|
+
gas: gasLimit,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
return blobInputs ? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: feesPerGas.maxFeePerBlobGas! } : baseTxData;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Returns when all monitor loops have stopped. */
|
|
656
|
+
public async waitMonitoringStopped(timeoutSeconds = 10) {
|
|
657
|
+
const account = this.getSenderAddress().toString();
|
|
658
|
+
await retryUntil(
|
|
659
|
+
() => this.txs.every(tx => TerminalTxUtilsState.includes(tx.status)),
|
|
660
|
+
`monitoring stopped for ${account}`,
|
|
661
|
+
timeoutSeconds,
|
|
662
|
+
0.1,
|
|
663
|
+
).catch(() => this.logger.warn(`Timeout waiting for monitoring loops to stop for ${account}`));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Sends a transaction and monitors it until completion
|
|
668
|
+
* @param request - The transaction request (to, data, value)
|
|
669
|
+
* @param gasConfig - Optional gas configuration
|
|
670
|
+
* @returns The receipt of the successful transaction
|
|
671
|
+
*/
|
|
672
|
+
public async sendAndMonitorTransaction(
|
|
673
|
+
request: L1TxRequest,
|
|
674
|
+
gasConfig?: L1TxConfig,
|
|
675
|
+
blobInputs?: L1BlobInputs,
|
|
676
|
+
): Promise<{ receipt: TransactionReceipt; state: L1TxState }> {
|
|
677
|
+
const { state } = await this.sendTransaction(request, gasConfig, blobInputs);
|
|
678
|
+
try {
|
|
679
|
+
const receipt = await this.monitorTransaction(state);
|
|
680
|
+
return { receipt, state };
|
|
681
|
+
} catch (err) {
|
|
682
|
+
if (err instanceof TimeoutError) {
|
|
683
|
+
// Snapshot the ladder now: the fire-and-forget cancellation mutates state.feesPerGas moments later.
|
|
684
|
+
throw new L1TxTimeoutError(err.message, {
|
|
685
|
+
feesPerGasHistory: state.feesPerGasHistory ? [...state.feesPerGasHistory] : undefined,
|
|
686
|
+
finalFeesPerGas: state.feesPerGas,
|
|
687
|
+
attempts: state.txHashes.length,
|
|
688
|
+
nonce: state.nonce,
|
|
689
|
+
gasLimit: state.gasLimit,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
throw err;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
public override async simulate(
|
|
697
|
+
request: L1TxRequest & { gas?: bigint; from?: Hex },
|
|
698
|
+
_blockOverrides: BlockOverrides<bigint, number> = {},
|
|
699
|
+
stateOverrides: StateOverride = [],
|
|
700
|
+
abi: Abi = RollupAbi,
|
|
701
|
+
_gasConfig?: L1TxUtilsConfig & { fallbackGasEstimate?: bigint; ignoreBlockGasLimit?: boolean },
|
|
702
|
+
): Promise<{ gasUsed: bigint; result: `0x${string}` }> {
|
|
703
|
+
const blockOverrides = { ..._blockOverrides };
|
|
704
|
+
const gasConfig = merge(this.config, _gasConfig);
|
|
705
|
+
|
|
706
|
+
// Fee fields are deliberately omitted. Supplying them makes the node apply the EIP-1559 upfront funds
|
|
707
|
+
// check (sender balance >= gas * maxFeePerGas) against the worst-case gas cap below, which rejects the
|
|
708
|
+
// whole simulation request before any execution. Omitted fee fields default to a zero gas price.
|
|
709
|
+
const call: any = {
|
|
710
|
+
to: request.to!,
|
|
711
|
+
data: request.data,
|
|
712
|
+
from: request.from ?? this.getSenderAddress().toString(),
|
|
713
|
+
gas: request.gas ?? MAX_L1_TX_LIMIT,
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
if (!request.gas && !gasConfig.ignoreBlockGasLimit) {
|
|
717
|
+
// MAX_L1_TX_LIMIT is set as call.gas, ensure block gasLimit is sufficient
|
|
718
|
+
blockOverrides.gasLimit = MAX_L1_TX_LIMIT;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return await this._simulate(call, blockOverrides, stateOverrides, gasConfig, abi);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Attempts to cancel a transaction by sending a 0-value tx to self with same nonce but higher gas prices
|
|
726
|
+
* Only sends the cancellation if the original tx is still pending, not if it was dropped
|
|
727
|
+
* @returns The hash of the cancellation transaction
|
|
728
|
+
*/
|
|
729
|
+
protected async attemptTxCancellation(state: L1TxState): Promise<void> {
|
|
730
|
+
const isBlobTx = state.blobInputs !== undefined;
|
|
731
|
+
const { nonce, feesPerGas: previousFeesPerGas } = state;
|
|
732
|
+
const account = this.getSenderAddress().toString();
|
|
733
|
+
|
|
734
|
+
// Do not send cancellation if interrupted
|
|
735
|
+
if (this.interrupted) {
|
|
736
|
+
this.logger.warn(
|
|
737
|
+
`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as interrupted`,
|
|
738
|
+
{ nonce, account },
|
|
739
|
+
);
|
|
740
|
+
await this.updateState(state, TxUtilsState.NOT_MINED);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Resolve the pending nonce and cancellation fees per gas up front. These are read-only RPC calls, so
|
|
745
|
+
// they are safe to retry; a transient RPC failure here must not permanently abandon the cancellation
|
|
746
|
+
// (which would leave the nonce stuck and the tx state stranded short of CANCELLED). We retry with a
|
|
747
|
+
// real backoff before giving up, unlike getFeesPerGas's tight internal retry which can be exhausted by
|
|
748
|
+
// a brief node hiccup. The retried block performs no state-changing send, so retrying cannot
|
|
749
|
+
// double-send the cancellation tx.
|
|
750
|
+
const { currentNonce, cancelFeesPerGas } = await retry(
|
|
751
|
+
async () => {
|
|
752
|
+
const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
753
|
+
if (currentNonce >= nonce) {
|
|
754
|
+
// Get fees per gas with higher priority fee for cancellation
|
|
755
|
+
const cancelFeesPerGas = await this.getFeesPerGas(
|
|
756
|
+
{
|
|
757
|
+
...this.config,
|
|
758
|
+
// Use high bump for cancellation to ensure it replaces the original tx
|
|
759
|
+
priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
|
|
760
|
+
},
|
|
761
|
+
isBlobTx,
|
|
762
|
+
state.txHashes.length,
|
|
763
|
+
previousFeesPerGas,
|
|
764
|
+
);
|
|
765
|
+
return { currentNonce, cancelFeesPerGas };
|
|
766
|
+
}
|
|
767
|
+
return { currentNonce, cancelFeesPerGas: undefined };
|
|
768
|
+
},
|
|
769
|
+
`Preparing cancellation for L1 tx from account ${account} with nonce ${nonce}`,
|
|
770
|
+
makeBackoff(CANCELLATION_PREP_RETRY_BACKOFF_S),
|
|
771
|
+
this.logger,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
if (cancelFeesPerGas === undefined) {
|
|
775
|
+
this.logger.verbose(
|
|
776
|
+
`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`,
|
|
777
|
+
{ nonce, account, currentNonce },
|
|
778
|
+
);
|
|
779
|
+
await this.updateState(state, TxUtilsState.NOT_MINED);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelFeesPerGas;
|
|
784
|
+
this.logger.verbose(
|
|
785
|
+
`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction from account ${account} with nonce ${nonce} after time out`,
|
|
786
|
+
{
|
|
787
|
+
maxFeePerGas: formatGwei(maxFeePerGas),
|
|
788
|
+
maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
|
|
789
|
+
...(maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(maxFeePerBlobGas) }),
|
|
790
|
+
},
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// Send 0-value tx to self with higher gas price
|
|
794
|
+
state.feesPerGas = cancelFeesPerGas;
|
|
795
|
+
state.lastSentAtL1Ts = new Date(await this.getL1Timestamp());
|
|
796
|
+
|
|
797
|
+
const txData = this.makeTxData(state, { isCancelTx: true });
|
|
798
|
+
const signedRequest = await this.prepareSignedTransaction(txData);
|
|
799
|
+
const cancelTxHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
|
|
800
|
+
|
|
801
|
+
state.cancelTxHashes.push(cancelTxHash);
|
|
802
|
+
await this.updateState(state, TxUtilsState.CANCELLED);
|
|
803
|
+
|
|
804
|
+
this.logger.warn(`Sent cancellation tx ${cancelTxHash} for timed out tx from ${account} with nonce ${nonce}`, {
|
|
805
|
+
nonce,
|
|
806
|
+
cancelFeesPerGas,
|
|
807
|
+
isBlobTx,
|
|
808
|
+
txHashes: state.txHashes,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Do not await the cancel tx to be mined
|
|
812
|
+
void this.monitorTransaction(state).catch(err => {
|
|
813
|
+
this.logger.error(`Failed to mine cancellation tx ${cancelTxHash} for nonce ${nonce} account ${account}`, err, {
|
|
814
|
+
nonce,
|
|
815
|
+
account,
|
|
816
|
+
cancelTxHash,
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Returns L1 timestamps in milliseconds */
|
|
822
|
+
private async getL1Timestamp() {
|
|
823
|
+
const { timestamp } = await this.client.getBlock({ blockTag: 'latest', includeTransactions: false });
|
|
824
|
+
return Number(timestamp) * 1000;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** Makes empty blob inputs for the cancellation tx. */
|
|
828
|
+
protected makeEmptyBlobInputs(maxFeePerBlobGas: bigint): Required<L1BlobInputs> {
|
|
829
|
+
if (!this.kzg) {
|
|
830
|
+
throw new Error('Cannot make empty blob inputs for cancellation without kzg');
|
|
831
|
+
}
|
|
832
|
+
const blobData = new Uint8Array(131072).fill(0);
|
|
833
|
+
return { blobs: [blobData], kzg: this.kzg, maxFeePerBlobGas };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Creates a new delayer instance. */
|
|
837
|
+
protected createDelayer(opts: { ethereumSlotDuration: bigint | number }, bindings: LoggerBindings): Delayer {
|
|
838
|
+
return createDelayer(this.dateProvider, opts, bindings);
|
|
839
|
+
}
|
|
840
|
+
}
|