@@ -5,13 +5,14 @@ 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(Math.ceil(gas), MAX_GAS_PRICE_GWEI);
8+const minGas = gas => Math.min(gas, MAX_GAS_PRICE_GWEI);
10−const feeToInt = f => (f < 1 ? 1 : Math.round(f));
12const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
10const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
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;
15+const weiToGwei = wei => roundGwei(wei / 1_000_000_000);
16const calculateMaxFee = (baseFee, maxPriorityFee) =>
17const calculateMaxFee = (baseFee, maxPriorityFee) =>
17 feeToWei(Math.round(2 * baseFee + maxPriorityFee));
18 feeToWei(Math.round(2 * baseFee + maxPriorityFee));
@@ -24,82 +25,75 @@ export const defaultGasValues = value => ({
24 price: value,
25 price: value,
25 wait: '1',
26 wait: '1',
26 maxFeePerGas: value,
27 maxFeePerGas: value,
27− maxPriorityFeePerGas: 1,
28+ maxPriorityFeePerGas: Math.min(1, value),
28 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
29 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
30 average: {
31 average: {
31 price: value,
32 price: value,
32 wait: '1',
33 wait: '1',
33 maxFeePerGas: value,
34 maxFeePerGas: value,
34− maxPriorityFeePerGas: 1,
35+ maxPriorityFeePerGas: Math.min(1, value),
35 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
36 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
38 price: value,
39 price: value,
39 wait: '1',
40 wait: '1',
40 maxFeePerGas: value,
41 maxFeePerGas: value,
41− maxPriorityFeePerGas: 1,
42+ maxPriorityFeePerGas: Math.min(1, value),
42 suggestedBaseFeePerGas: value > 2 ? value - 1 : 1,
43 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
46−const getGasForNetwork = async providerUrl => {
77+const getGasForNetwork = async web3 => {
48− const [feeResponse, baseFeeResponse] = await Promise.all([
79+ const history = await web3.eth.getFeeHistory(
49− fetch(
80+ FEE_HISTORY_BLOCKS,
50− 'https://beaconcha.in/api/v1/execution/gasnow',
81+ 'latest',
51− {
82+ REWARD_PERCENTILES
57− `${providerUrl}/v2/api` +
85+ // baseFeePerGas includes one extra entry: the base fee of the next block
58− '?module=gastracker' +
86+ const nextBaseFeeWei = Number(history.baseFeePerGas.at(-1));
59− '&action=gasoracle' +
87+ const baseFeeGwei = nextBaseFeeWei / 1_000_000_000;
60− '&apikey=CG52E4R96W56GIKUI4IJ8CH9EZIXPUW1W8' +
88+
61− '&chainid=1',
89+ const [low, average, fast] = REWARD_PERCENTILES.map((_, i) =>
62− {
90+ median(history.reward.map(rewards => Number(rewards[i])))
64− cache: 'no-cache',
92+
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
79− fast: {
94+ fast: tierFor(baseFeeGwei, fast, 15),
80− price: minGas(weiToGwei(feeJson.data.rapid)),
95+ average: tierFor(baseFeeGwei, average, 60),
81− wait: formatWait(15),
96+ low: tierFor(baseFeeGwei, low, 180),
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
104 } catch (e) {
98 } catch (e) {
105 console.warn(e);
99 console.warn(e);
@@ -107,14 +101,10 @@ const getGasForNetwork = async providerUrl => {
110−export default async function getSuggestedGasPrice(networkType) {
104+export default async function getSuggestedGasPrice(networkType, web3) {
111− switch (networkType) {
112− case NETWORK_TYPES.GOERLI:
113− return getGasForNetwork('https://api-goerli.etherscan.io');
114− case NETWORK_TYPES.OFFLINE:
105+ if (networkType === NETWORK_TYPES.OFFLINE || !web3) {
115− return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
106+ return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
116− case NETWORK_TYPES.LOCAL:
118− return getGasForNetwork('https://api.etherscan.io');
109+ return getGasForNetwork(web3);