on master help sign in
28 files changed+450−235
since splitsnapshots
unifiedsplit
.gitignore+1 DEVELOPMENT.md+6−2 legal/privacy policy.md+11−11 notes.md+1 package.json+1−1 src/Bridge.tsx+12−5 src/components/BrowserWarning.tsx+27 src/components/Greeting.jsx+65−13 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/flags.js+1 src/lib/getSuggestedGasPrice.js+61−71 src/lib/lib.js+4 src/lib/network.js+9−8 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/lib/useRoller.ts+65−46 src/views/Activate/ActivateView.scss+31 src/views/Activate/ActivateView.tsx+16 src/views/Login/LoginSelector.tsx+9−3 src/views/Point/MigrateL2.tsx+42−26 src/views/UrbitOS/NetworkKeys.tsx+16−6 vite.config.ts+8−7
.gitignore+1
@@ -11,6 +11,7 @@
1111
1212# misc
1313.DS_Store
14+.env
1415.env.local
1516.env.development.local
1617.env.test.local
DEVELOPMENT.md+6−2
@@ -200,14 +200,18 @@ Start a local fake ship from the master branch in the [urbit repo](https://githu
200200:: Run these in the dojo:
201201::
202202> |mount /=base=
203−> :azimuth %resub
203+> |nuke %azimuth
204+> |rein %base [%.y %azimuth]
204205> :azimuth|watch 'http://0.0.0.0:8545' %local
206+> :azimuth %resub
205207> :azimuth &azimuth-poke [%kick ~]
206208> |rein %base [& %roller] [& %roller-rpc] [& %azimuth-rpc]
207209> :roller|local
208210> :roller|setkey '58d62eb79797502bc0f66cd3e7a49d00287bff53a2734b799ef09cb746340ed0'
209211> :roller|quota 9.999
210−> |cors-approve 'https://127.0.0.1:3000'
212+:: If you run into issues, also add 'https://localhost:3000' the the eyre CORS approvals
213+> |eyre/cors/approve 'https://127.0.0.1:3000'
214+::
211215:: Once L2 txs have been sent via Bridge, this will manually commit and batch them, avoiding waiting for the timer
212216::
213217> :roller|commit
legal/privacy policy.md+11−11
@@ -1,7 +1,7 @@
11
2−Effective date: June 01, 2018
2+Effective date: December 11, 2025
33
4−Tlon Corporation ("us", "we", or "our") operates the website and the Urbit mobile application (the "Service").
4+The Urbit Foundation in cooperation with Tlon Corporation, ("us", "we", or "our") operate the bridge.urbit.org website (the "Service").
55
66This page informs you of our policies regarding the collection, use, and disclosure of personal data when you use our Service and the choices you have associated with that data.
77
@@ -11,7 +11,7 @@ We use your data to provide and improve the Service. By using the Service, you a
1111
1212**Service**
1313
14−Service means the website and the Urbit mobile application operated by Tlon Corporation
14+Service means the bridge.urbit.org website.
1515
1616**Personal Data**
1717
@@ -81,7 +81,7 @@ Security Cookies. We use Security Cookies for security purposes.
8181
8282**Use of Data**
8383
84−Tlon Corporation uses the collected data for various purposes:
84+We use the collected data for various purposes:
8585
8686- To provide and maintain our Service
8787
@@ -99,13 +99,13 @@ Tlon Corporation uses the collected data for various purposes:
9999
100100- Legal Basis for Processing Personal Data Under General Data Protection Regulation (GDPR)
101101
102−If you are from the European Economic Area (EEA), Tlon Corporation legal basis for collecting and using the personal information described in this Privacy Policy depends on the Personal Data we collect and the specific context in which we collect it.
102+If you are from the European Economic Area (EEA), our legal basis for collecting and using the personal information described in this Privacy Policy depends on the Personal Data we collect and the specific context in which we collect it.
103103
104−**Tlon Corporation may process your Personal Data because:**
104+**Tlon Corporation and The Urbit Foundation may process your Personal Data because:**
105105
106106- We need to perform a contract with you
107107
108−- You have given us permission to do so
108+- You have given us permission to do so as a part of using The Service,
109109
110110- The processing is in our legitimate interests and it's not overridden by your rights
111111
@@ -113,9 +113,9 @@ If you are from the European Economic Area (EEA), Tlon Corporation legal basis f
113113
114114- Retention of Data
115115
116−Tlon Corporation will retain your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
116+We will retain your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
117117
118−Tlon Corporation will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of our Service, or we are legally obligated to retain this data for longer time periods.
118+We will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of our Service, or we are legally obligated to retain this data for longer time periods.
119119
120120**Transfer of Data**
121121
@@ -131,7 +131,7 @@ Tlon Corporation will take all steps reasonably necessary to ensure that your da
131131
132132Legal Requirements
133133
134−Tlon Corporation may disclose your Personal Data in the good faith belief that such action is necessary to:
134+We may disclose your Personal Data in the good faith belief that such action is necessary to:
135135
136136- To comply with a legal obligation
137137
@@ -207,4 +207,4 @@ You are advised to review this Privacy Policy periodically for any changes. Chan
207207
208208If you have any questions about this Privacy Policy, please contact us:
209209
210−By email: support@urbit.org
210+By email: support@urbit.org & support@tlon.io
notes.mdnew file+1
@@ -0,0 +1 @@
1+WIP notes
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/Greeting.jsx+65−13
@@ -3,6 +3,15 @@ import cn from 'classnames';
33import ob from 'urbit-ob';
44import { Grid, Text, LinkButton } from 'indigo-react';
55
6+import {
7+ Box,
8+ Button,
9+ Checkbox,
10+ Icon,
11+ RadioButton,
12+ Row,
13+} from '@tlon/indigo-react';
14+
615import useWasGreeted from 'lib/useWasGreeted';
716
817const TEXT_STYLE = 'f5';
@@ -26,27 +35,70 @@ export default function ActivateDisclaimer({ point }) {
2635 <Text className={cn(TEXT_STYLE, 'block mb4')}>
2736 Welcome <span className="mono">{pointName}</span>,
2837 </Text>
29−
3038 <Text className={cn(TEXT_STYLE, 'block mb2')}>
31− As of this moment, you own a piece of Urbit. No one can take it from
32− you, and you can keep it for the rest of your life.
39+ As of this moment, you own a piece of Urbit.
40+ Keep your login details safe. No one can recover them for you.
3341 </Text>
34− <Text className={cn(TEXT_STYLE, 'block mb2')}>
35− Keep your Master Ticket safe. No one can recover it for you. But it
36− can get you back into Urbit at any time.
37− </Text>
38− <Text className={cn(TEXT_STYLE, 'block mb2')}>Right now you can:</Text>
42+ <Text className={cn(TEXT_STYLE, 'block mb2')}>Now that you own your identity, the next step is to run your own operating system:</Text>
3943 </Grid.Item>
4044
41− <Grid.Item full as={LinkButton} href="https://docs.urbit.org/get-on-urbit#get-the-urbit-runtime">
42− <Text className={cn(TEXT_STYLE, 'block mb2')}>
43− Boot Arvo, the Urbit OS
44− </Text>
45+ <Grid.Item full>
46+ <Box className="info-list">
47+ <Row className="info-row">
48+ <Box className="info-message">
49+ Comfortable with a command line?
50+ <br></br>
51+ <a
52+ style={{ fontWeight: 600 }}
53+ className="underline"
54+ target="_blank"
55+ rel="noreferrer"
56+ href="https://docs.urbit.org/get-on-urbit#get-the-urbit-runtime">
57+ Run it using Vere↗
58+ </a>
59+ </Box>
60+ </Row>
61+ <Row className="info-row">
62+ <Box className="info-message">
63+ Graphical interfaces more your speed?
64+
65+ <br></br>
66+ <a
67+ style={{ fontWeight: 600 }}
68+ className="underline"
69+ target="_blank"
70+ rel="noreferrer"
71+ href="https://nativeplanet.io/software">
72+ Check out Groundseg↗
73+ </a>
74+ </Box>
75+ </Row>
76+ <Row className="info-row">
77+ <Box className="info-message">
78+ Want a dedicated Urbit device to do it all for you?
79+
80+ <br></br>
81+ <a
82+ style={{ fontWeight: 600 }}
83+ className="underline"
84+ target="_blank"
85+ rel="noreferrer"
86+ href="https://nativeplanet.io/hardware">
87+ Get Native Planet hardware↗
88+ </a>
89+ </Box>
90+ </Row>
91+ <Row className="info-row">
92+ <Box style={{ fontStyle: 'italic' }} className="info-message">
93+ Not ready to self-host? Contact <a className="bold underline" href="mailto:support@tlon.io">support@tlon.io</a> to have them host your Urbit for you!
94+ </Box>
95+ </Row>
96+ </Box>
4597 </Grid.Item>
4698
4799 <Grid.Item full>
48100 <Text className={cn(TEXT_STYLE, 'block mb4')}>
49− Welcome to Urbit. See you online.
101+ Welcome to Urbit. See you on the network.
50102 </Text>
51103 </Grid.Item>
52104 <Grid.Item full as={LinkButton} onClick={dismiss}>
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/flags.js+1
@@ -2,6 +2,7 @@ import { version } from '../../package.json';
22
33// are we running bridge in a development build
44export const isDevelopment = import.meta.env.DEV === true;
5+// TODO update to Sepolia
56export const isGoerli = import.meta.env.VITE_GOERLI === 'true';
67export const isMainnet = import.meta.env.VITE_MAINNET === 'true';
78export const versionLabel =
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/network.js+9−8
@@ -1,3 +1,4 @@
1+// TODO update to Sepolia from Goerli
12import { isDevelopment, isGoerli, isMainnet } from './flags';
23import {
34 ETHEREUM_GOERLI_CHAIN_ID,
@@ -16,12 +17,12 @@ const renderNetworkType = network =>
1617 network === NETWORK_TYPES.OFFLINE
1718 ? 'Offline'
1819 : network === NETWORK_TYPES.GOERLI
19− ? 'Goerli'
20− : network === NETWORK_TYPES.MAINNET
21− ? 'Main Network'
22− : network === NETWORK_TYPES.LOCAL
23− ? 'Local Node'
24− : 'Offline';
20+ ? 'Goerli'
21+ : network === NETWORK_TYPES.MAINNET
22+ ? 'Main Network'
23+ : network === NETWORK_TYPES.LOCAL
24+ ? 'Local Node'
25+ : 'Offline';
2526
2627const chainIdToNetworkType = chainId => {
2728 switch (chainId) {
@@ -38,8 +39,8 @@ const expectedChainId = () => {
3839 return isGoerli
3940 ? ETHEREUM_GOERLI_CHAIN_ID
4041 : isDevelopment && !isMainnet
41− ? ETHEREUM_LOCAL_CHAIN_ID
42− : ETHEREUM_MAINNET_CHAIN_ID;
42+ ? ETHEREUM_LOCAL_CHAIN_ID
43+ : ETHEREUM_MAINNET_CHAIN_ID;
4344};
4445
4546export {
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/lib/useRoller.ts+65−46
@@ -145,16 +145,17 @@ export default function useRoller() {
145145 }
146146
147147 const pointNum = Number(point);
148− const rawDetails = await api.getPoint(pointNum);
149− const isL2 = isL2Spawn(rawDetails?.dominion);
150−
151− const details = toL1Details(rawDetails);
152148
153149 try {
154− if (isL2) {
155− const l2Quota = isL2 ? await api.getRemainingQuota(pointNum) : 0;
156− const l2Allowance = isL2 ? await api.getAllowance(pointNum) : 0;
150+ // Try fetching from the roller
151+ const rawDetails = await api.getPoint(pointNum); // this fails for newly spawned L1 point until roller updates
152+ const details = toL1Details(rawDetails);
153+ const isL2 = isL2Spawn(rawDetails?.dominion);
157154
155+ if (isL2) {
156+ // Scenario 1: L2 point exists in roller state - use roller data
157+ const l2Quota = await api.getRemainingQuota(pointNum);
158+ const l2Allowance = await api.getAllowance(pointNum);
158159 return new Point({
159160 value: pointNum,
160161 details,
@@ -162,30 +163,48 @@ export default function useRoller() {
162163 l2Quota,
163164 l2Allowance,
164165 });
166+ } else {
167+ // Scenario 2: L1 point exists in roller state - use combination of chain and roller data
168+ const _contracts = need.contracts(contracts);
169+ const l1Details = await azimuth.azimuth.getPoint(
170+ _contracts,
171+ pointNum
172+ );
173+ // Override with roller data for accuracy, L1 data here could be stale
174+ l1Details.sponsor = details.sponsor;
175+ l1Details.escapeRequested = details.escapeRequested;
176+ l1Details.escapeRequestedTo = details.escapeRequestedTo;
177+ return new Point({
178+ value: pointNum,
179+ details: l1Details,
180+ address: _wallet.address,
181+ });
165182 }
166−
167− const _contracts = need.contracts(contracts);
168− const l1Details = await azimuth.azimuth.getPoint(_contracts, point);
169−
170− l1Details.sponsor = details.sponsor;
171− l1Details.escapeRequested = details.escapeRequested;
172− l1Details.escapeRequestedTo = details.escapeRequestedTo;
173−
174− return new Point({
175− value: pointNum,
176− details: l1Details,
177− address: _wallet.address,
178− });
179183 } catch (e) {
180− console.warn(e);
181− // Just return a placeholder Point
182− const details: L1Point = toL1Details();
183− return new Point({
184− value: pointNum,
185− details,
186− address: _wallet.address,
187− isPlaceholder: true,
188− });
184+ // roller failed to fetch point
185+ try {
186+ // Scenario 3: Newly spawned L1 point - not in roller state yet, use chain data only
187+ const _contracts = need.contracts(contracts);
188+ const l1Details = await azimuth.azimuth.getPoint(
189+ _contracts,
190+ pointNum
191+ );
192+ return new Point({
193+ value: pointNum,
194+ details: l1Details,
195+ address: _wallet.address,
196+ });
197+ } catch (e2) {
198+ // Scenario 4: Some other issue, point doesn’t exist on chain or in roller state. Just return placeholder.
199+ console.warn('Failed to fetch point data:', e2);
200+ const details: L1Point = toL1Details();
201+ return new Point({
202+ value: pointNum,
203+ details,
204+ address: _wallet.address,
205+ isPlaceholder: true,
206+ });
207+ }
189208 }
190209 },
191210 [api, wallet, contracts]
@@ -309,14 +328,14 @@ export default function useRoller() {
309328 proxy === 'own'
310329 ? await api.getOwnedPoints(address)
311330 : proxy === 'manage'
312− ? await api.getManagerFor(address)
313− : proxy === 'vote'
314− ? await api.getVotingFor(address)
315− : proxy === 'transfer'
316− ? await api.getTransferringFor(address)
317− : proxy === 'spawn'
318− ? await api.getSpawningFor(address)
319− : [];
331+ ? await api.getManagerFor(address)
332+ : proxy === 'vote'
333+ ? await api.getVotingFor(address)
334+ : proxy === 'transfer'
335+ ? await api.getTransferringFor(address)
336+ : proxy === 'spawn'
337+ ? await api.getSpawningFor(address)
338+ : [];
320339
321340 return points;
322341 },
@@ -365,14 +384,14 @@ export default function useRoller() {
365384 const networkSeed = customNetworkSeed
366385 ? customNetworkSeed
367386 : await attemptNetworkSeedDerivation({
368− urbitWallet,
369− wallet,
370− authMnemonic,
371− details: point,
372− authToken,
373− point: point.value,
374− revision: nextRevision,
375− });
387+ urbitWallet,
388+ wallet,
389+ authMnemonic,
390+ details: point,
391+ authToken,
392+ point: point.value,
393+ revision: nextRevision,
394+ });
376395 const txHash = await submitL2Transaction({
377396 api,
378397 wallet: _wallet,
@@ -422,7 +441,7 @@ export default function useRoller() {
422441 let nonce = await api.getNonce({ ship: point, proxy });
423442 const progress = onUpdate
424443 ? (state: number) => onUpdate({ type: 'progress', state })
425− : () => {};
444+ : () => { };
426445
427446 let requests = [];
428447
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/Login/LoginSelector.tsx+9−3
@@ -105,9 +105,15 @@ export default function LoginSelector({
105105 if (expectedChainId() === ETHEREUM_LOCAL_CHAIN_ID) {
106106 await window.ethereum.request({
107107 method: "wallet_addEthereumChain",
108− params: [{chainId: ETHEREUM_LOCAL_CHAIN_ID,
109− chainName: "Ganache",
110− rpcUrls: ["http://127.0.0.1:8545"]
108+ params: [{
109+ chainId: ETHEREUM_LOCAL_CHAIN_ID,
110+ chainName: "Ganache",
111+ rpcUrls: ["http://127.0.0.1:8545"],
112+ nativeCurrency: {
113+ name: "Ether",
114+ symbol: "ETH",
115+ decimals: 18
116+ }
111117 }]
112118 });
113119 }
src/views/Point/MigrateL2.tsx+42−26
@@ -85,11 +85,11 @@ const useMigrate = () => {
8585 setSelectedPoint(selectedPoint);
8686 return transfer || isPlanet(selectedPoint)
8787 ? ecliptic.transferPoint(
88− _contracts,
89− selectedPoint,
90− DUMMY_L2_ADDRESS,
91− false
92− )
88+ _contracts,
89+ selectedPoint,
90+ DUMMY_L2_ADDRESS,
91+ false
92+ )
9393 : ecliptic.setSpawnProxy(_contracts, selectedPoint, DUMMY_L2_ADDRESS);
9494 },
9595 [_contracts]
@@ -245,10 +245,25 @@ export default function MigrateL2() {
245245 )}
246246 {isStar && (
247247 <Box className="mt4">
248− Transferring this point will allow you to conduct all
249− transactions on Layer 2. Setting the spawn proxy of a star to
250− Layer 2 will allow you to create planet invites for free, but
251− will require you to do all other transactions on Layer 1.
248+ <div className="bold">Transferring ship</div>
249+ Allows you to conduct all
250+ transactions subsidized on Layer 2, with the tradeoff
251+ of lost visibility to the Ethereum network.
252+
253+ <br></br>
254+ <br></br>
255+ <div className="bold">Set spawn proxy</div>
256+ Moves spawning functionality to Layer 2
257+ enabling subsizided planet creation
258+ while retaining visibility to the Ethereum network.
259+ <br></br>
260+ <a
261+ className="bold underline"
262+ target="_blank"
263+ rel="noreferrer"
264+ href="https://docs.urbit.org/user-manual/id/l2-star">
265+ Learn More about the L2 for stars↗
266+ </a>
252267 </Box>
253268 )}
254269 {isGalaxy && (
@@ -256,15 +271,17 @@ export default function MigrateL2() {
256271 Setting the spawn proxy of this galaxy to Layer 2 will allow you
257272 to spawn stars on Layer 2 for free, but will still require you
258273 to do all other transactions on Layer 1.
274+ <br></br>
275+ <br></br>
276+ <div className="bold">MIGRATING YOUR GALAXY'S SPAWN PROXY IS NOT RECOMMENDED, AND IS IRREVERSIBLE.</div>
259277 </Box>
260278 )}
261279 </Box>
262280 <Grid.Item
263281 full
264282 as={InlineEthereumTransaction}
265− label={`${
266− transfer ? 'Transfer ship' : 'Set spawn proxy'
267− } to Layer 2`}
283+ label={`${transfer ? 'Transfer ship' : 'Set spawn proxy'
284+ } to Layer 2`}
268285 {...bind}
269286 onReturn={() => pop()}
270287 />
@@ -275,16 +292,8 @@ export default function MigrateL2() {
275292 return (
276293 <Box className="content">
277294 <Box className="message">
278− You are about to migrate a node from Layer 1 to Layer 2. This will
279− make transactions faster and cheaper. Here are some things you should
280− know.{' '}
281− <a
282− className="bold"
283− target="_blank"
284− rel="noreferrer"
285− href="https://docs.urbit.org/user-manual/id/layer-2-for-planets">
286− Learn More
287− </a>
295+ You are about to migrate an Urbit ID from Layer 1 Ethereum to the Layer 2 rollup. This will
296+ make transactions cheaper. Here are some things you should know.{' '}
288297 </Box>
289298 {!hideInfo && (
290299 <Box className="info-list">
@@ -305,9 +314,9 @@ export default function MigrateL2() {
305314 <Box className="icon">$</Box>
306315 </Box>
307316 <Box className="info-message">
308− A migration is expensive, but a one-time fee. The estimated
309− price is <span className="bold">$120</span>. Transaction fees
310− will be subsidized on Layer 2.
317+ Migration incurs a one-time fee. The estimated
318+ price is generally less than <span className="bold">$10</span>. Transaction fees
319+ are subsidized by Tlon on Layer 2, making them effectively free.
311320 </Box>
312321 </Row>
313322 <Row className="info-row">
@@ -316,10 +325,17 @@ export default function MigrateL2() {
316325 </Box>
317326 {/* <HistoryIcon className="icon" /> */}
318327 <Box className="info-message">
319− All transactions will now be pushed at the end of the day. A
328+ Subsidized Layer 2 transactions are batched, generally hourly. A
320329 timer will show when the next push is.
321330 </Box>
322331 </Row>
332+ <a
333+ className="bold underline"
334+ target="_blank"
335+ rel="noreferrer"
336+ href="https://docs.urbit.org/user-manual/id/layer-2-for-planets">
337+ Learn More↗
338+ </a>
323339 </Box>
324340 )}
325341 {!hideInfo && (
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+}
vite.config.ts+8−7
@@ -1,17 +1,18 @@
11import { defineConfig } from 'vite'
2+import GlobalPolyFill from "@esbuild-plugins/node-globals-polyfill";
3+import nodePolyfills from 'rollup-plugin-node-polyfills';
4+import inject from '@rollup/plugin-inject'
25import react from '@vitejs/plugin-react'
3−import comlink from 'vite-plugin-comlink'
6+import svgr from 'vite-plugin-svgr';
7+import { comlink } from 'vite-plugin-comlink'
8+import { resolve } from 'path';
49import basicSsl from '@vitejs/plugin-basic-ssl'
5−import svgr from 'vite-plugin-svgr'
6−import { nodePolyfills } from 'vite-plugin-node-polyfills'
7−import GlobalPolyFill from '@esbuild-plugins/node-globals-polyfill'
8−import inject from '@rollup/plugin-inject'
910import fs from 'fs'
1011import path from 'path'
11−import { resolve } from 'path'
1212
13+// https://vitejs.dev/config/
1314export default defineConfig(({ mode }) => {
14− // Try to use mkcert certificates first
15+
1516 const certPath = path.resolve(__dirname, 'localhost+2.pem')
1617 const keyPath = path.resolve(__dirname, 'localhost+2-key.pem')
1718