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