on master help sign in
12 files changed+145−105
since splitsnapshots
unifiedsplit
package.json+1−1 src/components/InlineEthereumTransaction.jsx+4−7 src/components/L2/Dropdowns/FeeDropdown.tsx+4−1 src/lib/authToken.ts+4−3 src/lib/getSuggestedGasPrice.js+61−71 src/lib/lib.js+4 src/lib/reticket.js+3−4 src/lib/txn.ts+1−1 src/lib/useEthereumTransaction.js+9−13 src/lib/useGasPrice.js+7−4 src/views/Activate/ActivateView.scss+31 src/views/Activate/ActivateView.tsx+16
package.json+1−1
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "3.3.0",
3+ "version": "3.3.1",
44 "description": "An application for interacting with Azimuth",
55 "repository": {
66 "type": "git",
src/components/InlineEthereumTransaction.jsx+4−7
@@ -13,7 +13,7 @@ import { toBN } from 'web3-utils';
1313
1414import { useExploreTxUrls } from 'lib/explorer';
1515import { hexify } from 'lib/txn';
16−import { safeFromWei, safeToWei } from 'lib/lib';
16+import { safeFromWei, gweiToWei } from 'lib/lib';
1717
1818import { composeValidator, buildCheckboxValidator } from 'form/validators';
1919import BridgeForm from 'form/BridgeForm';
@@ -107,12 +107,9 @@ export default function InlineEthereumTransaction({
107107 const maxCost = useMemo(
108108 () =>
109109 safeFromWei(
110− safeToWei(
111− toBN(gasLimit)
112− .mul(toBN(gasPrice.price))
113− .mul(toBN(numTxs)),
114− 'gwei'
115− ),
110+ toBN(gasLimit)
111+ .mul(gweiToWei(gasPrice.price))
112+ .mul(toBN(numTxs)),
116113 'ether'
117114 ),
118115 [gasLimit, gasPrice, numTxs]
src/components/L2/Dropdowns/FeeDropdown.tsx+4−1
@@ -43,7 +43,10 @@ export default function FeeDropdown({
4343 );
4444
4545 const handleCustom = (e: React.ChangeEvent<HTMLInputElement>) => {
46− const cleanedValue = e.target.value.replace(/[^0-9]/g, '');
46+ // Allow digits and a single decimal point
47+ const cleanedValue = e.target.value
48+ .replace(/[^0-9.]/g, '')
49+ .replace(/(\..*)\./g, '$1');
4750 const cleanedNum = Number(cleanedValue);
4851 setCustom(cleanedValue);
4952 setGasPrice(defaultGasValues(cleanedNum).average);
src/lib/authToken.ts+4−3
@@ -1,7 +1,7 @@
11import { crypto } from 'bitcoinjs-lib';
22import { ecdsaSign } from 'secp256k1';
33import Web3 from 'web3';
4−import { hexToBytes } from 'web3-utils';
4+import { hexToBytes, utf8ToHex } from 'web3-utils';
55import { hashPersonalMessage } from '@ethereumjs/util';
66import { WALLET_TYPES } from './constants';
77import BridgeWallet from './types/BridgeWallet';
@@ -76,10 +76,11 @@ const getMetamaskAuthToken = ({ address, web3 }: MetamaskAuthTokenArgs) => {
7676 // but web3.eth.personal.sign hits it semi-reliably?
7777 // no idea what's going on, we should figure it out,
7878 // 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
7981 return window.ethereum.request({
8082 method: 'personal_sign',
81− params: [MESSAGE, address],
82− from: address,
83+ params: [utf8ToHex(MESSAGE), address],
8384 });
8485 } else {
8586 return web3.eth.personal.sign(MESSAGE, address, '');
src/lib/getSuggestedGasPrice.js+61−71
@@ -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}
src/lib/lib.js+4
@@ -26,6 +26,9 @@ const safeToWei = (num, source) => {
2626 }
2727};
2828
29+// Converts a (possibly fractional) gwei amount to a wei BN
30+const gweiToWei = gwei => toBN(toWei(Number(gwei).toFixed(9), 'gwei'));
31+
2932const compose = (...fs) =>
3033 fs.reduceRight(
3134 (pF, nF) => (...args) => nF(pF(...args)),
@@ -73,6 +76,7 @@ const patpStringLength = byteLength =>
7376export {
7477 safeFromWei,
7578 safeToWei,
79+ gweiToWei,
7680 compose,
7781 allFalse,
7882 isLast,
src/lib/reticket.js+3−4
@@ -16,7 +16,7 @@ import {
1616import getSuggestedGasPrice from './getSuggestedGasPrice';
1717import { GAS_LIMITS, INITIAL_NETWORK_KEY_REVISION } from './constants';
1818import { toBN } from 'web3-utils';
19−import { safeToWei } from './lib';
19+import { gweiToWei } from './lib';
2020import { isGalaxy, isPlanet } from './utils/point';
2121
2222export const TRANSACTION_PROGRESS = {
@@ -146,10 +146,9 @@ export async function reticketPointBetweenWallets({
146146
147147 progress(TRANSACTION_PROGRESS.SIGNING);
148148
149− const gasPriceData = (await getSuggestedGasPrice(networkType)).average;
150− const gasPriceGwei = gasPriceData.price.toFixed();
149+ const gasPriceData = (await getSuggestedGasPrice(networkType, web3)).average;
151150 const chainId = await web3.eth.net.getId();
152− const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
151+ const gasPriceWeiBN = gweiToWei(gasPriceData.price);
153152 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
154153 const totalCost = txs.reduce(
155154 (acc, tx) => acc.add(gasPriceWeiBN.mul(toBN(tx.gas))),
src/lib/txn.ts+1−1
@@ -99,7 +99,7 @@ const signTransaction = async ({
9999
100100 // from user dropdown
101101 const maxPriorityFeePerGas = toWei(
102− String(Math.round(gasPriceData.maxPriorityFeePerGas)),
102+ Number(gasPriceData.maxPriorityFeePerGas).toFixed(9),
103103 'gwei'
104104 );
105105
src/lib/useEthereumTransaction.js+9−13
@@ -23,7 +23,7 @@ import { ensureFundsFor } from 'lib/tank';
2323import useDeepEqualReference from 'lib/useDeepEqualReference';
2424import useGasPrice from 'lib/useGasPrice';
2525import { timeout } from 'lib/timeout';
26−import { safeToWei, safeFromWei } from 'lib/lib';
26+import { safeFromWei, gweiToWei } from 'lib/lib';
2727import { useWalletConnect } from './useWalletConnect';
2828import { Transaction } from '@ethereumjs/tx';
2929
@@ -181,10 +181,9 @@ export default function useEthereumTransaction(
181181
182182 const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
183183
184− const costGwei = toBN(gasLimit)
185− .mul(toBN(gasPrice.price))
184+ const cost = toBN(gasLimit)
185+ .mul(gweiToWei(gasPrice.price))
186186 .mul(toBN(rawTxs.length));
187− const cost = safeToWei(costGwei, 'gwei');
188187 let usedTank = false;
189188 // if this ethereum transaction is being executed by a specific point
190189 // see if we can use the tank
@@ -226,15 +225,12 @@ export default function useEthereumTransaction(
226225
227226 setFinalCost(
228227 safeFromWei(
229− safeToWei(
230− receipts.reduce(
231− (acc, receipt) =>
232− toBN(receipt.gasUsed)
233− .mul(toBN(gasPrice.price))
234− .add(acc),
235− toBN(0)
236− ),
237− 'gwei'
228+ receipts.reduce(
229+ (acc, receipt) =>
230+ toBN(receipt.gasUsed)
231+ .mul(gweiToWei(gasPrice.price))
232+ .add(acc),
233+ toBN(0)
238234 ),
239235 'ether'
240236 )
src/lib/useGasPrice.js+7−4
@@ -16,7 +16,7 @@ import { DEFAULT_GAS_PRICE_GWEI } from './constants';
1616 */
1717export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
1818 const [mounted, setMounted] = useState(false);
19− const { networkType } = useNetwork();
19+ const { networkType, web3 } = useNetwork();
2020 const [suggestedGasPrices, setSuggestedGasPrices] = useState(
2121 defaultGasValues(initialGasPrice)
2222 ); // fast, average, low. In gwei
@@ -29,19 +29,22 @@ export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
2929 return;
3030 }
3131
32− const suggestedGasPrices = await getSuggestedGasPrices(networkType);
32+ const suggestedGasPrices = await getSuggestedGasPrices(
33+ networkType,
34+ web3.getOrElse(null)
35+ );
3336 console.log(suggestedGasPrices);
3437
3538 // TODO: BN
3639 setSuggestedGasPrices(suggestedGasPrices);
3740 setGasPrice(suggestedGasPrices.average);
38− }, [mounted, networkType]);
41+ }, [mounted, networkType, web3]);
3942
4043 useEffect(() => {
4144 setMounted(true);
4245 fetchPrices();
4346 return () => setMounted(false);
44− }, [mounted, networkType, fetchPrices]);
47+ }, [mounted, networkType, web3, fetchPrices]);
4548
4649 const resetGasPrice = useCallback(
4750 () => setGasPrice(suggestedGasPrices.average),
src/views/Activate/ActivateView.scss+31
@@ -1,3 +1,34 @@
11.activate-view {
22 min-height: 440px;
33}
4+
5+.activation-qualifier {
6+ /* Container appearance */
7+ margin-top: 1rem;
8+ // color: #000000; /* default text colour */
9+ padding: 1rem; /* some inner space */
10+ border-radius: 5px;
11+ background: rgba(0, 0, 0, 0.02);
12+ font-family: 'Inter';
13+ background: #fff4e5;
14+ color: #663c00;
15+ border: 1px solid #ffb74d;
16+}
17+
18+/* Each note inside the container */
19+.activation-qualifier .activation-note {
20+ margin-bottom: 1rem; /* space between notes */
21+}
22+
23+/* Remove the bottom margin from the last note */
24+.activation-qualifier .activation-note:last-child {
25+ margin-bottom: 0;
26+}
27+
28+/* Link styling – only for links that have the .linkout class */
29+.activation-qualifier a.linkout {
30+ color: #663c00;
31+ text-decoration: underline;
32+ font-weight: 600;
33+}
34+
src/views/Activate/ActivateView.tsx+16
@@ -60,6 +60,22 @@ export default function ActivateView({
6060 </Box>
6161 </BodyPane>
6262 </Window>
63+ <div className="activation-qualifier">
64+ <div className="activation-note"> Planet activation codes will give you a self-custody <a className="linkout" href="https://docs.urbit.org/user-manual/id/layer-2-for-planets">Layer 2 Urbit 'planet'↗</a> and a Master Ticket wallet.
65+ </div>
66+ <div className="activation-note">
67+ This is a legacy feature not actively maintained in Bridge, if you run into issues we recommend you either: <br></br>
68+ <ol>
69+ <li>
70+ <a className="linkout" href="https://join.tlon.io/0v3.r87kb.fjpft.3k7b5.pbsr5.5em17">Claim a free Layer 2 planet from Tlon↗</a>
71+ </li>
72+ <br></br>
73+ <li>
74+ <a className="linkout" href="https://docs.urbit.org/user-manual/id/get-id#l1-planet-markets">Buy a Layer 1 identity on Ethereum↗</a>
75+ </li>
76+ </ol>
77+ </div>
78+ </div>
6379 </Grid.Item>
6480 </Grid>
6581 );