@aztec/ethereum 2.1.0-rc.29 → 2.1.0-rc.30

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.
@@ -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 { createLogger } from '@aztec/foundation/log';
3
3
  import { makeBackoff, retry } from '@aztec/foundation/retry';
4
4
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
@@ -7,6 +7,7 @@ import { MethodNotFoundRpcError, MethodNotSupportedRpcError, decodeErrorResult,
7
7
  import { defaultL1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
8
8
  import { BLOCK_TIME_MS, LARGE_GAS_LIMIT, MIN_BLOB_REPLACEMENT_BUMP_PERCENTAGE, MIN_REPLACEMENT_BUMP_PERCENTAGE, WEI_CONST } from './constants.js';
9
9
  import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './utils.js';
10
+ const HISTORICAL_BLOCK_COUNT = 5;
10
11
  export class ReadOnlyL1TxUtils {
11
12
  client;
12
13
  logger;
@@ -35,21 +36,128 @@ export class ReadOnlyL1TxUtils {
35
36
  return this.client.getBlockNumber();
36
37
  }
37
38
  /**
39
+ * Analyzes pending transactions and recent fee history to determine a competitive priority fee.
40
+ * Falls back to network estimate if data is unavailable or fails.
41
+ * @param networkEstimateResult - Result from estimateMaxPriorityFeePerGas RPC call
42
+ * @param pendingBlockResult - Result from getBlock with pending tag RPC call
43
+ * @param feeHistoryResult - Result from getFeeHistory RPC call
44
+ * @returns A competitive priority fee based on pending txs and recent block history
45
+ */ getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult) {
46
+ const networkEstimate = networkEstimateResult.status === 'fulfilled' && typeof networkEstimateResult.value === 'bigint' ? networkEstimateResult.value : 0n;
47
+ let competitiveFee = networkEstimate;
48
+ if (pendingBlockResult.status === 'fulfilled' && pendingBlockResult.value !== null && pendingBlockResult.value.transactions && pendingBlockResult.value.transactions.length > 0) {
49
+ const pendingBlock = pendingBlockResult.value;
50
+ // Extract priority fees from pending transactions
51
+ const pendingFees = pendingBlock.transactions.map((tx)=>{
52
+ // Transaction can be just a hash string, so we need to check if it's an object
53
+ if (typeof tx === 'string') {
54
+ return 0n;
55
+ }
56
+ const fee = tx.maxPriorityFeePerGas || 0n;
57
+ // Debug: Log suspicious fees
58
+ if (fee > 100n * WEI_CONST) {
59
+ this.logger?.warn('Suspicious high priority fee in pending tx', {
60
+ txHash: tx.hash,
61
+ maxPriorityFeePerGas: formatGwei(fee),
62
+ maxFeePerGas: formatGwei(tx.maxFeePerGas || 0n),
63
+ maxFeePerBlobGas: tx.maxFeePerBlobGas ? formatGwei(tx.maxFeePerBlobGas) : 'N/A'
64
+ });
65
+ }
66
+ return fee;
67
+ }).filter((fee)=>fee > 0n);
68
+ if (pendingFees.length > 0) {
69
+ // Use 75th percentile of pending fees to be competitive
70
+ const sortedPendingFees = [
71
+ ...pendingFees
72
+ ].sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
73
+ const percentile75Index = Math.floor((sortedPendingFees.length - 1) * 0.75);
74
+ const pendingCompetitiveFee = sortedPendingFees[percentile75Index];
75
+ if (pendingCompetitiveFee > competitiveFee) {
76
+ competitiveFee = pendingCompetitiveFee;
77
+ }
78
+ this.logger?.debug('Analyzed pending transactions for competitive pricing', {
79
+ pendingTxCount: pendingFees.length,
80
+ pendingP75: formatGwei(pendingCompetitiveFee)
81
+ });
82
+ }
83
+ }
84
+ if (feeHistoryResult.status === 'fulfilled' && feeHistoryResult.value !== null && feeHistoryResult.value.reward && feeHistoryResult.value.reward.length > 0) {
85
+ const feeHistory = feeHistoryResult.value;
86
+ // Extract 75th percentile fees from each block
87
+ const percentile75Fees = feeHistory.reward.map((rewards)=>rewards[0] || 0n).filter((fee)=>fee > 0n);
88
+ if (percentile75Fees.length > 0) {
89
+ // Calculate median of the 75th percentile fees across blocks
90
+ const medianHistoricalFee = median(percentile75Fees) ?? 0n;
91
+ // Debug: Log suspicious fees from history
92
+ if (medianHistoricalFee > 100n * WEI_CONST) {
93
+ this.logger?.warn('Suspicious high fee in history', {
94
+ historicalMedian: formatGwei(medianHistoricalFee),
95
+ allP75Fees: percentile75Fees.map((f)=>formatGwei(f))
96
+ });
97
+ }
98
+ if (medianHistoricalFee > competitiveFee) {
99
+ competitiveFee = medianHistoricalFee;
100
+ }
101
+ this.logger?.debug('Analyzed fee history for competitive pricing', {
102
+ historicalMedian: formatGwei(medianHistoricalFee)
103
+ });
104
+ }
105
+ }
106
+ // Sanity check: cap competitive fee at 100x network estimate to avoid using unrealistic fees
107
+ // (e.g., Anvil returns inflated historical fees that don't reflect actual network conditions)
108
+ const maxReasonableFee = networkEstimate * 100n;
109
+ if (competitiveFee > maxReasonableFee) {
110
+ this.logger?.warn('Competitive fee exceeds sanity cap, using capped value', {
111
+ competitiveFee: formatGwei(competitiveFee),
112
+ networkEstimate: formatGwei(networkEstimate),
113
+ cappedTo: formatGwei(maxReasonableFee)
114
+ });
115
+ competitiveFee = maxReasonableFee;
116
+ }
117
+ // Log final decision
118
+ if (competitiveFee > networkEstimate) {
119
+ this.logger?.debug('Using competitive fee from market analysis', {
120
+ networkEstimate: formatGwei(networkEstimate),
121
+ competitive: formatGwei(competitiveFee)
122
+ });
123
+ }
124
+ return competitiveFee;
125
+ }
126
+ /**
38
127
  * Gets the current gas price with bounds checking
39
128
  */ async getGasPrice(gasConfigOverrides, isBlobTx = false, attempt = 0, previousGasPrice) {
40
129
  const gasConfig = merge(this.config, gasConfigOverrides);
41
- const block = await this.client.getBlock({
42
- blockTag: 'latest'
43
- });
44
- const baseFee = block.baseFeePerGas ?? 0n;
130
+ // Make all RPC calls in parallel upfront with retry logic
131
+ const latestBlockPromise = this.tryTwice(()=>this.client.getBlock({
132
+ blockTag: 'latest'
133
+ }), 'Getting latest block');
134
+ const networkEstimatePromise = gasConfig.fixedPriorityFeePerGas ? null : this.tryTwice(()=>this.client.estimateMaxPriorityFeePerGas(), 'Estimating max priority fee per gas');
135
+ const pendingBlockPromise = gasConfig.fixedPriorityFeePerGas ? null : this.tryTwice(()=>this.client.getBlock({
136
+ blockTag: 'pending',
137
+ includeTransactions: true
138
+ }), 'Getting pending block');
139
+ const feeHistoryPromise = gasConfig.fixedPriorityFeePerGas ? null : this.tryTwice(()=>this.client.getFeeHistory({
140
+ blockCount: HISTORICAL_BLOCK_COUNT,
141
+ rewardPercentiles: [
142
+ 75
143
+ ]
144
+ }), 'Getting fee history');
145
+ const blobBaseFeePromise = isBlobTx ? this.tryTwice(()=>this.client.getBlobBaseFee(), 'Getting blob base fee') : null;
146
+ const [latestBlockResult, networkEstimateResult, pendingBlockResult, feeHistoryResult, blobBaseFeeResult] = await Promise.allSettled([
147
+ latestBlockPromise,
148
+ networkEstimatePromise ?? Promise.resolve(0n),
149
+ pendingBlockPromise ?? Promise.resolve(null),
150
+ feeHistoryPromise ?? Promise.resolve(null),
151
+ blobBaseFeePromise ?? Promise.resolve(0n)
152
+ ]);
153
+ // Extract results
154
+ const baseFee = latestBlockResult.status === 'fulfilled' && typeof latestBlockResult.value === 'object' && latestBlockResult.value.baseFeePerGas ? latestBlockResult.value.baseFeePerGas : 0n;
45
155
  // Get blob base fee if available
46
156
  let blobBaseFee = 0n;
47
- if (isBlobTx) {
48
- try {
49
- blobBaseFee = await retry(()=>this.client.getBlobBaseFee(), 'Getting L1 blob base fee', makeBackoff(times(2, ()=>1)), this.logger, true);
50
- } catch {
51
- this.logger?.warn('Failed to get L1 blob base fee', attempt);
52
- }
157
+ if (isBlobTx && blobBaseFeeResult.status === 'fulfilled' && typeof blobBaseFeeResult.value === 'bigint') {
158
+ blobBaseFee = blobBaseFeeResult.value;
159
+ } else if (isBlobTx) {
160
+ this.logger?.warn('Failed to get L1 blob base fee', attempt);
53
161
  }
54
162
  let priorityFee;
55
163
  if (gasConfig.fixedPriorityFeePerGas) {
@@ -59,8 +167,8 @@ export class ReadOnlyL1TxUtils {
59
167
  // try to maintain precision up to 1000000 wei
60
168
  priorityFee = BigInt(gasConfig.fixedPriorityFeePerGas * 1_000_000) * (WEI_CONST / 1_000_000n);
61
169
  } else {
62
- // Get initial priority fee from the network
63
- priorityFee = await this.client.estimateMaxPriorityFeePerGas();
170
+ // Get competitive priority fee (includes network estimate + analysis)
171
+ priorityFee = this.getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult);
64
172
  }
65
173
  let maxFeePerGas = baseFee;
66
174
  let maxFeePerBlobGas = blobBaseFee;
@@ -81,26 +189,50 @@ export class ReadOnlyL1TxUtils {
81
189
  // multiply by 100 & divide by 100 to maintain some precision
82
190
  const minPriorityFee = previousGasPrice.maxPriorityFeePerGas * (100_00n + BigInt(bumpPercentage * 1_00)) / 100_00n;
83
191
  const minMaxFee = previousGasPrice.maxFeePerGas * (100_00n + BigInt(bumpPercentage * 1_00)) / 100_00n;
84
- // Add priority fee to maxFeePerGas
85
- maxFeePerGas += priorityFee;
86
- // Use maximum between current network values and minimum required values
87
- priorityFee = priorityFee > minPriorityFee ? priorityFee : minPriorityFee;
192
+ let competitivePriorityFee = priorityFee;
193
+ if (!gasConfig.fixedPriorityFeePerGas) {
194
+ // Apply bump percentage to competitive fee
195
+ competitivePriorityFee = priorityFee * (100_00n + BigInt(configBump * 1_00)) / 100_00n;
196
+ this.logger?.debug(`Speed-up attempt ${attempt}: using competitive fee strategy`, {
197
+ networkEstimate: formatGwei(priorityFee),
198
+ competitiveFee: formatGwei(competitivePriorityFee),
199
+ minRequired: formatGwei(minPriorityFee),
200
+ bumpPercentage: configBump
201
+ });
202
+ }
203
+ // Use maximum between competitive fee and minimum required bump
204
+ const finalPriorityFee = competitivePriorityFee > minPriorityFee ? competitivePriorityFee : minPriorityFee;
205
+ const feeSource = finalPriorityFee === competitivePriorityFee ? 'competitive' : 'minimum-bump';
206
+ priorityFee = finalPriorityFee;
207
+ // Add the final priority fee to maxFeePerGas
208
+ maxFeePerGas += finalPriorityFee;
88
209
  maxFeePerGas = maxFeePerGas > minMaxFee ? maxFeePerGas : minMaxFee;
210
+ if (!gasConfig.fixedPriorityFeePerGas) {
211
+ this.logger?.debug(`Speed-up fee decision: using ${feeSource} fee`, {
212
+ finalPriorityFee: formatGwei(finalPriorityFee)
213
+ });
214
+ }
89
215
  } else {
90
- // first attempt, just bump priority fee, unless it's a fixed config
216
+ // First attempt: apply configured bump percentage to competitive fee
91
217
  // multiply by 100 & divide by 100 to maintain some precision
92
218
  if (!gasConfig.fixedPriorityFeePerGas) {
93
219
  priorityFee = priorityFee * (100_00n + BigInt((gasConfig.priorityFeeBumpPercentage || 0) * 1_00)) / 100_00n;
220
+ this.logger?.debug('Initial transaction: using competitive fee from market analysis', {
221
+ networkEstimate: formatGwei(priorityFee)
222
+ });
94
223
  }
95
224
  maxFeePerGas += priorityFee;
96
225
  }
226
+ // maxGwei and maxBlobGwei are hard limits
227
+ const effectiveMaxGwei = gasConfig.maxGwei * WEI_CONST;
228
+ const effectiveMaxBlobGwei = gasConfig.maxBlobGwei * WEI_CONST;
97
229
  // Ensure we don't exceed maxGwei
98
- const maxGweiInWei = gasConfig.maxGwei * WEI_CONST;
99
- maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas;
230
+ if (effectiveMaxGwei > 0n) {
231
+ maxFeePerGas = maxFeePerGas > effectiveMaxGwei ? effectiveMaxGwei : maxFeePerGas;
232
+ }
100
233
  // Ensure we don't exceed maxBlobGwei
101
- if (maxFeePerBlobGas) {
102
- const maxBlobGweiInWei = gasConfig.maxBlobGwei * WEI_CONST;
103
- maxFeePerBlobGas = maxFeePerBlobGas > maxBlobGweiInWei ? maxBlobGweiInWei : maxFeePerBlobGas;
234
+ if (maxFeePerBlobGas && effectiveMaxBlobGwei > 0n) {
235
+ maxFeePerBlobGas = maxFeePerBlobGas > effectiveMaxBlobGwei ? effectiveMaxBlobGwei : maxFeePerBlobGas;
104
236
  }
105
237
  // Ensure priority fee doesn't exceed max fee
106
238
  const maxPriorityFeePerGas = priorityFee > maxFeePerGas ? maxFeePerGas : priorityFee;
@@ -291,4 +423,9 @@ export class ReadOnlyL1TxUtils {
291
423
  });
292
424
  return bumpedGasLimit;
293
425
  }
426
+ /**
427
+ * Helper function to retry RPC calls twice
428
+ */ tryTwice(fn, description) {
429
+ return retry(fn, description, makeBackoff(times(2, ()=>0)), this.logger, true);
430
+ }
294
431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/ethereum",
3
- "version": "2.1.0-rc.29",
3
+ "version": "2.1.0-rc.30",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -31,10 +31,10 @@
31
31
  "../package.common.json"
32
32
  ],
33
33
  "dependencies": {
34
- "@aztec/blob-lib": "2.1.0-rc.29",
35
- "@aztec/constants": "2.1.0-rc.29",
36
- "@aztec/foundation": "2.1.0-rc.29",
37
- "@aztec/l1-artifacts": "2.1.0-rc.29",
34
+ "@aztec/blob-lib": "2.1.0-rc.30",
35
+ "@aztec/constants": "2.1.0-rc.30",
36
+ "@aztec/foundation": "2.1.0-rc.30",
37
+ "@aztec/l1-artifacts": "2.1.0-rc.30",
38
38
  "@viem/anvil": "^0.0.10",
39
39
  "dotenv": "^16.0.3",
40
40
  "lodash.chunk": "^4.2.0",
@@ -1273,7 +1273,7 @@ export const addMultipleValidators = async (
1273
1273
  data: encodeFunctionData({
1274
1274
  abi: MultiAdderArtifact.contractAbi,
1275
1275
  functionName: 'addValidators',
1276
- args: [c, BigInt(chunkSize)],
1276
+ args: [c, BigInt(0)],
1277
1277
  }),
1278
1278
  },
1279
1279
  {
@@ -1292,15 +1292,18 @@ export const addMultipleValidators = async (
1292
1292
  while (true) {
1293
1293
  // If the queue is empty, we can break
1294
1294
  if ((await rollup.getEntryQueueLength()) == 0n) {
1295
+ logger.debug('Entry queue is empty, stopping flush attempts');
1295
1296
  break;
1296
1297
  }
1297
1298
 
1298
1299
  // If there are no available validator flushes, no need to even try
1299
1300
  if ((await rollup.getAvailableValidatorFlushes()) == 0n) {
1301
+ logger.debug('No available validator flushes, stopping flush attempts');
1300
1302
  break;
1301
1303
  }
1302
1304
 
1303
1305
  // Note that we are flushing at most `chunkSize` at each call
1306
+ logger.debug(`Flushing entry queue for ${chunkSize} validators`);
1304
1307
  await deployer.l1TxUtils.sendAndMonitorTransaction(
1305
1308
  {
1306
1309
  to: rollup.address,
@@ -69,14 +69,14 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
69
69
  ...numberConfigHelper(20),
70
70
  },
71
71
  maxGwei: {
72
- description: 'Maximum gas price in gwei',
72
+ description: 'Maximum gas price in gwei to be used for transactions.',
73
73
  env: 'L1_GAS_PRICE_MAX',
74
- ...bigintConfigHelper(500n),
74
+ ...bigintConfigHelper(2000n),
75
75
  },
76
76
  maxBlobGwei: {
77
77
  description: 'Maximum blob fee per gas in gwei',
78
78
  env: 'L1_BLOB_FEE_PER_GAS_MAX',
79
- ...bigintConfigHelper(1_500n),
79
+ ...bigintConfigHelper(3000n),
80
80
  },
81
81
  priorityFeeBumpPercentage: {
82
82
  description: 'How much to increase priority fee by each attempt (percentage)',
@@ -106,7 +106,7 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
106
106
  stallTimeMs: {
107
107
  description: 'How long before considering tx stalled',
108
108
  env: 'L1_TX_MONITOR_STALL_TIME_MS',
109
- ...numberConfigHelper(24_000), // 24s, 2 ethereum slots
109
+ ...numberConfigHelper(12_000), // 12s, 1 ethereum slot
110
110
  },
111
111
  txTimeoutMs: {
112
112
  description: 'How long to wait for a tx to be mined before giving up. Set to 0 to disable.',
@@ -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
- const block = await this.client.getBlock({ blockTag: 'latest' });
77
- const baseFee = block.baseFeePerGas ?? 0n;
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
- try {
83
- blobBaseFee = await retry<bigint>(
84
- () => this.client.getBlobBaseFee(),
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 initial priority fee from the network
104
- priorityFee = await this.client.estimateMaxPriorityFeePerGas();
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
- // Add priority fee to maxFeePerGas
133
- maxFeePerGas += priorityFee;
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
- // Use maximum between current network values and minimum required values
136
- priorityFee = priorityFee > minPriorityFee ? priorityFee : minPriorityFee;
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
- // first attempt, just bump priority fee, unless it's a fixed config
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
- const maxGweiInWei = gasConfig.maxGwei! * WEI_CONST;
149
- maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas;
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
- const maxBlobGweiInWei = gasConfig.maxBlobGwei! * WEI_CONST;
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
  }