on master help sign in
17 files changed+232−119
since splitsnapshots
unifiedsplit
package.json+1−1 src/Bridge.tsx+12−5 src/components/BrowserWarning.tsx+27 src/components/InlineEthereumTransaction.jsx+4−7 src/components/L2/Dropdowns/FeeDropdown.tsx+4−1 src/components/L2/Headers/L2PointHeader.tsx−3 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/useBrowser.ts+32 src/lib/useEthereumTransaction.js+9−13 src/lib/useGasPrice.js+7−4 src/views/Activate/ActivateView.scss+31 src/views/Activate/ActivateView.tsx+16 src/views/UrbitOS/NetworkKeys.tsx+16−6
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/Bridge.tsx+12−5
@@ -20,6 +20,9 @@ import light from '@tlon/indigo-light';
2020
2121import WithErrorBoundary from 'components/WithErrorBoundary';
2222import GlobalErrorBoundary from 'components/GlobalErrorBoundary';
23+import { useBrowser } from 'lib/useBrowser';
24+import { BrowserWarning } from 'components/BrowserWarning';
25+
2326import Modal from 'components/L2/Modal';
2427import { useRollerStore } from 'store/rollerStore';
2528import { Box } from '@tlon/indigo-react';
@@ -29,8 +32,8 @@ import { useRollerPoller } from 'lib/useRollerPoller';
2932const INITIAL_NETWORK_TYPE = isGoerli
3033 ? NETWORK_TYPES.GOERLI
3134 : isDevelopment && !isMainnet
32− ? NETWORK_TYPES.LOCAL
33− : NETWORK_TYPES.MAINNET;
35+ ? NETWORK_TYPES.LOCAL
36+ : NETWORK_TYPES.MAINNET;
3437
3538// NB(shrugs): modify these variables to change the default local state.
3639const SHOULD_STUB_LOCAL = import.meta.env.VITE_STUB_LOCAL === 'true';
@@ -38,9 +41,9 @@ const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
3841
3942const INITIAL_WALLET = IS_STUBBED
4043 ? walletFromMnemonic(
41− import.meta.env.VITE_DEV_MNEMONIC as string,
42− import.meta.env.VITE_HD_PATH as string
43− )
44+ import.meta.env.VITE_DEV_MNEMONIC as string,
45+ import.meta.env.VITE_HD_PATH as string
46+ )
4447 : undefined;
4548const INITIAL_MNEMONIC = IS_STUBBED
4649 ? Just(import.meta.env.VITE_DEV_MNEMONIC)
@@ -82,6 +85,9 @@ function Bridge() {
8285 const initialRoutes = useInitialRoutes();
8386 const { loading, modalText, setModalText } = useRollerStore();
8487
88+ const browser = useBrowser();
89+ const showBrowserWarning = browser && !browser.isChromium;
90+
8591 // full reload if the user changes their selected network in Metamask
8692 //
8793 useEffect(() => {
@@ -103,6 +109,7 @@ function Bridge() {
103109 initialWallet={INITIAL_WALLET}
104110 initialMnemonic={INITIAL_MNEMONIC}
105111 initialPointCursor={INITIAL_POINT_CURSOR}>
112+ <BrowserWarning show={!!showBrowserWarning} />
106113 <IndigoApp>
107114 <ThemeProvider theme={light}>
108115 <Router />
src/components/BrowserWarning.tsxnew file+27
@@ -0,0 +1,27 @@
1+import React from 'react';
2+import { Box } from '@tlon/indigo-react';
3+import styled from 'styled-components';
4+
5+const WarningBox = styled(Box)`
6+ background: #fff4e5;
7+ color: #663c00;
8+ border: 1px solid #ffb74d;
9+ padding: 0.75rem 1rem;
10+ text-align: center;
11+ font-weight: 600;
12+ margin-bottom: 1rem;
13+`;
14+
15+interface Props {
16+ show: boolean;
17+}
18+export const BrowserWarning: React.FC<Props> = ({ show }) => {
19+ if (!show) return null;
20+ return (
21+ <WarningBox role="alert">
22+ For the best experience we recommend using a Chromium‑based browser
23+ (Chrome, Brave, etc.).
24+ </WarningBox>
25+ );
26+};
27+
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/components/L2/Headers/L2PointHeader.tsx−3
@@ -92,9 +92,6 @@ const L2PointHeader = ({
9292 <Row>
9393 <Row className="info">
9494 {renderInviteButton()}
95− <Box className="history" onClick={goToHistory}>
96− History
97− </Box>
9895 {points && <InviteConverter points={points} />}
9996 </Row>
10097 </Row>
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/useBrowser.tsnew file+32
@@ -0,0 +1,32 @@
1+import { useEffect, useState } from 'react';
2+
3+export function useBrowser() {
4+ const [browser, setBrowser] = useState<{
5+ name: 'chrome' | 'brave' | 'safari' | 'firefox' | 'edge' | 'other';
6+ isChromium: boolean;
7+ } | null>(null);
8+
9+ useEffect(() => {
10+ const ua = navigator.userAgent;
11+ const vendor = navigator.vendor ?? '';
12+
13+ const isChromium = !!(window as any).chrome;
14+ const isBrave = !!(navigator as any).brave;
15+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua);
16+ const isFirefox = /Firefox/.test(ua);
17+ const isEdge = /Edg/.test(ua);
18+ const isChrome = isChromium && !isBrave && !isEdge && /Chrome/.test(ua);
19+
20+ let name: typeof browser['name'] = 'other';
21+ if (isBrave) name = 'brave';
22+ else if (isChrome) name = 'chrome';
23+ else if (isSafari) name = 'safari';
24+ else if (isFirefox) name = 'firefox';
25+ else if (isEdge) name = 'edge';
26+
27+ setBrowser({ name, isChromium });
28+ }, []);
29+
30+ return browser;
31+}
32+
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 );
src/views/UrbitOS/NetworkKeys.tsx+16−6
@@ -279,8 +279,8 @@ export default function UrbitOSNetworkKeys({
279279 const viewTitle = completed
280280 ? 'Network Keys have been set'
281281 : hasKeys
282− ? 'Set Network Keys'
283− : 'Initiate Network Keys';
282+ ? 'Set Network Keys'
283+ : 'Initiate Network Keys';
284284
285285 const bootMessage = () => (
286286 <Text as="p" textAlign={'center'}>
@@ -311,9 +311,19 @@ export default function UrbitOSNetworkKeys({
311311
312312 const waitMessage = () => (
313313 <Text as="p" textAlign={'center'}>
314− <strong>Important:</strong> it may take minutes to hours for the
315− keys to become usable. Please be patient, trying to use the new keys
316− during this period may result in an invalid keys error.
314+ <strong>Important:</strong> It may take minutes to hours for the
315+ keys to become usable as they propogate across the network.
316+ Please be patient, trying to use the new keys
317+ during this period may result in an{' '}
318+ <
319+ Text as="code"
320+ mono
321+ bg={'washedGray'}
322+ padding={1}
323+ borderRadius={1}
324+ textAlign={'center'}>
325+ invalid keys</Text>
326+ {' '}error.
317327 </Text>
318328 );
319329
@@ -439,4 +449,4 @@ export default function UrbitOSNetworkKeys({
439449 </BodyPane>
440450 </Window>
441451 );
442−}
452+}