@aztec/ethereum 3.0.0-nightly.20251003 → 3.0.0-nightly.20251004
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,9 +1,9 @@
|
|
|
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 { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
5
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
6
|
-
import {
|
|
6
|
+
import { retryUntil } from '@aztec/foundation/retry';
|
|
7
7
|
import { sleep } from '@aztec/foundation/sleep';
|
|
8
8
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
9
|
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
|
|
@@ -12,7 +12,6 @@ import pickBy from 'lodash.pickby';
|
|
|
12
12
|
import {
|
|
13
13
|
type Abi,
|
|
14
14
|
type BlockOverrides,
|
|
15
|
-
type GetTransactionReturnType,
|
|
16
15
|
type Hex,
|
|
17
16
|
type NonceManager,
|
|
18
17
|
type PrepareTransactionRequestRequest,
|
|
@@ -31,12 +30,15 @@ import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
|
|
|
31
30
|
import { LARGE_GAS_LIMIT } from './constants.js';
|
|
32
31
|
import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
|
|
33
32
|
import {
|
|
33
|
+
DroppedTransactionError,
|
|
34
34
|
type L1BlobInputs,
|
|
35
|
-
type
|
|
35
|
+
type L1TxConfig,
|
|
36
36
|
type L1TxRequest,
|
|
37
37
|
type L1TxState,
|
|
38
38
|
type SigningCallback,
|
|
39
|
+
TerminalTxUtilsState,
|
|
39
40
|
TxUtilsState,
|
|
41
|
+
UnknownMinedTxError,
|
|
40
42
|
} from './types.js';
|
|
41
43
|
|
|
42
44
|
const MAX_L1_TX_STATES = 32;
|
|
@@ -72,12 +74,12 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
72
74
|
l1TxState.status = newState;
|
|
73
75
|
const sender = this.getSenderAddress().toString();
|
|
74
76
|
this.logger.debug(
|
|
75
|
-
`
|
|
77
|
+
`Tx state changed from ${TxUtilsState[oldState]} to ${TxUtilsState[newState]} for nonce ${l1TxState.nonce} account ${sender}`,
|
|
76
78
|
);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
public updateConfig(newConfig: Partial<L1TxUtilsConfig>) {
|
|
80
|
-
this.config =
|
|
82
|
+
this.config = merge(this.config, newConfig);
|
|
81
83
|
this.logger.info(
|
|
82
84
|
'Updated L1TxUtils config',
|
|
83
85
|
pickBy(newConfig, (_, key) => key in l1TxUtilsConfigMappings),
|
|
@@ -112,12 +114,16 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
112
114
|
*/
|
|
113
115
|
public async sendTransaction(
|
|
114
116
|
request: L1TxRequest,
|
|
115
|
-
gasConfigOverrides?:
|
|
117
|
+
gasConfigOverrides?: L1TxConfig,
|
|
116
118
|
blobInputs?: L1BlobInputs,
|
|
117
119
|
stateChange: TxUtilsState = TxUtilsState.SENT,
|
|
118
120
|
): Promise<{ txHash: Hex; state: L1TxState }> {
|
|
121
|
+
if (this.interrupted) {
|
|
122
|
+
throw new InterruptError(`Transaction sending is interrupted`);
|
|
123
|
+
}
|
|
124
|
+
|
|
119
125
|
try {
|
|
120
|
-
const gasConfig =
|
|
126
|
+
const gasConfig = merge(this.config, gasConfigOverrides);
|
|
121
127
|
const account = this.getSenderAddress().toString();
|
|
122
128
|
|
|
123
129
|
let gasLimit: bigint;
|
|
@@ -128,34 +134,16 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
128
134
|
} else {
|
|
129
135
|
gasLimit = await this.estimateGas(account, request, gasConfig);
|
|
130
136
|
}
|
|
131
|
-
this.logger
|
|
137
|
+
this.logger.trace(`Computed gas limit ${gasLimit}`, { gasLimit, ...request });
|
|
132
138
|
|
|
133
139
|
const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
|
|
134
140
|
|
|
135
|
-
if (gasConfig.txTimeoutAt && this.dateProvider.now() > gasConfig.txTimeoutAt.getTime()) {
|
|
136
|
-
throw new Error('Transaction timed out before sending');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
141
|
const nonce = await this.nonceManager.consume({
|
|
140
142
|
client: this.client,
|
|
141
143
|
address: account,
|
|
142
144
|
chainId: this.client.chain.id,
|
|
143
145
|
});
|
|
144
146
|
|
|
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
147
|
const baseTxData = {
|
|
160
148
|
...request,
|
|
161
149
|
gas: gasLimit,
|
|
@@ -168,29 +156,60 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
168
156
|
? { ...baseTxData, ...blobInputs, maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! }
|
|
169
157
|
: baseTxData;
|
|
170
158
|
|
|
159
|
+
const now = new Date(await this.getL1Timestamp());
|
|
160
|
+
if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
|
|
161
|
+
throw new TimeoutError(
|
|
162
|
+
`Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.interrupted) {
|
|
167
|
+
throw new InterruptError(`Transaction sending is interrupted`);
|
|
168
|
+
}
|
|
169
|
+
|
|
171
170
|
const signedRequest = await this.prepareSignedTransaction(txData);
|
|
172
171
|
const txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
|
|
173
172
|
|
|
174
|
-
l1TxState
|
|
173
|
+
const l1TxState: L1TxState = {
|
|
174
|
+
txHashes: [txHash],
|
|
175
|
+
cancelTxHashes: [],
|
|
176
|
+
gasPrice,
|
|
177
|
+
request,
|
|
178
|
+
status: TxUtilsState.IDLE,
|
|
179
|
+
nonce,
|
|
180
|
+
gasLimit,
|
|
181
|
+
txConfigOverrides: gasConfigOverrides ?? {},
|
|
182
|
+
blobInputs,
|
|
183
|
+
sentAtL1Ts: now,
|
|
184
|
+
lastSentAtL1Ts: now,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
this.updateState(l1TxState, stateChange);
|
|
188
|
+
|
|
175
189
|
this.txs.push(l1TxState);
|
|
176
190
|
if (this.txs.length > MAX_L1_TX_STATES) {
|
|
177
191
|
this.txs.shift();
|
|
178
192
|
}
|
|
179
193
|
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
this.logger.info(`Sent L1 transaction ${txHash}`, {
|
|
195
|
+
to: request.to,
|
|
196
|
+
value: request.value,
|
|
197
|
+
nonce,
|
|
198
|
+
account,
|
|
199
|
+
sentAt: now,
|
|
182
200
|
gasLimit,
|
|
183
201
|
maxFeePerGas: formatGwei(gasPrice.maxFeePerGas),
|
|
184
202
|
maxPriorityFeePerGas: formatGwei(gasPrice.maxPriorityFeePerGas),
|
|
185
|
-
gasConfig: cleanGasConfig,
|
|
186
203
|
...(gasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(gasPrice.maxFeePerBlobGas) }),
|
|
204
|
+
isBlobTx: !!blobInputs,
|
|
205
|
+
txConfig: gasConfigOverrides,
|
|
187
206
|
});
|
|
188
207
|
|
|
189
208
|
return { txHash, state: l1TxState };
|
|
190
209
|
} catch (err: any) {
|
|
191
210
|
const viemError = formatViemError(err, request.abi);
|
|
192
|
-
this.logger
|
|
193
|
-
|
|
211
|
+
this.logger.error(`Failed to send L1 transaction`, viemError, {
|
|
212
|
+
request: pick(request, 'to', 'value'),
|
|
194
213
|
});
|
|
195
214
|
throw viemError;
|
|
196
215
|
}
|
|
@@ -205,11 +224,12 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
205
224
|
try {
|
|
206
225
|
const receipt = await this.client.getTransactionReceipt({ hash });
|
|
207
226
|
if (receipt) {
|
|
227
|
+
const account = this.getSenderAddress().toString();
|
|
208
228
|
const what = isCancelTx ? 'Cancellation L1 transaction' : 'L1 transaction';
|
|
209
229
|
if (receipt.status === 'reverted') {
|
|
210
|
-
this.logger
|
|
230
|
+
this.logger.warn(`${what} ${hash} with nonce ${nonce} reverted`, { receipt, nonce, account });
|
|
211
231
|
} else {
|
|
212
|
-
this.logger
|
|
232
|
+
this.logger.info(`${what} ${hash} with nonce ${nonce} mined`, { receipt, nonce, account });
|
|
213
233
|
}
|
|
214
234
|
return receipt;
|
|
215
235
|
}
|
|
@@ -224,50 +244,89 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Returns whether a given tx should be considered as timed out and no longer monitored.
|
|
249
|
+
* Relies on the txTimeout setting for user txs, and on the txCancellationTimeout for cancel txs.
|
|
250
|
+
* @remarks We check against the latestBlockTimestamp as opposed to the current time to avoid a race condition where
|
|
251
|
+
* the tx is mined in a block with the same timestamp as txTimeoutAt, but our execution node has not yet processed it,
|
|
252
|
+
* or the loop here has not yet checked the tx before that timeout.
|
|
253
|
+
*/
|
|
254
|
+
private isTxTimedOut(state: L1TxState, l1Timestamp: number) {
|
|
255
|
+
const account = this.getSenderAddress().toString();
|
|
256
|
+
const { nonce } = state;
|
|
257
|
+
|
|
258
|
+
const txConfig = merge(this.config, state.txConfigOverrides);
|
|
259
|
+
const { txTimeoutAt, txTimeoutMs, maxSpeedUpAttempts } = txConfig;
|
|
260
|
+
const isCancelTx = state.cancelTxHashes.length > 0;
|
|
261
|
+
|
|
262
|
+
this.logger.trace(`Tx timeout check for ${account} with nonce ${nonce}`, {
|
|
263
|
+
nonce,
|
|
264
|
+
account,
|
|
265
|
+
l1Timestamp,
|
|
266
|
+
lastAttemptSent: state.lastSentAtL1Ts.getTime(),
|
|
267
|
+
initialTxTime: state.sentAtL1Ts.getTime(),
|
|
268
|
+
now: this.dateProvider.now(),
|
|
269
|
+
txTimeoutAt,
|
|
270
|
+
txTimeoutMs,
|
|
271
|
+
interrupted: this.interrupted,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (this.interrupted) {
|
|
275
|
+
this.logger.warn(`Tx monitoring interrupted during nonce ${nonce} for ${account} check`, { nonce, account });
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isCancelTx) {
|
|
280
|
+
// Note that we check against the lastSentAt time for cancellations, since we time them out
|
|
281
|
+
// after the last attempt to submit them, not the initial time.
|
|
282
|
+
const attempts = state.cancelTxHashes.length;
|
|
283
|
+
return (
|
|
284
|
+
attempts > maxSpeedUpAttempts &&
|
|
285
|
+
state.lastSentAtL1Ts.getTime() + txConfig.txCancellationFinalTimeoutMs! <= l1Timestamp
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
(txTimeoutAt !== undefined && l1Timestamp >= txTimeoutAt.getTime()) ||
|
|
291
|
+
(txTimeoutMs !== undefined && txTimeoutMs > 0 && l1Timestamp - state.sentAtL1Ts.getTime() >= txTimeoutMs)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
227
295
|
/**
|
|
228
296
|
* Monitors a transaction until completion, handling speed-ups if needed
|
|
229
297
|
*/
|
|
230
298
|
protected async monitorTransaction(state: L1TxState): Promise<TransactionReceipt> {
|
|
231
|
-
const { request, nonce,
|
|
232
|
-
const
|
|
299
|
+
const { request, nonce, gasLimit, blobInputs, txConfigOverrides: gasConfigOverrides } = state;
|
|
300
|
+
const gasConfig = merge(this.config, gasConfigOverrides);
|
|
301
|
+
const { maxSpeedUpAttempts, stallTimeMs } = gasConfig;
|
|
302
|
+
const isCancelTx = state.cancelTxHashes.length > 0;
|
|
303
|
+
const txHashes = isCancelTx ? state.cancelTxHashes : state.txHashes;
|
|
233
304
|
const isBlobTx = !!blobInputs;
|
|
234
305
|
const account = this.getSenderAddress().toString();
|
|
235
306
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const initialTxTime = lastAttemptSent;
|
|
244
|
-
let txTimedOut = false;
|
|
245
|
-
let latestBlockTimestamp: bigint | undefined;
|
|
246
|
-
|
|
247
|
-
// We check against the latestBlockTimestamp as opposed to the current time to avoid a race condition where
|
|
248
|
-
// the tx is mined in a block with the same timestamp as txTimeoutAt, but our execution node has not yet processed it,
|
|
249
|
-
// or the loop here has not yet checked the tx before that timeout.
|
|
250
|
-
const isTimedOut = () =>
|
|
251
|
-
(gasConfig.txTimeoutAt &&
|
|
252
|
-
latestBlockTimestamp !== undefined &&
|
|
253
|
-
Number(latestBlockTimestamp) * 1000 >= gasConfig.txTimeoutAt.getTime()) ||
|
|
254
|
-
(gasConfig.txTimeoutMs !== undefined && this.dateProvider.now() - initialTxTime > gasConfig.txTimeoutMs) ||
|
|
255
|
-
this.interrupted ||
|
|
256
|
-
false;
|
|
257
|
-
|
|
258
|
-
while (!txTimedOut) {
|
|
307
|
+
const initialTxHash = txHashes[0];
|
|
308
|
+
let currentTxHash = initialTxHash;
|
|
309
|
+
let l1Timestamp: number;
|
|
310
|
+
|
|
311
|
+
while (true) {
|
|
312
|
+
l1Timestamp = await this.getL1Timestamp();
|
|
313
|
+
|
|
259
314
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
315
|
+
const timePassed = l1Timestamp - state.lastSentAtL1Ts.getTime();
|
|
316
|
+
const [currentNonce, pendingNonce] = await Promise.all([
|
|
317
|
+
this.client.getTransactionCount({ address: account, blockTag: 'latest' }),
|
|
318
|
+
this.client.getTransactionCount({ address: account, blockTag: 'pending' }),
|
|
319
|
+
]);
|
|
264
320
|
|
|
265
|
-
const currentNonce = await this.client.getTransactionCount({ address: account });
|
|
266
321
|
// If the current nonce on our account is greater than our transaction's nonce then a tx with the same nonce has been mined.
|
|
267
322
|
if (currentNonce > nonce) {
|
|
323
|
+
// We try getting the receipt twice, since sometimes anvil fails to return it if the tx has just been mined
|
|
268
324
|
const receipt =
|
|
269
|
-
(await this.tryGetTxReceipt(cancelTxHashes, nonce, true)) ??
|
|
270
|
-
(await this.tryGetTxReceipt(txHashes, nonce, false))
|
|
325
|
+
(await this.tryGetTxReceipt(state.cancelTxHashes, nonce, true)) ??
|
|
326
|
+
(await this.tryGetTxReceipt(state.txHashes, nonce, false)) ??
|
|
327
|
+
(await sleep(500)) ??
|
|
328
|
+
(await this.tryGetTxReceipt(state.cancelTxHashes, nonce, true)) ??
|
|
329
|
+
(await this.tryGetTxReceipt(state.txHashes, nonce, false));
|
|
271
330
|
|
|
272
331
|
if (receipt) {
|
|
273
332
|
this.updateState(state, TxUtilsState.MINED);
|
|
@@ -278,62 +337,39 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
278
337
|
// If we get here then we have checked all of our tx versions and not found anything.
|
|
279
338
|
// We should consider the nonce as MINED
|
|
280
339
|
this.updateState(state, TxUtilsState.MINED);
|
|
281
|
-
throw new
|
|
340
|
+
throw new UnknownMinedTxError(nonce, account);
|
|
282
341
|
}
|
|
283
342
|
|
|
284
|
-
this
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const tx = await retry<GetTransactionReturnType>(
|
|
296
|
-
() => this.client.getTransaction({ hash: currentTxHash }),
|
|
297
|
-
`Getting L1 transaction ${currentTxHash}`,
|
|
298
|
-
makeGetTransactionBackoff(),
|
|
299
|
-
this.logger,
|
|
300
|
-
true,
|
|
301
|
-
);
|
|
302
|
-
const timePassed = this.dateProvider.now() - lastAttemptSent;
|
|
303
|
-
|
|
304
|
-
if (tx && timePassed < gasConfig.stallTimeMs!) {
|
|
305
|
-
this.logger?.debug(`L1 transaction ${currentTxHash} pending. Time passed: ${timePassed}ms.`);
|
|
306
|
-
|
|
307
|
-
// Check timeout before continuing
|
|
308
|
-
txTimedOut = isTimedOut();
|
|
309
|
-
if (txTimedOut) {
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
343
|
+
// If this is a cancel tx and its nonce is no longer on the mempool, we consider it dropped and stop monitoring
|
|
344
|
+
// If it is a regular tx, we let the loop speed it up after the stall time
|
|
345
|
+
if (isCancelTx && pendingNonce <= nonce && timePassed >= gasConfig.txUnseenConsideredDroppedMs) {
|
|
346
|
+
this.logger.warn(
|
|
347
|
+
`Cancellation tx with nonce ${nonce} for account ${account} has been dropped from the visible mempool`,
|
|
348
|
+
{ nonce, account, pendingNonce, timePassed },
|
|
349
|
+
);
|
|
350
|
+
this.updateState(state, TxUtilsState.NOT_MINED);
|
|
351
|
+
this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
|
|
352
|
+
throw new DroppedTransactionError(nonce, account);
|
|
353
|
+
}
|
|
312
354
|
|
|
313
|
-
|
|
314
|
-
|
|
355
|
+
// Break if the tx has timed out (ie expired)
|
|
356
|
+
if (this.isTxTimedOut(state, l1Timestamp)) {
|
|
357
|
+
break;
|
|
315
358
|
}
|
|
316
359
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
isBlobTx,
|
|
322
|
-
attempts,
|
|
323
|
-
tx.maxFeePerGas && tx.maxPriorityFeePerGas
|
|
324
|
-
? {
|
|
325
|
-
maxFeePerGas: tx.maxFeePerGas,
|
|
326
|
-
maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
|
|
327
|
-
maxFeePerBlobGas: tx.maxFeePerBlobGas,
|
|
328
|
-
}
|
|
329
|
-
: undefined,
|
|
330
|
-
);
|
|
360
|
+
// Speed up the transaction if it appears to be stuck (exceeded stall time and still have retry attempts)
|
|
361
|
+
const attempts = txHashes.length;
|
|
362
|
+
if (timePassed >= stallTimeMs && attempts <= maxSpeedUpAttempts) {
|
|
363
|
+
const newGasPrice = await this.getGasPrice(gasConfig, isBlobTx, attempts, state.gasPrice);
|
|
331
364
|
state.gasPrice = newGasPrice;
|
|
332
365
|
|
|
333
|
-
this.logger
|
|
334
|
-
`
|
|
335
|
-
`
|
|
366
|
+
this.logger.debug(
|
|
367
|
+
`Tx ${currentTxHash} with nonce ${nonce} from ${account} appears stuck. ` +
|
|
368
|
+
`Attempting speed-up ${attempts}/${maxSpeedUpAttempts} ` +
|
|
369
|
+
`with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei.`,
|
|
336
370
|
{
|
|
371
|
+
account,
|
|
372
|
+
nonce,
|
|
337
373
|
maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
|
|
338
374
|
maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
|
|
339
375
|
...(newGasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(newGasPrice.maxFeePerBlobGas) }),
|
|
@@ -359,58 +395,109 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
359
395
|
this.updateState(state, TxUtilsState.SPEED_UP);
|
|
360
396
|
}
|
|
361
397
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
this.logger.verbose(
|
|
399
|
+
`Sent L1 speed-up tx ${newHash} replacing ${currentTxHash} for nonce ${nonce} from ${account}`,
|
|
400
|
+
{
|
|
401
|
+
nonce,
|
|
402
|
+
account,
|
|
403
|
+
gasLimit,
|
|
404
|
+
maxFeePerGas: formatGwei(newGasPrice.maxFeePerGas),
|
|
405
|
+
maxPriorityFeePerGas: formatGwei(newGasPrice.maxPriorityFeePerGas),
|
|
406
|
+
txConfig: state.txConfigOverrides,
|
|
407
|
+
...(newGasPrice.maxFeePerBlobGas && { maxFeePerBlobGas: formatGwei(newGasPrice.maxFeePerBlobGas) }),
|
|
408
|
+
},
|
|
409
|
+
);
|
|
370
410
|
|
|
371
411
|
currentTxHash = newHash;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
412
|
+
txHashes.push(currentTxHash);
|
|
413
|
+
state.lastSentAtL1Ts = new Date(l1Timestamp);
|
|
414
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
415
|
+
continue;
|
|
375
416
|
}
|
|
417
|
+
|
|
418
|
+
this.logger.debug(
|
|
419
|
+
`Tx ${currentTxHash} from ${account} with nonce ${nonce} still pending after ${timePassed}ms`,
|
|
420
|
+
{
|
|
421
|
+
account,
|
|
422
|
+
nonce,
|
|
423
|
+
pendingNonce,
|
|
424
|
+
attempts,
|
|
425
|
+
timePassed,
|
|
426
|
+
isBlobTx,
|
|
427
|
+
isCancelTx,
|
|
428
|
+
...pick(state.gasPrice, 'maxFeePerGas', 'maxPriorityFeePerGas', 'maxFeePerBlobGas'),
|
|
429
|
+
...pick(
|
|
430
|
+
gasConfig,
|
|
431
|
+
'txUnseenConsideredDroppedMs',
|
|
432
|
+
'txCancellationFinalTimeoutMs',
|
|
433
|
+
'maxSpeedUpAttempts',
|
|
434
|
+
'stallTimeMs',
|
|
435
|
+
'txTimeoutAt',
|
|
436
|
+
'txTimeoutMs',
|
|
437
|
+
),
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
376
441
|
await sleep(gasConfig.checkIntervalMs!);
|
|
377
442
|
} catch (err: any) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (viemError.message?.includes('reverted')) {
|
|
381
|
-
throw viemError;
|
|
443
|
+
if (err instanceof DroppedTransactionError || err instanceof UnknownMinedTxError) {
|
|
444
|
+
throw err;
|
|
382
445
|
}
|
|
446
|
+
|
|
447
|
+
const viemError = formatViemError(err);
|
|
448
|
+
this.logger.error(`Error while monitoring L1 tx ${currentTxHash}`, viemError, { nonce, account });
|
|
383
449
|
await sleep(gasConfig.checkIntervalMs!);
|
|
384
450
|
}
|
|
385
|
-
// Check if tx has timed out.
|
|
386
|
-
txTimedOut = isTimedOut();
|
|
387
451
|
}
|
|
388
452
|
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
453
|
+
// Oh no, the transaction has timed out!
|
|
454
|
+
if (isCancelTx || !gasConfig.cancelTxOnTimeout) {
|
|
455
|
+
// If this was already a cancellation tx, or we are configured to not cancel txs, we just mark it as NOT_MINED
|
|
456
|
+
// and reset the nonce manager, so the next tx that comes along can reuse the nonce if/when this tx gets dropped.
|
|
457
|
+
// This is the nastiest scenario for us, since the new tx could acquire the next nonce, but then this tx is dropped,
|
|
458
|
+
// and the new tx would never get mined. Eventually, the new tx would also drop.
|
|
392
459
|
this.updateState(state, TxUtilsState.NOT_MINED);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
460
|
+
this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
|
|
461
|
+
} else {
|
|
462
|
+
// Otherwise we fire the cancellation without awaiting to avoid blocking the caller,
|
|
463
|
+
// and monitor it in the background so we can speed it up as needed.
|
|
464
|
+
void this.attemptTxCancellation(state).catch(err => {
|
|
465
|
+
this.updateState(state, TxUtilsState.NOT_MINED);
|
|
466
|
+
this.logger.error(`Failed to send cancellation for timed out tx ${initialTxHash} with nonce ${nonce}`, err, {
|
|
467
|
+
account,
|
|
468
|
+
nonce,
|
|
469
|
+
initialTxHash,
|
|
399
470
|
});
|
|
400
471
|
});
|
|
401
472
|
}
|
|
402
473
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
474
|
+
const what = isCancelTx ? 'Cancellation L1' : 'L1';
|
|
475
|
+
this.logger.warn(`${what} transaction ${initialTxHash} with nonce ${nonce} from ${account} timed out`, {
|
|
476
|
+
initialTxHash,
|
|
477
|
+
currentTxHash,
|
|
478
|
+
nonce,
|
|
479
|
+
account,
|
|
480
|
+
txTimeoutAt: gasConfig.txTimeoutAt?.getTime(),
|
|
406
481
|
txTimeoutMs: gasConfig.txTimeoutMs,
|
|
407
|
-
txInitialTime:
|
|
482
|
+
txInitialTime: state.sentAtL1Ts.getTime(),
|
|
483
|
+
l1Timestamp,
|
|
408
484
|
now: this.dateProvider.now(),
|
|
409
|
-
attempts,
|
|
485
|
+
attempts: txHashes.length - 1,
|
|
410
486
|
isInterrupted: this.interrupted,
|
|
411
487
|
});
|
|
412
488
|
|
|
413
|
-
throw new TimeoutError(`L1 transaction ${
|
|
489
|
+
throw new TimeoutError(`L1 transaction ${initialTxHash} timed out`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Returns when all monitor loops have stopped. */
|
|
493
|
+
public async waitMonitoringStopped(timeoutSeconds = 10) {
|
|
494
|
+
const account = this.getSenderAddress().toString();
|
|
495
|
+
await retryUntil(
|
|
496
|
+
() => this.txs.every(tx => TerminalTxUtilsState.includes(tx.status)),
|
|
497
|
+
`monitoring stopped for ${account}`,
|
|
498
|
+
timeoutSeconds,
|
|
499
|
+
0.1,
|
|
500
|
+
).catch(() => this.logger.warn(`Timeout waiting for monitoring loops to stop for ${account}`));
|
|
414
501
|
}
|
|
415
502
|
|
|
416
503
|
/**
|
|
@@ -421,7 +508,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
421
508
|
*/
|
|
422
509
|
public async sendAndMonitorTransaction(
|
|
423
510
|
request: L1TxRequest,
|
|
424
|
-
gasConfig?:
|
|
511
|
+
gasConfig?: L1TxConfig,
|
|
425
512
|
blobInputs?: L1BlobInputs,
|
|
426
513
|
): Promise<{ receipt: TransactionReceipt; state: L1TxState }> {
|
|
427
514
|
const { state } = await this.sendTransaction(request, gasConfig, blobInputs);
|
|
@@ -437,7 +524,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
437
524
|
_gasConfig?: L1TxUtilsConfig & { fallbackGasEstimate?: bigint; ignoreBlockGasLimit?: boolean },
|
|
438
525
|
): Promise<{ gasUsed: bigint; result: `0x${string}` }> {
|
|
439
526
|
const blockOverrides = { ..._blockOverrides };
|
|
440
|
-
const gasConfig =
|
|
527
|
+
const gasConfig = merge(this.config, _gasConfig);
|
|
441
528
|
const gasPrice = await this.getGasPrice(gasConfig, false);
|
|
442
529
|
|
|
443
530
|
const call: any = {
|
|
@@ -459,11 +546,36 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
459
546
|
|
|
460
547
|
/**
|
|
461
548
|
* Attempts to cancel a transaction by sending a 0-value tx to self with same nonce but higher gas prices
|
|
549
|
+
* Only sends the cancellation if the original tx is still pending, not if it was dropped
|
|
462
550
|
* @returns The hash of the cancellation transaction
|
|
463
551
|
*/
|
|
464
|
-
protected async attemptTxCancellation(state: L1TxState
|
|
552
|
+
protected async attemptTxCancellation(state: L1TxState) {
|
|
465
553
|
const isBlobTx = state.blobInputs !== undefined;
|
|
466
554
|
const { nonce, gasPrice: previousGasPrice } = state;
|
|
555
|
+
const account = this.getSenderAddress().toString();
|
|
556
|
+
|
|
557
|
+
// Do not send cancellation if interrupted
|
|
558
|
+
if (this.interrupted) {
|
|
559
|
+
this.logger.warn(
|
|
560
|
+
`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as interrupted`,
|
|
561
|
+
{ nonce, account },
|
|
562
|
+
);
|
|
563
|
+
this.updateState(state, TxUtilsState.NOT_MINED);
|
|
564
|
+
this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check if the original tx is still pending
|
|
569
|
+
const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
570
|
+
if (currentNonce < nonce) {
|
|
571
|
+
this.logger.verbose(
|
|
572
|
+
`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`,
|
|
573
|
+
{ nonce, account, currentNonce },
|
|
574
|
+
);
|
|
575
|
+
this.updateState(state, TxUtilsState.NOT_MINED);
|
|
576
|
+
this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
467
579
|
|
|
468
580
|
// Get gas price with higher priority fee for cancellation
|
|
469
581
|
const cancelGasPrice = await this.getGasPrice(
|
|
@@ -473,13 +585,13 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
473
585
|
priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
|
|
474
586
|
},
|
|
475
587
|
isBlobTx,
|
|
476
|
-
|
|
588
|
+
state.txHashes.length,
|
|
477
589
|
previousGasPrice,
|
|
478
590
|
);
|
|
479
591
|
|
|
480
592
|
const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelGasPrice;
|
|
481
|
-
this.logger
|
|
482
|
-
`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction ${
|
|
593
|
+
this.logger.verbose(
|
|
594
|
+
`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction from account ${account} with nonce ${nonce} after time out`,
|
|
483
595
|
{
|
|
484
596
|
maxFeePerGas: formatGwei(maxFeePerGas),
|
|
485
597
|
maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
|
|
@@ -508,18 +620,30 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
508
620
|
state.gasPrice = cancelGasPrice;
|
|
509
621
|
state.gasLimit = 21_000n;
|
|
510
622
|
state.cancelTxHashes.push(cancelTxHash);
|
|
623
|
+
state.lastSentAtL1Ts = new Date(await this.getL1Timestamp());
|
|
511
624
|
|
|
512
625
|
this.updateState(state, TxUtilsState.CANCELLED);
|
|
513
626
|
|
|
514
|
-
this.logger
|
|
627
|
+
this.logger.warn(`Sent cancellation tx ${cancelTxHash} for timed out tx from ${account} with nonce ${nonce}`, {
|
|
515
628
|
nonce,
|
|
516
|
-
txData,
|
|
629
|
+
txData: baseTxData,
|
|
517
630
|
isBlobTx,
|
|
518
631
|
txHashes: state.txHashes,
|
|
519
632
|
});
|
|
520
633
|
|
|
521
|
-
|
|
522
|
-
|
|
634
|
+
// Do not await the cancel tx to be mined
|
|
635
|
+
void this.monitorTransaction(state).catch(err => {
|
|
636
|
+
this.logger.error(`Failed to mine cancellation tx ${cancelTxHash} for nonce ${nonce} account ${account}`, err, {
|
|
637
|
+
nonce,
|
|
638
|
+
account,
|
|
639
|
+
cancelTxHash,
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private async getL1Timestamp() {
|
|
645
|
+
const { timestamp } = await this.client.getBlock({ blockTag: 'latest', includeTransactions: false });
|
|
646
|
+
return Number(timestamp) * 1000;
|
|
523
647
|
}
|
|
524
648
|
|
|
525
649
|
/** Makes empty blob inputs for the cancellation tx. To be overridden in L1TxUtilsWithBlobs. */
|