@aztec/ethereum 3.0.0-nightly.20251031 → 3.0.0-nightly.20251101
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.d.ts.map +1 -1
- package/dest/config.js +37 -5
- package/dest/l1_artifacts.d.ts +4248 -0
- package/dest/l1_artifacts.d.ts.map +1 -1
- package/dest/l1_tx_utils/config.js +4 -4
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +13 -0
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +160 -23
- package/package.json +5 -5
- package/src/config.ts +41 -5
- package/src/l1_tx_utils/config.ts +4 -4
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +214 -27
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getKeys, merge, pick, times } from '@aztec/foundation/collection';
|
|
1
|
+
import { getKeys, median, merge, pick, times } from '@aztec/foundation/collection';
|
|
2
2
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
3
3
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
4
4
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
import type { GasPrice, L1BlobInputs, L1TxRequest, TransactionStats } from './types.js';
|
|
34
34
|
import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './utils.js';
|
|
35
35
|
|
|
36
|
+
const HISTORICAL_BLOCK_COUNT = 5;
|
|
37
|
+
|
|
36
38
|
export class ReadOnlyL1TxUtils {
|
|
37
39
|
public config: Required<L1TxUtilsConfig>;
|
|
38
40
|
protected interrupted = false;
|
|
@@ -63,6 +65,124 @@ export class ReadOnlyL1TxUtils {
|
|
|
63
65
|
return this.client.getBlockNumber();
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Analyzes pending transactions and recent fee history to determine a competitive priority fee.
|
|
70
|
+
* Falls back to network estimate if data is unavailable or fails.
|
|
71
|
+
* @param networkEstimateResult - Result from estimateMaxPriorityFeePerGas RPC call
|
|
72
|
+
* @param pendingBlockResult - Result from getBlock with pending tag RPC call
|
|
73
|
+
* @param feeHistoryResult - Result from getFeeHistory RPC call
|
|
74
|
+
* @returns A competitive priority fee based on pending txs and recent block history
|
|
75
|
+
*/
|
|
76
|
+
protected getCompetitivePriorityFee(
|
|
77
|
+
networkEstimateResult: PromiseSettledResult<bigint | null>,
|
|
78
|
+
pendingBlockResult: PromiseSettledResult<Awaited<ReturnType<ViemClient['getBlock']>> | null>,
|
|
79
|
+
feeHistoryResult: PromiseSettledResult<Awaited<ReturnType<ViemClient['getFeeHistory']>> | null>,
|
|
80
|
+
): bigint {
|
|
81
|
+
const networkEstimate =
|
|
82
|
+
networkEstimateResult.status === 'fulfilled' && typeof networkEstimateResult.value === 'bigint'
|
|
83
|
+
? networkEstimateResult.value
|
|
84
|
+
: 0n;
|
|
85
|
+
let competitiveFee = networkEstimate;
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
pendingBlockResult.status === 'fulfilled' &&
|
|
89
|
+
pendingBlockResult.value !== null &&
|
|
90
|
+
pendingBlockResult.value.transactions &&
|
|
91
|
+
pendingBlockResult.value.transactions.length > 0
|
|
92
|
+
) {
|
|
93
|
+
const pendingBlock = pendingBlockResult.value;
|
|
94
|
+
// Extract priority fees from pending transactions
|
|
95
|
+
const pendingFees = pendingBlock.transactions
|
|
96
|
+
.map(tx => {
|
|
97
|
+
// Transaction can be just a hash string, so we need to check if it's an object
|
|
98
|
+
if (typeof tx === 'string') {
|
|
99
|
+
return 0n;
|
|
100
|
+
}
|
|
101
|
+
const fee = tx.maxPriorityFeePerGas || 0n;
|
|
102
|
+
// Debug: Log suspicious fees
|
|
103
|
+
if (fee > 100n * WEI_CONST) {
|
|
104
|
+
this.logger?.warn('Suspicious high priority fee in pending tx', {
|
|
105
|
+
txHash: tx.hash,
|
|
106
|
+
maxPriorityFeePerGas: formatGwei(fee),
|
|
107
|
+
maxFeePerGas: formatGwei(tx.maxFeePerGas || 0n),
|
|
108
|
+
maxFeePerBlobGas: tx.maxFeePerBlobGas ? formatGwei(tx.maxFeePerBlobGas) : 'N/A',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return fee;
|
|
112
|
+
})
|
|
113
|
+
.filter((fee: bigint) => fee > 0n);
|
|
114
|
+
|
|
115
|
+
if (pendingFees.length > 0) {
|
|
116
|
+
// Use 75th percentile of pending fees to be competitive
|
|
117
|
+
const sortedPendingFees = [...pendingFees].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
118
|
+
const percentile75Index = Math.floor((sortedPendingFees.length - 1) * 0.75);
|
|
119
|
+
const pendingCompetitiveFee = sortedPendingFees[percentile75Index];
|
|
120
|
+
|
|
121
|
+
if (pendingCompetitiveFee > competitiveFee) {
|
|
122
|
+
competitiveFee = pendingCompetitiveFee;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.logger?.debug('Analyzed pending transactions for competitive pricing', {
|
|
126
|
+
pendingTxCount: pendingFees.length,
|
|
127
|
+
pendingP75: formatGwei(pendingCompetitiveFee),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
feeHistoryResult.status === 'fulfilled' &&
|
|
133
|
+
feeHistoryResult.value !== null &&
|
|
134
|
+
feeHistoryResult.value.reward &&
|
|
135
|
+
feeHistoryResult.value.reward.length > 0
|
|
136
|
+
) {
|
|
137
|
+
const feeHistory = feeHistoryResult.value;
|
|
138
|
+
// Extract 75th percentile fees from each block
|
|
139
|
+
const percentile75Fees = feeHistory.reward!.map(rewards => rewards[0] || 0n).filter(fee => fee > 0n);
|
|
140
|
+
|
|
141
|
+
if (percentile75Fees.length > 0) {
|
|
142
|
+
// Calculate median of the 75th percentile fees across blocks
|
|
143
|
+
const medianHistoricalFee = median(percentile75Fees) ?? 0n;
|
|
144
|
+
|
|
145
|
+
// Debug: Log suspicious fees from history
|
|
146
|
+
if (medianHistoricalFee > 100n * WEI_CONST) {
|
|
147
|
+
this.logger?.warn('Suspicious high fee in history', {
|
|
148
|
+
historicalMedian: formatGwei(medianHistoricalFee),
|
|
149
|
+
allP75Fees: percentile75Fees.map(f => formatGwei(f)),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (medianHistoricalFee > competitiveFee) {
|
|
154
|
+
competitiveFee = medianHistoricalFee;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.logger?.debug('Analyzed fee history for competitive pricing', {
|
|
158
|
+
historicalMedian: formatGwei(medianHistoricalFee),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Sanity check: cap competitive fee at 100x network estimate to avoid using unrealistic fees
|
|
164
|
+
// (e.g., Anvil returns inflated historical fees that don't reflect actual network conditions)
|
|
165
|
+
const maxReasonableFee = networkEstimate * 100n;
|
|
166
|
+
if (competitiveFee > maxReasonableFee) {
|
|
167
|
+
this.logger?.warn('Competitive fee exceeds sanity cap, using capped value', {
|
|
168
|
+
competitiveFee: formatGwei(competitiveFee),
|
|
169
|
+
networkEstimate: formatGwei(networkEstimate),
|
|
170
|
+
cappedTo: formatGwei(maxReasonableFee),
|
|
171
|
+
});
|
|
172
|
+
competitiveFee = maxReasonableFee;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Log final decision
|
|
176
|
+
if (competitiveFee > networkEstimate) {
|
|
177
|
+
this.logger?.debug('Using competitive fee from market analysis', {
|
|
178
|
+
networkEstimate: formatGwei(networkEstimate),
|
|
179
|
+
competitive: formatGwei(competitiveFee),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return competitiveFee;
|
|
184
|
+
}
|
|
185
|
+
|
|
66
186
|
/**
|
|
67
187
|
* Gets the current gas price with bounds checking
|
|
68
188
|
*/
|
|
@@ -73,23 +193,54 @@ export class ReadOnlyL1TxUtils {
|
|
|
73
193
|
previousGasPrice?: typeof attempt extends 0 ? never : GasPrice,
|
|
74
194
|
): Promise<GasPrice> {
|
|
75
195
|
const gasConfig = merge(this.config, gasConfigOverrides);
|
|
76
|
-
|
|
77
|
-
|
|
196
|
+
|
|
197
|
+
// Make all RPC calls in parallel upfront with retry logic
|
|
198
|
+
const latestBlockPromise = this.tryTwice(
|
|
199
|
+
() => this.client.getBlock({ blockTag: 'latest' }),
|
|
200
|
+
'Getting latest block',
|
|
201
|
+
);
|
|
202
|
+
const networkEstimatePromise = gasConfig.fixedPriorityFeePerGas
|
|
203
|
+
? null
|
|
204
|
+
: this.tryTwice(() => this.client.estimateMaxPriorityFeePerGas(), 'Estimating max priority fee per gas');
|
|
205
|
+
const pendingBlockPromise = gasConfig.fixedPriorityFeePerGas
|
|
206
|
+
? null
|
|
207
|
+
: this.tryTwice(
|
|
208
|
+
() => this.client.getBlock({ blockTag: 'pending', includeTransactions: true }),
|
|
209
|
+
'Getting pending block',
|
|
210
|
+
);
|
|
211
|
+
const feeHistoryPromise = gasConfig.fixedPriorityFeePerGas
|
|
212
|
+
? null
|
|
213
|
+
: this.tryTwice(
|
|
214
|
+
() => this.client.getFeeHistory({ blockCount: HISTORICAL_BLOCK_COUNT, rewardPercentiles: [75] }),
|
|
215
|
+
'Getting fee history',
|
|
216
|
+
);
|
|
217
|
+
const blobBaseFeePromise = isBlobTx
|
|
218
|
+
? this.tryTwice(() => this.client.getBlobBaseFee(), 'Getting blob base fee')
|
|
219
|
+
: null;
|
|
220
|
+
|
|
221
|
+
const [latestBlockResult, networkEstimateResult, pendingBlockResult, feeHistoryResult, blobBaseFeeResult] =
|
|
222
|
+
await Promise.allSettled([
|
|
223
|
+
latestBlockPromise,
|
|
224
|
+
networkEstimatePromise ?? Promise.resolve(0n),
|
|
225
|
+
pendingBlockPromise ?? Promise.resolve(null),
|
|
226
|
+
feeHistoryPromise ?? Promise.resolve(null),
|
|
227
|
+
blobBaseFeePromise ?? Promise.resolve(0n),
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
// Extract results
|
|
231
|
+
const baseFee =
|
|
232
|
+
latestBlockResult.status === 'fulfilled' &&
|
|
233
|
+
typeof latestBlockResult.value === 'object' &&
|
|
234
|
+
latestBlockResult.value.baseFeePerGas
|
|
235
|
+
? latestBlockResult.value.baseFeePerGas
|
|
236
|
+
: 0n;
|
|
78
237
|
|
|
79
238
|
// Get blob base fee if available
|
|
80
239
|
let blobBaseFee = 0n;
|
|
81
|
-
if (isBlobTx) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'Getting L1 blob base fee',
|
|
86
|
-
makeBackoff(times(2, () => 1)),
|
|
87
|
-
this.logger,
|
|
88
|
-
true,
|
|
89
|
-
);
|
|
90
|
-
} catch {
|
|
91
|
-
this.logger?.warn('Failed to get L1 blob base fee', attempt);
|
|
92
|
-
}
|
|
240
|
+
if (isBlobTx && blobBaseFeeResult.status === 'fulfilled' && typeof blobBaseFeeResult.value === 'bigint') {
|
|
241
|
+
blobBaseFee = blobBaseFeeResult.value;
|
|
242
|
+
} else if (isBlobTx) {
|
|
243
|
+
this.logger?.warn('Failed to get L1 blob base fee', attempt);
|
|
93
244
|
}
|
|
94
245
|
|
|
95
246
|
let priorityFee: bigint;
|
|
@@ -100,8 +251,8 @@ export class ReadOnlyL1TxUtils {
|
|
|
100
251
|
// try to maintain precision up to 1000000 wei
|
|
101
252
|
priorityFee = BigInt(gasConfig.fixedPriorityFeePerGas * 1_000_000) * (WEI_CONST / 1_000_000n);
|
|
102
253
|
} else {
|
|
103
|
-
// Get
|
|
104
|
-
priorityFee =
|
|
254
|
+
// Get competitive priority fee (includes network estimate + analysis)
|
|
255
|
+
priorityFee = this.getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult);
|
|
105
256
|
}
|
|
106
257
|
let maxFeePerGas = baseFee;
|
|
107
258
|
|
|
@@ -115,6 +266,7 @@ export class ReadOnlyL1TxUtils {
|
|
|
115
266
|
// same for blob gas fee
|
|
116
267
|
maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n;
|
|
117
268
|
}
|
|
269
|
+
|
|
118
270
|
if (attempt > 0) {
|
|
119
271
|
const configBump =
|
|
120
272
|
gasConfig.priorityFeeRetryBumpPercentage ?? defaultL1TxUtilsConfig.priorityFeeRetryBumpPercentage!;
|
|
@@ -129,29 +281,57 @@ export class ReadOnlyL1TxUtils {
|
|
|
129
281
|
(previousGasPrice!.maxPriorityFeePerGas * (100_00n + BigInt(bumpPercentage * 1_00))) / 100_00n;
|
|
130
282
|
const minMaxFee = (previousGasPrice!.maxFeePerGas * (100_00n + BigInt(bumpPercentage * 1_00))) / 100_00n;
|
|
131
283
|
|
|
132
|
-
|
|
133
|
-
|
|
284
|
+
let competitivePriorityFee = priorityFee;
|
|
285
|
+
if (!gasConfig.fixedPriorityFeePerGas) {
|
|
286
|
+
// Apply bump percentage to competitive fee
|
|
287
|
+
competitivePriorityFee = (priorityFee * (100_00n + BigInt(configBump * 1_00))) / 100_00n;
|
|
288
|
+
|
|
289
|
+
this.logger?.debug(`Speed-up attempt ${attempt}: using competitive fee strategy`, {
|
|
290
|
+
networkEstimate: formatGwei(priorityFee),
|
|
291
|
+
competitiveFee: formatGwei(competitivePriorityFee),
|
|
292
|
+
minRequired: formatGwei(minPriorityFee),
|
|
293
|
+
bumpPercentage: configBump,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Use maximum between competitive fee and minimum required bump
|
|
298
|
+
const finalPriorityFee = competitivePriorityFee > minPriorityFee ? competitivePriorityFee : minPriorityFee;
|
|
299
|
+
const feeSource = finalPriorityFee === competitivePriorityFee ? 'competitive' : 'minimum-bump';
|
|
134
300
|
|
|
135
|
-
|
|
136
|
-
|
|
301
|
+
priorityFee = finalPriorityFee;
|
|
302
|
+
// Add the final priority fee to maxFeePerGas
|
|
303
|
+
maxFeePerGas += finalPriorityFee;
|
|
137
304
|
maxFeePerGas = maxFeePerGas > minMaxFee ? maxFeePerGas : minMaxFee;
|
|
305
|
+
|
|
306
|
+
if (!gasConfig.fixedPriorityFeePerGas) {
|
|
307
|
+
this.logger?.debug(`Speed-up fee decision: using ${feeSource} fee`, {
|
|
308
|
+
finalPriorityFee: formatGwei(finalPriorityFee),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
138
311
|
} else {
|
|
139
|
-
//
|
|
312
|
+
// First attempt: apply configured bump percentage to competitive fee
|
|
140
313
|
// multiply by 100 & divide by 100 to maintain some precision
|
|
141
314
|
if (!gasConfig.fixedPriorityFeePerGas) {
|
|
142
315
|
priorityFee = (priorityFee * (100_00n + BigInt((gasConfig.priorityFeeBumpPercentage || 0) * 1_00))) / 100_00n;
|
|
316
|
+
this.logger?.debug('Initial transaction: using competitive fee from market analysis', {
|
|
317
|
+
networkEstimate: formatGwei(priorityFee),
|
|
318
|
+
});
|
|
143
319
|
}
|
|
144
320
|
maxFeePerGas += priorityFee;
|
|
145
321
|
}
|
|
146
322
|
|
|
323
|
+
// maxGwei and maxBlobGwei are hard limits
|
|
324
|
+
const effectiveMaxGwei = gasConfig.maxGwei! * WEI_CONST;
|
|
325
|
+
const effectiveMaxBlobGwei = gasConfig.maxBlobGwei! * WEI_CONST;
|
|
326
|
+
|
|
147
327
|
// Ensure we don't exceed maxGwei
|
|
148
|
-
|
|
149
|
-
|
|
328
|
+
if (effectiveMaxGwei > 0n) {
|
|
329
|
+
maxFeePerGas = maxFeePerGas > effectiveMaxGwei ? effectiveMaxGwei : maxFeePerGas;
|
|
330
|
+
}
|
|
150
331
|
|
|
151
332
|
// Ensure we don't exceed maxBlobGwei
|
|
152
|
-
if (maxFeePerBlobGas) {
|
|
153
|
-
|
|
154
|
-
maxFeePerBlobGas = maxFeePerBlobGas > maxBlobGweiInWei ? maxBlobGweiInWei : maxFeePerBlobGas;
|
|
333
|
+
if (maxFeePerBlobGas && effectiveMaxBlobGwei > 0n) {
|
|
334
|
+
maxFeePerBlobGas = maxFeePerBlobGas > effectiveMaxBlobGwei ? effectiveMaxBlobGwei : maxFeePerBlobGas;
|
|
155
335
|
}
|
|
156
336
|
|
|
157
337
|
// Ensure priority fee doesn't exceed max fee
|
|
@@ -369,4 +549,11 @@ export class ReadOnlyL1TxUtils {
|
|
|
369
549
|
});
|
|
370
550
|
return bumpedGasLimit;
|
|
371
551
|
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Helper function to retry RPC calls twice
|
|
555
|
+
*/
|
|
556
|
+
private tryTwice<T>(fn: () => Promise<T>, description: string): Promise<T> {
|
|
557
|
+
return retry<T>(fn, description, makeBackoff(times(2, () => 0)), this.logger, true);
|
|
558
|
+
}
|
|
372
559
|
}
|