on master help sign in
package.json+1−1
since splitsnapshots
unifiedsplit
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "3.3.0",
3+ "version": "3.3.1",
4 "description": "An application for interacting with Azimuth",
4 "description": "An application for interacting with Azimuth",
5 "repository": {
5 "repository": {
6 "type": "git",
6 "type": "git",
src/components/InlineEthereumTransaction.jsx+4−7
@@ -13,7 +13,7 @@ import { toBN } from 'web3-utils';
13
13
14import { useExploreTxUrls } from 'lib/explorer';
14import { useExploreTxUrls } from 'lib/explorer';
15import { hexify } from 'lib/txn';
15import { hexify } from 'lib/txn';
16−import { safeFromWei, safeToWei } from 'lib/lib';
16+import { safeFromWei, gweiToWei } from 'lib/lib';
17
17
18import { composeValidator, buildCheckboxValidator } from 'form/validators';
18import { composeValidator, buildCheckboxValidator } from 'form/validators';
19import BridgeForm from 'form/BridgeForm';
19import BridgeForm from 'form/BridgeForm';
@@ -107,12 +107,9 @@ export default function InlineEthereumTransaction({
107 const maxCost = useMemo(
107 const maxCost = useMemo(
108 () =>
108 () =>
109 safeFromWei(
109 safeFromWei(
110− safeToWei(
111− toBN(gasLimit)
110+ toBN(gasLimit)
112− .mul(toBN(gasPrice.price))
111+ .mul(gweiToWei(gasPrice.price))
113− .mul(toBN(numTxs)),
112+ .mul(toBN(numTxs)),
114− 'gwei'
115− ),
116 'ether'
113 'ether'
117 ),
114 ),
118 [gasLimit, gasPrice, numTxs]
115 [gasLimit, gasPrice, numTxs]
src/components/L2/Dropdowns/FeeDropdown.tsx+4−1
@@ -43,7 +43,10 @@ export default function FeeDropdown({
43 );
43 );
44
44
45 const handleCustom = (e: React.ChangeEvent<HTMLInputElement>) => {
45 const handleCustom = (e: React.ChangeEvent<HTMLInputElement>) => {
46+ // Allow digits and a single decimal point
46− const cleanedValue = e.target.value.replace(/[^0-9]/g, '');
47+ const cleanedValue = e.target.value
48+ .replace(/[^0-9.]/g, '')
49+ .replace(/(\..*)\./g, '$1');
47 const cleanedNum = Number(cleanedValue);
50 const cleanedNum = Number(cleanedValue);
48 setCustom(cleanedValue);
51 setCustom(cleanedValue);
49 setGasPrice(defaultGasValues(cleanedNum).average);
52 setGasPrice(defaultGasValues(cleanedNum).average);
src/lib/authToken.ts+4−3
@@ -1,7 +1,7 @@
1import { crypto } from 'bitcoinjs-lib';
1import { crypto } from 'bitcoinjs-lib';
2import { ecdsaSign } from 'secp256k1';
2import { ecdsaSign } from 'secp256k1';
3import Web3 from 'web3';
3import Web3 from 'web3';
4−import { hexToBytes } from 'web3-utils';
4+import { hexToBytes, utf8ToHex } from 'web3-utils';
5import { hashPersonalMessage } from '@ethereumjs/util';
5import { hashPersonalMessage } from '@ethereumjs/util';
6import { WALLET_TYPES } from './constants';
6import { WALLET_TYPES } from './constants';
7import BridgeWallet from './types/BridgeWallet';
7import BridgeWallet from './types/BridgeWallet';
@@ -76,10 +76,11 @@ const getMetamaskAuthToken = ({ address, web3 }: MetamaskAuthTokenArgs) => {
76 // but web3.eth.personal.sign hits it semi-reliably?
76 // but web3.eth.personal.sign hits it semi-reliably?
77 // no idea what's going on, we should figure it out,
77 // no idea what's going on, we should figure it out,
78 // but we apply this bandaid to hopefully stop the bleeding.
78 // but we apply this bandaid to hopefully stop the bleeding.
79+ //NOTE personal_sign expects hex-encoded data; signing the hex of
80+ // MESSAGE yields the same signature as the other wallet types
79 return window.ethereum.request({
81 return window.ethereum.request({
80 method: 'personal_sign',
82 method: 'personal_sign',
81− params: [MESSAGE, address],
83+ params: [utf8ToHex(MESSAGE), address],
82− from: address,
83 });
84 });
84 } else {
85 } else {
85 return web3.eth.personal.sign(MESSAGE, address, '');
86 return web3.eth.personal.sign(MESSAGE, address, '');
src/lib/getSuggestedGasPrice.js+61−71
@@ -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);
9−
10−const feeToInt = f => (f < 1 ? 1 : Math.round(f));
11
9
12const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
10const feeToWei = fee => Web3.utils.toHex(Web3.utils.toWei(String(fee), 'gwei'));
13
11
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);
15
16
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,
29 },
30 },
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,
36 },
37 },
37 low: {
38 low: {
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,
43 },
44 },
44});
45});
45
46
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
46−const getGasForNetwork = async providerUrl => {
77+const getGasForNetwork = async web3 => {
47 try {
78 try {
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
52− method: 'get',
53− cache: 'no-cache',
54− }
55− ),
83+ );
56− fetch(
84+
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])))
63− method: 'GET',
91+ );
64− cache: 'no-cache',
92+
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
78 return {
93 return {
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
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− },
103 };
97 };
104 } catch (e) {
98 } catch (e) {
105 console.warn(e);
99 console.warn(e);
@@ -107,14 +101,10 @@ const getGasForNetwork = async providerUrl => {
107 }
101 }
108};
102};
109
103
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:
117− default:
118− return getGasForNetwork('https://api.etherscan.io');
119 }
107 }
108+
109+ return getGasForNetwork(web3);
120}
110}
src/lib/lib.js+4
@@ -26,6 +26,9 @@ const safeToWei = (num, source) => {
26 }
26 }
27};
27};
28
28
29+// Converts a (possibly fractional) gwei amount to a wei BN
30+const gweiToWei = gwei => toBN(toWei(Number(gwei).toFixed(9), 'gwei'));
31+
29const compose = (...fs) =>
32const compose = (...fs) =>
30 fs.reduceRight(
33 fs.reduceRight(
31 (pF, nF) => (...args) => nF(pF(...args)),
34 (pF, nF) => (...args) => nF(pF(...args)),
@@ -73,6 +76,7 @@ const patpStringLength = byteLength =>
73export {
76export {
74 safeFromWei,
77 safeFromWei,
75 safeToWei,
78 safeToWei,
79+ gweiToWei,
76 compose,
80 compose,
77 allFalse,
81 allFalse,
78 isLast,
82 isLast,
src/lib/reticket.js+3−4
@@ -16,7 +16,7 @@ import {
16import getSuggestedGasPrice from './getSuggestedGasPrice';
16import getSuggestedGasPrice from './getSuggestedGasPrice';
17import { GAS_LIMITS, INITIAL_NETWORK_KEY_REVISION } from './constants';
17import { GAS_LIMITS, INITIAL_NETWORK_KEY_REVISION } from './constants';
18import { toBN } from 'web3-utils';
18import { toBN } from 'web3-utils';
19−import { safeToWei } from './lib';
19+import { gweiToWei } from './lib';
20import { isGalaxy, isPlanet } from './utils/point';
20import { isGalaxy, isPlanet } from './utils/point';
21
21
22export const TRANSACTION_PROGRESS = {
22export const TRANSACTION_PROGRESS = {
@@ -146,10 +146,9 @@ export async function reticketPointBetweenWallets({
146
146
147 progress(TRANSACTION_PROGRESS.SIGNING);
147 progress(TRANSACTION_PROGRESS.SIGNING);
148
148
149− const gasPriceData = (await getSuggestedGasPrice(networkType)).average;
149+ const gasPriceData = (await getSuggestedGasPrice(networkType, web3)).average;
150− const gasPriceGwei = gasPriceData.price.toFixed();
151 const chainId = await web3.eth.net.getId();
150 const chainId = await web3.eth.net.getId();
152− const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
151+ const gasPriceWeiBN = gweiToWei(gasPriceData.price);
153 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
152 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
154 const totalCost = txs.reduce(
153 const totalCost = txs.reduce(
155 (acc, tx) => acc.add(gasPriceWeiBN.mul(toBN(tx.gas))),
154 (acc, tx) => acc.add(gasPriceWeiBN.mul(toBN(tx.gas))),
src/lib/txn.ts+1−1
@@ -99,7 +99,7 @@ const signTransaction = async ({
99
99
100 // from user dropdown
100 // from user dropdown
101 const maxPriorityFeePerGas = toWei(
101 const maxPriorityFeePerGas = toWei(
102− String(Math.round(gasPriceData.maxPriorityFeePerGas)),
102+ Number(gasPriceData.maxPriorityFeePerGas).toFixed(9),
103 'gwei'
103 'gwei'
104 );
104 );
105
105
src/lib/useEthereumTransaction.js+9−13
@@ -23,7 +23,7 @@ import { ensureFundsFor } from 'lib/tank';
23import useDeepEqualReference from 'lib/useDeepEqualReference';
23import useDeepEqualReference from 'lib/useDeepEqualReference';
24import useGasPrice from 'lib/useGasPrice';
24import useGasPrice from 'lib/useGasPrice';
25import { timeout } from 'lib/timeout';
25import { timeout } from 'lib/timeout';
26−import { safeToWei, safeFromWei } from 'lib/lib';
26+import { safeFromWei, gweiToWei } from 'lib/lib';
27import { useWalletConnect } from './useWalletConnect';
27import { useWalletConnect } from './useWalletConnect';
28import { Transaction } from '@ethereumjs/tx';
28import { Transaction } from '@ethereumjs/tx';
29
29
@@ -181,10 +181,9 @@ export default function useEthereumTransaction(
181
181
182 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
182 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
183
183
184− const costGwei = toBN(gasLimit)
184+ const cost = toBN(gasLimit)
185− .mul(toBN(gasPrice.price))
185+ .mul(gweiToWei(gasPrice.price))
186 .mul(toBN(rawTxs.length));
186 .mul(toBN(rawTxs.length));
187− const cost = safeToWei(costGwei, 'gwei');
188 let usedTank = false;
187 let usedTank = false;
189 // if this ethereum transaction is being executed by a specific point
188 // if this ethereum transaction is being executed by a specific point
190 // see if we can use the tank
189 // see if we can use the tank
@@ -226,15 +225,12 @@ export default function useEthereumTransaction(
226
225
227 setFinalCost(
226 setFinalCost(
228 safeFromWei(
227 safeFromWei(
229− safeToWei(
230− receipts.reduce(
228+ receipts.reduce(
231− (acc, receipt) =>
229+ (acc, receipt) =>
232− toBN(receipt.gasUsed)
230+ toBN(receipt.gasUsed)
233− .mul(toBN(gasPrice.price))
231+ .mul(gweiToWei(gasPrice.price))
234− .add(acc),
232+ .add(acc),
235− toBN(0)
233+ toBN(0)
236− ),
237− 'gwei'
238 ),
234 ),
239 'ether'
235 'ether'
240 )
236 )
src/lib/useGasPrice.js+7−4
@@ -16,7 +16,7 @@ import { DEFAULT_GAS_PRICE_GWEI } from './constants';
16 */
16 */
17export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
17export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
18 const [mounted, setMounted] = useState(false);
18 const [mounted, setMounted] = useState(false);
19− const { networkType } = useNetwork();
19+ const { networkType, web3 } = useNetwork();
20 const [suggestedGasPrices, setSuggestedGasPrices] = useState(
20 const [suggestedGasPrices, setSuggestedGasPrices] = useState(
21 defaultGasValues(initialGasPrice)
21 defaultGasValues(initialGasPrice)
22 ); // fast, average, low. In gwei
22 ); // fast, average, low. In gwei
@@ -29,19 +29,22 @@ export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
29 return;
29 return;
30 }
30 }
31
31
32− const suggestedGasPrices = await getSuggestedGasPrices(networkType);
32+ const suggestedGasPrices = await getSuggestedGasPrices(
33+ networkType,
34+ web3.getOrElse(null)
35+ );
33 console.log(suggestedGasPrices);
36 console.log(suggestedGasPrices);
34
37
35 // TODO: BN
38 // TODO: BN
36 setSuggestedGasPrices(suggestedGasPrices);
39 setSuggestedGasPrices(suggestedGasPrices);
37 setGasPrice(suggestedGasPrices.average);
40 setGasPrice(suggestedGasPrices.average);
38− }, [mounted, networkType]);
41+ }, [mounted, networkType, web3]);
39
42
40 useEffect(() => {
43 useEffect(() => {
41 setMounted(true);
44 setMounted(true);
42 fetchPrices();
45 fetchPrices();
43 return () => setMounted(false);
46 return () => setMounted(false);
44− }, [mounted, networkType, fetchPrices]);
47+ }, [mounted, networkType, web3, fetchPrices]);
45
48
46 const resetGasPrice = useCallback(
49 const resetGasPrice = useCallback(
47 () => setGasPrice(suggestedGasPrices.average),
50 () => setGasPrice(suggestedGasPrices.average),