@aztec/ethereum 2.1.9-rc.3 → 2.1.9-rc.4

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 (40) hide show
  1. package/dest/deploy_l1_contracts.d.ts.map +1 -1
  2. package/dest/deploy_l1_contracts.js +9 -8
  3. package/dest/l1_tx_utils/constants.d.ts +6 -0
  4. package/dest/l1_tx_utils/constants.d.ts.map +1 -1
  5. package/dest/l1_tx_utils/constants.js +25 -0
  6. package/dest/l1_tx_utils/fee-strategies/index.d.ts +10 -0
  7. package/dest/l1_tx_utils/fee-strategies/index.d.ts.map +1 -0
  8. package/dest/l1_tx_utils/fee-strategies/index.js +12 -0
  9. package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts +8 -0
  10. package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts.map +1 -0
  11. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +129 -0
  12. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.d.ts +23 -0
  13. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.d.ts.map +1 -0
  14. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +191 -0
  15. package/dest/l1_tx_utils/fee-strategies/types.d.ts +51 -0
  16. package/dest/l1_tx_utils/fee-strategies/types.d.ts.map +1 -0
  17. package/dest/l1_tx_utils/fee-strategies/types.js +3 -0
  18. package/dest/l1_tx_utils/index.d.ts +2 -0
  19. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  20. package/dest/l1_tx_utils/index.js +2 -0
  21. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +235 -0
  22. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -0
  23. package/dest/l1_tx_utils/l1_fee_analyzer.js +506 -0
  24. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +0 -13
  25. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  26. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +30 -143
  27. package/dest/utils.d.ts +13 -1
  28. package/dest/utils.d.ts.map +1 -1
  29. package/dest/utils.js +18 -0
  30. package/package.json +6 -5
  31. package/src/deploy_l1_contracts.ts +7 -6
  32. package/src/l1_tx_utils/constants.ts +11 -0
  33. package/src/l1_tx_utils/fee-strategies/index.ts +22 -0
  34. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +163 -0
  35. package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +245 -0
  36. package/src/l1_tx_utils/fee-strategies/types.ts +56 -0
  37. package/src/l1_tx_utils/index.ts +2 -0
  38. package/src/l1_tx_utils/l1_fee_analyzer.ts +802 -0
  39. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +42 -186
  40. 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,149 +38,37 @@ 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
- // 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 = 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([
147
- latestBlockPromise,
148
- networkEstimatePromise,
149
- pendingBlockPromise,
150
- feeHistoryPromise,
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;
155
- // Get blob base fee if available
156
- let blobBaseFee = 0n;
157
- if (isBlobTx && blobBaseFeeResult.status === 'fulfilled' && typeof blobBaseFeeResult.value === 'bigint') {
158
- blobBaseFee = blobBaseFeeResult.value;
159
- } else if (isBlobTx) {
44
+ // Execute strategy - it handles all RPC calls internally and returns everything we need
45
+ const strategyResult = await retry(()=>CurrentStrategy.execute(this.client, {
46
+ gasConfig,
47
+ isBlobTx,
48
+ logger: this.logger
49
+ }), 'Executing priority fee strategy', makeBackoff(times(2, ()=>0)), this.logger, true);
50
+ const { latestBlock, blobBaseFee, priorityFee: strategyPriorityFee } = strategyResult;
51
+ // Extract base fee from latest block
52
+ const baseFee = latestBlock.baseFeePerGas ?? 0n;
53
+ // Handle blob base fee
54
+ if (isBlobTx && blobBaseFee === undefined) {
160
55
  this.logger?.warn('Failed to get L1 blob base fee', attempt);
161
56
  }
162
- // Get competitive priority fee
163
- let priorityFee = this.getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult);
164
- // Apply minimum priority fee as a floor if configured
57
+ let priorityFee = strategyPriorityFee;
58
+ // Apply minimum priority fee floor if configured
165
59
  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)
60
+ const minimumPriorityFee = BigInt(Math.trunc(gasConfig.minimumPriorityFeePerGas * Number(WEI_CONST)));
61
+ if (priorityFee < minimumPriorityFee) {
62
+ this.logger?.debug('Applying minimum priority fee floor', {
63
+ calculatedPriorityFee: formatGwei(priorityFee),
64
+ minimumPriorityFeePerGas: gasConfig.minimumPriorityFeePerGas,
65
+ appliedFee: formatGwei(minimumPriorityFee)
177
66
  });
67
+ priorityFee = minimumPriorityFee;
178
68
  }
179
69
  }
180
70
  let maxFeePerGas = baseFee;
181
- let maxFeePerBlobGas = blobBaseFee;
71
+ let maxFeePerBlobGas = blobBaseFee ?? 0n;
182
72
  // Bump base fee so it's valid for next blocks if it stalls
183
73
  const numBlocks = Math.ceil(gasConfig.stallTimeMs / BLOCK_TIME_MS);
184
74
  for(let i = 0; i < numBlocks; i++){
@@ -248,7 +138,7 @@ export class ReadOnlyL1TxUtils {
248
138
  baseFee: formatGwei(baseFee),
249
139
  maxFeePerGas: formatGwei(maxFeePerGas),
250
140
  maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
251
- blobBaseFee: formatGwei(blobBaseFee),
141
+ blobBaseFee: formatGwei(blobBaseFee ?? 0n),
252
142
  maxFeePerBlobGas: formatGwei(maxFeePerBlobGas)
253
143
  });
254
144
  return {
@@ -275,14 +165,16 @@ export class ReadOnlyL1TxUtils {
275
165
  ...request,
276
166
  ..._blobInputs,
277
167
  maxFeePerBlobGas: gasPrice.maxFeePerBlobGas,
278
- gas: LARGE_GAS_LIMIT
168
+ gas: LARGE_GAS_LIMIT,
169
+ blockTag: 'latest'
279
170
  });
280
171
  this.logger?.trace(`Estimated gas for blob tx: ${initialEstimate}`);
281
172
  } else {
282
173
  initialEstimate = await this.client.estimateGas({
283
174
  account,
284
175
  ...request,
285
- gas: LARGE_GAS_LIMIT
176
+ gas: LARGE_GAS_LIMIT,
177
+ blockTag: 'latest'
286
178
  });
287
179
  this.logger?.trace(`Estimated gas for non-blob tx: ${initialEstimate}`);
288
180
  }
@@ -423,9 +315,4 @@ export class ReadOnlyL1TxUtils {
423
315
  });
424
316
  return bumpedGasLimit;
425
317
  }
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
- }
431
318
  }
package/dest/utils.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Fr } from '@aztec/foundation/fields';
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
+ /**
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;
25
37
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AACnD,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;gBAET,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,GAAG,EAAE;CAKlD;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,0BAA0B,CAAC;AACnD,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;gBAET,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,GAAG,EAAE;CAKlD;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": "2.1.9-rc.3",
3
+ "version": "2.1.9-rc.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -8,6 +8,7 @@
8
8
  "./contracts": "./dest/contracts/index.js",
9
9
  "./deploy-l1-contracts": "./dest/deploy_l1_contracts.js",
10
10
  "./l1-contract-addresses": "./dest/l1_contract_addresses.js",
11
+ "./l1-fee-analysis": "./dest/l1_tx_utils/l1_fee_analyzer.js",
11
12
  "./l1-tx-utils-with-blobs": "./dest/l1_tx_utils/index-blobs.js",
12
13
  "./utils": "./dest/utils.js"
13
14
  },
@@ -31,10 +32,10 @@
31
32
  "../package.common.json"
32
33
  ],
33
34
  "dependencies": {
34
- "@aztec/blob-lib": "2.1.9-rc.3",
35
- "@aztec/constants": "2.1.9-rc.3",
36
- "@aztec/foundation": "2.1.9-rc.3",
37
- "@aztec/l1-artifacts": "2.1.9-rc.3",
35
+ "@aztec/blob-lib": "2.1.9-rc.4",
36
+ "@aztec/constants": "2.1.9-rc.4",
37
+ "@aztec/foundation": "2.1.9-rc.4",
38
+ "@aztec/l1-artifacts": "2.1.9-rc.4",
38
39
  "@viem/anvil": "^0.0.10",
39
40
  "dotenv": "^16.0.3",
40
41
  "lodash.chunk": "^4.2.0",
@@ -276,7 +276,14 @@ export const deploySharedContracts = async (
276
276
  feeAssetAddress = deployedFee.address;
277
277
  logger.verbose(`Deployed Fee Asset at ${feeAssetAddress}`);
278
278
 
279
+ const deployedStaking = await deployer.deploy(StakingAssetArtifact, ['Staking', 'STK', l1Client.account.address]);
280
+ stakingAssetAddress = deployedStaking.address;
281
+ logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`);
282
+
283
+ await deployer.waitForDeployments();
284
+
279
285
  // Mint a tiny bit of tokens to satisfy coin-issuer constraints
286
+ // This must happen AFTER the FeeAsset deployment is mined
280
287
  const { txHash } = await deployer.sendTransaction({
281
288
  to: feeAssetAddress.toString(),
282
289
  data: encodeFunctionData({
@@ -287,12 +294,6 @@ export const deploySharedContracts = async (
287
294
  });
288
295
  await l1Client.waitForTransactionReceipt({ hash: txHash });
289
296
  logger.verbose(`Minted tiny bit of tokens to satisfy coin-issuer constraints in ${txHash}`);
290
-
291
- const deployedStaking = await deployer.deploy(StakingAssetArtifact, ['Staking', 'STK', l1Client.account.address]);
292
- stakingAssetAddress = deployedStaking.address;
293
- logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`);
294
-
295
- await deployer.waitForDeployments();
296
297
  }
297
298
 
298
299
  const gseAddress = (
@@ -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
+ type PriorityFeeStrategy,
8
+ type PriorityFeeStrategyContext,
9
+ type PriorityFeeStrategyResult,
10
+ } from './types.js';
11
+
12
+ export { P75AllTxsPriorityFeeStrategy } from './p75_competitive.js';
13
+ export { P75BlobTxsOnlyPriorityFeeStrategy } from './p75_competitive_blob_txs_only.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,163 @@
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
+ * Our current competitive priority fee strategy.
17
+ * Analyzes p75 of pending transactions and 5-block fee history to determine a competitive priority fee.
18
+ * Falls back to network estimate if data is unavailable.
19
+ */
20
+ export const P75AllTxsPriorityFeeStrategy: PriorityFeeStrategy = {
21
+ name: 'Competitive (P75 + History) - CURRENT',
22
+ id: 'p75_pending_txs_and_history_all_txs',
23
+
24
+ async execute(client: ViemClient, context: PriorityFeeStrategyContext): Promise<PriorityFeeStrategyResult> {
25
+ const { isBlobTx, logger } = context;
26
+
27
+ // Fire all RPC calls in parallel
28
+ const [latestBlockResult, blobBaseFeeResult, networkEstimateResult, pendingBlockResult, feeHistoryResult] =
29
+ await Promise.allSettled([
30
+ client.getBlock({ blockTag: 'latest' }),
31
+ isBlobTx ? client.getBlobBaseFee() : Promise.resolve(undefined),
32
+ client.estimateMaxPriorityFeePerGas().catch(() => 0n),
33
+ client.getBlock({ blockTag: 'pending', includeTransactions: true }).catch(() => null),
34
+ client
35
+ .getFeeHistory({
36
+ blockCount: HISTORICAL_BLOCK_COUNT,
37
+ rewardPercentiles: [75],
38
+ blockTag: 'latest',
39
+ })
40
+ .catch(() => null),
41
+ ]);
42
+
43
+ // Extract latest block
44
+ if (latestBlockResult.status === 'rejected') {
45
+ throw new Error(`Failed to get latest block: ${latestBlockResult.reason}`);
46
+ }
47
+ const latestBlock = latestBlockResult.value;
48
+
49
+ // Extract blob base fee (only for blob txs)
50
+ let blobBaseFee: bigint | undefined;
51
+ if (isBlobTx) {
52
+ if (blobBaseFeeResult.status === 'fulfilled' && typeof blobBaseFeeResult.value === 'bigint') {
53
+ blobBaseFee = blobBaseFeeResult.value;
54
+ } else {
55
+ logger?.warn('Failed to get L1 blob base fee');
56
+ }
57
+ }
58
+
59
+ // Extract network estimate
60
+ const networkEstimate =
61
+ networkEstimateResult.status === 'fulfilled' && typeof networkEstimateResult.value === 'bigint'
62
+ ? networkEstimateResult.value
63
+ : 0n;
64
+
65
+ let competitiveFee = networkEstimate;
66
+ const debugInfo: Record<string, string | number> = {
67
+ networkEstimateGwei: formatGwei(networkEstimate),
68
+ };
69
+
70
+ // Extract pending block
71
+ const pendingBlock = pendingBlockResult.status === 'fulfilled' ? pendingBlockResult.value : null;
72
+
73
+ // Analyze pending block transactions
74
+ if (pendingBlock?.transactions && pendingBlock.transactions.length > 0) {
75
+ const pendingFees = pendingBlock.transactions
76
+ .map(tx => {
77
+ if (typeof tx === 'string') {
78
+ return 0n;
79
+ }
80
+ return tx.maxPriorityFeePerGas || 0n;
81
+ })
82
+ .filter((fee: bigint) => fee > 0n);
83
+
84
+ if (pendingFees.length > 0) {
85
+ // Use 75th percentile of pending fees to be competitive
86
+ const pendingCompetitiveFee = calculatePercentile(pendingFees, 75);
87
+
88
+ if (pendingCompetitiveFee > competitiveFee) {
89
+ competitiveFee = pendingCompetitiveFee;
90
+ }
91
+
92
+ debugInfo.pendingTxCount = pendingFees.length;
93
+ debugInfo.pendingP75Gwei = formatGwei(pendingCompetitiveFee);
94
+
95
+ logger?.debug('Analyzed pending transactions for competitive pricing', {
96
+ pendingTxCount: pendingFees.length,
97
+ pendingP75: formatGwei(pendingCompetitiveFee),
98
+ });
99
+ }
100
+ }
101
+
102
+ // Extract fee history
103
+ const feeHistory = feeHistoryResult.status === 'fulfilled' ? feeHistoryResult.value : null;
104
+
105
+ // Analyze fee history
106
+ if (feeHistory?.reward && feeHistory.reward.length > 0) {
107
+ // Extract 75th percentile fees from each block
108
+ const percentile75Fees = feeHistory.reward.map(rewards => rewards[0] || 0n).filter(fee => fee > 0n);
109
+
110
+ if (percentile75Fees.length > 0) {
111
+ // Calculate median of the 75th percentile fees across blocks
112
+ const medianHistoricalFee = median(percentile75Fees) ?? 0n;
113
+
114
+ // Debug: Log suspicious fees from history
115
+ if (medianHistoricalFee > 100n * WEI_CONST) {
116
+ logger?.warn('Suspicious high fee in history', {
117
+ historicalMedian: formatGwei(medianHistoricalFee),
118
+ allP75Fees: percentile75Fees.map(f => formatGwei(f)),
119
+ });
120
+ }
121
+
122
+ if (medianHistoricalFee > competitiveFee) {
123
+ competitiveFee = medianHistoricalFee;
124
+ }
125
+
126
+ debugInfo.historicalMedianGwei = formatGwei(medianHistoricalFee);
127
+
128
+ logger?.debug('Analyzed fee history for competitive pricing', {
129
+ historicalMedian: formatGwei(medianHistoricalFee),
130
+ });
131
+ }
132
+ }
133
+
134
+ // Sanity check: cap competitive fee at 100x network estimate to avoid using unrealistic fees
135
+ const maxReasonableFee = networkEstimate * 100n;
136
+ if (competitiveFee > maxReasonableFee && networkEstimate > 0n) {
137
+ logger?.warn('Competitive fee exceeds sanity cap, using capped value', {
138
+ competitiveFee: formatGwei(competitiveFee),
139
+ networkEstimate: formatGwei(networkEstimate),
140
+ cappedTo: formatGwei(maxReasonableFee),
141
+ });
142
+ competitiveFee = maxReasonableFee;
143
+ debugInfo.cappedToGwei = formatGwei(maxReasonableFee);
144
+ }
145
+
146
+ // Log final decision
147
+ if (competitiveFee > networkEstimate) {
148
+ logger?.debug('Using competitive fee from market analysis', {
149
+ networkEstimate: formatGwei(networkEstimate),
150
+ competitive: formatGwei(competitiveFee),
151
+ });
152
+ }
153
+
154
+ debugInfo.finalFeeGwei = formatGwei(competitiveFee);
155
+
156
+ return {
157
+ priorityFee: competitiveFee,
158
+ latestBlock,
159
+ blobBaseFee,
160
+ debugInfo,
161
+ };
162
+ },
163
+ };