on master help sign in
.gitignore+1
since splitsnapshots
unifiedsplit
@@ -11,6 +11,7 @@
11
11
12# misc
12# misc
13.DS_Store
13.DS_Store
14+.env
14.env.local
15.env.local
15.env.development.local
16.env.development.local
16.env.test.local
17.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
200:: Run these in the dojo:
200:: Run these in the dojo:
201::
201::
202> |mount /=base=
202> |mount /=base=
203−> :azimuth %resub
203+> |nuke %azimuth
204+> |rein %base [%.y %azimuth]
204> :azimuth|watch 'http://0.0.0.0:8545' %local
205> :azimuth|watch 'http://0.0.0.0:8545' %local
206+> :azimuth %resub
205> :azimuth &azimuth-poke [%kick ~]
207> :azimuth &azimuth-poke [%kick ~]
206> |rein %base [& %roller] [& %roller-rpc] [& %azimuth-rpc]
208> |rein %base [& %roller] [& %roller-rpc] [& %azimuth-rpc]
207> :roller|local
209> :roller|local
208> :roller|setkey '58d62eb79797502bc0f66cd3e7a49d00287bff53a2734b799ef09cb746340ed0'
210> :roller|setkey '58d62eb79797502bc0f66cd3e7a49d00287bff53a2734b799ef09cb746340ed0'
209> :roller|quota 9.999
211> :roller|quota 9.999
212+:: If you run into issues, also add 'https://localhost:3000' the the eyre CORS approvals
210−> |cors-approve 'https://127.0.0.1:3000'
213+> |eyre/cors/approve 'https://127.0.0.1:3000'
214+::
211:: Once L2 txs have been sent via Bridge, this will manually commit and batch them, avoiding waiting for the timer
215:: Once L2 txs have been sent via Bridge, this will manually commit and batch them, avoiding waiting for the timer
212::
216::
213> :roller|commit
217> :roller|commit
legal/privacy policy.md+11−11
@@ -1,7 +1,7 @@
1
1
2−Effective date: June 01, 2018
2+Effective date: December 11, 2025
3
3
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").
5
5
6This 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.
6This 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.
7
7
@@ -11,7 +11,7 @@ We use your data to provide and improve the Service. By using the Service, you a
11
11
12**Service**
12**Service**
13
13
14−Service means the website and the Urbit mobile application operated by Tlon Corporation
14+Service means the bridge.urbit.org website.
15
15
16**Personal Data**
16**Personal Data**
17
17
@@ -81,7 +81,7 @@ Security Cookies. We use Security Cookies for security purposes.
81
81
82**Use of Data**
82**Use of Data**
83
83
84−Tlon Corporation uses the collected data for various purposes:
84+We use the collected data for various purposes:
85
85
86- To provide and maintain our Service
86- To provide and maintain our Service
87
87
@@ -99,13 +99,13 @@ Tlon Corporation uses the collected data for various purposes:
99
99
100- Legal Basis for Processing Personal Data Under General Data Protection Regulation (GDPR)
100- Legal Basis for Processing Personal Data Under General Data Protection Regulation (GDPR)
101
101
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.
103
103
104−**Tlon Corporation may process your Personal Data because:**
104+**Tlon Corporation and The Urbit Foundation may process your Personal Data because:**
105
105
106- We need to perform a contract with you
106- We need to perform a contract with you
107
107
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,
109
109
110- The processing is in our legitimate interests and it's not overridden by your rights
110- The processing is in our legitimate interests and it's not overridden by your rights
111
111
@@ -113,9 +113,9 @@ If you are from the European Economic Area (EEA), Tlon Corporation legal basis f
113
113
114- Retention of Data
114- Retention of Data
115
115
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.
117
117
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.
119
119
120**Transfer of Data**
120**Transfer of Data**
121
121
@@ -131,7 +131,7 @@ Tlon Corporation will take all steps reasonably necessary to ensure that your da
131
131
132Legal Requirements
132Legal Requirements
133
133
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:
135
135
136- To comply with a legal obligation
136- To comply with a legal obligation
137
137
@@ -207,4 +207,4 @@ You are advised to review this Privacy Policy periodically for any changes. Chan
207
207
208If you have any questions about this Privacy Policy, please contact us:
208If you have any questions about this Privacy Policy, please contact us:
209
209
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-lock.json+2−2
@@ -1,12 +1,12 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "3.2.14",
3+ "version": "3.3.0",
4 "lockfileVersion": 2,
4 "lockfileVersion": 2,
5 "requires": true,
5 "requires": true,
6 "packages": {
6 "packages": {
7 "": {
7 "": {
8 "name": "bridge",
8 "name": "bridge",
9− "version": "3.2.14",
9+ "version": "3.3.0",
10 "license": "MIT",
10 "license": "MIT",
11 "dependencies": {
11 "dependencies": {
12 "@ethereumjs/common": "^3.0.0",
12 "@ethereumjs/common": "^3.0.0",
package.json+2−2
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "3.2.14",
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",
@@ -8,7 +8,7 @@
8 },
8 },
9 "main": "src/index.js",
9 "main": "src/index.js",
10 "private": true,
10 "private": true,
11− "author": "Tlon",
11+ "author": "The Urbit Foundation",
12 "license": "MIT",
12 "license": "MIT",
13 "engines": {
13 "engines": {
14 "node": "16.18.0"
14 "node": "16.18.0"
src/Bridge.tsx+12−5
@@ -20,6 +20,9 @@ import light from '@tlon/indigo-light';
20
20
21import WithErrorBoundary from 'components/WithErrorBoundary';
21import WithErrorBoundary from 'components/WithErrorBoundary';
22import GlobalErrorBoundary from 'components/GlobalErrorBoundary';
22import GlobalErrorBoundary from 'components/GlobalErrorBoundary';
23+import { useBrowser } from 'lib/useBrowser';
24+import { BrowserWarning } from 'components/BrowserWarning';
25+
23import Modal from 'components/L2/Modal';
26import Modal from 'components/L2/Modal';
24import { useRollerStore } from 'store/rollerStore';
27import { useRollerStore } from 'store/rollerStore';
25import { Box } from '@tlon/indigo-react';
28import { Box } from '@tlon/indigo-react';
@@ -29,8 +32,8 @@ import { useRollerPoller } from 'lib/useRollerPoller';
29const INITIAL_NETWORK_TYPE = isGoerli
32const INITIAL_NETWORK_TYPE = isGoerli
30 ? NETWORK_TYPES.GOERLI
33 ? NETWORK_TYPES.GOERLI
31 : isDevelopment && !isMainnet
34 : isDevelopment && !isMainnet
32− ? NETWORK_TYPES.LOCAL
35+ ? NETWORK_TYPES.LOCAL
33− : NETWORK_TYPES.MAINNET;
36+ : NETWORK_TYPES.MAINNET;
34
37
35// NB(shrugs): modify these variables to change the default local state.
38// NB(shrugs): modify these variables to change the default local state.
36const SHOULD_STUB_LOCAL = import.meta.env.VITE_STUB_LOCAL === 'true';
39const SHOULD_STUB_LOCAL = import.meta.env.VITE_STUB_LOCAL === 'true';
@@ -38,9 +41,9 @@ const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
38
41
39const INITIAL_WALLET = IS_STUBBED
42const INITIAL_WALLET = IS_STUBBED
40 ? walletFromMnemonic(
43 ? walletFromMnemonic(
41− import.meta.env.VITE_DEV_MNEMONIC as string,
44+ import.meta.env.VITE_DEV_MNEMONIC as string,
42− import.meta.env.VITE_HD_PATH as string
45+ import.meta.env.VITE_HD_PATH as string
43− )
46+ )
44 : undefined;
47 : undefined;
45const INITIAL_MNEMONIC = IS_STUBBED
48const INITIAL_MNEMONIC = IS_STUBBED
46 ? Just(import.meta.env.VITE_DEV_MNEMONIC)
49 ? Just(import.meta.env.VITE_DEV_MNEMONIC)
@@ -82,6 +85,9 @@ function Bridge() {
82 const initialRoutes = useInitialRoutes();
85 const initialRoutes = useInitialRoutes();
83 const { loading, modalText, setModalText } = useRollerStore();
86 const { loading, modalText, setModalText } = useRollerStore();
84
87
88+ const browser = useBrowser();
89+ const showBrowserWarning = browser && !browser.isChromium;
90+
85 // full reload if the user changes their selected network in Metamask
91 // full reload if the user changes their selected network in Metamask
86 //
92 //
87 useEffect(() => {
93 useEffect(() => {
@@ -103,6 +109,7 @@ function Bridge() {
103 initialWallet={INITIAL_WALLET}
109 initialWallet={INITIAL_WALLET}
104 initialMnemonic={INITIAL_MNEMONIC}
110 initialMnemonic={INITIAL_MNEMONIC}
105 initialPointCursor={INITIAL_POINT_CURSOR}>
111 initialPointCursor={INITIAL_POINT_CURSOR}>
112+ <BrowserWarning show={!!showBrowserWarning} />
106 <IndigoApp>
113 <IndigoApp>
107 <ThemeProvider theme={light}>
114 <ThemeProvider theme={light}>
108 <Router />
115 <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';
3import ob from 'urbit-ob';
3import ob from 'urbit-ob';
4import { Grid, Text, LinkButton } from 'indigo-react';
4import { Grid, Text, LinkButton } from 'indigo-react';
5
5
6+import {
7+ Box,
8+ Button,
9+ Checkbox,
10+ Icon,
11+ RadioButton,
12+ Row,
13+} from '@tlon/indigo-react';
14+
6import useWasGreeted from 'lib/useWasGreeted';
15import useWasGreeted from 'lib/useWasGreeted';
7
16
8const TEXT_STYLE = 'f5';
17const TEXT_STYLE = 'f5';
@@ -26,27 +35,70 @@ export default function ActivateDisclaimer({ point }) {
26 <Text className={cn(TEXT_STYLE, 'block mb4')}>
35 <Text className={cn(TEXT_STYLE, 'block mb4')}>
27 Welcome <span className="mono">{pointName}</span>,
36 Welcome <span className="mono">{pointName}</span>,
28 </Text>
37 </Text>
29−
30 <Text className={cn(TEXT_STYLE, 'block mb2')}>
38 <Text className={cn(TEXT_STYLE, 'block mb2')}>
31− As of this moment, you own a piece of Urbit. No one can take it from
39+ As of this moment, you own a piece of Urbit.
32− you, and you can keep it for the rest of your life.
40+ Keep your login details safe. No one can recover them for you.
33 </Text>
41 </Text>
34− <Text className={cn(TEXT_STYLE, 'block mb2')}>
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>
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>
39 </Grid.Item>
43 </Grid.Item>
40
44
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"
41− <Grid.Item full as={LinkButton} href="https://docs.urbit.org/get-on-urbit#get-the-urbit-runtime">
56+ href="https://docs.urbit.org/get-on-urbit#get-the-urbit-runtime">
42− <Text className={cn(TEXT_STYLE, 'block mb2')}>
57+ Run it using Vere↗
43− Boot Arvo, the Urbit OS
58+ </a>
44− </Text>
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>
45 </Grid.Item>
97 </Grid.Item>
46
98
47 <Grid.Item full>
99 <Grid.Item full>
48 <Text className={cn(TEXT_STYLE, 'block mb4')}>
100 <Text className={cn(TEXT_STYLE, 'block mb4')}>
49− Welcome to Urbit. See you online.
101+ Welcome to Urbit. See you on the network.
50 </Text>
102 </Text>
51 </Grid.Item>
103 </Grid.Item>
52 <Grid.Item full as={LinkButton} onClick={dismiss}>
104 <Grid.Item full as={LinkButton} onClick={dismiss}>
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/components/L2/Headers/L2PointHeader.tsx−3
@@ -92,9 +92,6 @@ const L2PointHeader = ({
92 <Row>
92 <Row>
93 <Row className="info">
93 <Row className="info">
94 {renderInviteButton()}
94 {renderInviteButton()}
95− <Box className="history" onClick={goToHistory}>
96− History
97− </Box>
98 {points && <InviteConverter points={points} />}
95 {points && <InviteConverter points={points} />}
99 </Row>
96 </Row>
100 </Row>
97 </Row>
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/flags.js+1
@@ -2,6 +2,7 @@ import { version } from '../../package.json';
2
2
3// are we running bridge in a development build
3// are we running bridge in a development build
4export const isDevelopment = import.meta.env.DEV === true;
4export const isDevelopment = import.meta.env.DEV === true;
5+// TODO update to Sepolia
5export const isGoerli = import.meta.env.VITE_GOERLI === 'true';
6export const isGoerli = import.meta.env.VITE_GOERLI === 'true';
6export const isMainnet = import.meta.env.VITE_MAINNET === 'true';
7export const isMainnet = import.meta.env.VITE_MAINNET === 'true';
7export const versionLabel =
8export const versionLabel =
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/network.js+9−8
@@ -1,3 +1,4 @@
1+// TODO update to Sepolia from Goerli
1import { isDevelopment, isGoerli, isMainnet } from './flags';
2import { isDevelopment, isGoerli, isMainnet } from './flags';
2import {
3import {
3 ETHEREUM_GOERLI_CHAIN_ID,
4 ETHEREUM_GOERLI_CHAIN_ID,
@@ -16,12 +17,12 @@ const renderNetworkType = network =>
16 network === NETWORK_TYPES.OFFLINE
17 network === NETWORK_TYPES.OFFLINE
17 ? 'Offline'
18 ? 'Offline'
18 : network === NETWORK_TYPES.GOERLI
19 : network === NETWORK_TYPES.GOERLI
19− ? 'Goerli'
20+ ? 'Goerli'
20− : network === NETWORK_TYPES.MAINNET
21+ : network === NETWORK_TYPES.MAINNET
21− ? 'Main Network'
22+ ? 'Main Network'
22− : network === NETWORK_TYPES.LOCAL
23+ : network === NETWORK_TYPES.LOCAL
23− ? 'Local Node'
24+ ? 'Local Node'
24− : 'Offline';
25+ : 'Offline';
25
26
26const chainIdToNetworkType = chainId => {
27const chainIdToNetworkType = chainId => {
27 switch (chainId) {
28 switch (chainId) {
@@ -38,8 +39,8 @@ const expectedChainId = () => {
38 return isGoerli
39 return isGoerli
39 ? ETHEREUM_GOERLI_CHAIN_ID
40 ? ETHEREUM_GOERLI_CHAIN_ID
40 : isDevelopment && !isMainnet
41 : isDevelopment && !isMainnet
41− ? ETHEREUM_LOCAL_CHAIN_ID
42+ ? ETHEREUM_LOCAL_CHAIN_ID
42− : ETHEREUM_MAINNET_CHAIN_ID;
43+ : ETHEREUM_MAINNET_CHAIN_ID;
43};
44};
44
45
45export {
46export {
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/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';
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),
src/lib/useRoller.ts+65−46
@@ -145,16 +145,17 @@ export default function useRoller() {
145 }
145 }
146
146
147 const pointNum = Number(point);
147 const pointNum = Number(point);
148− const rawDetails = await api.getPoint(pointNum);
149− const isL2 = isL2Spawn(rawDetails?.dominion);
150−
151− const details = toL1Details(rawDetails);
152
148
153 try {
149 try {
154− if (isL2) {
150+ // Try fetching from the roller
155− const l2Quota = isL2 ? await api.getRemainingQuota(pointNum) : 0;
151+ const rawDetails = await api.getPoint(pointNum); // this fails for newly spawned L1 point until roller updates
156− const l2Allowance = isL2 ? await api.getAllowance(pointNum) : 0;
152+ const details = toL1Details(rawDetails);
153+ const isL2 = isL2Spawn(rawDetails?.dominion);
157
154
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);
158 return new Point({
159 return new Point({
159 value: pointNum,
160 value: pointNum,
160 details,
161 details,
@@ -162,30 +163,48 @@ export default function useRoller() {
162 l2Quota,
163 l2Quota,
163 l2Allowance,
164 l2Allowance,
164 });
165 });
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+ });
165 }
182 }
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− });
179 } catch (e) {
183 } catch (e) {
180− console.warn(e);
184+ // roller failed to fetch point
181− // Just return a placeholder 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);
182− const details: L1Point = toL1Details();
200+ const details: L1Point = toL1Details();
183− return new Point({
201+ return new Point({
184− value: pointNum,
202+ value: pointNum,
185− details,
203+ details,
186− address: _wallet.address,
204+ address: _wallet.address,
187− isPlaceholder: true,
205+ isPlaceholder: true,
188− });
206+ });
207+ }
189 }
208 }
190 },
209 },
191 [api, wallet, contracts]
210 [api, wallet, contracts]
@@ -309,14 +328,14 @@ export default function useRoller() {
309 proxy === 'own'
328 proxy === 'own'
310 ? await api.getOwnedPoints(address)
329 ? await api.getOwnedPoints(address)
311 : proxy === 'manage'
330 : proxy === 'manage'
312− ? await api.getManagerFor(address)
331+ ? await api.getManagerFor(address)
313− : proxy === 'vote'
332+ : proxy === 'vote'
314− ? await api.getVotingFor(address)
333+ ? await api.getVotingFor(address)
315− : proxy === 'transfer'
334+ : proxy === 'transfer'
316− ? await api.getTransferringFor(address)
335+ ? await api.getTransferringFor(address)
317− : proxy === 'spawn'
336+ : proxy === 'spawn'
318− ? await api.getSpawningFor(address)
337+ ? await api.getSpawningFor(address)
319− : [];
338+ : [];
320
339
321 return points;
340 return points;
322 },
341 },
@@ -365,14 +384,14 @@ export default function useRoller() {
365 const networkSeed = customNetworkSeed
384 const networkSeed = customNetworkSeed
366 ? customNetworkSeed
385 ? customNetworkSeed
367 : await attemptNetworkSeedDerivation({
386 : await attemptNetworkSeedDerivation({
368− urbitWallet,
387+ urbitWallet,
369− wallet,
388+ wallet,
370− authMnemonic,
389+ authMnemonic,
371− details: point,
390+ details: point,
372− authToken,
391+ authToken,
373− point: point.value,
392+ point: point.value,
374− revision: nextRevision,
393+ revision: nextRevision,
375− });
394+ });
376 const txHash = await submitL2Transaction({
395 const txHash = await submitL2Transaction({
377 api,
396 api,
378 wallet: _wallet,
397 wallet: _wallet,
@@ -422,7 +441,7 @@ export default function useRoller() {
422 let nonce = await api.getNonce({ ship: point, proxy });
441 let nonce = await api.getNonce({ ship: point, proxy });
423 const progress = onUpdate
442 const progress = onUpdate
424 ? (state: number) => onUpdate({ type: 'progress', state })
443 ? (state: number) => onUpdate({ type: 'progress', state })
425− : () => {};
444+ : () => { };
426
445
427 let requests = [];
446 let requests = [];
428
447
src/views/Activate/ActivateView.scss+31
@@ -1,3 +1,34 @@
1.activate-view {
1.activate-view {
2 min-height: 440px;
2 min-height: 440px;
3}
3}
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({
60 </Box>
60 </Box>
61 </BodyPane>
61 </BodyPane>
62 </Window>
62 </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>
63 </Grid.Item>
79 </Grid.Item>
64 </Grid>
80 </Grid>
65 );
81 );
src/views/Login/LoginSelector.tsx+9−3
@@ -105,9 +105,15 @@ export default function LoginSelector({
105 if (expectedChainId() === ETHEREUM_LOCAL_CHAIN_ID) {
105 if (expectedChainId() === ETHEREUM_LOCAL_CHAIN_ID) {
106 await window.ethereum.request({
106 await window.ethereum.request({
107 method: "wallet_addEthereumChain",
107 method: "wallet_addEthereumChain",
108+ params: [{
108− params: [{chainId: ETHEREUM_LOCAL_CHAIN_ID,
109+ chainId: ETHEREUM_LOCAL_CHAIN_ID,
109− chainName: "Ganache",
110+ chainName: "Ganache",
110− rpcUrls: ["http://127.0.0.1:8545"]
111+ rpcUrls: ["http://127.0.0.1:8545"],
112+ nativeCurrency: {
113+ name: "Ether",
114+ symbol: "ETH",
115+ decimals: 18
116+ }
111 }]
117 }]
112 });
118 });
113 }
119 }
src/views/Point/MigrateL2.tsx+42−26
@@ -85,11 +85,11 @@ const useMigrate = () => {
85 setSelectedPoint(selectedPoint);
85 setSelectedPoint(selectedPoint);
86 return transfer || isPlanet(selectedPoint)
86 return transfer || isPlanet(selectedPoint)
87 ? ecliptic.transferPoint(
87 ? ecliptic.transferPoint(
88− _contracts,
88+ _contracts,
89− selectedPoint,
89+ selectedPoint,
90− DUMMY_L2_ADDRESS,
90+ DUMMY_L2_ADDRESS,
91− false
91+ false
92− )
92+ )
93 : ecliptic.setSpawnProxy(_contracts, selectedPoint, DUMMY_L2_ADDRESS);
93 : ecliptic.setSpawnProxy(_contracts, selectedPoint, DUMMY_L2_ADDRESS);
94 },
94 },
95 [_contracts]
95 [_contracts]
@@ -245,10 +245,25 @@ export default function MigrateL2() {
245 )}
245 )}
246 {isStar && (
246 {isStar && (
247 <Box className="mt4">
247 <Box className="mt4">
248+ <div className="bold">Transferring ship</div>
248− Transferring this point will allow you to conduct all
249+ Allows you to conduct all
249− transactions on Layer 2. Setting the spawn proxy of a star to
250+ transactions subsidized on Layer 2, with the tradeoff
250− Layer 2 will allow you to create planet invites for free, but
251+ of lost visibility to the Ethereum network.
251− will require you to do all other transactions on Layer 1.
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>
252 </Box>
267 </Box>
253 )}
268 )}
254 {isGalaxy && (
269 {isGalaxy && (
@@ -256,15 +271,17 @@ export default function MigrateL2() {
256 Setting the spawn proxy of this galaxy to Layer 2 will allow you
271 Setting the spawn proxy of this galaxy to Layer 2 will allow you
257 to spawn stars on Layer 2 for free, but will still require you
272 to spawn stars on Layer 2 for free, but will still require you
258 to do all other transactions on Layer 1.
273 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>
259 </Box>
277 </Box>
260 )}
278 )}
261 </Box>
279 </Box>
262 <Grid.Item
280 <Grid.Item
263 full
281 full
264 as={InlineEthereumTransaction}
282 as={InlineEthereumTransaction}
265− label={`${
266− transfer ? 'Transfer ship' : 'Set spawn proxy'
283+ label={`${transfer ? 'Transfer ship' : 'Set spawn proxy'
267− } to Layer 2`}
284+ } to Layer 2`}
268 {...bind}
285 {...bind}
269 onReturn={() => pop()}
286 onReturn={() => pop()}
270 />
287 />
@@ -275,16 +292,8 @@ export default function MigrateL2() {
275 return (
292 return (
276 <Box className="content">
293 <Box className="content">
277 <Box className="message">
294 <Box className="message">
278− You are about to migrate a node from Layer 1 to Layer 2. This will
295+ You are about to migrate an Urbit ID from Layer 1 Ethereum to the Layer 2 rollup. This will
279− make transactions faster and cheaper. Here are some things you should
296+ make transactions cheaper. Here are some things you should know.{' '}
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>
288 </Box>
297 </Box>
289 {!hideInfo && (
298 {!hideInfo && (
290 <Box className="info-list">
299 <Box className="info-list">
@@ -305,9 +314,9 @@ export default function MigrateL2() {
305 <Box className="icon">$</Box>
314 <Box className="icon">$</Box>
306 </Box>
315 </Box>
307 <Box className="info-message">
316 <Box className="info-message">
308− A migration is expensive, but a one-time fee. The estimated
317+ Migration incurs a one-time fee. The estimated
309− price is <span className="bold">$120</span>. Transaction fees
318+ price is generally less than <span className="bold">$10</span>. Transaction fees
310− will be subsidized on Layer 2.
319+ are subsidized by Tlon on Layer 2, making them effectively free.
311 </Box>
320 </Box>
312 </Row>
321 </Row>
313 <Row className="info-row">
322 <Row className="info-row">
@@ -316,10 +325,17 @@ export default function MigrateL2() {
316 </Box>
325 </Box>
317 {/* <HistoryIcon className="icon" /> */}
326 {/* <HistoryIcon className="icon" /> */}
318 <Box className="info-message">
327 <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
320 timer will show when the next push is.
329 timer will show when the next push is.
321 </Box>
330 </Box>
322 </Row>
331 </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>
323 </Box>
339 </Box>
324 )}
340 )}
325 {!hideInfo && (
341 {!hideInfo && (
src/views/UrbitOS/NetworkKeys.tsx+16−6
@@ -279,8 +279,8 @@ export default function UrbitOSNetworkKeys({
279 const viewTitle = completed
279 const viewTitle = completed
280 ? 'Network Keys have been set'
280 ? 'Network Keys have been set'
281 : hasKeys
281 : hasKeys
282− ? 'Set Network Keys'
282+ ? 'Set Network Keys'
283− : 'Initiate Network Keys';
283+ : 'Initiate Network Keys';
284
284
285 const bootMessage = () => (
285 const bootMessage = () => (
286 <Text as="p" textAlign={'center'}>
286 <Text as="p" textAlign={'center'}>
@@ -311,9 +311,19 @@ export default function UrbitOSNetworkKeys({
311
311
312 const waitMessage = () => (
312 const waitMessage = () => (
313 <Text as="p" textAlign={'center'}>
313 <Text as="p" textAlign={'center'}>
314− <strong>Important:</strong> it may take minutes to hours for the
314+ <strong>Important:</strong> It may take minutes to hours for the
315+ keys to become usable as they propogate across the network.
315− keys to become usable. Please be patient, trying to use the new keys
316+ Please be patient, trying to use the new keys
316− during this period may result in an invalid keys error.
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.
317 </Text>
327 </Text>
318 );
328 );
319
329
@@ -439,4 +449,4 @@ export default function UrbitOSNetworkKeys({
439 </BodyPane>
449 </BodyPane>
440 </Window>
450 </Window>
441 );
451 );
442−}
452+}
vite.config.ts+8−7
@@ -1,17 +1,18 @@
1import { defineConfig } from 'vite'
1import { 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'
2import react from '@vitejs/plugin-react'
5import react from '@vitejs/plugin-react'
6+import svgr from 'vite-plugin-svgr';
3−import comlink from 'vite-plugin-comlink'
7+import { comlink } from 'vite-plugin-comlink'
8+import { resolve } from 'path';
4import basicSsl from '@vitejs/plugin-basic-ssl'
9import 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'
9import fs from 'fs'
10import fs from 'fs'
10import path from 'path'
11import path from 'path'
11−import { resolve } from 'path'
12
12
13+// https://vitejs.dev/config/
13export default defineConfig(({ mode }) => {
14export default defineConfig(({ mode }) => {
14− // Try to use mkcert certificates first
15+
15 const certPath = path.resolve(__dirname, 'localhost+2.pem')
16 const certPath = path.resolve(__dirname, 'localhost+2.pem')
16 const keyPath = path.resolve(__dirname, 'localhost+2-key.pem')
17 const keyPath = path.resolve(__dirname, 'localhost+2-key.pem')
17
18