@aztec/ethereum 3.0.0-nightly.20251216 → 3.0.0-nightly.20251218

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.
Files changed (45) hide show
  1. package/dest/deploy_aztec_l1_contracts.d.ts +1 -1
  2. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  3. package/dest/deploy_aztec_l1_contracts.js +10 -8
  4. package/dest/l1_contract_addresses.d.ts +1 -1
  5. package/dest/l1_contract_addresses.d.ts.map +1 -1
  6. package/dest/l1_contract_addresses.js +3 -3
  7. package/dest/l1_tx_utils/constants.d.ts +7 -1
  8. package/dest/l1_tx_utils/constants.d.ts.map +1 -1
  9. package/dest/l1_tx_utils/constants.js +25 -0
  10. package/dest/l1_tx_utils/fee-strategies/index.d.ts +9 -0
  11. package/dest/l1_tx_utils/fee-strategies/index.d.ts.map +1 -0
  12. package/dest/l1_tx_utils/fee-strategies/index.js +11 -0
  13. package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts +18 -0
  14. package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts.map +1 -0
  15. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +111 -0
  16. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.d.ts +32 -0
  17. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.d.ts.map +1 -0
  18. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +173 -0
  19. package/dest/l1_tx_utils/fee-strategies/types.d.ts +64 -0
  20. package/dest/l1_tx_utils/fee-strategies/types.d.ts.map +1 -0
  21. package/dest/l1_tx_utils/fee-strategies/types.js +24 -0
  22. package/dest/l1_tx_utils/index.d.ts +3 -1
  23. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  24. package/dest/l1_tx_utils/index.js +2 -0
  25. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +232 -0
  26. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -0
  27. package/dest/l1_tx_utils/l1_fee_analyzer.js +506 -0
  28. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -10
  29. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  30. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +43 -121
  31. package/dest/utils.d.ts +14 -2
  32. package/dest/utils.d.ts.map +1 -1
  33. package/dest/utils.js +18 -0
  34. package/package.json +6 -5
  35. package/src/deploy_aztec_l1_contracts.ts +14 -7
  36. package/src/l1_contract_addresses.ts +22 -20
  37. package/src/l1_tx_utils/constants.ts +11 -0
  38. package/src/l1_tx_utils/fee-strategies/index.ts +22 -0
  39. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +159 -0
  40. package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +241 -0
  41. package/src/l1_tx_utils/fee-strategies/types.ts +88 -0
  42. package/src/l1_tx_utils/index.ts +2 -0
  43. package/src/l1_tx_utils/l1_fee_analyzer.ts +803 -0
  44. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +47 -158
  45. package/src/utils.ts +29 -0
@@ -1,4 +1,4 @@
1
- import { getKeys, median, merge, pick, times } from '@aztec/foundation/collection';
1
+ import { getKeys, 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';
@@ -6,8 +6,10 @@ import pickBy from 'lodash.pickby';
6
6
  import { MethodNotFoundRpcError, MethodNotSupportedRpcError, decodeErrorResult, formatGwei, getContractError, hexToBytes } from 'viem';
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
+ import { P75AllTxsPriorityFeeStrategy } from './fee-strategies/index.js';
9
10
  import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './utils.js';
10
- const HISTORICAL_BLOCK_COUNT = 5;
11
+ // Change this to the current strategy we want to use
12
+ const CurrentStrategy = P75AllTxsPriorityFeeStrategy;
11
13
  export class ReadOnlyL1TxUtils {
12
14
  client;
13
15
  logger;
@@ -36,119 +38,32 @@ export class ReadOnlyL1TxUtils {
36
38
  return this.client.getBlockNumber();
37
39
  }
38
40
  /**
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
- /**
127
41
  * Gets the current gas price with bounds checking
128
42
  */ async getGasPrice(gasConfigOverrides, isBlobTx = false, attempt = 0, previousGasPrice) {
129
43
  const gasConfig = merge(this.config, gasConfigOverrides);
130
44
  // Make all RPC calls in parallel upfront with retry logic
45
+ // First 2 calls are necessary to complete
131
46
  const latestBlockPromise = this.tryTwice(()=>this.client.getBlock({
132
47
  blockTag: 'latest'
133
48
  }), 'Getting latest block');
134
- const networkEstimatePromise = this.tryTwice(()=>this.client.estimateMaxPriorityFeePerGas(), 'Estimating max priority fee per gas');
135
- const pendingBlockPromise = this.tryTwice(()=>this.client.getBlock({
136
- blockTag: 'pending',
137
- includeTransactions: true
138
- }), 'Getting pending block');
139
- const feeHistoryPromise = 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([
49
+ let blobBaseFeePromise = null;
50
+ if (isBlobTx) {
51
+ blobBaseFeePromise = this.tryTwice(()=>this.client.getBlobBaseFee(), 'Getting blob base fee');
52
+ }
53
+ // Get strategy promises for priority fee calculation
54
+ const strategyPromises = CurrentStrategy.getRequiredPromises(this.client, {
55
+ isBlobTx
56
+ });
57
+ const strategyPromiseKeys = [];
58
+ const strategyPromisesArr = [];
59
+ for (const [key, promise] of Object.entries(strategyPromises)){
60
+ strategyPromiseKeys.push(key);
61
+ strategyPromisesArr.push(this.tryTwice(()=>promise, `Getting strategy data for ${key}`));
62
+ }
63
+ const [latestBlockResult, blobBaseFeeResult, ...strategyResults] = await Promise.allSettled([
147
64
  latestBlockPromise,
148
- networkEstimatePromise,
149
- pendingBlockPromise,
150
- feeHistoryPromise,
151
- blobBaseFeePromise ?? Promise.resolve(0n)
65
+ blobBaseFeePromise ?? Promise.resolve(0n),
66
+ ...strategyPromisesArr
152
67
  ]);
153
68
  // Extract results
154
69
  const baseFee = latestBlockResult.status === 'fulfilled' && typeof latestBlockResult.value === 'object' && latestBlockResult.value.baseFeePerGas ? latestBlockResult.value.baseFeePerGas : 0n;
@@ -159,22 +74,29 @@ export class ReadOnlyL1TxUtils {
159
74
  } else if (isBlobTx) {
160
75
  this.logger?.warn('Failed to get L1 blob base fee', attempt);
161
76
  }
162
- // Get competitive priority fee
163
- let priorityFee = this.getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult);
164
- // Apply minimum priority fee as a floor if configured
77
+ let priorityFee;
78
+ // Get competitive priority fee using strategy
79
+ // Reconstruct the results object with the same keys as the promises
80
+ const resultsObject = {};
81
+ strategyPromiseKeys.forEach((key, index)=>{
82
+ resultsObject[key] = strategyResults[index];
83
+ });
84
+ const result = CurrentStrategy.calculate(resultsObject, {
85
+ gasConfig,
86
+ isBlobTx,
87
+ logger: this.logger
88
+ });
89
+ priorityFee = result.priorityFee;
90
+ // Apply minimum priority fee floor if configured
165
91
  if (gasConfig.minimumPriorityFeePerGas) {
166
- const minimumFee = BigInt(Math.trunc(gasConfig.minimumPriorityFeePerGas * Number(WEI_CONST)));
167
- if (minimumFee > priorityFee) {
168
- this.logger?.debug('Using minimum priority fee as floor', {
169
- minimumPriorityFeePerGas: formatGwei(minimumFee),
170
- competitiveFee: formatGwei(priorityFee)
171
- });
172
- priorityFee = minimumFee;
173
- } else {
174
- this.logger?.debug('Competitive fee exceeds minimum, using competitive fee', {
175
- minimumPriorityFeePerGas: formatGwei(minimumFee),
176
- competitiveFee: formatGwei(priorityFee)
92
+ const minimumPriorityFee = BigInt(Math.trunc(gasConfig.minimumPriorityFeePerGas * Number(WEI_CONST)));
93
+ if (priorityFee < minimumPriorityFee) {
94
+ this.logger?.debug('Applying minimum priority fee floor', {
95
+ calculatedPriorityFee: formatGwei(priorityFee),
96
+ minimumPriorityFeePerGas: gasConfig.minimumPriorityFeePerGas,
97
+ appliedFee: formatGwei(minimumPriorityFee)
177
98
  });
99
+ priorityFee = minimumPriorityFee;
178
100
  }
179
101
  }
180
102
  let maxFeePerGas = baseFee;
package/dest/utils.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Fr } from '@aztec/foundation/curves/bn254';
2
2
  import type { Logger } from '@aztec/foundation/log';
3
- import { type Abi, type ContractEventName, type DecodeEventLogReturnType, type Hex, type Log } from 'viem';
3
+ import { type Abi, type ContractEventName, type DecodeEventLogReturnType, type FormattedTransaction, type Hex, type Log } from 'viem';
4
4
  export interface L2Claim {
5
5
  claimSecret: Fr;
6
6
  claimAmount: Fr;
@@ -22,4 +22,16 @@ export declare function prettyLogViemErrorMsg(err: any): any;
22
22
  */
23
23
  export declare function formatViemError(error: any, abi?: Abi): FormattedViemError;
24
24
  export declare function tryGetCustomErrorName(err: any): string | undefined;
25
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxFQUFFLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUN6RCxPQUFPLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUdwRCxPQUFPLEVBQ0wsS0FBSyxHQUFHLEVBRVIsS0FBSyxpQkFBaUIsRUFFdEIsS0FBSyx3QkFBd0IsRUFDN0IsS0FBSyxHQUFHLEVBQ1IsS0FBSyxHQUFHLEVBR1QsTUFBTSxNQUFNLENBQUM7QUFFZCxNQUFNLFdBQVcsT0FBTztJQUN0QixXQUFXLEVBQUUsRUFBRSxDQUFDO0lBQ2hCLFdBQVcsRUFBRSxFQUFFLENBQUM7SUFDaEIsV0FBVyxFQUFFLEdBQUcsQ0FBQztJQUNqQixnQkFBZ0IsRUFBRSxNQUFNLENBQUM7Q0FDMUI7QUFFRCxxQkFBYSxrQkFBbUIsU0FBUSxLQUFLO0lBQzNDLFlBQVksQ0FBQyxFQUFFLEdBQUcsRUFBRSxDQUFDO0lBRXJCLFlBQVksT0FBTyxFQUFFLE1BQU0sRUFBRSxZQUFZLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFJaEQ7Q0FDRjtBQUVELHdCQUFnQixZQUFZLENBQzFCLEtBQUssQ0FBQyxJQUFJLFNBQVMsR0FBRyxHQUFHLFNBQVMsT0FBTyxFQUFFLEVBQzNDLFVBQVUsU0FBUyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsRUFDMUMsVUFBVSxHQUFHLHdCQUF3QixDQUFDLElBQUksRUFBRSxVQUFVLEVBQUUsR0FBRyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxFQUUvRSxJQUFJLEVBQUUsR0FBRyxFQUFFLEVBQ1gsT0FBTyxFQUFFLEdBQUcsRUFDWixHQUFHLEVBQUUsSUFBSSxFQUNULFNBQVMsRUFBRSxVQUFVLEVBQ3JCLE1BQU0sQ0FBQyxFQUFFLENBQUMsR0FBRyxFQUFFLFVBQVUsS0FBSyxPQUFPLEVBQ3JDLE1BQU0sQ0FBQyxFQUFFLE1BQU0sR0FDZCxVQUFVLENBTVo7QUFFRCx3QkFBZ0IsZUFBZSxDQUM3QixLQUFLLENBQUMsSUFBSSxTQUFTLEdBQUcsR0FBRyxTQUFTLE9BQU8sRUFBRSxFQUMzQyxVQUFVLFNBQVMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLEVBQzFDLFVBQVUsR0FBRyx3QkFBd0IsQ0FBQyxJQUFJLEVBQUUsVUFBVSxFQUFFLEdBQUcsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLENBQUMsRUFFL0UsSUFBSSxFQUFFLEdBQUcsRUFBRSxFQUNYLE9BQU8sRUFBRSxHQUFHLEVBQ1osR0FBRyxFQUFFLElBQUksRUFDVCxTQUFTLEVBQUUsVUFBVSxFQUNyQixNQUFNLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxVQUFVLEtBQUssT0FBTyxFQUNyQyxNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsVUFBVSxHQUFHLFNBQVMsQ0FnQnhCO0FBRUQsd0JBQWdCLHFCQUFxQixDQUFDLEdBQUcsRUFBRSxHQUFHLE9BVzdDO0FBMEJEOzs7OztHQUtHO0FBQ0gsd0JBQWdCLGVBQWUsQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFLEdBQUcsR0FBRSxHQUFlLEdBQUcsa0JBQWtCLENBd0VwRjtBQXlCRCx3QkFBZ0IscUJBQXFCLENBQUMsR0FBRyxFQUFFLEdBQUcsc0JBYTdDIn0=
25
+ /**
26
+ * Type guard to check if a transaction is a blob transaction (EIP-4844).
27
+ * Blob transactions have maxFeePerBlobGas and blobVersionedHashes fields.
28
+ */
29
+ export declare function isBlobTransaction(tx: FormattedTransaction): tx is FormattedTransaction & {
30
+ maxFeePerBlobGas: bigint;
31
+ blobVersionedHashes: readonly Hex[];
32
+ };
33
+ /**
34
+ * Calculates a percentile from an array of bigints
35
+ */
36
+ export declare function calculatePercentile(values: bigint[], percentile: number): bigint;
37
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxFQUFFLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUN6RCxPQUFPLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUdwRCxPQUFPLEVBQ0wsS0FBSyxHQUFHLEVBRVIsS0FBSyxpQkFBaUIsRUFFdEIsS0FBSyx3QkFBd0IsRUFDN0IsS0FBSyxvQkFBb0IsRUFDekIsS0FBSyxHQUFHLEVBQ1IsS0FBSyxHQUFHLEVBR1QsTUFBTSxNQUFNLENBQUM7QUFFZCxNQUFNLFdBQVcsT0FBTztJQUN0QixXQUFXLEVBQUUsRUFBRSxDQUFDO0lBQ2hCLFdBQVcsRUFBRSxFQUFFLENBQUM7SUFDaEIsV0FBVyxFQUFFLEdBQUcsQ0FBQztJQUNqQixnQkFBZ0IsRUFBRSxNQUFNLENBQUM7Q0FDMUI7QUFFRCxxQkFBYSxrQkFBbUIsU0FBUSxLQUFLO0lBQzNDLFlBQVksQ0FBQyxFQUFFLEdBQUcsRUFBRSxDQUFDO0lBRXJCLFlBQVksT0FBTyxFQUFFLE1BQU0sRUFBRSxZQUFZLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFJaEQ7Q0FDRjtBQUVELHdCQUFnQixZQUFZLENBQzFCLEtBQUssQ0FBQyxJQUFJLFNBQVMsR0FBRyxHQUFHLFNBQVMsT0FBTyxFQUFFLEVBQzNDLFVBQVUsU0FBUyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsRUFDMUMsVUFBVSxHQUFHLHdCQUF3QixDQUFDLElBQUksRUFBRSxVQUFVLEVBQUUsR0FBRyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxFQUUvRSxJQUFJLEVBQUUsR0FBRyxFQUFFLEVBQ1gsT0FBTyxFQUFFLEdBQUcsRUFDWixHQUFHLEVBQUUsSUFBSSxFQUNULFNBQVMsRUFBRSxVQUFVLEVBQ3JCLE1BQU0sQ0FBQyxFQUFFLENBQUMsR0FBRyxFQUFFLFVBQVUsS0FBSyxPQUFPLEVBQ3JDLE1BQU0sQ0FBQyxFQUFFLE1BQU0sR0FDZCxVQUFVLENBTVo7QUFFRCx3QkFBZ0IsZUFBZSxDQUM3QixLQUFLLENBQUMsSUFBSSxTQUFTLEdBQUcsR0FBRyxTQUFTLE9BQU8sRUFBRSxFQUMzQyxVQUFVLFNBQVMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLEVBQzFDLFVBQVUsR0FBRyx3QkFBd0IsQ0FBQyxJQUFJLEVBQUUsVUFBVSxFQUFFLEdBQUcsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLENBQUMsRUFFL0UsSUFBSSxFQUFFLEdBQUcsRUFBRSxFQUNYLE9BQU8sRUFBRSxHQUFHLEVBQ1osR0FBRyxFQUFFLElBQUksRUFDVCxTQUFTLEVBQUUsVUFBVSxFQUNyQixNQUFNLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxVQUFVLEtBQUssT0FBTyxFQUNyQyxNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsVUFBVSxHQUFHLFNBQVMsQ0FnQnhCO0FBRUQsd0JBQWdCLHFCQUFxQixDQUFDLEdBQUcsRUFBRSxHQUFHLE9BVzdDO0FBMEJEOzs7OztHQUtHO0FBQ0gsd0JBQWdCLGVBQWUsQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFLEdBQUcsR0FBRSxHQUFlLEdBQUcsa0JBQWtCLENBd0VwRjtBQXlCRCx3QkFBZ0IscUJBQXFCLENBQUMsR0FBRyxFQUFFLEdBQUcsc0JBYTdDO0FBRUQ7OztHQUdHO0FBQ0gsd0JBQWdCLGlCQUFpQixDQUFDLEVBQUUsRUFBRSxvQkFBb0IsR0FBRyxFQUFFLElBQUksb0JBQW9CLEdBQUc7SUFDeEYsZ0JBQWdCLEVBQUUsTUFBTSxDQUFDO0lBQ3pCLG1CQUFtQixFQUFFLFNBQVMsR0FBRyxFQUFFLENBQUM7Q0FDckMsQ0FPQTtBQUVEOztHQUVHO0FBQ0gsd0JBQWdCLG1CQUFtQixDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsRUFBRSxVQUFVLEVBQUUsTUFBTSxHQUFHLE1BQU0sQ0FPaEYifQ==
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,EACL,KAAK,GAAG,EAER,KAAK,iBAAiB,EAEtB,KAAK,wBAAwB,EAC7B,KAAK,GAAG,EACR,KAAK,GAAG,EAGT,MAAM,MAAM,CAAC;AAEd,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,EAAE,CAAC;IAChB,WAAW,EAAE,EAAE,CAAC;IAChB,WAAW,EAAE,GAAG,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC;IAErB,YAAY,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,GAAG,EAAE,EAIhD;CACF;AAED,wBAAgB,YAAY,CAC1B,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,EAC3C,UAAU,SAAS,iBAAiB,CAAC,IAAI,CAAC,EAC1C,UAAU,GAAG,wBAAwB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,EAE/E,IAAI,EAAE,GAAG,EAAE,EACX,OAAO,EAAE,GAAG,EACZ,GAAG,EAAE,IAAI,EACT,SAAS,EAAE,UAAU,EACrB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,EACrC,MAAM,CAAC,EAAE,MAAM,GACd,UAAU,CAMZ;AAED,wBAAgB,eAAe,CAC7B,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,EAC3C,UAAU,SAAS,iBAAiB,CAAC,IAAI,CAAC,EAC1C,UAAU,GAAG,wBAAwB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,EAE/E,IAAI,EAAE,GAAG,EAAE,EACX,OAAO,EAAE,GAAG,EACZ,GAAG,EAAE,IAAI,EACT,SAAS,EAAE,UAAU,EACrB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,EACrC,MAAM,CAAC,EAAE,MAAM,GACd,UAAU,GAAG,SAAS,CAgBxB;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,OAW7C;AA0BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,GAAE,GAAe,GAAG,kBAAkB,CAwEpF;AAyBD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,sBAa7C"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,EACL,KAAK,GAAG,EAER,KAAK,iBAAiB,EAEtB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,GAAG,EACR,KAAK,GAAG,EAGT,MAAM,MAAM,CAAC;AAEd,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,EAAE,CAAC;IAChB,WAAW,EAAE,EAAE,CAAC;IAChB,WAAW,EAAE,GAAG,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC;IAErB,YAAY,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,GAAG,EAAE,EAIhD;CACF;AAED,wBAAgB,YAAY,CAC1B,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,EAC3C,UAAU,SAAS,iBAAiB,CAAC,IAAI,CAAC,EAC1C,UAAU,GAAG,wBAAwB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,EAE/E,IAAI,EAAE,GAAG,EAAE,EACX,OAAO,EAAE,GAAG,EACZ,GAAG,EAAE,IAAI,EACT,SAAS,EAAE,UAAU,EACrB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,EACrC,MAAM,CAAC,EAAE,MAAM,GACd,UAAU,CAMZ;AAED,wBAAgB,eAAe,CAC7B,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,EAC3C,UAAU,SAAS,iBAAiB,CAAC,IAAI,CAAC,EAC1C,UAAU,GAAG,wBAAwB,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,EAE/E,IAAI,EAAE,GAAG,EAAE,EACX,OAAO,EAAE,GAAG,EACZ,GAAG,EAAE,IAAI,EACT,SAAS,EAAE,UAAU,EACrB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,EACrC,MAAM,CAAC,EAAE,MAAM,GACd,UAAU,GAAG,SAAS,CAgBxB;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,OAW7C;AA0BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,GAAE,GAAe,GAAG,kBAAkB,CAwEpF;AAyBD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,sBAa7C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,oBAAoB,GAAG,EAAE,IAAI,oBAAoB,GAAG;IACxF,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,SAAS,GAAG,EAAE,CAAC;CACrC,CAOA;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAOhF"}
package/dest/utils.js CHANGED
@@ -162,3 +162,21 @@ export function tryGetCustomErrorName(err) {
162
162
  return undefined;
163
163
  }
164
164
  }
165
+ /**
166
+ * Type guard to check if a transaction is a blob transaction (EIP-4844).
167
+ * Blob transactions have maxFeePerBlobGas and blobVersionedHashes fields.
168
+ */ export function isBlobTransaction(tx) {
169
+ return 'maxFeePerBlobGas' in tx && tx.maxFeePerBlobGas !== undefined && 'blobVersionedHashes' in tx && tx.blobVersionedHashes !== undefined;
170
+ }
171
+ /**
172
+ * Calculates a percentile from an array of bigints
173
+ */ export function calculatePercentile(values, percentile) {
174
+ if (values.length === 0) {
175
+ return 0n;
176
+ }
177
+ const sorted = [
178
+ ...values
179
+ ].sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
180
+ const index = Math.ceil((sorted.length - 1) * (percentile / 100));
181
+ return sorted[index];
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/ethereum",
3
- "version": "3.0.0-nightly.20251216",
3
+ "version": "3.0.0-nightly.20251218",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./account": "./dest/account.js",
@@ -17,6 +17,7 @@
17
17
  "./forwarder-proxy": "./dest/forwarder_proxy.js",
18
18
  "./l1-artifacts": "./dest/l1_artifacts.js",
19
19
  "./l1-contract-addresses": "./dest/l1_contract_addresses.js",
20
+ "./l1-fee-analysis": "./dest/l1_tx_utils/l1_fee_analyzer.js",
20
21
  "./l1-reader": "./dest/l1_reader.js",
21
22
  "./l1-tx-utils": "./dest/l1_tx_utils/index.js",
22
23
  "./l1-tx-utils-with-blobs": "./dest/l1_tx_utils/index-blobs.js",
@@ -48,10 +49,10 @@
48
49
  "../package.common.json"
49
50
  ],
50
51
  "dependencies": {
51
- "@aztec/blob-lib": "3.0.0-nightly.20251216",
52
- "@aztec/constants": "3.0.0-nightly.20251216",
53
- "@aztec/foundation": "3.0.0-nightly.20251216",
54
- "@aztec/l1-artifacts": "3.0.0-nightly.20251216",
52
+ "@aztec/blob-lib": "3.0.0-nightly.20251218",
53
+ "@aztec/constants": "3.0.0-nightly.20251218",
54
+ "@aztec/foundation": "3.0.0-nightly.20251218",
55
+ "@aztec/l1-artifacts": "3.0.0-nightly.20251218",
55
56
  "@viem/anvil": "^0.0.10",
56
57
  "dotenv": "^16.0.3",
57
58
  "lodash.chunk": "^4.2.0",
@@ -13,7 +13,7 @@ import { spawn } from 'child_process';
13
13
  import { dirname, resolve } from 'path';
14
14
  import readline from 'readline';
15
15
  import type { Hex } from 'viem';
16
- import { foundry, mainnet } from 'viem/chains';
16
+ import { foundry, mainnet, sepolia } from 'viem/chains';
17
17
 
18
18
  import { createEthereumChain, isAnvilTestChain } from './chain.js';
19
19
  import { createExtendedL1Client } from './client.js';
@@ -246,10 +246,20 @@ export async function deployAztecL1Contracts(
246
246
  const FORGE_SCRIPT = 'script/deploy/DeployAztecL1Contracts.s.sol';
247
247
  await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
248
248
 
249
+ // Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
250
+ const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
251
+ const shouldVerify = isVerifiableChain && !!process.env.ETHERSCAN_API_KEY;
252
+
253
+ if (isVerifiableChain && !process.env.ETHERSCAN_API_KEY) {
254
+ logger.warn(
255
+ `Deploying to chain ${chainId} (${chainId === mainnet.id ? 'mainnet' : 'sepolia'}) without ETHERSCAN_API_KEY. ` +
256
+ `Contracts will NOT be verified on Etherscan. Set ETHERSCAN_API_KEY environment variable to enable verification.`,
257
+ );
258
+ }
259
+
249
260
  // From heuristic testing. More caused issues with anvil.
250
261
  const MAGIC_ANVIL_BATCH_SIZE = 12;
251
262
  // Anvil seems to stall with unbounded batch size. Otherwise no max batch size is desirable.
252
- // On sepolia and mainnet, we verify on etherscan (if etherscan API key is in env)
253
263
  const forgeArgs = [
254
264
  'script',
255
265
  FORGE_SCRIPT,
@@ -260,11 +270,10 @@ export async function deployAztecL1Contracts(
260
270
  '--rpc-url',
261
271
  rpcUrl,
262
272
  '--broadcast',
263
- ...(chainId === foundry.id ? ['--batch-size', MAGIC_ANVIL_BATCH_SIZE.toString()] : ['--verify']),
273
+ ...(chainId === foundry.id ? ['--batch-size', MAGIC_ANVIL_BATCH_SIZE.toString()] : []),
274
+ ...(shouldVerify ? ['--verify'] : []),
264
275
  ];
265
276
  const forgeEnv = {
266
- // Protect against root leaving deployment files in docker that cannot be used later.
267
- FOUNDRY_BROADCAST: process.getuid?.() === 0 ? 'broadcast-root' : undefined,
268
277
  // Env vars required by l1-contracts/script/deploy/DeploymentConfiguration.sol.
269
278
  NETWORK: getActiveNetworkName(),
270
279
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
@@ -523,8 +532,6 @@ export const deployRollupForUpgrade = async (
523
532
  '--broadcast',
524
533
  ];
525
534
  const forgeEnv = {
526
- // Protect against root leaving deployment files in docker that cannot be used later.
527
- FOUNDRY_BROADCAST: process.getuid?.() === 0 ? 'broadcast-root' : undefined,
528
535
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
529
536
  // Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
530
537
  REGISTRY_ADDRESS: registryAddress.toString(),
@@ -1,6 +1,6 @@
1
1
  import type { ConfigMappingsType } from '@aztec/foundation/config';
2
2
  import { EthAddress } from '@aztec/foundation/eth-address';
3
- import { type ZodFor, schemas } from '@aztec/foundation/schemas';
3
+ import { schemas, zodFor } from '@aztec/foundation/schemas';
4
4
 
5
5
  import { z } from 'zod';
6
6
 
@@ -35,25 +35,27 @@ export type L1ContractAddresses = {
35
35
  dateGatedRelayerAddress?: EthAddress | undefined;
36
36
  };
37
37
 
38
- export const L1ContractAddressesSchema = z.object({
39
- rollupAddress: schemas.EthAddress,
40
- registryAddress: schemas.EthAddress,
41
- inboxAddress: schemas.EthAddress,
42
- outboxAddress: schemas.EthAddress,
43
- feeJuiceAddress: schemas.EthAddress,
44
- stakingAssetAddress: schemas.EthAddress,
45
- feeJuicePortalAddress: schemas.EthAddress,
46
- coinIssuerAddress: schemas.EthAddress,
47
- rewardDistributorAddress: schemas.EthAddress,
48
- governanceProposerAddress: schemas.EthAddress,
49
- governanceAddress: schemas.EthAddress,
50
- slashFactoryAddress: schemas.EthAddress.optional(),
51
- feeAssetHandlerAddress: schemas.EthAddress.optional(),
52
- stakingAssetHandlerAddress: schemas.EthAddress.optional(),
53
- zkPassportVerifierAddress: schemas.EthAddress.optional(),
54
- gseAddress: schemas.EthAddress.optional(),
55
- dateGatedRelayerAddress: schemas.EthAddress.optional(),
56
- }) satisfies ZodFor<L1ContractAddresses>;
38
+ export const L1ContractAddressesSchema = zodFor<L1ContractAddresses>()(
39
+ z.object({
40
+ rollupAddress: schemas.EthAddress,
41
+ registryAddress: schemas.EthAddress,
42
+ inboxAddress: schemas.EthAddress,
43
+ outboxAddress: schemas.EthAddress,
44
+ feeJuiceAddress: schemas.EthAddress,
45
+ stakingAssetAddress: schemas.EthAddress,
46
+ feeJuicePortalAddress: schemas.EthAddress,
47
+ coinIssuerAddress: schemas.EthAddress,
48
+ rewardDistributorAddress: schemas.EthAddress,
49
+ governanceProposerAddress: schemas.EthAddress,
50
+ governanceAddress: schemas.EthAddress,
51
+ slashFactoryAddress: schemas.EthAddress.optional(),
52
+ feeAssetHandlerAddress: schemas.EthAddress.optional(),
53
+ stakingAssetHandlerAddress: schemas.EthAddress.optional(),
54
+ zkPassportVerifierAddress: schemas.EthAddress.optional(),
55
+ gseAddress: schemas.EthAddress.optional(),
56
+ dateGatedRelayerAddress: schemas.EthAddress.optional(),
57
+ }),
58
+ );
57
59
 
58
60
  const parseEnv = (val: string) => EthAddress.fromString(val);
59
61
 
@@ -16,3 +16,14 @@ export const MIN_BLOB_REPLACEMENT_BUMP_PERCENTAGE = 100;
16
16
 
17
17
  // Avg ethereum block time is ~12s
18
18
  export const BLOCK_TIME_MS = 12_000;
19
+
20
+ // Gas per blob (EIP-4844)
21
+ export const GAS_PER_BLOB = 131072n;
22
+
23
+ // Blob capacity schedule based on Ethereum upgrades
24
+ export const BLOB_CAPACITY_SCHEDULE = [
25
+ { timestamp: 1734357600, target: 14, max: 21 }, // BPO2: Dec 17, 2025
26
+ { timestamp: 1733752800, target: 10, max: 15 }, // BPO1: Dec 9, 2025
27
+ { timestamp: 1733234400, target: 6, max: 9 }, // Fusaka: Dec 3, 2025
28
+ { timestamp: 0, target: 6, max: 9 }, // Pectra/earlier
29
+ ];
@@ -0,0 +1,22 @@
1
+ import { P75AllTxsPriorityFeeStrategy } from './p75_competitive.js';
2
+ import { P75BlobTxsOnlyPriorityFeeStrategy } from './p75_competitive_blob_txs_only.js';
3
+ import type { PriorityFeeStrategy } from './types.js';
4
+
5
+ export {
6
+ HISTORICAL_BLOCK_COUNT,
7
+ executeStrategy,
8
+ type PriorityFeeStrategy,
9
+ type PriorityFeeStrategyContext,
10
+ type PriorityFeeStrategyResult,
11
+ } from './types.js';
12
+
13
+ export { P75AllTxsPriorityFeeStrategy } from './p75_competitive.js';
14
+
15
+ /**
16
+ * Default list of priority fee strategies to analyze.
17
+ * Add more strategies here for comparison.
18
+ */
19
+ export const DEFAULT_PRIORITY_FEE_STRATEGIES: PriorityFeeStrategy[] = [
20
+ P75AllTxsPriorityFeeStrategy,
21
+ P75BlobTxsOnlyPriorityFeeStrategy,
22
+ ];
@@ -0,0 +1,159 @@
1
+ import { median } from '@aztec/foundation/collection';
2
+
3
+ import { formatGwei } from 'viem';
4
+
5
+ import type { ViemClient } from '../../types.js';
6
+ import { calculatePercentile } from '../../utils.js';
7
+ import { WEI_CONST } from '../constants.js';
8
+ import {
9
+ HISTORICAL_BLOCK_COUNT,
10
+ type PriorityFeeStrategy,
11
+ type PriorityFeeStrategyContext,
12
+ type PriorityFeeStrategyResult,
13
+ } from './types.js';
14
+
15
+ /**
16
+ * Type for the promises required by the competitive strategy
17
+ */
18
+ type P75AllTxsStrategyPromises = {
19
+ networkEstimate: Promise<bigint>;
20
+ pendingBlock: Promise<Awaited<ReturnType<ViemClient['getBlock']>> | null>;
21
+ feeHistory: Promise<Awaited<ReturnType<ViemClient['getFeeHistory']>> | null>;
22
+ };
23
+
24
+ /**
25
+ * Our current competitive priority fee strategy.
26
+ * Analyzes p75 of pending transactions and 5-block fee history to determine a competitive priority fee.
27
+ * Falls back to network estimate if data is unavailable.
28
+ */
29
+ export const P75AllTxsPriorityFeeStrategy: PriorityFeeStrategy<P75AllTxsStrategyPromises> = {
30
+ name: 'Competitive (P75 + History) - CURRENT',
31
+ id: 'p75_pending_txs_and_history_all_txs',
32
+
33
+ getRequiredPromises(client: ViemClient): P75AllTxsStrategyPromises {
34
+ return {
35
+ networkEstimate: client.estimateMaxPriorityFeePerGas().catch(() => 0n),
36
+ pendingBlock: client.getBlock({ blockTag: 'pending', includeTransactions: true }).catch(() => null),
37
+ feeHistory: client
38
+ .getFeeHistory({
39
+ blockCount: HISTORICAL_BLOCK_COUNT,
40
+ rewardPercentiles: [75],
41
+ blockTag: 'latest',
42
+ })
43
+ .catch(() => null),
44
+ };
45
+ },
46
+
47
+ calculate(
48
+ results: {
49
+ [K in keyof P75AllTxsStrategyPromises]: PromiseSettledResult<Awaited<P75AllTxsStrategyPromises[K]>>;
50
+ },
51
+ context: PriorityFeeStrategyContext,
52
+ ): PriorityFeeStrategyResult {
53
+ const { logger } = context;
54
+
55
+ // Extract network estimate from settled result
56
+ const networkEstimate =
57
+ results.networkEstimate.status === 'fulfilled' && typeof results.networkEstimate.value === 'bigint'
58
+ ? results.networkEstimate.value
59
+ : 0n;
60
+
61
+ let competitiveFee = networkEstimate;
62
+ const debugInfo: Record<string, string | number> = {
63
+ networkEstimateGwei: formatGwei(networkEstimate),
64
+ };
65
+
66
+ // Extract pending block from settled result
67
+ const pendingBlock = results.pendingBlock.status === 'fulfilled' ? results.pendingBlock.value : null;
68
+
69
+ // Analyze pending block transactions
70
+ if (pendingBlock?.transactions && pendingBlock.transactions.length > 0) {
71
+ const pendingFees = pendingBlock.transactions
72
+ .map(tx => {
73
+ if (typeof tx === 'string') {
74
+ return 0n;
75
+ }
76
+ const fee = tx.maxPriorityFeePerGas || 0n;
77
+
78
+ return fee;
79
+ })
80
+ .filter((fee: bigint) => fee > 0n);
81
+
82
+ if (pendingFees.length > 0) {
83
+ // Use 75th percentile of pending fees to be competitive
84
+ const pendingCompetitiveFee = calculatePercentile(pendingFees, 75);
85
+
86
+ if (pendingCompetitiveFee > competitiveFee) {
87
+ competitiveFee = pendingCompetitiveFee;
88
+ }
89
+
90
+ debugInfo.pendingTxCount = pendingFees.length;
91
+ debugInfo.pendingP75Gwei = formatGwei(pendingCompetitiveFee);
92
+
93
+ logger?.debug('Analyzed pending transactions for competitive pricing', {
94
+ pendingTxCount: pendingFees.length,
95
+ pendingP75: formatGwei(pendingCompetitiveFee),
96
+ });
97
+ }
98
+ }
99
+
100
+ // Extract fee history from settled result
101
+ const feeHistory = results.feeHistory.status === 'fulfilled' ? results.feeHistory.value : null;
102
+
103
+ // Analyze fee history
104
+ if (feeHistory?.reward && feeHistory.reward.length > 0) {
105
+ // Extract 75th percentile fees from each block
106
+ const percentile75Fees = feeHistory.reward.map(rewards => rewards[0] || 0n).filter(fee => fee > 0n);
107
+
108
+ if (percentile75Fees.length > 0) {
109
+ // Calculate median of the 75th percentile fees across blocks
110
+ const medianHistoricalFee = median(percentile75Fees) ?? 0n;
111
+
112
+ // Debug: Log suspicious fees from history
113
+ if (medianHistoricalFee > 100n * WEI_CONST) {
114
+ logger?.warn('Suspicious high fee in history', {
115
+ historicalMedian: formatGwei(medianHistoricalFee),
116
+ allP75Fees: percentile75Fees.map(f => formatGwei(f)),
117
+ });
118
+ }
119
+
120
+ if (medianHistoricalFee > competitiveFee) {
121
+ competitiveFee = medianHistoricalFee;
122
+ }
123
+
124
+ debugInfo.historicalMedianGwei = formatGwei(medianHistoricalFee);
125
+
126
+ logger?.debug('Analyzed fee history for competitive pricing', {
127
+ historicalMedian: formatGwei(medianHistoricalFee),
128
+ });
129
+ }
130
+ }
131
+
132
+ // Sanity check: cap competitive fee at 100x network estimate to avoid using unrealistic fees
133
+ const maxReasonableFee = networkEstimate * 100n;
134
+ if (competitiveFee > maxReasonableFee && networkEstimate > 0n) {
135
+ logger?.warn('Competitive fee exceeds sanity cap, using capped value', {
136
+ competitiveFee: formatGwei(competitiveFee),
137
+ networkEstimate: formatGwei(networkEstimate),
138
+ cappedTo: formatGwei(maxReasonableFee),
139
+ });
140
+ competitiveFee = maxReasonableFee;
141
+ debugInfo.cappedToGwei = formatGwei(maxReasonableFee);
142
+ }
143
+
144
+ // Log final decision
145
+ if (competitiveFee > networkEstimate) {
146
+ logger?.debug('Using competitive fee from market analysis', {
147
+ networkEstimate: formatGwei(networkEstimate),
148
+ competitive: formatGwei(competitiveFee),
149
+ });
150
+ }
151
+
152
+ debugInfo.finalFeeGwei = formatGwei(competitiveFee);
153
+
154
+ return {
155
+ priorityFee: competitiveFee,
156
+ debugInfo,
157
+ };
158
+ },
159
+ };