@@ -5,13 +5,14 @@ import Web3 from 'web3';
55// ethgasstation returns values in floating point, one order of magitude
66// more than gwei. see: https://docs.ethgasstation.info
77// we don't want to charge users more than the gas tank funds
8−const minGas = gas => Math.min(Math.ceil(gas), MAX_GAS_PRICE_GWEI);
9−
10−const feeToInt = f => (f < 1 ? 1 : Math.round(f));
8+const minGas = gas => Math.min(gas, MAX_GAS_PRICE_GWEI);
119
1210const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
1311
14−const weiToGwei = wei => Math.ceil(wei / 1_000_000_000);
12+// Gwei with milligwei precision, rounded up so we never underpay
13+const roundGwei = gwei => Math.ceil(gwei * 1000) / 1000;
14+
15+const weiToGwei = wei => roundGwei(wei / 1_000_000_000);
1516
1617const calculateMaxFee = (baseFee, maxPriorityFee) =>
1718 feeToWei(Math.round(2 * baseFee + maxPriorityFee));
@@ -24,82 +25,75 @@ export const defaultGasValues = value => ({
2425 price: value,
2526 wait: '1',
2627 maxFeePerGas: value,
27− maxPriorityFeePerGas: 1,
28+ maxPriorityFeePerGas: Math.min(1, value),
2829 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
2930 },
3031 average: {
3132 price: value,
3233 wait: '1',
3334 maxFeePerGas: value,
34− maxPriorityFeePerGas: 1,
35+ maxPriorityFeePerGas: Math.min(1, value),
3536 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
3637 },
3738 low: {
3839 price: value,
3940 wait: '1',
4041 maxFeePerGas: value,
41− maxPriorityFeePerGas: 1,
42+ maxPriorityFeePerGas: Math.min(1, value),
4243 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
4344 },
4445});
4546
46−const getGasForNetwork = async providerUrl => {
47+const median = values => {
48+ const sorted = [...values].sort((a, b) => a - b);
49+ const mid = Math.floor(sorted.length / 2);
50+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
51+};
52+
53+// Number of recent blocks to sample, and the priority fee percentiles
54+// used for the low, average and fast tiers respectively
55+const FEE_HISTORY_BLOCKS = 20;
56+const REWARD_PERCENTILES = [10, 50, 90];
57+// Floor for the priority fee, since zero tip transactions may never be mined
58+const MIN_PRIORITY_FEE_GWEI = 0.001;
59+
60+const tierFor = (baseFeeGwei, priorityFeeWei, wait) => {
61+ const maxPriorityFeePerGas = Math.max(
62+ weiToGwei(priorityFeeWei),
63+ MIN_PRIORITY_FEE_GWEI
64+ );
65+ const maxFeePerGas = roundGwei(baseFeeGwei + maxPriorityFeePerGas);
66+ return {
67+ price: minGas(maxFeePerGas),
68+ wait: formatWait(wait),
69+ maxFeePerGas,
70+ maxPriorityFeePerGas,
71+ suggestedBaseFeePerGas: baseFeeGwei,
72+ };
73+};
74+
75+// Estimates fees from the connected node via eth_feeHistory, in the same
76+// manner as wallets do. See: https://docs.alchemy.com/docs/how-to-build-a-gas-fee-estimator-using-eip-1559
77+const getGasForNetwork = async web3 => {
4778 try {
48− const [feeResponse, baseFeeResponse] = await Promise.all([
49− fetch(
50− 'https://beaconcha.in/api/v1/execution/gasnow',
51− {
52− method: 'get',
53− cache: 'no-cache',
54− }
55− ),
56− fetch(
57− `${providerUrl}/v2/api` +
58− '?module=gastracker' +
59− '&action=gasoracle' +
60− '&apikey=CG52E4R96W56GIKUI4IJ8CH9EZIXPUW1W8' +
61− '&chainid=1',
62− {
63− method: 'GET',
64− cache: 'no-cache',
65− }
66− ),
67− ]);
68−
69− const [feeJson, baseFeeJson] = await Promise.all([
70− feeResponse.json(),
71− baseFeeResponse.json(),
72− ]);
73−
74− const suggestedBaseFeePerGas = Number(baseFeeJson.result.suggestBaseFee);
75−
76− // Calculations derived from:
77− // https://www.blocknative.com/blog/eip-1559-fees
79+ const history = await web3.eth.getFeeHistory(
80+ FEE_HISTORY_BLOCKS,
81+ 'latest',
82+ REWARD_PERCENTILES
83+ );
84+
85+ // baseFeePerGas includes one extra entry: the base fee of the next block
86+ const nextBaseFeeWei = Number(history.baseFeePerGas.at(-1));
87+ const baseFeeGwei = nextBaseFeeWei / 1_000_000_000;
88+
89+ const [low, average, fast] = REWARD_PERCENTILES.map((_, i) =>
90+ median(history.reward.map(rewards => Number(rewards[i])))
91+ );
92+
7893 return {
79− fast: {
80− price: minGas(weiToGwei(feeJson.data.rapid)),
81− wait: formatWait(15),
82− maxFeePerGas: weiToGwei(feeJson.data.rapid),
83− maxPriorityFeePerGas: feeToInt(
84− weiToGwei(feeJson.data.rapid) - suggestedBaseFeePerGas
85− )
86− },
87− average: {
88− price: minGas(weiToGwei(feeJson.data.fast)),
89− wait: formatWait(60),
90− maxFeePerGas: weiToGwei(feeJson.data.fast),
91− maxPriorityFeePerGas: feeToInt(
92− weiToGwei(feeJson.data.fast) - suggestedBaseFeePerGas
93− )
94− },
95− low: {
96− price: minGas(weiToGwei(feeJson.data.slow)),
97− wait: formatWait(180),
98− maxFeePerGas: weiToGwei(feeJson.data.slow),
99− maxPriorityFeePerGas: feeToInt(
100− weiToGwei(feeJson.data.slow) - suggestedBaseFeePerGas
101− )
102− },
94+ fast: tierFor(baseFeeGwei, fast, 15),
95+ average: tierFor(baseFeeGwei, average, 60),
96+ low: tierFor(baseFeeGwei, low, 180),
10397 };
10498 } catch (e) {
10599 console.warn(e);
@@ -107,14 +101,10 @@ const getGasForNetwork = async providerUrl => {
107101 }
108102};
109103
110−export default async function getSuggestedGasPrice(networkType) {
111− switch (networkType) {
112− case NETWORK_TYPES.GOERLI:
113− return getGasForNetwork('https://api-goerli.etherscan.io');
114− case NETWORK_TYPES.OFFLINE:
115− return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
116− case NETWORK_TYPES.LOCAL:
117− default:
118− return getGasForNetwork('https://api.etherscan.io');
104+export default async function getSuggestedGasPrice(networkType, web3) {
105+ if (networkType === NETWORK_TYPES.OFFLINE || !web3) {
106+ return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
119107 }
108+
109+ return getGasForNetwork(web3);
120110}