@@ -5,14 +5,13 @@ import Web3 from 'web3';
5// ethgasstation returns values in floating point, one order of magitude
5// ethgasstation returns values in floating point, one order of magitude
6// more than gwei. see: https://docs.ethgasstation.info
6// more than gwei. see: https://docs.ethgasstation.info
7// we don't want to charge users more than the gas tank funds
7// we don't want to charge users more than the gas tank funds
8−const minGas = gas => Math.min(gas, MAX_GAS_PRICE_GWEI);
8+const minGas = gas => Math.min(Math.ceil(gas), MAX_GAS_PRICE_GWEI);
10−const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
10+const feeToInt = f => (f < 1 ? 1 : Math.round(f));
12−// Gwei with milligwei precision, rounded up so we never underpay
12+const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
13−const roundGwei = gwei => Math.ceil(gwei * 1000) / 1000;
15−const weiToGwei = wei => roundGwei(wei / 1_000_000_000);
14+const weiToGwei = wei => Math.ceil(wei / 1_000_000_000);
17const calculateMaxFee = (baseFee, maxPriorityFee) =>
16const calculateMaxFee = (baseFee, maxPriorityFee) =>
18 feeToWei(Math.round(2 * baseFee + maxPriorityFee));
17 feeToWei(Math.round(2 * baseFee + maxPriorityFee));
@@ -25,75 +24,82 @@ export const defaultGasValues = value => ({
25 price: value,
24 price: value,
26 wait: '1',
25 wait: '1',
27 maxFeePerGas: value,
26 maxFeePerGas: value,
28− maxPriorityFeePerGas: Math.min(1, value),
27+ maxPriorityFeePerGas: 1,
29 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
28 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
31 average: {
30 average: {
32 price: value,
31 price: value,
33 wait: '1',
32 wait: '1',
34 maxFeePerGas: value,
33 maxFeePerGas: value,
35− maxPriorityFeePerGas: Math.min(1, value),
34+ maxPriorityFeePerGas: 1,
36 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
35 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
39 price: value,
38 price: value,
40 wait: '1',
39 wait: '1',
41 maxFeePerGas: value,
40 maxFeePerGas: value,
42− maxPriorityFeePerGas: Math.min(1, value),
41+ maxPriorityFeePerGas: 1,
43 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
42 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
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;
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;
60−const tierFor = (baseFeeGwei, priorityFeeWei, wait) => {
61− const maxPriorityFeePerGas = Math.max(
62− weiToGwei(priorityFeeWei),
63− MIN_PRIORITY_FEE_GWEI
65− const maxFeePerGas = roundGwei(baseFeeGwei + maxPriorityFeePerGas);
67− price: minGas(maxFeePerGas),
68− wait: formatWait(wait),
70− maxPriorityFeePerGas,
71− suggestedBaseFeePerGas: baseFeeGwei,
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 => {
46+const getGasForNetwork = async providerUrl => {
79− const history = await web3.eth.getFeeHistory(
48+ const [feeResponse, baseFeeResponse] = await Promise.all([
80− FEE_HISTORY_BLOCKS,
49+ fetch(
81− 'latest',
50+ 'https://beaconcha.in/api/v1/execution/gasnow',
82− REWARD_PERCENTILES
51+ {
85− // baseFeePerGas includes one extra entry: the base fee of the next block
57+ `${providerUrl}/v2/api` +
86− const nextBaseFeeWei = Number(history.baseFeePerGas.at(-1));
58+ '?module=gastracker' +
87− const baseFeeGwei = nextBaseFeeWei / 1_000_000_000;
59+ '&action=gasoracle' +
88−
60+ '&apikey=CG52E4R96W56GIKUI4IJ8CH9EZIXPUW1W8' +
89− const [low, average, fast] = REWARD_PERCENTILES.map((_, i) =>
61+ '&chainid=1',
90− median(history.reward.map(rewards => Number(rewards[i])))
62+ {
92−
64+ cache: 'no-cache',
69+ const [feeJson, baseFeeJson] = await Promise.all([
71+ baseFeeResponse.json(),
74+ const suggestedBaseFeePerGas = Number(baseFeeJson.result.suggestBaseFee);
76+ // Calculations derived from:
77+ // https://www.blocknative.com/blog/eip-1559-fees
94− fast: tierFor(baseFeeGwei, fast, 15),
79+ fast: {
95− average: tierFor(baseFeeGwei, average, 60),
80+ price: minGas(weiToGwei(feeJson.data.rapid)),
96− low: tierFor(baseFeeGwei, low, 180),
81+ wait: formatWait(15),
82+ maxFeePerGas: weiToGwei(feeJson.data.rapid),
83+ maxPriorityFeePerGas: feeToInt(
84+ weiToGwei(feeJson.data.rapid) - suggestedBaseFeePerGas
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
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
98 } catch (e) {
104 } catch (e) {
99 console.warn(e);
105 console.warn(e);
@@ -101,10 +107,14 @@ const getGasForNetwork = async web3 => {
104−export default async function getSuggestedGasPrice(networkType, web3) {
110+export default async function getSuggestedGasPrice(networkType) {
111+ switch (networkType) {
112+ case NETWORK_TYPES.GOERLI:
113+ return getGasForNetwork('https://api-goerli.etherscan.io');
105− if (networkType === NETWORK_TYPES.OFFLINE || !web3) {
114+ case NETWORK_TYPES.OFFLINE:
106− return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
115+ return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
116+ case NETWORK_TYPES.LOCAL:
118+ return getGasForNetwork('https://api.etherscan.io');
109− return getGasForNetwork(web3);