on master help sign in
Merge branch 'master' into lf/auth-token
94d4cefFang
@@ -1,2 +1,2 @@
1−REACT_APP_INFURA_ENDPOINT='b7d2af9f01534031ba773374f766ef65'
1+REACT_APP_INFURA_ENDPOINT='2599df54929b47099bda360958d75aaf'
2
2
.github/ISSUE_TEMPLATE/bridge-bug-reportnew file+40
@@ -0,0 +1,40 @@
1+---
2+name: Bridge Bug report
3+about: 'Use this template to file a bug for Bridge'
4+title: ''
5+labels: landscape
6+assignees: ''
7+
8+---
9+
10+**Describe the bug**
11+A clear and concise description of what the bug is.
12+
13+**To Reproduce**
14+Steps to reproduce the behavior:
15+1. Go to '...'
16+2. Click on '....'
17+3. Scroll down to '....'
18+4. See error
19+
20+**Expected behavior**
21+A clear and concise description of what you expected to happen.
22+
23+**Screenshots**
24+If applicable, add screenshots to help explain your problem. If possible, please also screenshot your browser's dev console. Here are [Chrome's docs](https://developers.google.com/web/tools/chrome-devtools/open) for using this feature.
25+
26+**Desktop (please complete the following information):**
27+ - OS: [e.g. MacOS 10.15.3]
28+ - Browser [e.g. chrome, safari]
29+
30+**Smartphone (please complete the following information):**
31+ - Device: [e.g. iPhone6]
32+ - OS: [e.g. iOS8.1]
33+ - Browser [e.g. stock browser, safari]
34+
35+**Login method (please complete the following information):**
36+How you were logged into bridge when the error occured. e.g. Metamask, Mnemonic, Master Ticket etc.
37+
38+
39+**Additional context**
40+Add any other context about the problem here.
migrations/1_migration.js+12
@@ -130,6 +130,18 @@ module.exports = async function(deployer) {
130 await ecliptic.spawn(131328, own);
130 await ecliptic.spawn(131328, own);
131 await ecliptic.spawn(512, own);
131 await ecliptic.spawn(512, own);
132 await sending.setPoolSize(256, 65792, 1000);
132 await sending.setPoolSize(256, 65792, 1000);
133+ case 'RESIDENTS':
134+ await ecliptic.createGalaxy(0, own);
135+ await ecliptic.configureKeys(0, '0x123', '0x456', 1, false);
136+ await ecliptic.spawn(256, own);
137+ await ecliptic.configureKeys(256, '0x456', '0x789', 1, false);
138+ for (let i = 2; i < 30; i++) {
139+ // await ecliptic.spawn(256 * i, user1);
140+ const offset = 65536 * i;
141+ await ecliptic.spawn(offset + 256, own);
142+ await ecliptic.transferPoint(offset + 256, user1, false);
143+ }
144+
133 default:
145 default:
134 return;
146 return;
135 }
147 }
package-lock.json+4−4
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "2.3.5",
3+ "version": "2.5.0",
4 "lockfileVersion": 1,
4 "lockfileVersion": 1,
5 "requires": true,
5 "requires": true,
6 "dependencies": {
6 "dependencies": {
@@ -2659,9 +2659,9 @@
2659 }
2659 }
2660 },
2660 },
2661 "azimuth-js": {
2661 "azimuth-js": {
2662− "version": "0.18.0",
2662+ "version": "0.19.0",
2663− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.18.0.tgz",
2663+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.19.0.tgz",
2664− "integrity": "sha512-i8mkXIfWN9GKQcd8KsjQNbf393HVOadIMN6v40KgRnMIjM6dMgUP5zoEbCsG2Zn0SgLRgw0kiNyXHGVitdumiw==",
2664+ "integrity": "sha512-8OYrzCMgKCatTNzoNccFkp7fB+4ozol+OUfEo3kJ8TLMZOadT2Zk6ThGVWK1FPZupNcMuZ+s9OI+pqjFIRhwPQ==",
2665 "requires": {
2665 "requires": {
2666 "azimuth-solidity": "1.2.1",
2666 "azimuth-solidity": "1.2.1",
2667 "ethereumjs-util": "^5.2.0",
2667 "ethereumjs-util": "^5.2.0",
package.json+4−2
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "2.3.5",
3+ "version": "2.5.0",
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",
@@ -13,13 +13,14 @@
13 "engines": {
13 "engines": {
14 "node": "11.0.0"
14 "node": "11.0.0"
15 },
15 },
16+ "proxy": "http://localhost:4000",
16 "dependencies": {
17 "dependencies": {
17 "@hot-loader/react-dom": "^16.8.6",
18 "@hot-loader/react-dom": "^16.8.6",
18 "@ledgerhq/hw-app-eth": "^5.5.0",
19 "@ledgerhq/hw-app-eth": "^5.5.0",
19 "@ledgerhq/hw-transport-u2f": "^5.5.0",
20 "@ledgerhq/hw-transport-u2f": "^5.5.0",
20 "@welldone-software/why-did-you-render": "^3.2.3",
21 "@welldone-software/why-did-you-render": "^3.2.3",
21 "async-retry": "^1.2.3",
22 "async-retry": "^1.2.3",
22− "azimuth-js": "^0.18.0",
23+ "azimuth-js": "^0.19.0",
23 "azimuth-solidity": "^1.2.0",
24 "azimuth-solidity": "^1.2.0",
24 "bip32": "^1.0.2",
25 "bip32": "^1.0.2",
25 "bip39": "^2.5.0",
26 "bip39": "^2.5.0",
@@ -86,6 +87,7 @@
86 "pilot": "HTTPS=true npm-run-all pilot:setup start pilot:cleanup --continue-on-error",
87 "pilot": "HTTPS=true npm-run-all pilot:setup start pilot:cleanup --continue-on-error",
87 "pilot-invites": "WITH_TEST_STATE=INVITES npm run pilot",
88 "pilot-invites": "WITH_TEST_STATE=INVITES npm run pilot",
88 "pilot-release": "WITH_TEST_STATE=STAR_RELEASE npm run pilot",
89 "pilot-release": "WITH_TEST_STATE=STAR_RELEASE npm run pilot",
90+ "pilot-residents": "WITH_TEST_STATE=RESIDENTS npm run pilot",
89 "pilot:cleanup": "pkill -f ganache-cli",
91 "pilot:cleanup": "pkill -f ganache-cli",
90 "lint:check": "eslint 'src/**/*.js'",
92 "lint:check": "eslint 'src/**/*.js'",
91 "lint:fix": "eslint --fix 'src/**/*.js'",
93 "lint:fix": "eslint --fix 'src/**/*.js'",
src/Bridge.js+3−1
@@ -42,6 +42,8 @@ function useInitialRoutes() {
42 const [hasDisclaimed] = useHasDisclaimed();
42 const [hasDisclaimed] = useHasDisclaimed();
43 const hasImpliedTicket = !!useImpliedTicket();
43 const hasImpliedTicket = !!useImpliedTicket();
44
44
45+ const isActivateUrl = window.location.pathname === '/activate';
46+
45 if (IS_STUBBED) {
47 if (IS_STUBBED) {
46 return [
48 return [
47 { key: ROUTE_NAMES.LOGIN },
49 { key: ROUTE_NAMES.LOGIN },
@@ -50,7 +52,7 @@ function useInitialRoutes() {
50 ];
52 ];
51 }
53 }
52
54
53− if (hasImpliedTicket) {
55+ if (hasImpliedTicket || isActivateUrl) {
54 return [{ key: ROUTE_NAMES.ACTIVATE }];
56 return [{ key: ROUTE_NAMES.ACTIVATE }];
55 }
57 }
56
58
src/assets/copy.svg+2−6
@@ -1,7 +1,3 @@
1−<svg class="svg-icon" viewBox="0 0 20 20">
1+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2− <path fill="black" d="M18.378,1.062H3.855c-0.309,0-0.559,0.25-0.559,0.559c0,0.309,0.25,0.559,0.559,0.559h13.964v13.964
2+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 3H4V10H6V6V5H7H9V3ZM10 5H12H13V6V13V14H12H7H6V13V11H4H3V10V3V2H4H9H10V3V5ZM7 6H12V13H7V6Z" fill="black"/>
3− c0,0.309,0.25,0.559,0.559,0.559c0.31,0,0.56-0.25,0.56-0.559V1.621C18.938,1.312,18.688,1.062,18.378,1.062z M16.144,3.296H1.621
4− c-0.309,0-0.559,0.25-0.559,0.559v14.523c0,0.31,0.25,0.56,0.559,0.56h14.523c0.309,0,0.559-0.25,0.559-0.56V3.855
5− C16.702,3.546,16.452,3.296,16.144,3.296z M15.586,17.262c0,0.31-0.25,0.558-0.56,0.558H2.738c-0.309,0-0.559-0.248-0.559-0.558
6− V4.972c0-0.309,0.25-0.559,0.559-0.559h12.289c0.31,0,0.56,0.25,0.56,0.559V17.262z"></path>
7</svg>
3</svg>
src/assets/info.svgnew file+3
@@ -0,0 +1,3 @@
1+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M6 -5.24537e-07C2.68503 -8.1434e-07 8.1434e-07 2.68503 5.24537e-07 6C2.34733e-07 9.31497 2.68503 12 6 12C9.31497 12 12 9.31497 12 6C12 2.68503 9.31497 -2.34733e-07 6 -5.24537e-07ZM6 0.723491C8.91892 0.723491 11.2765 3.08732 11.2765 6C11.2765 8.91892 8.91892 11.2765 6 11.2765C3.08108 11.2765 0.723492 8.91892 0.723492 6C0.723492 3.08732 3.08108 0.723491 6 0.723491ZM5.57588 9.19335L6.42412 9.19335L6.42 5.22L5.58 5.22L5.57588 9.19335ZM6 2.75676C5.69127 2.75676 5.43867 3.00935 5.43867 3.31809C5.43867 3.62682 5.69127 3.87942 6 3.87942C6.30873 3.87942 6.56133 3.62682 6.56133 3.31809C6.56133 3.00936 6.30873 2.75676 6 2.75676Z" fill="#B1B2B3"/>
3+</svg>
src/components/Buttons.js−9
@@ -23,12 +23,3 @@ export const OfflineButton = props => (
23 Offline
23 Offline
24 </OutButton>
24 </OutButton>
25);
25);
26−
27−export const BootUrbitOSButton = props => (
28− <OutButton
29− href="https://urbit.org/docs/getting-started"
30− detail="Boot your computer"
31− {...props}>
32− Boot Urbit OS
33− </OutButton>
34−);
src/components/CopyButton.js+28
@@ -2,6 +2,8 @@ import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import { LinkButton } from 'indigo-react';
3import { LinkButton } from 'indigo-react';
4
4
5+import { ForwardButton } from 'components/Buttons';
6+
5import useCopiable from 'lib/useCopiable';
7import useCopiable from 'lib/useCopiable';
6
8
7const renderDefaultCopyText = didCopy => (didCopy ? 'Copied!' : 'Copy');
9const renderDefaultCopyText = didCopy => (didCopy ? 'Copied!' : 'Copy');
@@ -27,3 +29,29 @@ export default function CopyButton({
27 </As>
29 </As>
28 );
30 );
29}
31}
32+
33+export function CopyButtonWide({
34+ as: As = ForwardButton,
35+ text,
36+ children,
37+ accessory = renderDefaultCopyText,
38+ ...rest
39+}) {
40+ const [doCopy, didCopy] = useCopiable(text);
41+ return (
42+ <As
43+ onClick={doCopy}
44+ accessory={
45+ <LinkButton
46+ className={{
47+ black: !didCopy,
48+ green3: didCopy,
49+ }}>
50+ {accessory(didCopy)}
51+ </LinkButton>
52+ }
53+ {...rest}>
54+ {children}
55+ </As>
56+ );
57+}
src/components/Greeting.js−6
@@ -3,18 +3,12 @@ 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 { useLocalRouter } from 'lib/LocalRouter';
7−
8import useWasGreeted from 'lib/useWasGreeted';
6import useWasGreeted from 'lib/useWasGreeted';
9−import useCurrentPermissions from 'lib/useCurrentPermissions';
10
7
11const TEXT_STYLE = 'f5';
8const TEXT_STYLE = 'f5';
12
9
13export default function ActivateDisclaimer({ point }) {
10export default function ActivateDisclaimer({ point }) {
14− const { push, names } = useLocalRouter();
15−
16 const [wasGreeted, setWasGreeted] = useWasGreeted();
11 const [wasGreeted, setWasGreeted] = useWasGreeted();
17− const { isActiveOwner } = useCurrentPermissions();
18
12
19 const pointName = ob.patp(point);
13 const pointName = ob.patp(point);
20
14
src/components/InlineEthereumTransaction.js+97−38
@@ -1,4 +1,4 @@
1−import React, { useMemo, useCallback } from 'react';
1+import React, { useMemo, useCallback, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import {
3import {
4 Grid,
4 Grid,
@@ -9,10 +9,13 @@ import {
9 H5,
9 H5,
10 Text,
10 Text,
11} from 'indigo-react';
11} from 'indigo-react';
12+import { toBN } from 'web3-utils';
13+
14+import { ReactComponent as InfoIcon } from 'assets/info.svg';
12
15
13import { useExploreTxUrls } from 'lib/explorer';
16import { useExploreTxUrls } from 'lib/explorer';
14import { hexify } from 'lib/txn';
17import { hexify } from 'lib/txn';
15−import pluralize from 'lib/pluralize';
18+import { safeFromWei, safeToWei } from 'lib/lib';
16
19
17import { composeValidator, buildCheckboxValidator } from 'form/validators';
20import { composeValidator, buildCheckboxValidator } from 'form/validators';
18import BridgeForm from 'form/BridgeForm';
21import BridgeForm from 'form/BridgeForm';
@@ -45,6 +48,9 @@ export default function InlineEthereumTransaction({
45 needFunds,
48 needFunds,
46 signedTransactions,
49 signedTransactions,
47 confirmationProgress,
50 confirmationProgress,
51+ gasLimit,
52+ unsignedTransactions,
53+ finalCost,
48
54
49 // additional from parent
55 // additional from parent
50 label = 'Generate & Sign Transaction',
56 label = 'Generate & Sign Transaction',
@@ -52,7 +58,11 @@ export default function InlineEthereumTransaction({
52 onReturn,
58 onReturn,
53}) {
59}) {
54 // show receipt after successful broadcast
60 // show receipt after successful broadcast
55− const showReceipt = broadcasted || confirmed || completed;
61+ const [showReceipt, setShowReceipt] = useState(false);
62+ const toggleShowReceipt = useCallback(() => setShowReceipt(!showReceipt), [
63+ setShowReceipt,
64+ showReceipt,
65+ ]);
56 // show configure controls pre-broadcast
66 // show configure controls pre-broadcast
57 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
67 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
58 // show the send/loading button while signed, broadcasting, or confirme
68 // show the send/loading button while signed, broadcasting, or confirme
@@ -62,6 +72,12 @@ export default function InlineEthereumTransaction({
62 // show signed tx only when signing (for offline usage)
72 // show signed tx only when signing (for offline usage)
63 const showSignedTx = signed;
73 const showSignedTx = signed;
64
74
75+ const [showGasDetails, _setShowGasDetails] = useState(false);
76+ const toggleGasDetails = useCallback(
77+ () => _setShowGasDetails(!showGasDetails),
78+ [_setShowGasDetails, showGasDetails]
79+ );
80+
65 const validate = useMemo(
81 const validate = useMemo(
66 () =>
82 () =>
67 composeValidator({
83 composeValidator({
@@ -89,9 +105,29 @@ export default function InlineEthereumTransaction({
89 );
105 );
90 } else if (completed) {
106 } else if (completed) {
91 return (
107 return (
92− <Grid.Item full className="pv4 black f5">
108+ <>
93− Transaction Complete
109+ <Grid.Divider />
110+ <Grid.Item full as={RestartButton} onClick={onReturn}>
111+ Return
112+ </Grid.Item>
113+ <Grid.Divider />
114+ {showReceipt && (
115+ <TransactionReceipt
116+ txHashes={txHashes}
117+ finalCost={finalCost}
118+ onClose={toggleShowReceipt}
119+ />
120+ )}
121+
122+ {!showReceipt && (
123+ <Grid.Item
124+ className="underline mv2 pointer"
125+ full
126+ onClick={toggleShowReceipt}>
127+ View Receipt
94− </Grid.Item>
128+ </Grid.Item>
129+ )}
130+ </>
95 );
131 );
96 } else if (showBroadcastButton) {
132 } else if (showBroadcastButton) {
97 return (
133 return (
@@ -137,6 +173,30 @@ export default function InlineEthereumTransaction({
137 [signedTransactions]
173 [signedTransactions]
138 );
174 );
139
175
176+ const numTxs = useMemo(() => (unsignedTransactions || []).length || 1, [
177+ unsignedTransactions,
178+ ]);
179+ const maxCost = useMemo(
180+ () =>
181+ safeFromWei(
182+ safeToWei(
183+ toBN(gasLimit)
184+ .mul(toBN(gasPrice))
185+ .mul(toBN(numTxs)),
186+ 'gwei'
187+ ),
188+ 'ether'
189+ ),
190+ [gasLimit, gasPrice, numTxs]
191+ );
192+
193+ const gasInfo = useMemo(() => {
194+ const extra = numTxs === 1 ? '' : `* ${numTxs} txs`;
195+ return showGasDetails
196+ ? `${gasPrice} Gwei * ${gasLimit} ${extra} = ${maxCost} ETH`
197+ : `${maxCost} ETH`;
198+ }, [showGasDetails, maxCost, gasPrice, gasLimit, numTxs]);
199+
140 return (
200 return (
141 <Grid className={cn(className, 'mt1')}>
201 <Grid className={cn(className, 'mt1')}>
142 <BridgeForm validate={validate} onValues={onValues}>
202 <BridgeForm validate={validate} onValues={onValues}>
@@ -172,6 +232,7 @@ export default function InlineEthereumTransaction({
172 />
232 />
173
233
174 <Condition when="useAdvanced" is={true}>
234 <Condition when="useAdvanced" is={true}>
235+ <Grid.Item full className="mb2"></Grid.Item>
175 <Grid.Divider />
236 <Grid.Divider />
176 <Grid.Item
237 <Grid.Item
177 full
238 full
@@ -179,9 +240,17 @@ export default function InlineEthereumTransaction({
179 row
240 row
180 justify="between"
241 justify="between"
181 className="mt2">
242 className="mt2">
182− <Flex.Item as={H5}>Gas Price</Flex.Item>
243+ <Flex.Item as={H5}>Transaction Fee</Flex.Item>
244+
183− <Flex.Item as={H5}>{gasPrice} Gwei</Flex.Item>
245+ <Flex.Item as={H5}>
246+ {gasInfo}{' '}
247+ <InfoIcon
248+ className="pointer"
249+ onClick={toggleGasDetails}
250+ />
251+ </Flex.Item>
184 </Grid.Item>
252 </Grid.Item>
253+
185 {/* TODO(shrugs): move to indigo/RangeInput */}
254 {/* TODO(shrugs): move to indigo/RangeInput */}
186 <Grid.Item
255 <Grid.Item
187 full
256 full
@@ -221,30 +290,12 @@ export default function InlineEthereumTransaction({
221 <Condition when="viewSigned" is={true}>
290 <Condition when="viewSigned" is={true}>
222 <SignedTransactionList
291 <SignedTransactionList
223 serializedTxsHex={serializedTxsHex}
292 serializedTxsHex={serializedTxsHex}
224− gasPrice={gasPrice}
293+ maxCost={maxCost}
225 nonce={nonce}
294 nonce={nonce}
226 />
295 />
227 </Condition>
296 </Condition>
228 </>
297 </>
229 )}
298 )}
230−
231− {showReceipt && (
232− <>
233− <Grid.Divider />
234− <HashReceiptList txHashes={txHashes} />
235−
236− <Grid.Divider />
237− </>
238− )}
239−
240− {completed && (
241− <>
242− <Grid.Item full as={RestartButton} onClick={onReturn}>
243− Return
244− </Grid.Item>
245− <Grid.Divider />
246− </>
247− )}
248 </>
299 </>
249 )}
300 )}
250 </BridgeForm>
301 </BridgeForm>
@@ -252,9 +303,9 @@ export default function InlineEthereumTransaction({
252 );
303 );
253}
304}
254
305
255−function SignedTransactionList({ serializedTxsHex, nonce, gasPrice }) {
306+function SignedTransactionList({ serializedTxsHex, nonce, maxCost }) {
256 return serializedTxsHex.map((serializedTxHex, i) => (
307 return serializedTxsHex.map((serializedTxHex, i) => (
257− <React.Fragment key="i">
308+ <React.Fragment key={i}>
258 <Grid.Divider />
309 <Grid.Divider />
259 <Grid.Item full as={Flex} justify="between" className="pv4 black f5">
310 <Grid.Item full as={Flex} justify="between" className="pv4 black f5">
260 <Flex.Item>Nonce</Flex.Item>
311 <Flex.Item>Nonce</Flex.Item>
@@ -262,8 +313,8 @@ function SignedTransactionList({ serializedTxsHex, nonce, gasPrice }) {
262 </Grid.Item>
313 </Grid.Item>
263 <Grid.Divider />
314 <Grid.Divider />
264 <Grid.Item full as={Flex} justify="between" className="pv4 black f5">
315 <Grid.Item full as={Flex} justify="between" className="pv4 black f5">
265− <Flex.Item>Gas Price</Flex.Item>
316+ <Flex.Item>Transaction Cost</Flex.Item>
266− <Flex.Item>{gasPrice.toFixed()} Gwei</Flex.Item>
317+ <Flex.Item>{maxCost} ETH</Flex.Item>
267 </Grid.Item>
318 </Grid.Item>
268 <Grid.Divider />
319 <Grid.Divider />
269 <Grid.Item full as={Flex} justify="between" className="mt3 mb2">
320 <Grid.Item full as={Flex} justify="between" className="mt3 mb2">
@@ -278,34 +329,42 @@ function SignedTransactionList({ serializedTxsHex, nonce, gasPrice }) {
278 ));
329 ));
279}
330}
280
331
281−function HashReceiptList({ txHashes }) {
332+function TransactionReceipt({ txHashes, finalCost, onClose }) {
282 const txUrls = useExploreTxUrls(txHashes);
333 const txUrls = useExploreTxUrls(txHashes);
283− const header = pluralize(txHashes.length, 'Hash', 'Hashes');
284 return (
334 return (
285 <>
335 <>
336+ <Grid.Item full as={Flex} justify="between" className="pv2 black f5">
337+ <Flex.Item>Receipt</Flex.Item>
338+ <Flex.Item onClick={onClose} className="underline pointer">
339+ Close
340+ </Flex.Item>
341+ </Grid.Item>
342+ <Grid.Item full as={Flex} justify="between" className="pv2 black f5">
343+ <Flex.Item>Transaction Cost</Flex.Item>
344+ <Flex.Item className="mono">{finalCost} ETH</Flex.Item>
345+ </Grid.Item>
286 <Grid.Divider />
346 <Grid.Divider />
287 <Grid.Item full as={Flex} col className="pv4">
347 <Grid.Item full as={Flex} col className="pv4">
288− <Flex.Item as={H5}>Transaction {header}</Flex.Item>
348+ <Flex.Item as={H5}>Receipt</Flex.Item>
289
349
290 {txHashes &&
350 {txHashes &&
291 txHashes.map((txHash, i) => (
351 txHashes.map((txHash, i) => (
292− <Flex.Item as={Flex}>
352+ <Flex.Item as={Flex} className="mv3">
293 <>
353 <>
294 <Flex.Item
354 <Flex.Item
295 key={i}
355 key={i}
296 flex
356 flex
297 as="code"
357 as="code"
298− className="f6 mono gray4 wrap">
358+ className="f6 mono gray4 wrap ">
299 {txHash}
359 {txHash}
300 </Flex.Item>
360 </Flex.Item>
301− <Flex.Item as={LinkButton} href={txUrls[i]}>
361+ <Flex.Item className="ml8" as={LinkButton} href={txUrls[i]}>
302 Etherscan↗
362 Etherscan↗
303 </Flex.Item>
363 </Flex.Item>
304 </>
364 </>
305 </Flex.Item>
365 </Flex.Item>
306 ))}
366 ))}
307 </Grid.Item>
367 </Grid.Item>
308− <Grid.Divider />
309 </>
368 </>
310 );
369 );
311}
370}
src/components/LoginButton.jsnew file+27
@@ -0,0 +1,27 @@
1+import React, { useRef } from 'react';
2+import { Button } from 'indigo-react';
3+
4+export default function LoginButton({ url, className, code, ...rest }) {
5+ const formEl = useRef(null);
6+ return (
7+ <form
8+ style={{ display: 'contents' }}
9+ action={url}
10+ method="POST"
11+ target="_blank"
12+ ref={formEl}>
13+ <input name="password" value={code} type="hidden" />
14+ <input name="redirect" value="/" type="hidden" />
15+ <Button
16+ type="submit"
17+ accessory="↗"
18+ solid
19+ success
20+ className={className}
21+ onClick={() => formEl.current.submit()}
22+ {...rest}>
23+ Open OS
24+ </Button>
25+ </form>
26+ );
27+}
src/components/MiniBackButton.js+2−4
@@ -2,12 +2,10 @@ import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import { IconButton } from 'indigo-react';
3import { IconButton } from 'indigo-react';
4
4
5−import { ReactComponent as Back } from 'assets/back.svg';
6−
7export default ({ className, isExit = false, ...rest }) => {
5export default ({ className, isExit = false, ...rest }) => {
8 return (
6 return (
9− <IconButton {...rest}>
10− {isExit ? '⏏' : <Back className={cn('black', className)} />}
7+ <IconButton className={cn('black', className)} {...rest}>
8+ {'<-'}
11 </IconButton>
9 </IconButton>
12 );
10 );
13};
11};
src/components/NetworkingKeys.jsnew file+109
@@ -0,0 +1,109 @@
1+import React from 'react';
2+import { Grid, Flex, H5, Text } from 'indigo-react';
3+import { Just, Nothing } from 'folktale/maybe';
4+
5+import { formatDotsWithTime } from 'lib/dateFormat';
6+import { segmentNetworkKey, CURVE_ZERO_ADDR } from 'lib/keys';
7+
8+import { usePointCache } from 'store/pointCache';
9+
10+const chainKeyProp = name => d =>
11+ d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
12+
13+const renderNetworkKey = key => {
14+ const segments = segmentNetworkKey(key);
15+
16+ if (!segments) {
17+ return 'Unset';
18+ }
19+
20+ return segments.join('\n');
21+};
22+
23+export default function NetworkingKeys({ point }) {
24+ const { getDetails, getRekeyDate } = usePointCache();
25+
26+ const details = getDetails(point);
27+
28+ const hasKeys = details.matchWith({
29+ Nothing: () => false,
30+ Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
31+ // we actually don't mind the default NaN behavior of parseInt here,
32+ // since NaN > 0 === false and that's a reasonable result
33+ });
34+
35+ const renderNetworkKeySection = (title, key) => (
36+ <>
37+ <Grid.Item full as={H5} className="mt3 gray4">
38+ {title}
39+ </Grid.Item>
40+ {key.matchWith({
41+ Nothing: () => (
42+ <Grid.Item full as="code" className="f5 mono gray4">
43+ Unset
44+ </Grid.Item>
45+ ),
46+ Just: ({ value: key }) => (
47+ <>
48+ <Grid.Item full as="code" className="f5 mono wrap">
49+ 0x
50+ </Grid.Item>
51+ <Grid.Item full as="code" className="f5 mono wrap">
52+ {renderNetworkKey(key)}
53+ </Grid.Item>
54+ </>
55+ ),
56+ })}
57+ </>
58+ );
59+
60+ const renderDetail = (title, value) => (
61+ <Flex.Item as={Flex} col>
62+ <Flex.Item as={H5} className="gray4">
63+ {title}
64+ </Flex.Item>
65+ {value.matchWith({
66+ Nothing: () => (
67+ <Flex.Item as={Text} className="f5 gray4">
68+ Unset
69+ </Flex.Item>
70+ ),
71+ Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
72+ })}
73+ </Flex.Item>
74+ );
75+
76+ return (
77+ <>
78+ {renderNetworkKeySection(
79+ 'Authentication',
80+ details.chain(chainKeyProp('authenticationKey'))
81+ )}
82+ {renderNetworkKeySection(
83+ 'Encryption',
84+ details.chain(chainKeyProp('encryptionKey'))
85+ )}
86+ <Grid.Item full as={Flex} row justify="between" className="mt3">
87+ {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
88+ {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
89+ {renderDetail(
90+ 'Crypto Suite Ver.',
91+ details.map(d => d.cryptoSuiteVersion)
92+ )}
93+ </Grid.Item>
94+ {hasKeys && (
95+ <Grid.Item full as={Flex} row justify="between" className="mt3">
96+ {renderDetail(
97+ 'Last Set',
98+ getRekeyDate(point).map(date =>
99+ date.matchWith({
100+ Ok: r => formatDotsWithTime(r.value),
101+ Error: () => Nothing(),
102+ })
103+ )
104+ )}
105+ </Grid.Item>
106+ )}
107+ </>
108+ );
109+}
src/components/View.js+15−10
@@ -8,18 +8,17 @@ import MiniBackButton from './MiniBackButton';
8import { useHistory } from 'store/history';
8import { useHistory } from 'store/history';
9import NavHeader from './NavHeader';
9import NavHeader from './NavHeader';
10
10
11−const EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH = 2;
12−
13// View is a top-level component that all Views must render to inherit styling
11// View is a top-level component that all Views must render to inherit styling
14function View({
12function View({
15 className,
13 className,
16 children,
14 children,
17 inset = false,
15 inset = false,
18 full = false,
16 full = false,
17+ hideBack = false,
19 pop,
18 pop,
20 ...rest
19 ...rest
21}) {
20}) {
22− const { size } = useHistory();
21+ const { reset } = useHistory();
23 const isMobile = useBreakpoints([true, false, false]);
22 const isMobile = useBreakpoints([true, false, false]);
24 const shouldInset = inset && !isMobile;
23 const shouldInset = inset && !isMobile;
25
24
@@ -30,17 +29,19 @@ function View({
30
29
31 const goBack = useCallback(() => pop(), [pop]);
30 const goBack = useCallback(() => pop(), [pop]);
32
31
33− const goLogout = useCallback(() => pop(size - 1), [pop, size]);
32+ const goLogout = reset;
34
33
35− const showBackButton = size > 1 && !!pop;
34+ const showBackButton = !!pop;
36− const backIsLogout = size === EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH;
37
35
38 const Header = useCallback(
36 const Header = useCallback(
39 ({ logout }) => {
37 ({ logout }) => {
40 return showBackButton ? (
38 return showBackButton ? (
41 <Flex.Item
39 <Flex.Item
42 as={Flex}
40 as={Flex}
41+ className={cn(
42+ insetPadding,
43− className={cn(insetPadding, 'flex-row-r justify-between pb5')}>
43+ 'flex-row-r justify-between pb5 align-center'
44+ )}>
44 <Flex.Item
45 <Flex.Item
45 onClick={goLogout}
46 onClick={goLogout}
46 as="a"
47 as="a"
@@ -70,13 +71,17 @@ function View({
70 className
71 className
71 )}
72 )}
72 {...rest}>
73 {...rest}>
74+ <Flex.Item
75+ as={Flex}
76+ col
77+ style={{ width: '48px' }}
73− <Flex.Item as={Flex} col className={cn(insetPadding)}>
78+ className={cn(insetPadding)}>
74− {showBackButton && !backIsLogout && (
79+ {showBackButton && !hideBack && (
75 <MiniBackButton
80 <MiniBackButton
76 hpadding={!isMobile}
81 hpadding={!isMobile}
77 vpadding={isMobile}
82 vpadding={isMobile}
78− isExit={backIsLogout}
79 onClick={goBack}
83 onClick={goBack}
84+ className="mt1"
80 />
85 />
81 )}
86 )}
82 </Flex.Item>
87 </Flex.Item>
src/form/EmailChipInput.js+1−1
@@ -41,7 +41,7 @@ const EmailChipInput = ({ className, name, label, disabled }) => {
41 );
41 );
42
42
43 const addToChips = useCallback(() => {
43 const addToChips = useCallback(() => {
44− fields.push(value);
44+ fields.concat(value.split(/[\n\t, ]+/));
45 setValue('');
45 setValue('');
46 }, [value, fields, setValue]);
46 }, [value, fields, setValue]);
47
47
src/index.js−32
@@ -7,36 +7,4 @@ if (process.env.NODE_ENV !== 'production') {
7 require('@welldone-software/why-did-you-render')(React);
7 require('@welldone-software/why-did-you-render')(React);
8}
8}
9
9
10−if (process.env.NODE_ENV === 'production') {
11− console.log(`
12− MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
13− MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
14− MMMMMMMMMMMMMMMMMMWN0kdlc:::::clox0XWMMMMMMMMMMMMMMMMM
15− MMMMMMMMMMMMMMMN0o;.. .;oONMMMMMMMMMMMMMM
16− MMMMMMMMMMMMMKd,. .':ldkOOOOOkdo:'. 'oKWMMMMMMMMMMM
17− MMMMMMMMMMMXo. .;d0NMMMMMMMMMMMMMN0d;. .lKMMMMMMMMMM
18− MMMMMMMMMWk' .:OWMMMMMMMMMMMMMMMMMMMW0l. .xWMMMMMMMM
19− MMMMMMMMWd. ,OWMMMMMMMMMMMMMMMMMMMMMMMW0; .oNMMMMMMM
20− MMMMMMMWx. ;KMMMMMMMMMMMMMMMMMMMMMMMMMMMXc .oWMMMMMM
21− MMMMMMM0' ,KMMMMMMMMMWNWMMMMMMMMMMMMMMMMMX: .OMMMMMM
22− MMMMMMWo .xMMMMMMWOl;'',:oxONMMWOc:clOWMMMk. lWMMMMM
23− MMMMMMN: '0MMMMMK: ,loc. .dWMMMK, ;XMMMMM
24− MMMMMMN: ,KMMMMK; ... .lNMMMMX; ,KMMMMM
25− MMMMMMWc .OMMMMx. ,OXKkc;' .;kNMMMMM0' :NMMMMM
26− MMMMMMMx. lNMMMN0OOOXMMMMMWN0xddkKWMMMMMMWo dMMMMMM
27− MMMMMMMNc .xWMMMMMMMMMMMMMMMMMMMMMMMMMMMWk. ;XMMMMMM
28− MMMMMMMMK; .dNMMMMMMMMMMMMMMMMMMMMMMMMMWx. ,0MMMMMMM
29− MMMMMMMMMK: :0WMMMMMMMMMMMMMMMMMMMMMW0c. ;0MMMMMMMM
30− MMMMMMMMMMNd. .:kNMMMMMMMMMMMMMMMMMNOc. .oXMMMMMMMMM
31− MMMMMMMMMMMMKo. 'cdOKNWMMMMMWNXOxc' .l0WMMMMMMMMMM
32− MMMMMMMMMMMMMMXx:. .',;:::;,'. .:xXMMMMMMMMMMMMM
33− MMMMMMMMMMMMMMMMMXOoc,'.. ...,:okXWMMMMMMMMMMMMMMM
34− MMMMMMMMMMMMMMMMMMMMMWNK00OOO00KNWMMMMMMMMMMMMMMMMMMMM
35− MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
36− MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
37−
38− Like Urbit? Want to work with us? Try apply@tlon.io.
39− `);
40−}
41−
42ReactDOM.render(<Bridge />, document.getElementById('root'));
10ReactDOM.render(<Bridge />, document.getElementById('root'));
src/indigo-react/indigo.css+5
@@ -387,6 +387,11 @@ caption,
387 line-height: 1.33333;
387 line-height: 1.33333;
388 font-weight: 400; }
388 font-weight: 400; }
389
389
390+.f7 {
391+ font-size: 0.5rem;
392+ line-height: 1.33333;
393+ font-weight: 400; }
394+
390@media only screen and (min-width: 550px) {
395@media only screen and (min-width: 550px) {
391 .f1-ns {
396 .f1-ns {
392 font-size: 3rem;
397 font-size: 3rem;
src/lib/SolarisClient.jsnew file+105
@@ -0,0 +1,105 @@
1+/* eslint-disable prettier/prettier */
2+
3+//
4+// Warning: this file is auto-generated.
5+//
6+
7+export default class SolarisClient {
8+ constructor(endpoint) {
9+ this.endpoint = endpoint;
10+ }
11+
12+ getHealthzUrl() {
13+ return this.endpoint + '/healthz';
14+ }
15+
16+ async getHealthz() {
17+ return await fetch(this.getHealthzUrl(), {
18+ method: 'GET',
19+ headers: {
20+ Accept: 'application/json',
21+ },
22+ });
23+ }
24+
25+ getUrl() {
26+ return this.endpoint + '/';
27+ }
28+
29+ async get() {
30+ return await fetch(this.getUrl(), {
31+ method: 'GET',
32+ headers: {
33+ Accept: 'application/json',
34+ },
35+ });
36+ }
37+
38+ getImagesUrl() {
39+ return this.endpoint + '/images';
40+ }
41+
42+ async getImages() {
43+ return await fetch(this.getImagesUrl(), {
44+ method: 'GET',
45+ headers: {
46+ Accept: 'application/json',
47+ },
48+ });
49+ }
50+
51+ getShipsUrl() {
52+ return this.endpoint + '/ships';
53+ }
54+
55+ async getShips() {
56+ return await fetch(this.getShipsUrl(), {
57+ method: 'GET',
58+ headers: {
59+ Accept: 'application/json',
60+ },
61+ });
62+ }
63+
64+ getShipsByPatpUrl(patp) {
65+ return this.endpoint + '/ships/' + encodeURIComponent(patp) + '';
66+ }
67+
68+ async getShipsByPatp(patp) {
69+ return await fetch(this.getShipsByPatpUrl(patp), {
70+ method: 'GET',
71+ headers: {
72+ Accept: 'application/json',
73+ },
74+ });
75+ }
76+
77+ getShipsByPatpDetailsUrl(patp) {
78+ return this.endpoint + '/ships/' + encodeURIComponent(patp) + '/details';
79+ }
80+
81+ async getShipsByPatpDetails(patp) {
82+ return await fetch(this.getShipsByPatpDetailsUrl(patp), {
83+ method: 'GET',
84+ headers: {
85+ Accept: 'application/json',
86+ },
87+ });
88+ }
89+
90+ postShipsUrl() {
91+ return this.endpoint + '/ships';
92+ }
93+
94+ async postShips(data) {
95+ return await fetch(this.postShipsUrl(), {
96+ method: 'POST',
97+ cache: 'no-cache',
98+ body: JSON.stringify(data),
99+ headers: {
100+ Accept: 'application/json',
101+ 'Content-Type': 'application/json',
102+ },
103+ });
104+ }
105+}
src/lib/constants.js+3−3
@@ -1,7 +1,7 @@
1import { isDevelopment } from './flags';
1import { isDevelopment } from './flags';
2
2
3−const CHECK_BLOCK_EVERY_MS = isDevelopment ? 1000 : 5000;
3+const CHECK_BLOCK_EVERY_MS = isDevelopment ? 1000 : 10000;
4−const DEFAULT_GAS_PRICE_GWEI = 20;
4+const DEFAULT_GAS_PRICE_GWEI = 40;
5
5
6const MIN_GALAXY = 0;
6const MIN_GALAXY = 0;
7const MAX_GALAXY = 255;
7const MAX_GALAXY = 255;
@@ -31,7 +31,7 @@ const GAS_LIMITS = {
31 REJECT: 200000, //NOTE no samples
31 REJECT: 200000, //NOTE no samples
32 DETACH: 200000, //NOTE no samples
32 DETACH: 200000, //NOTE no samples
33 //
33 //
34− GIFT_PLANET: 420000, //NOTE low sample size, //NOTE also update in gas tank
34+ GIFT_PLANET: 450000, //NOTE low sample size, //NOTE also update in gas tank
35 //
35 //
36 SEND_ETH: 21000,
36 SEND_ETH: 21000,
37 //
37 //
src/lib/getSuggestedGasPrice.js+1−1
@@ -24,7 +24,7 @@ export default async function getSuggestedGasPrice(networkType) {
24 // more than gwei. see: https://docs.ethgasstation.info
24 // more than gwei. see: https://docs.ethgasstation.info
25 const suggestedGasPrice = Math.ceil(json.fast / 10); // to gwei
25 const suggestedGasPrice = Math.ceil(json.fast / 10); // to gwei
26
26
27− // we don't want to charge users more than our default 20 gwei
27+ // we don't want to charge users more than our default gwei
28 return Math.min(suggestedGasPrice, DEFAULT_GAS_PRICE_GWEI);
28 return Math.min(suggestedGasPrice, DEFAULT_GAS_PRICE_GWEI);
29 } catch (e) {
29 } catch (e) {
30 return DEFAULT_GAS_PRICE_GWEI;
30 return DEFAULT_GAS_PRICE_GWEI;
src/lib/hosting.jsnew file+26
@@ -0,0 +1,26 @@
1+export const getVaneName = code => {
2+ switch (code) {
3+ case 'a':
4+ return 'Ames';
5+ case 'b':
6+ return 'Behn';
7+ case 'c':
8+ return 'Clay';
9+ case 'd':
10+ return 'Dill';
11+ case 'e':
12+ return 'Eyre';
13+ case 'f':
14+ return 'Ford';
15+ case 'g':
16+ return 'Gall';
17+ case 'i':
18+ return 'Iris';
19+ case 'j':
20+ return 'Jael';
21+ default:
22+ return '';
23+ }
24+};
25+
26+export const getVaneNumber = code => 1 + 'abcdefgij'.indexOf(code);
src/lib/proxy.js+4−4
@@ -23,13 +23,13 @@ export const proxyTypeToHuman = proxyType => {
23export const proxyTypeToHumanDescription = proxyType => {
23export const proxyTypeToHumanDescription = proxyType => {
24 switch (proxyType) {
24 switch (proxyType) {
25 case PROXY_TYPE.MANAGEMENT:
25 case PROXY_TYPE.MANAGEMENT:
26− return 'The management proxy is allowed to manage networking keys.';
26+ return 'Your management key can configure networking settings (network keys and sponsorship)';
27 case PROXY_TYPE.SPAWN:
27 case PROXY_TYPE.SPAWN:
28− return 'The spawn proxy is allowed to spawn points.';
28+ return 'Your spawn key can spawn points.';
29 case PROXY_TYPE.TRANSFER:
29 case PROXY_TYPE.TRANSFER:
30− return 'The transfer proxy is allowed to transfer this point.';
30+ return 'Your transfer key can transfer this point.';
31 case PROXY_TYPE.VOTING:
31 case PROXY_TYPE.VOTING:
32− return "The voting proxy is allowed to vote on this point's behalf.";
32+ return "Your voting key can vote on this point's behalf.";
33 default:
33 default:
34 throw new Error(`Unknown proxyType: ${proxyType}`);
34 throw new Error(`Unknown proxyType: ${proxyType}`);
35 }
35 }
src/lib/routeNames.js+5−1
@@ -5,10 +5,10 @@ export const ROUTE_NAMES = {
5 ACTIVATE: Symbol('ACTIVATE'),
5 ACTIVATE: Symbol('ACTIVATE'),
6 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
6 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
8+ SENATE: Symbol('SENATE'),
8 //
9 //
9 LOGIN: Symbol('LOGIN'),
10 LOGIN: Symbol('LOGIN'),
10 LOGIN_OTHER: Symbol('OTHER'),
11 LOGIN_OTHER: Symbol('OTHER'),
11− ADMIN: Symbol('ADMIN'),
12 REDOWNLOAD: Symbol('REDOWNLOAD'),
12 REDOWNLOAD: Symbol('REDOWNLOAD'),
13 RETICKET: Symbol('RETICKET'),
13 RETICKET: Symbol('RETICKET'),
14 PERMISSIONS: Symbol('PERMISSIONS'),
14 PERMISSIONS: Symbol('PERMISSIONS'),
@@ -24,4 +24,8 @@ export const ROUTE_NAMES = {
24 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
24 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
25 INVITE_COHORT: Symbol('INVITE_COHORT'),
25 INVITE_COHORT: Symbol('INVITE_COHORT'),
26 STAR_RELEASE: Symbol('STAR_RELEASE'),
26 STAR_RELEASE: Symbol('STAR_RELEASE'),
27+ URBIT_OS: Symbol('URBIT_OS'),
28+ URBIT_ID: Symbol('URBIT_ID'),
29+ RESIDENTS: Symbol('RESIDENTS'),
30+ ADOPT: Symbol('ADOPT'),
27};
31};
src/lib/router.js+8−4
@@ -1,12 +1,10 @@
1import Disclaimer from 'views/Disclaimer';
1import Disclaimer from 'views/Disclaimer';
2import Login from 'views/Login.js';
2import Login from 'views/Login.js';
3import Other from 'views/Login/Other.js';
3import Other from 'views/Login/Other.js';
4−import Admin from 'views/Admin';
5import Senate from 'views/Senate';
4import Senate from 'views/Senate';
6import CreateGalaxy from 'views/CreateGalaxy';
5import CreateGalaxy from 'views/CreateGalaxy';
7import IssueChild from 'views/IssueChild';
6import IssueChild from 'views/IssueChild';
8import StarRelease from 'views/Release';
7import StarRelease from 'views/Release';
9−import SigilGenerator from 'views/SigilGenerator';
10
8
11import Point from 'views/Point';
9import Point from 'views/Point';
12import Points from 'views/Points';
10import Points from 'views/Points';
@@ -16,6 +14,10 @@ import PartySetPoolSize from 'views/Party/PartySetPoolSize';
16import InviteCohort from 'views/Invite/Cohort';
14import InviteCohort from 'views/Invite/Cohort';
17import AcceptTransfer from 'views/AcceptTransfer';
15import AcceptTransfer from 'views/AcceptTransfer';
18import CancelTransfer from 'views/CancelTransfer';
16import CancelTransfer from 'views/CancelTransfer';
17+import UrbitOS from 'views/UrbitOS';
18+import UrbitID from 'views/UrbitID';
19+import Residents from 'views/Residents';
20+import Adopt from 'views/Adopt';
19
21
20import { ROUTE_NAMES } from './routeNames';
22import { ROUTE_NAMES } from './routeNames';
21
23
@@ -28,7 +30,6 @@ export const ROUTES = {
28 [ROUTE_NAMES.POINT]: Point,
30 [ROUTE_NAMES.POINT]: Point,
29 [ROUTE_NAMES.CREATE_GALAXY]: CreateGalaxy,
31 [ROUTE_NAMES.CREATE_GALAXY]: CreateGalaxy,
30 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
32 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
31− [ROUTE_NAMES.ADMIN]: Admin,
32 [ROUTE_NAMES.SENATE]: Senate,
33 [ROUTE_NAMES.SENATE]: Senate,
33 // TODO: nest this route under the Party.js router
34 // TODO: nest this route under the Party.js router
34 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
35 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
@@ -37,5 +38,8 @@ export const ROUTES = {
37 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
38 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
38 [ROUTE_NAMES.INVITE_COHORT]: InviteCohort,
39 [ROUTE_NAMES.INVITE_COHORT]: InviteCohort,
39 [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
40 [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
40− [ROUTE_NAMES.SIGIL_GENERATOR]: SigilGenerator,
41+ [ROUTE_NAMES.URBIT_OS]: UrbitOS,
42+ [ROUTE_NAMES.URBIT_ID]: UrbitID,
43+ [ROUTE_NAMES.RESIDENTS]: Residents,
44+ [ROUTE_NAMES.ADOPT]: Adopt,
41};
45};
src/lib/txn.js+1
@@ -160,6 +160,7 @@ const waitForTransactionConfirm = (web3, txHash) => {
160 if (!success) {
160 if (!success) {
161 return bail(new Error('Transaction failed.'));
161 return bail(new Error('Transaction failed.'));
162 }
162 }
163+ return receipt;
163 }, RETRY_OPTIONS);
164 }, RETRY_OPTIONS);
164};
165};
165
166
src/lib/useEthereumTransaction.js+24−2
@@ -22,7 +22,7 @@ import { ensureFundsFor } from 'lib/tank';
22import useDeepEqualReference from 'lib/useDeepEqualReference';
22import useDeepEqualReference from 'lib/useDeepEqualReference';
23import useGasPrice from 'lib/useGasPrice';
23import useGasPrice from 'lib/useGasPrice';
24import timeout from 'lib/timeout';
24import timeout from 'lib/timeout';
25−import { safeToWei } from 'lib/lib';
25+import { safeToWei, safeFromWei } from 'lib/lib';
26
26
27const STATE = {
27const STATE = {
28 NONE: 'NONE',
28 NONE: 'NONE',
@@ -73,6 +73,8 @@ export default function useEthereumTransaction(
73 const [needFunds, setNeedFunds] = useState();
73 const [needFunds, setNeedFunds] = useState();
74 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
74 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
75
75
76+ const [finalCost, setFinalCost] = useState();
77+
76 const initializing = nonce === undefined || chainId === undefined;
78 const initializing = nonce === undefined || chainId === undefined;
77 const constructed = !!unsignedTransactions;
79 const constructed = !!unsignedTransactions;
78 const isDefaultState = state === STATE.NONE;
80 const isDefaultState = state === STATE.NONE;
@@ -186,10 +188,26 @@ export default function useEthereumTransaction(
186
188
187 setConfirmationProgress(0.2);
189 setConfirmationProgress(0.2);
188
190
189− await Promise.all(
191+ const receipts = await Promise.all(
190 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
192 txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
191 );
193 );
192
194
195+ setFinalCost(
196+ safeFromWei(
197+ safeToWei(
198+ receipts.reduce(
199+ (acc, receipt) =>
200+ toBN(receipt.gasUsed)
201+ .mul(toBN(gasPrice))
202+ .add(acc),
203+ toBN(0)
204+ ),
205+ 'gwei'
206+ ),
207+ 'ether'
208+ )
209+ );
210+
193 setConfirmationProgress(0.9);
211 setConfirmationProgress(0.9);
194
212
195 setState(STATE.CONFIRMED);
213 setState(STATE.CONFIRMED);
@@ -217,6 +235,7 @@ export default function useEthereumTransaction(
217 setError(undefined);
235 setError(undefined);
218 setNeedFunds(undefined);
236 setNeedFunds(undefined);
219 setConfirmationProgress(0.0);
237 setConfirmationProgress(0.0);
238+ setFinalCost(undefined);
220 }, [resetGasPrice, setError]);
239 }, [resetGasPrice, setError]);
221
240
222 useEffect(() => {
241 useEffect(() => {
@@ -312,6 +331,9 @@ export default function useEthereumTransaction(
312 nonce,
331 nonce,
313 chainId,
332 chainId,
314 needFunds,
333 needFunds,
334+ gasLimit,
335+ unsignedTransactions,
336+ finalCost,
315 });
337 });
316
338
317 return { ...values, bind: values };
339 return { ...values, bind: values };
src/lib/useKeyfileGenerator.js+1
@@ -128,6 +128,7 @@ export default function useKeyfileGenerator(manualNetworkSeed) {
128 download,
128 download,
129 filename,
129 filename,
130 notice,
130 notice,
131+ keyfile,
131 code,
132 code,
132 };
133 };
133
134
src/lib/useLocaHosting.jsnew file+51
@@ -0,0 +1,51 @@
1+import { useState, useEffect, useMemo } from 'react';
2+import { Just } from 'folktale/maybe';
3+import * as ob from 'urbit-ob';
4+
5+import { usePointCursor } from 'store/pointCursor';
6+
7+const DOMAIN = 'arvo.network';
8+
9+const STATE = {
10+ UNKNOWN: 'UNKNOWN',
11+ MISSING: 'MISSING',
12+ RUNNING: 'RUNNING',
13+};
14+
15+export default function useLocalHosting() {
16+ const [status, setStatus] = useState(STATE.UNKNOWN);
17+
18+ const { pointCursor } = usePointCursor();
19+
20+ useEffect(() => {
21+ const updateStatus = async point => {
22+ const patp = ob.patp(point).slice(1);
23+ try {
24+ const { who } = await fetch(`https://${patp}.${DOMAIN}/who.json`).then(
25+ r => r.json()
26+ );
27+ if (who === patp) {
28+ setStatus(STATE.RUNNING);
29+ } else {
30+ setStatus(STATE.MISSING);
31+ }
32+ } catch (e) {
33+ setStatus(STATE.MISSING);
34+ }
35+ };
36+ if (Just.hasInstance(pointCursor)) {
37+ updateStatus(pointCursor.value);
38+ }
39+ }, [pointCursor]);
40+
41+ const url = useMemo(
42+ () =>
43+ pointCursor.value &&
44+ `https://${ob.patp(pointCursor.value).slice(1)}.${DOMAIN}`,
45+ [pointCursor]
46+ );
47+
48+ const running = useMemo(() => status === STATE.RUNNING, [status]);
49+
50+ return { running, url };
51+}
src/lib/useRouter.js+3−1
@@ -88,7 +88,9 @@ export default function useRouter({
88 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
88 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
89 routes,
89 routes,
90 ]);
90 ]);
91+ const data = useMemo(() => {
91− const data = useMemo(() => last(routes).data || NULL_DATA, [routes]);
92+ return last(routes).data || NULL_DATA;
93+ }, [routes]);
92 const Route = useMemo(() => views[peek().key], [views, peek]);
94 const Route = useMemo(() => views[peek().key], [views, peek]);
93
95
94 // Scroll to top of page with each route transition
96 // Scroll to top of page with each route transition
src/lib/validators.js+9
@@ -94,6 +94,9 @@ export const validateHexLength = l => v =>
94export const validateMaximumLength = l => v =>
94export const validateMaximumLength = l => v =>
95 v.length > l && `Must be ${l} characters or fewer.`;
95 v.length > l && `Must be ${l} characters or fewer.`;
96
96
97+export const validateMinimumLength = l => v =>
98+ v.length < l && `Must be ${l} characters or more.`;
99+
97export const validateGreaterThan = l => v =>
100export const validateGreaterThan = l => v =>
98 !(v > l) && `Must be at least ${l}.`;
101 !(v > l) && `Must be at least ${l}.`;
99
102
@@ -104,6 +107,9 @@ export const validateInSet = (set, error) => v => !set.has(v) && error;
104export const validateMaximumPatpByteLength = byteLength =>
107export const validateMaximumPatpByteLength = byteLength =>
105 validateMaximumLength(patpStringLength(byteLength));
108 validateMaximumLength(patpStringLength(byteLength));
106
109
110+export const validateMinimumPatpByteLength = byteLength =>
111+ validateMinimumLength(patpStringLength(byteLength));
112+
107export const validateNotNullAddress = validateNotAny([
113export const validateNotNullAddress = validateNotAny([
108 ETH_ZERO_ADDR,
114 ETH_ZERO_ADDR,
109 ETH_ZERO_ADDR_SHORT,
115 ETH_ZERO_ADDR_SHORT,
@@ -113,3 +119,6 @@ export const validateUnique = arr => {
113 const res = [...new Set(arr)].length !== arr.length && 'Must be unique';
119 const res = [...new Set(arr)].length !== arr.length && 'Must be unique';
114 return res;
120 return res;
115};
121};
122+
123+export const validateChild = ourShip => ship =>
124+ ourShip !== ob.sein(ship) && `This point is not a child of ${ourShip}.`;
src/store/Provider.js+2
@@ -9,6 +9,7 @@ import { StarReleaseCacheProvider } from './starRelease';
9import { PointCacheProvider } from './pointCache';
9import { PointCacheProvider } from './pointCache';
10import { TxnCursorProvider } from './txnCursor';
10import { TxnCursorProvider } from './txnCursor';
11import { MediaQueryProvider } from 'lib/MediaQuery';
11import { MediaQueryProvider } from 'lib/MediaQuery';
12+import { HostingProvider } from './hosting';
12
13
13// nest all of the providers within each other to avoid hella depth
14// nest all of the providers within each other to avoid hella depth
14export default nest([
15export default nest([
@@ -21,4 +22,5 @@ export default nest([
21 TxnCursorProvider,
22 TxnCursorProvider,
22 MediaQueryProvider,
23 MediaQueryProvider,
23 StarReleaseCacheProvider,
24 StarReleaseCacheProvider,
25+ HostingProvider,
24]);
26]);
src/store/hosting.jsnew file+37
@@ -0,0 +1,37 @@
1+import React, { createContext, useContext, forwardRef } from 'react';
2+
3+import useHostingStore from './lib/useHostingStore';
4+
5+import { NETWORK_TYPES } from '../lib/network';
6+
7+import { useNetwork } from 'store/network';
8+
9+export const HostingContext = createContext(null);
10+
11+const HOSTING_DETAILS = {
12+ DEV: { url: '', domain: 'liam.tlon.network', disabled: true },
13+ MAINNET: { url: '', domain: '', disabled: true },
14+};
15+
16+export function HostingProvider({ children }) {
17+ const { networkType } = useNetwork();
18+ const { url, domain, disabled } =
19+ networkType === NETWORK_TYPES.MAINNET
20+ ? HOSTING_DETAILS.MAINNET
21+ : HOSTING_DETAILS.DEV;
22+ const store = useHostingStore(url, domain, disabled);
23+ return (
24+ <HostingContext.Provider value={store}>{children}</HostingContext.Provider>
25+ );
26+}
27+
28+export function useHosting() {
29+ return useContext(HostingContext);
30+}
31+
32+export const withHosting = Component =>
33+ forwardRef((props, ref) => (
34+ <HostingContext.Consumer>
35+ {store => <Component ref={ref} {...store} {...props} />}
36+ </HostingContext.Consumer>
37+ ));
src/store/lib/useHostingStore.jsnew file+244
@@ -0,0 +1,244 @@
1+import { useState, useCallback, useMemo } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+import * as ob from 'urbit-ob';
4+
5+import { getVaneName, getVaneNumber } from 'lib/hosting';
6+import SolarisClient from 'lib/SolarisClient';
7+
8+import { usePointCursor } from 'store/pointCursor';
9+
10+const STATE = {
11+ UNKNOWN: 'UNKNOWN',
12+ MISSING: 'MISSING',
13+ RUNNING: 'RUNNING',
14+ PENDING: 'PENDING',
15+};
16+
17+function useHostingStore(url, domain, disabled) {
18+ const client = new SolarisClient(url);
19+
20+ const { pointCursor } = usePointCursor();
21+
22+ const [bootProgress, setBootProgress] = useState(0);
23+ const [bootMessage, setBootMessage] = useState('Assembling Urbit');
24+ const [startTime] = useState(Date.now());
25+
26+ const [status, _setStatus] = useState(STATE.UNKNOWN);
27+ const setStatus = useCallback(
28+ s => {
29+ _setStatus(s);
30+ },
31+ [_setStatus]
32+ );
33+
34+ const [sysEvents, setSysEvents] = useState([]);
35+ const [newEvents, setNewEvents] = useState([]);
36+ const [runEvents, setRunEvents] = useState([]);
37+
38+ const [error, _setError] = useState();
39+ const setError = useCallback(
40+ error => {
41+ _setError(error);
42+ if (error) {
43+ setStatus(STATE.UNKNOWN);
44+ console.error(error);
45+ }
46+ },
47+ [_setError, setStatus]
48+ );
49+
50+ const unknown = status === STATE.UNKNOWN;
51+ const missing = status === STATE.MISSING;
52+ const running = status === STATE.RUNNING;
53+ const pending = status === STATE.PENDING;
54+
55+ let syncStatus;
56+
57+ const resetEvents = useCallback(() => {
58+ setSysEvents([]);
59+ setNewEvents([]);
60+ setRunEvents([]);
61+ }, [setSysEvents, setNewEvents, setRunEvents]);
62+
63+ const newSource = useCallback(
64+ (url, retry = 0) => {
65+ let source = new EventSource(url);
66+
67+ source.addEventListener('sys', event => {
68+ const sysEvent = JSON.parse(event.data);
69+ const time = new Date(sysEvent.object.firstTimestamp).getTime();
70+ if (time > startTime) {
71+ setSysEvents(previous => [...previous, sysEvent]);
72+ }
73+ });
74+
75+ source.addEventListener('new', event => {
76+ if (event.data !== '') {
77+ setNewEvents(previous => [...previous, event.data]);
78+ }
79+ if (event.data === '1-b') {
80+ setBootMessage('Compiling Hoon');
81+ setBootProgress(0.2);
82+ } else if (event.data === '1-c') {
83+ setBootProgress(0.25);
84+ } else if (event.data === '%arvo-assembly') {
85+ setBootMessage('Compiling Urbit OS');
86+ setBootProgress(0.3);
87+ } else if (event.data.startsWith('[%vane %')) {
88+ const vane = getVaneName(event.data.slice(8, 9));
89+ setBootMessage(`Compiling vane: ${vane}`);
90+ setBootProgress(0.3 + 0.05 * getVaneNumber(event.data.slice(8, 9)));
91+ } else if (event.data === 'pier: boot complete') {
92+ setBootMessage('Ship starting');
93+ setBootProgress(0.85);
94+ }
95+ });
96+
97+ source.addEventListener('run', event => {
98+ if (event.data === 'goad: recompiling all apps') {
99+ setBootProgress(1.0);
100+ setBootMessage('Ship Launched');
101+ syncStatus();
102+ }
103+ if (event.data !== '') {
104+ setRunEvents(previous => [...previous, event.data]);
105+ }
106+ });
107+
108+ source.addEventListener(
109+ 'error',
110+ error => {
111+ if (source.readyState === EventSource.CLOSED) {
112+ if (retry < 5) {
113+ newSource(url, retry + 1);
114+ return;
115+ }
116+ setError(error);
117+ }
118+ },
119+ false
120+ );
121+ },
122+ [setError, startTime, syncStatus]
123+ );
124+
125+ const getEvents = useCallback(() => {
126+ try {
127+ if (Nothing.hasInstance(pointCursor) || disabled) {
128+ return;
129+ }
130+ const patp = ob.patp(pointCursor.value);
131+ setError(undefined);
132+
133+ resetEvents();
134+
135+ const url = client.getShipsByPatpUrl(patp) + '/events';
136+ newSource(url);
137+ } catch (error) {
138+ setError(error);
139+ }
140+ }, [setError, disabled, resetEvents, client, pointCursor, newSource]);
141+
142+ syncStatus = useCallback(async () => {
143+ try {
144+ if (Nothing.hasInstance(pointCursor) || disabled) {
145+ return;
146+ }
147+ const patp = ob.patp(pointCursor.value);
148+ setError(undefined);
149+
150+ resetEvents();
151+
152+ const response = await client.getShipsByPatp(patp);
153+
154+ if (response.status === 404) {
155+ setStatus(STATE.MISSING);
156+ } else if (!response.ok) {
157+ setError(response.statusText);
158+ } else {
159+ const ship = await response.json();
160+
161+ if (ship.status === 'Pending') {
162+ setStatus(STATE.PENDING);
163+ getEvents();
164+ } else if (ship.status === 'Running') {
165+ setStatus(STATE.RUNNING);
166+ } else {
167+ setError(ship);
168+ }
169+ }
170+ } catch (error) {
171+ setError(error);
172+ }
173+ }, [
174+ client,
175+ getEvents,
176+ pointCursor,
177+ resetEvents,
178+ setError,
179+ setStatus,
180+ disabled,
181+ ]);
182+
183+ const hostedShipUrl = useMemo(
184+ () => `http://${ob.patp(pointCursor.getOrElse('')).slice(1)}.${domain}`,
185+ [pointCursor, domain]
186+ );
187+
188+ const create = useCallback(
189+ async keyfile => {
190+ try {
191+ if (Nothing.hasInstance(pointCursor) || disabled) {
192+ return;
193+ }
194+ const patp = ob.patp(pointCursor.value).slice(1);
195+ setError(undefined);
196+
197+ setStatus(STATE.PENDING);
198+ setBootProgress(0.1);
199+
200+ const response = await client.postShips({
201+ patp,
202+ key: keyfile,
203+ debug: true,
204+ fake: false,
205+ });
206+
207+ if (!response.ok) {
208+ setError(response.statusText);
209+ } else {
210+ getEvents();
211+ }
212+ } catch (error) {
213+ setError(error);
214+ }
215+ },
216+ [pointCursor, setError, setStatus, client, getEvents, disabled]
217+ );
218+
219+ return {
220+ error,
221+ unknown,
222+ missing,
223+ running,
224+ pending,
225+ url: hostedShipUrl,
226+
227+ sysEvents,
228+ newEvents,
229+ runEvents,
230+
231+ syncStatus,
232+ getEvents,
233+ create,
234+
235+ bootProgress,
236+ bootMessage,
237+
238+ disabled,
239+ // debugging
240+ status,
241+ };
242+}
243+
244+export default useHostingStore;
src/store/lib/usePointStore.js+12−1
@@ -6,6 +6,7 @@ import useRekeyDateStore from './useRekeyDateStore';
6import useInvitesStore from './useInvitesStore';
6import useInvitesStore from './useInvitesStore';
7import useControlledPointsStore from './useControlledPointsStore';
7import useControlledPointsStore from './useControlledPointsStore';
8import useEclipticOwnerStore from './useEclipticOwnerStore';
8import useEclipticOwnerStore from './useEclipticOwnerStore';
9+import useResidents from './useResidentsStore';
9
10
10export default function usePointStore() {
11export default function usePointStore() {
11 const { syncDetails, ...details } = useDetailsStore();
12 const { syncDetails, ...details } = useDetailsStore();
@@ -16,6 +17,7 @@ export default function usePointStore() {
16 syncControlledPoints,
17 syncControlledPoints,
17 ...controlledPoints
18 ...controlledPoints
18 } = useControlledPointsStore();
19 } = useControlledPointsStore();
20+ const { syncResidents, syncResidentCount, ...residents } = useResidents();
19 const ecliptic = useEclipticOwnerStore();
21 const ecliptic = useEclipticOwnerStore();
20
22
21 // sync all of the on-chain info required to display a known point
23 // sync all of the on-chain info required to display a known point
@@ -39,10 +41,17 @@ export default function usePointStore() {
39 syncForeignPoint(point),
41 syncForeignPoint(point),
40 syncKnownPoint(point),
42 syncKnownPoint(point),
41 //
43 //
44+ syncResidentCount(point),
42 syncDetails(point),
45 syncDetails(point),
43 syncInvites(point),
46 syncInvites(point),
44 ]),
47 ]),
48+ [
45− [syncForeignPoint, syncKnownPoint, syncDetails, syncInvites]
49+ syncForeignPoint,
50+ syncKnownPoint,
51+ syncDetails,
52+ syncInvites,
53+ syncResidentCount,
54+ ]
46 );
55 );
47
56
48 return {
57 return {
@@ -52,6 +61,7 @@ export default function usePointStore() {
52 ...invites,
61 ...invites,
53 ...controlledPoints,
62 ...controlledPoints,
54 ...ecliptic,
63 ...ecliptic,
64+ ...residents,
55 syncDetails,
65 syncDetails,
56 syncRekeyDate,
66 syncRekeyDate,
57 syncInvites,
67 syncInvites,
@@ -59,5 +69,6 @@ export default function usePointStore() {
59 syncKnownPoint,
69 syncKnownPoint,
60 syncOwnedPoint,
70 syncOwnedPoint,
61 syncForeignPoint,
71 syncForeignPoint,
72+ syncResidents,
62 };
73 };
63}
74}
src/store/lib/useResidentsStore.jsnew file+104
@@ -0,0 +1,104 @@
1+import { useState, useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+
4+import { azimuth } from 'azimuth-js';
5+
6+import { useNetwork } from 'store/network';
7+
8+import * as need from 'lib/need';
9+
10+const emptyCacheEntry = {
11+ residentCount: Nothing(),
12+ requestCount: Nothing(),
13+ residents: Nothing(),
14+ requests: Nothing(),
15+};
16+
17+export default function useResidents() {
18+ const [residentCache, _setResidentCache] = useState({});
19+
20+ const addToResidentCache = useCallback(
21+ (point, entry) =>
22+ _setResidentCache(cache => ({ ...cache, [point]: entry })),
23+ [_setResidentCache]
24+ );
25+
26+ const getResidents = useCallback(
27+ point => residentCache[point] || emptyCacheEntry,
28+ [residentCache]
29+ );
30+
31+ const { contracts } = useNetwork();
32+ const _contracts = need.contracts(contracts);
33+
34+ const syncResidentCount = useCallback(
35+ async point => {
36+ // Do nothing if we've already fetched in full
37+ // or we aren't a sponsor
38+ const pointSize = azimuth.getPointSize(point);
39+ if (Just.hasInstance(getResidents(point).residents)) {
40+ return;
41+ }
42+ if (pointSize === azimuth.PointSize.Planet) {
43+ addToResidentCache(point, {
44+ residentCount: Just(0),
45+ requestCount: Just(0),
46+ requests: Just([]),
47+ residents: Just([]),
48+ });
49+ return;
50+ }
51+
52+ const isGalaxy = pointSize === azimuth.PointSize.Galaxy;
53+ const [residentCount, requestCount] = await Promise.all([
54+ azimuth.getSponsoringCount(_contracts, point),
55+ azimuth.getEscapeRequestsCount(_contracts, point),
56+ ]);
57+
58+ // Galaxies sponsor themselves
59+ const _residentCount = isGalaxy
60+ ? residentCount.toNumber() - 1
61+ : residentCount.toNumber();
62+
63+ addToResidentCache(point, {
64+ residentCount: Just(_residentCount),
65+ requestCount: Just(requestCount.toNumber()),
66+ residents: Nothing(),
67+ requests: Nothing(),
68+ });
69+ },
70+ [addToResidentCache, _contracts, getResidents]
71+ );
72+
73+ const syncResidents = useCallback(
74+ async point => {
75+ if (azimuth.getPointSize(point) === azimuth.PointSize.Planet) {
76+ addToResidentCache(point, {
77+ residentCount: Just(0),
78+ requestCount: Just(0),
79+ requests: Just([]),
80+ residents: Just([]),
81+ });
82+
83+ return;
84+ }
85+ const [residents, requests] = await Promise.all([
86+ azimuth.getSponsoring(_contracts, point),
87+ azimuth.getEscapeRequests(_contracts, point),
88+ ]);
89+
90+ // Galaxies sponsor themselves
91+ const _residents = residents.filter(r => r !== point);
92+
93+ addToResidentCache(point, {
94+ residentCount: Just(_residents.length),
95+ requestCount: Just(requests.length),
96+ residents: Just(_residents),
97+ requests: Just(requests),
98+ });
99+ },
100+ [addToResidentCache, _contracts]
101+ );
102+
103+ return { getResidents, syncResidentCount, syncResidents };
104+}
src/views/Activate.js+8−1
@@ -1,4 +1,4 @@
1−import React from 'react';
1+import React, { useEffect } from 'react';
2
2
3import useRouter from 'lib/useRouter';
3import useRouter from 'lib/useRouter';
4import { LocalRouterProvider } from 'lib/LocalRouter';
4import { LocalRouterProvider } from 'lib/LocalRouter';
@@ -30,6 +30,13 @@ export default function Activate() {
30 initialRoutes: [{ key: NAMES.CODE }],
30 initialRoutes: [{ key: NAMES.CODE }],
31 });
31 });
32
32
33+ useEffect(() => {
34+ // remove potential /activate path on unmount
35+ return () => {
36+ window.history.replaceState(null, null, '');
37+ };
38+ }, []);
39+
33 // when we know the derived point, ensure we have the data to display it
40 // when we know the derived point, ensure we have the data to display it
34 useSyncKnownPoints(
41 useSyncKnownPoints(
35 [state.derivedPoint.getOrElse(null)].filter(p => p !== null)
42 [state.derivedPoint.getOrElse(null)].filter(p => p !== null)
src/views/Activate/ActivateCode.js+20−16
@@ -157,7 +157,7 @@ export default function ActivateCode() {
157 </Grid.Item>
157 </Grid.Item>
158 <Grid.Item full as={P} className="mb2">
158 <Grid.Item full as={P} className="mb2">
159 Someone has invited you to claim your Urbit identity and join the
159 Someone has invited you to claim your Urbit identity and join the
160− network. Enter your activation code to continue.
160+ network. {!impliedTicket && 'Enter your activation code to continue.'}
161 </Grid.Item>
161 </Grid.Item>
162 <BridgeForm
162 <BridgeForm
163 validate={validate}
163 validate={validate}
@@ -166,21 +166,25 @@ export default function ActivateCode() {
166 initialValues={initialValues}>
166 initialValues={initialValues}>
167 {({ validating, values, submitting, handleSubmit }) => (
167 {({ validating, values, submitting, handleSubmit }) => (
168 <>
168 <>
169+ {!impliedTicket && (
170+ <>
169− <Grid.Item
171+ <Grid.Item
170− full
172+ full
171− as={TicketInput}
173+ as={TicketInput}
172− type={values.showTicket ? 'text' : 'password'}
174+ type={values.showTicket ? 'text' : 'password'}
173− name="ticket"
175+ name="ticket"
174− label="Activation Code"
176+ label="Activation Code"
175− disabled={!activationAllowed}
177+ disabled={!activationAllowed}
176− />
178+ />
177−
179+
178− <Grid.Item
180+ <Grid.Item
179− full
181+ full
180− as={CheckboxInput}
182+ as={CheckboxInput}
181− name="showTicket"
183+ name="showTicket"
182− label="Show"
184+ label="Show"
183− />
185+ />
186+ </>
187+ )}
184
188
185 <Grid.Item full as={FormError} />
189 <Grid.Item full as={FormError} />
186
190
src/views/Admin.js−3
@@ -12,7 +12,6 @@ import Crumbs from 'components/Crumbs';
12
12
13import AdminHome from './Admin/AdminHome';
13import AdminHome from './Admin/AdminHome';
14import AdminEditPermissions from './Admin/AdminEditPermissions';
14import AdminEditPermissions from './Admin/AdminEditPermissions';
15−import AdminRedownload from './Admin/AdminRedownload';
16import AdminReticket from './Admin/AdminReticket';
15import AdminReticket from './Admin/AdminReticket';
17import AdminSetProxy from './Admin/AdminSetProxy';
16import AdminSetProxy from './Admin/AdminSetProxy';
18import AdminTransfer from './Admin/AdminTransfer';
17import AdminTransfer from './Admin/AdminTransfer';
@@ -22,7 +21,6 @@ import AdminNetworkingKeys from './Admin/AdminNetworkingKeys';
22const NAMES = {
21const NAMES = {
23 HOME: 'HOME',
22 HOME: 'HOME',
24 EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
23 EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
25− REDOWNLOAD: 'REDOWNLOAD',
26 RETICKET: 'RETICKET',
24 RETICKET: 'RETICKET',
27 SET_PROXY: 'SET_PROXY',
25 SET_PROXY: 'SET_PROXY',
28 TRANSFER: 'TRANSFER',
26 TRANSFER: 'TRANSFER',
@@ -33,7 +31,6 @@ const NAMES = {
33const VIEWS = {
31const VIEWS = {
34 [NAMES.HOME]: AdminHome,
32 [NAMES.HOME]: AdminHome,
35 [NAMES.EDIT_PERMISSIONS]: AdminEditPermissions,
33 [NAMES.EDIT_PERMISSIONS]: AdminEditPermissions,
36− [NAMES.REDOWNLOAD]: AdminRedownload,
37 [NAMES.RETICKET]: AdminReticket,
34 [NAMES.RETICKET]: AdminReticket,
38 [NAMES.SET_PROXY]: AdminSetProxy,
35 [NAMES.SET_PROXY]: AdminSetProxy,
39 [NAMES.TRANSFER]: AdminTransfer,
36 [NAMES.TRANSFER]: AdminTransfer,
src/views/Admin/AdminEditPermissions.jsdeleted−130
@@ -1,130 +0,0 @@
1−import React, { useCallback } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import { Grid, LinkButton } from 'indigo-react';
4−import { azimuth } from 'azimuth-js';
5−
6−import { useNetwork } from 'store/network';
7−import { usePointCursor } from 'store/pointCursor';
8−import { usePointCache } from 'store/pointCache';
9−
10−import * as need from 'lib/need';
11−import { isZeroAddress } from 'lib/wallet';
12−import { PROXY_TYPE, proxyTypeToHuman } from 'lib/proxy';
13−import { useLocalRouter } from 'lib/LocalRouter';
14−import capitalize from 'lib/capitalize';
15−import { eqAddr } from 'lib/wallet';
16−import useCurrentPermissions from 'lib/useCurrentPermissions';
17−
18−import ViewHeader from 'components/ViewHeader';
19−import { ForwardButton } from 'components/Buttons';
20−import { matchBlinkyDate } from 'components/Blinky';
21−import CopyButton from 'components/CopyButton';
22−import convertToInt from 'lib/convertToInt';
23−
24−export default function AdminEditPermissions() {
25− const { push, names } = useLocalRouter();
26− const { contracts } = useNetwork();
27− const { pointCursor } = usePointCursor();
28− const { getDetails, getRekeyDate } = usePointCache();
29−
30− const _contracts = need.contracts(contracts);
31−
32− const point = need.point(pointCursor);
33− const pointSize = azimuth.getPointSize(point);
34− const isParent = pointSize !== azimuth.PointSize.Planet;
35− const isSenate = pointSize === azimuth.PointSize.Galaxy;
36−
37− const details = need.details(getDetails(point));
38− const networkRevision = convertToInt(details.keyRevisionNumber, 10);
39− const { canManage, isOwner } = useCurrentPermissions();
40−
41− const goSetProxy = useCallback(
42− proxyType => push(names.SET_PROXY, { proxyType }),
43− [push, names]
44− );
45−
46− const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
47− push,
48− names,
49− ]);
50−
51− const renderProxyAction = (proxyType, address) => {
52− if (isZeroAddress(address)) {
53− address = 'Not set';
54− } else if (eqAddr(address, _contracts.delegatedSending.address)) {
55− address = `${address} (invites contract)`;
56− }
57−
58− return (
59− <>
60− <Grid.Item
61− full
62− as={ForwardButton}
63− disabled={!isOwner}
64− onClick={() => goSetProxy(proxyType)}
65− detail={address}
66− detailClassName="mono"
67− accessory={<LinkButton>Edit</LinkButton>}>
68− {capitalize(proxyTypeToHuman(proxyType))} Proxy Address
69− </Grid.Item>
70− <Grid.Divider />
71− </>
72− );
73− };
74−
75− const renderNetworkKeysDetail = () => {
76− if (networkRevision === 0) {
77− return 'Not yet configured.';
78− }
79−
80− const renderRevision = maybeDate => (
81− <>
82− Revision #{networkRevision}, since {matchBlinkyDate(maybeDate)}
83− </>
84− );
85−
86− return getRekeyDate(point).matchWith({
87− Nothing: () => renderRevision(Nothing()),
88− Just: p =>
89− p.value.matchWith({
90− Ok: r => renderRevision(Just(r.value)),
91− Error: 'Unknown',
92− }),
93− });
94− };
95−
96− return (
97− <Grid>
98− <Grid.Item full as={ViewHeader}>
99− Permissions
100− </Grid.Item>
101−
102− <Grid.Item
103− full
104− as={ForwardButton}
105− detail={details.owner}
106− detailClassName="mono"
107− accessory={<CopyButton text={details.owner} />}>
108− Ownership Address
109− </Grid.Item>
110− <Grid.Divider />
111−
112− {renderProxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy)}
113−
114− {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
115−
116− {isSenate && renderProxyAction(PROXY_TYPE.VOTING, details.votingProxy)}
117−
118− <Grid.Item
119− full
120− as={ForwardButton}
121− disabled={!canManage}
122− onClick={goNetworkingKeys}
123− detail={renderNetworkKeysDetail()}
124− accessory={<LinkButton>View</LinkButton>}>
125− Networking Keys
126− </Grid.Item>
127− <Grid.Divider />
128− </Grid>
129− );
130−}
src/views/Admin/AdminHome.jsdeleted−106
@@ -1,106 +0,0 @@
1−import React, { useCallback } from 'react';
2−import { Just } from 'folktale/maybe';
3−import { Grid, B } from 'indigo-react';
4−
5−import { useWallet } from 'store/wallet';
6−
7−import { useLocalRouter } from 'lib/LocalRouter';
8−import useCurrentPermissions from 'lib/useCurrentPermissions';
9−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
10−
11−import { ForwardButton } from 'components/Buttons';
12−import ViewHeader from 'components/ViewHeader';
13−import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
14−
15−export default function AdminHome() {
16− const { push, names } = useLocalRouter();
17− const { urbitWallet } = useWallet();
18−
19− const { bind: keyfileBind } = useKeyfileGenerator();
20−
21− const { isOwner, canTransfer, isTransferProxySet } = useCurrentPermissions();
22−
23− const goRedownload = useCallback(() => push(names.REDOWNLOAD), [push, names]);
24− const goReticket = useCallback(() => push(names.RETICKET), [push, names]);
25− const goEditPermissions = useCallback(() => push(names.EDIT_PERMISSIONS), [
26− push,
27− names,
28− ]);
29− const goCancelTransfer = useCallback(() => push(names.CANCEL_TRANSFER), [
30− names,
31− push,
32− ]);
33− const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
34−
35− const canDownloadPassport = Just.hasInstance(urbitWallet);
36−
37− return (
38− <>
39− <Grid>
40− <Grid.Item full as={ViewHeader}>
41− Admin
42− </Grid.Item>
43−
44− <Grid.Item
45− full
46− as={ForwardButton}
47− disabled={!canDownloadPassport}
48− onClick={goRedownload}
49− detail="Re-download your paper wallet"
50− disabledDetail={
51− <B className="wrap ws-normal">
52− · Master Ticket Authentication required.
53− </B>
54− }>
55− Download Passport
56− </Grid.Item>
57− <Grid.Divider />
58− <Grid.Item
59− full
60− as={DownloadKeyfileButton}
61− {...keyfileBind}
62− detail="Download your current Arvo Keyfile">
63− Download Arvo Keyfile
64− </Grid.Item>
65− <Grid.Divider />
66− <Grid.Item
67− full
68− as={ForwardButton}
69− disabled={!isOwner}
70− onClick={goReticket}
71− detail="Move to brand new wallet, resetting all permissions">
72− Reticket
73− </Grid.Item>
74− <Grid.Divider />
75− <Grid.Item
76− full
77− as={ForwardButton}
78− onClick={goEditPermissions}
79− detail="Proxies, networking keys, etc.">
80− Edit Permissions
81− </Grid.Item>
82− <Grid.Divider />
83− {isTransferProxySet ? (
84− <Grid.Item
85− full
86− as={ForwardButton}
87− onClick={goCancelTransfer}
88− detail="Cancel outgoing transfer"
89− disabled={!canTransfer}>
90− Cancel Outgoing Transfer
91− </Grid.Item>
92− ) : (
93− <Grid.Item
94− full
95− as={ForwardButton}
96− onClick={goTransfer}
97− detail="Transfer ownership of this ID"
98− disabled={!canTransfer}>
99− Transfer
100− </Grid.Item>
101− )}
102− <Grid.Divider />
103− </Grid>
104− </>
105− );
106−}
src/views/Admin/AdminNetworkingKeys.jsdeleted−434
@@ -1,434 +0,0 @@
1−import React, { useCallback, useMemo, useRef, useState } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import { Grid, Text, H5, Flex, ToggleInput, CheckboxInput } from 'indigo-react';
4−import * as azimuth from 'azimuth-js';
5−import ob from 'urbit-ob';
6−import { randomHex } from 'web3-utils';
7−
8−import { usePointCursor } from 'store/pointCursor';
9−import { usePointCache } from 'store/pointCache';
10−import { useNetwork } from 'store/network';
11−import { useWallet } from 'store/wallet';
12−
13−import { useLocalRouter } from 'lib/LocalRouter';
14−import * as need from 'lib/need';
15−import {
16− segmentNetworkKey,
17− attemptNetworkSeedDerivation,
18− deriveNetworkKeys,
19− CRYPTO_SUITE_VERSION,
20− CURVE_ZERO_ADDR,
21−} from 'lib/keys';
22−import { formatDotsWithTime } from 'lib/dateFormat';
23−import useEthereumTransaction from 'lib/useEthereumTransaction';
24−import { GAS_LIMITS } from 'lib/constants';
25−import { addHexPrefix } from 'lib/wallet';
26−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
27−
28−import ViewHeader from 'components/ViewHeader';
29−import { BootUrbitOSButton } from 'components/Buttons';
30−import FooterButton from 'components/FooterButton';
31−import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
32−import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
33−import NoticeBox from 'components/NoticeBox';
34−
35−import { HexInput } from 'form/Inputs';
36−import {
37− composeValidator,
38− buildCheckboxValidator,
39− buildHexValidator,
40−} from 'form/validators';
41−import BridgeForm from 'form/BridgeForm';
42−import Condition from 'form/Condition';
43−import FormError from 'form/FormError';
44−import convertToInt from 'lib/convertToInt';
45−
46−const chainKeyProp = name => d =>
47− d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
48−
49−const renderNetworkKey = key => {
50− const segments = segmentNetworkKey(key);
51−
52− if (!segments) {
53− return 'Unset';
54− }
55−
56− return segments.join('\n');
57−};
58−
59−function useSetKeys() {
60− const { urbitWallet, wallet, authMnemonic } = useWallet();
61− const { pointCursor } = usePointCursor();
62− const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
63− const { contracts } = useNetwork();
64−
65− const _point = need.point(pointCursor);
66− const _contracts = need.contracts(contracts);
67− const _details = need.details(getDetails(_point));
68−
69− const networkRevision = convertToInt(_details.keyRevisionNumber, 10);
70− const randomSeed = useRef();
71−
72− // NOTE: nd = 'nondeterministic' (can also be a 'manual' seed)
73− const [ndNetworkSeed, setNdNetworkSeed] = useState();
74−
75− const {
76− available: keyfileAvailable,
77− generating: keyfileGenerating,
78− filename,
79− bind: keyfileBind,
80− } = useKeyfileGenerator(ndNetworkSeed);
81−
82− const buildNetworkSeed = useCallback(
83− async manualSeed => {
84− if (manualSeed !== undefined) {
85− setNdNetworkSeed(manualSeed);
86− return manualSeed;
87− } else {
88− const newNetworkRevision = networkRevision + 1;
89− console.log(`deriving seed with revision ${newNetworkRevision}`);
90−
91− const networkSeed = await attemptNetworkSeedDerivation({
92− urbitWallet,
93− wallet,
94− authMnemonic,
95− details: _details,
96− revision: newNetworkRevision,
97− });
98−
99− if (Just.hasInstance(networkSeed)) {
100− return networkSeed.value;
101− }
102−
103− randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
104− setNdNetworkSeed(randomSeed.current);
105−
106− return randomSeed.current;
107− }
108− },
109− [_details, authMnemonic, networkRevision, urbitWallet, wallet]
110− );
111−
112− const { completed: _completed, ...rest } = useEthereumTransaction(
113− useCallback(
114− async (manualSeed, isDiscontinuity) => {
115− const seed = await buildNetworkSeed(manualSeed);
116− const pair = deriveNetworkKeys(seed);
117−
118− return azimuth.ecliptic.configureKeys(
119− _contracts,
120− _point,
121− addHexPrefix(pair.crypt.public),
122− addHexPrefix(pair.auth.public),
123− CRYPTO_SUITE_VERSION,
124− isDiscontinuity
125− );
126− },
127− [_contracts, _point, buildNetworkSeed]
128− ),
129− useCallback(
130− () => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
131− [_point, syncDetails, syncRekeyDate]
132− ),
133− GAS_LIMITS.CONFIGURE_KEYS
134− );
135−
136− // only treat the transaction as completed once we also have keys to download
137− const completed = _completed && keyfileAvailable && !keyfileGenerating;
138−
139− return {
140− completed,
141− ndNetworkSeed,
142− filename,
143− keyfileBind,
144− ...rest,
145− };
146−}
147−
148−export default function AdminNetworkingKeys() {
149− const { push, names, pop } = useLocalRouter();
150− const { pointCursor } = usePointCursor();
151− const { getDetails, getRekeyDate } = usePointCache();
152−
153− const point = need.point(pointCursor);
154− const details = getDetails(point);
155−
156− const hasKeys = details.matchWith({
157− Nothing: () => false,
158− Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
159− // we actually don't mind the default NaN behavior of parseInt here,
160− // since NaN > 0 === false and that's a reasonable result
161− });
162−
163− const [usedDiscontinuity, setUsedDiscontinuity] = useState(false);
164−
165− const {
166− isDefaultState,
167− construct,
168− unconstruct,
169− broadcasting,
170− completed,
171− inputsLocked,
172− filename,
173− bind,
174− keyfileBind,
175− } = useSetKeys();
176−
177− const validateForm = useCallback((values, errors) => {
178− if (values.useNetworkSeed && errors.networkSeed) {
179− return errors;
180− }
181− }, []);
182−
183− const validate = useMemo(
184− () =>
185− composeValidator(
186− {
187− useNetworkSeed: buildCheckboxValidator(),
188− networkSeed: buildHexValidator(64), // 64 chars
189− useDiscontinuity: buildCheckboxValidator(),
190− },
191− validateForm
192− ),
193− [validateForm]
194− );
195−
196− const onValues = useCallback(
197− ({ valid, values, form }) => {
198− if (valid) {
199− construct(
200− values.useNetworkSeed ? values.networkSeed : undefined,
201− values.useDiscontinuity
202− );
203− setUsedDiscontinuity(values.useDiscontinuity);
204− } else {
205− unconstruct();
206− }
207−
208− if (!values.useNetworkSeed && values.networkSeed) {
209− form.change('networkSeed', '');
210− }
211− },
212− [construct, unconstruct, setUsedDiscontinuity]
213− );
214−
215− const initialValues = useMemo(
216− () => ({
217− useNetworkSeed: false,
218− useDiscontinuity: false,
219− }),
220− []
221− );
222−
223− const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
224−
225− const usageMessage = useMemo(() => {
226− const need = 'You need this keyfile to authenticate with Arvo.';
227− const boot = (
228− <>
229− boot fresh with{' '}
230− <code>
231− urbit -w {ob.patp(point).slice(1)} -k {filename}
232− </code>
233− </>
234− );
235− if (usedDiscontinuity) {
236− return (
237− <>
238− {need} Since you broke continuity, (re)move your old files, and {boot}
239− </>
240− );
241− } else {
242− return (
243− <>
244− {need} If you have already booted Arvo, run{' '}
245− <code>|rekey '0wkeyfile.contents'</code>. Otherwise, {boot}
246− </>
247− );
248− }
249− }, [usedDiscontinuity, filename, point]);
250−
251− const renderTitle = () => {
252− if (completed) {
253− return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
254− }
255−
256− if (broadcasting) {
257− return 'Setting Network Keys...';
258− }
259−
260− if (!hasKeys) {
261− return 'Networking keys have not yet been set.';
262− }
263−
264− return 'Networking';
265− };
266−
267− const renderNetworkKeySection = (title, key) => (
268− <>
269− <Grid.Item full as={H5} className="mt3 gray4">
270− {title}
271− </Grid.Item>
272− {key.matchWith({
273− Nothing: () => (
274− <Grid.Item full as="code" className="f5 mono gray4">
275− Unset
276− </Grid.Item>
277− ),
278− Just: ({ value: key }) => (
279− <>
280− <Grid.Item full as="code" className="f5 mono wrap">
281− 0x
282− </Grid.Item>
283− <Grid.Item full as="code" className="f5 mono wrap">
284− {renderNetworkKey(key)}
285− </Grid.Item>
286− </>
287− ),
288− })}
289− </>
290− );
291−
292− const renderDetail = (title, value) => (
293− <Flex.Item as={Flex} col>
294− <Flex.Item as={H5} className="gray4">
295− {title}
296− </Flex.Item>
297− {value.matchWith({
298− Nothing: () => (
299− <Flex.Item as={Text} className="f5 gray4">
300− Unset
301− </Flex.Item>
302− ),
303− Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
304− })}
305− </Flex.Item>
306− );
307−
308− const renderDetails = () => {
309− return (
310− <>
311− {renderNetworkKeySection(
312− 'Authentication',
313− details.chain(chainKeyProp('authenticationKey'))
314− )}
315− {renderNetworkKeySection(
316− 'Encryption',
317− details.chain(chainKeyProp('encryptionKey'))
318− )}
319− <Grid.Item full as={Flex} row justify="between" className="mt3">
320− {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
321− {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
322− {renderDetail(
323− 'Crypto Suite Ver.',
324− details.map(d => d.cryptoSuiteVersion)
325− )}
326− </Grid.Item>
327− {hasKeys && (
328− <Grid.Item full as={Flex} row justify="between" className="mt3">
329− {renderDetail(
330− 'Last Set',
331− getRekeyDate(point).map(date =>
332− date.matchWith({
333− Ok: r => formatDotsWithTime(r.value),
334− Error: () => Nothing(),
335− })
336− )
337− )}
338− </Grid.Item>
339− )}
340− </>
341− );
342− };
343−
344− return (
345− <>
346− <Grid>
347− <Grid.Item full as={ViewHeader}>
348− {renderTitle()}
349− </Grid.Item>
350−
351− <Grid.Item full as={Text} className="mb3">
352− {hasKeys
353− ? 'Here are your public keys that authenticate your Arvo.'
354− : 'Set your networking keys to authenticate with Arvo.'}
355− </Grid.Item>
356−
357− {completed && (
358− <>
359− <Grid.Item full as={NoticeBox} className="mb3">
360− {usageMessage}
361− </Grid.Item>
362− <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
363− </>
364− )}
365−
366− {!completed && (
367− <BridgeForm
368− validate={validate}
369− onValues={onValues}
370− initialValues={initialValues}>
371− {() => (
372− <>
373− <Grid.Item
374− full
375− as={ToggleInput}
376− name="useNetworkSeed"
377− label="Use Custom Network Seed"
378− inverseLabel="Back to Derived Network Seed"
379− disabled={inputsLocked}
380− />
381− <Condition when="useNetworkSeed" is={true}>
382− <Grid.Item full as={NoticeBox} className="mb2">
383− When using a custom network seed, you'll need to download
384− your Arvo keyfile immediately after this transaction is
385− Bridge does not store your seed.
386− </Grid.Item>
387− <Grid.Item
388− full
389− as={HexInput}
390− name="networkSeed"
391− label="Network Seed (32 bytes)"
392− disabled={inputsLocked}
393− />
394− </Condition>
395− <Grid.Item
396− full
397− as={CheckboxInput}
398− name="useDiscontinuity"
399− label="Breach Continuity"
400− disabled={inputsLocked}
401− />
402−
403− <Grid.Item full as={FormError} />
404−
405− <Grid.Item
406− full
407− as={InlineEthereumTransaction}
408− {...bind}
409− label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
410− onReturn={() => pop()}
411− />
412− </>
413− )}
414− </BridgeForm>
415− )}
416−
417− {isDefaultState && renderDetails()}
418−
419− {completed && (
420− <>
421− <Grid.Item full as={BootUrbitOSButton} />
422− <Grid.Divider />
423− </>
424− )}
425− </Grid>
426−
427− {isDefaultState && (
428− <FooterButton onClick={goRelocate} disabled>
429− Relocate
430− </FooterButton>
431− )}
432− </>
433− );
434−}
src/views/Adopt.jsnew file+88
@@ -0,0 +1,88 @@
1+import React, { useCallback, useEffect, useMemo } from 'react';
2+import { Grid } from 'indigo-react';
3+import * as ob from 'urbit-ob';
4+import { ecliptic } from 'azimuth-js';
5+
6+import View from 'components/View';
7+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
8+import ViewHeader from 'components/ViewHeader';
9+
10+import { useNetwork } from 'store/network';
11+import { usePointCache } from 'store/pointCache';
12+import { usePointCursor } from 'store/pointCursor';
13+import { useHistory } from 'store/history';
14+
15+import { GAS_LIMITS } from 'lib/constants';
16+import useEthereumTransaction from 'lib/useEthereumTransaction';
17+import * as need from 'lib/need';
18+
19+function useAdopt() {
20+ const { contracts } = useNetwork();
21+ const _contracts = need.contracts(contracts);
22+
23+ const { pointCursor } = usePointCursor();
24+ const point = need.point(pointCursor);
25+
26+ const { syncResidents } = usePointCache();
27+
28+ return useEthereumTransaction(
29+ useCallback(
30+ (adoptee, denied) =>
31+ denied
32+ ? ecliptic.reject(_contracts, adoptee)
33+ : ecliptic.adopt(_contracts, adoptee),
34+ [_contracts]
35+ ),
36+ useCallback(() => syncResidents(point), [syncResidents, point]),
37+ GAS_LIMITS.DEFAULT
38+ );
39+}
40+
41+export default function Adopt() {
42+ const { pop, data } = useHistory();
43+
44+ const { adoptee, denied } = data;
45+
46+ const patp = useMemo(() => ob.patp(adoptee), [adoptee]);
47+
48+ const { construct, bind, completed } = useAdopt();
49+
50+ const body = useMemo(
51+ () =>
52+ denied ? (
53+ <>
54+ {completed ? 'Denied' : 'Denying'} adoption request to{' '}
55+ <span className="mono">{patp}</span>
56+ </>
57+ ) : (
58+ <>
59+ {completed ? 'Approved' : 'Approving'} adoption request to{' '}
60+ <span className="mono">{patp}</span>
61+ </>
62+ ),
63+ [patp, denied, completed]
64+ );
65+
66+ useEffect(() => {
67+ construct(adoptee, denied);
68+ }, [construct, adoptee, denied]);
69+
70+ return (
71+ <View pop={() => pop()} inset>
72+ <Grid>
73+ <Grid.Item full as={ViewHeader} className="mb2">
74+ Adopt
75+ </Grid.Item>
76+ <Grid.Item full className="gray4 f5 mv2">
77+ {body}
78+ </Grid.Item>
79+ <Grid.Item
80+ full
81+ {...bind}
82+ as={InlineEthereumTransaction}
83+ onReturn={() => pop()}
84+ />
85+ </Grid>
86+ </View>
87+ );
88+}
src/views/CancelTransfer.js+82−3
@@ -1,9 +1,88 @@
1−import React from 'react';
1+import React, { useCallback } from 'react';
2+import cn from 'classnames';
3+import { Grid, Text } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
2
5
3−import View from 'components/View';
6+import { useNetwork } from 'store/network';
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
4
9
5−import AdminCancelTransfer from './Admin/AdminCancelTransfer';
10+import * as need from 'lib/need';
6import { useLocalRouter } from 'lib/LocalRouter';
11import { useLocalRouter } from 'lib/LocalRouter';
12+import useCurrentPointName from 'lib/useCurrentPointName';
13+import useEthereumTransaction from 'lib/useEthereumTransaction';
14+import { GAS_LIMITS } from 'lib/constants';
15+import { ETH_ZERO_ADDR } from 'lib/wallet';
16+import useLifecycle from 'lib/useLifecycle';
17+
18+import ViewHeader from 'components/ViewHeader';
19+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
20+import View from 'components/ViewHeader';
21+
22+function useCancelTransfer() {
23+ const { contracts } = useNetwork();
24+ const { pointCursor } = usePointCursor();
25+ const { syncDetails } = usePointCache();
26+
27+ const _contracts = need.contracts(contracts);
28+ const _point = need.point(pointCursor);
29+
30+ const { construct, ...rest } = useEthereumTransaction(
31+ useCallback(
32+ () =>
33+ azimuth.ecliptic.setTransferProxy(_contracts, _point, ETH_ZERO_ADDR),
34+ [_contracts, _point]
35+ ),
36+ useCallback(() => syncDetails(_point), [_point, syncDetails]),
37+ GAS_LIMITS.SET_PROXY
38+ );
39+
40+ useLifecycle(() => {
41+ construct();
42+ });
43+
44+ return {
45+ ...rest,
46+ };
47+}
48+
49+function AdminCancelTransfer() {
50+ const { pop } = useLocalRouter();
51+ const { getDetails } = usePointCache();
52+ const { pointCursor } = usePointCursor();
53+ const _point = need.point(pointCursor);
54+
55+ const name = useCurrentPointName();
56+ const _details = need.details(getDetails(_point));
57+
58+ const { completed, bind } = useCancelTransfer();
59+
60+ return (
61+ <Grid>
62+ <Grid.Item full as={ViewHeader}>
63+ Cancel Outgoing Transfer
64+ </Grid.Item>
65+
66+ <Grid.Item
67+ full
68+ as={Text}
69+ className={cn('f5 wrap', {
70+ green3: completed,
71+ })}>
72+ {completed
73+ ? `The outgoing transfer of ${name} has been cancelled.`
74+ : `Cancel the outgoing transfer of ${name} to ${_details.transferProxy}.`}
75+ </Grid.Item>
76+
77+ <Grid.Item
78+ full
79+ as={InlineEthereumTransaction}
80+ {...bind}
81+ onReturn={() => pop()}
82+ />
83+ </Grid>
84+ );
85+}
7
86
8export default function CancelTransfer() {
87export default function CancelTransfer() {
9 const { pop } = useLocalRouter();
88 const { pop } = useLocalRouter();
src/views/Invite/Cohort.js+5−6
@@ -14,7 +14,6 @@ import Tabs from 'components/Tabs';
14import BarGraph from 'components/BarGraph';
14import BarGraph from 'components/BarGraph';
15import MaybeSigil from 'components/MaybeSigil';
15import MaybeSigil from 'components/MaybeSigil';
16import { matchBlinky } from 'components/Blinky';
16import { matchBlinky } from 'components/Blinky';
17−import { ForwardButton } from 'components/Buttons';
18import Chip from 'components/Chip';
17import Chip from 'components/Chip';
19import Crumbs from 'components/Crumbs';
18import Crumbs from 'components/Crumbs';
20
19
@@ -29,6 +28,7 @@ import { ReactComponent as SearchIcon } from 'assets/search.svg';
29import CopyButton from 'components/CopyButton';
28import CopyButton from 'components/CopyButton';
30import { useSyncForeignPoints } from 'lib/useSyncPoints';
29import { useSyncForeignPoints } from 'lib/useSyncPoints';
31import { useNetwork } from 'store/network';
30import { useNetwork } from 'store/network';
31+import NavHeader from 'components/NavHeader';
32
32
33function SearchInput({ className, value, onChange }) {
33function SearchInput({ className, value, onChange }) {
34 return (
34 return (
@@ -314,17 +314,16 @@ export default function InviteCohort() {
314
314
315 return (
315 return (
316 <View pop={pop} inset>
316 <View pop={pop} inset>
317− <Grid gap={3}>
317+ <NavHeader>
318− <Grid.Item
319− full
320− as={Crumbs}
318+ <Crumbs
321 className="mb1"
319 className="mb1"
322 routes={[
320 routes={[
323 { text: ob.patp(point), action: () => pop() },
321 { text: ob.patp(point), action: () => pop() },
324 { text: 'Invite Group' },
322 { text: 'Invite Group' },
325 ]}
323 ]}
326 />
324 />
327−
325+ </NavHeader>
326+ <Grid gap={3}>
328 <Grid.Item full>Invite Group</Grid.Item>
327 <Grid.Item full>Invite Group</Grid.Item>
329 <Grid.Item full>
328 <Grid.Item full>
330 <Flex align="center">
329 <Flex align="center">
src/views/IssueChild.js+2−1
@@ -15,6 +15,7 @@ import patp2dec from 'lib/patp2dec';
15import { getSpawnCandidate } from 'lib/child';
15import { getSpawnCandidate } from 'lib/child';
16import { useLocalRouter } from 'lib/LocalRouter';
16import { useLocalRouter } from 'lib/LocalRouter';
17import useConstant from 'lib/useConstant';
17import useConstant from 'lib/useConstant';
18+import { validateChild } from 'lib/validators';
18
19
19import ViewHeader from 'components/ViewHeader';
20import ViewHeader from 'components/ViewHeader';
20import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
21import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
@@ -111,7 +112,7 @@ export default function IssueChild() {
111 () =>
112 () =>
112 composeValidator(
113 composeValidator(
113 {
114 {
114− point: buildPointValidator(4),
115+ point: buildPointValidator(4, [validateChild(ob.patp(_point))]),
115 owner: buildAddressValidator(),
116 owner: buildAddressValidator(),
116 },
117 },
117 validateForm
118 validateForm
src/views/Login.js+1−1
@@ -57,7 +57,7 @@ export default function Login() {
57 }, [push, names]);
57 }, [push, names]);
58
58
59 return (
59 return (
60− <View pop={pop} inset>
60+ <View inset>
61 <Grid>
61 <Grid>
62 <Grid.Item full as={Text} className="flex justify-center mt9 mb7">
62 <Grid.Item full as={Text} className="flex justify-center mt9 mb7">
63 <Grid.Item as={Text} className="gray3">
63 <Grid.Item as={Text} className="gray3">
src/views/Point.js+140−95
@@ -10,21 +10,105 @@ import { useWallet } from 'store/wallet';
10import View from 'components/View';
10import View from 'components/View';
11import Greeting from 'components/Greeting';
11import Greeting from 'components/Greeting';
12import Passport from 'components/Passport';
12import Passport from 'components/Passport';
13−import { ForwardButton, BootUrbitOSButton } from 'components/Buttons';
14−import CopyButton from 'components/CopyButton';
15import Blinky, { matchBlinky } from 'components/Blinky';
13import Blinky, { matchBlinky } from 'components/Blinky';
16import BarGraph from 'components/BarGraph';
14import BarGraph from 'components/BarGraph';
17import Chip from 'components/Chip';
15import Chip from 'components/Chip';
18import InviteSigilList from 'components/InviteSigilList';
16import InviteSigilList from 'components/InviteSigilList';
17+import { ForwardButton } from 'components/Buttons';
19
18
20import * as need from 'lib/need';
19import * as need from 'lib/need';
21import useInvites from 'lib/useInvites';
20import useInvites from 'lib/useInvites';
22import { useSyncOwnedPoints } from 'lib/useSyncPoints';
21import { useSyncOwnedPoints } from 'lib/useSyncPoints';
23import useCurrentPermissions from 'lib/useCurrentPermissions';
22import useCurrentPermissions from 'lib/useCurrentPermissions';
24import { useLocalRouter } from 'lib/LocalRouter';
23import { useLocalRouter } from 'lib/LocalRouter';
25−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
26
24
27import Inviter from 'views/Invite/Inviter';
25import Inviter from 'views/Invite/Inviter';
26+import { usePointCache } from 'store/pointCache';
27+
28+function InviteForm({
29+ showInviteForm,
30+ setShowInviteForm,
31+ acceptedInvites,
32+ acceptedPoints,
33+ availableInvites,
34+ goCohort,
35+ pendingPoints,
36+ sentInvites,
37+ showInvites,
38+}) {
39+ const _totalInvites =
40+ sentInvites.getOrElse(0) + availableInvites.getOrElse(0);
41+ const _pendingInvites = pendingPoints.getOrElse([]).length;
42+
43+ return (
44+ <>
45+ <Grid.Item cols={[1, 11]}>
46+ Invite Group
47+ <br />
48+ </Grid.Item>
49+
50+ <Grid.Item
51+ className={cn('t-right underline pointer-hover', {
52+ gray4: sentInvites.getOrElse(0) === 0,
53+ })}
54+ onClick={goCohort}
55+ cols={[11, 13]}>
56+ View
57+ </Grid.Item>
58+ <Grid.Item full>
59+ <Flex align="center">
60+ <Flex.Item>
61+ {acceptedInvites.getOrElse(0)} / {_totalInvites}
62+ </Flex.Item>
63+ {_pendingInvites > 0 && (
64+ <Flex.Item as={Chip} className="bg-yellow1 yellow4">
65+ {_pendingInvites} pending
66+ </Flex.Item>
67+ )}
68+ </Flex>
69+ </Grid.Item>
70+
71+ {showInvites && (
72+ <>
73+ <Grid.Item
74+ full
75+ as={BarGraph}
76+ available={availableInvites}
77+ sent={sentInvites}
78+ accepted={acceptedInvites}
79+ />
80+ <Grid.Item
81+ full
82+ as={InviteSigilList}
83+ pendingPoints={pendingPoints}
84+ acceptedPoints={acceptedPoints}
85+ />
86+ </>
87+ )}
88+ {!showInvites && (
89+ <>
90+ <Grid.Item full className="b-gray4 b-dotted b1 self-center">
91+ <div className="p4 pv8 t-center gray4">
92+ Start your invite group by adding members
93+ </div>
94+ </Grid.Item>
95+ </>
96+ )}
97+ {!showInviteForm && availableInvites.getOrElse(0) > 0 && (
98+ <Grid.Item
99+ full
100+ solid
101+ as={Button}
102+ center
103+ onClick={() => setShowInviteForm(true)}>
104+ Add Members
105+ </Grid.Item>
106+ )}
107+ {showInviteForm && <Inviter />}
108+ <Grid.Item full className="mb2" />
109+ </>
110+ );
111+}
28
112
29export default function Point() {
113export default function Point() {
30 const { pop, push, names } = useLocalRouter();
114 const { pop, push, names } = useLocalRouter();
@@ -33,7 +117,9 @@ export default function Point() {
33 const { wallet } = useWallet();
117 const { wallet } = useWallet();
34 const point = need.point(pointCursor);
118 const point = need.point(pointCursor);
35
119
36− const { code, notice } = useKeyfileGenerator();
120+ const { getResidents } = usePointCache();
121+
122+ const { residentCount, requestCount } = getResidents(point);
37
123
38 const {
124 const {
39 isParent,
125 isParent,
@@ -60,22 +146,20 @@ export default function Point() {
60
146
61 const hasInvites = showInvites || availableInvites.getOrElse(0) !== 0;
147 const hasInvites = showInvites || availableInvites.getOrElse(0) !== 0;
62
148
63− const _totalInvites =
64− sentInvites.getOrElse(0) + availableInvites.getOrElse(0);
65− const _pendingInvites = pendingPoints.getOrElse([]).length;
66−
67 const loadedInvites = Just.hasInstance(availableInvites);
149 const loadedInvites = Just.hasInstance(availableInvites);
68 //
150 //
69 // availableInvites.getOrElse(0) === 0
151 // availableInvites.getOrElse(0) === 0
70
152
71− const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
72−
73 const goSenate = useCallback(() => push(names.SENATE), [push, names]);
153 const goSenate = useCallback(() => push(names.SENATE), [push, names]);
74
154
75− const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
76−
77 const goCohort = useCallback(() => push(names.INVITE_COHORT), [push, names]);
155 const goCohort = useCallback(() => push(names.INVITE_COHORT), [push, names]);
78
156
157+ const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
158+
159+ const goUrbitID = useCallback(() => push(names.URBIT_ID), [push, names]);
160+
161+ const goResidents = useCallback(() => push(names.RESIDENTS), [push, names]);
162+
79 const goPartiesSetPoolSize = useCallback(
163 const goPartiesSetPoolSize = useCallback(
80 () => push(names.PARTY_SET_POOL_SIZE),
164 () => push(names.PARTY_SET_POOL_SIZE),
81 [push, names]
165 [push, names]
@@ -132,6 +216,8 @@ export default function Point() {
132
216
133 const address = need.addressFromWallet(wallet);
217 const address = need.addressFromWallet(wallet);
134
218
219+ const _requestCount = requestCount.getOrElse(0);
220+
135 return (
221 return (
136 <View pop={pop} inset>
222 <View pop={pop} inset>
137 <Greeting point={point} />
223 <Greeting point={point} />
@@ -141,73 +227,36 @@ export default function Point() {
141 animationMode={'slide'}
227 animationMode={'slide'}
142 />
228 />
143 <Grid gap={3}>
229 <Grid gap={3}>
144− {isPlanet && hasInvites && (
230+ {isParent && (
145− <>
146− <Grid.Item cols={[1, 11]}>
231+ <Grid.Item
147− Invite Group
232+ full
148− <br />
233+ as={Flex}
149− </Grid.Item>
234+ justify="between"
150−
235+ onClick={goResidents}
151− <Grid.Item
152− className={cn('t-right underline pointer-hover', {
236+ className="mv1 pointer">
153− gray4: sentInvites.getOrElse(0) === 0,
154− })}
155− onClick={goCohort}
156− cols={[11, 13]}>
157− View
158− </Grid.Item>
237+ <Flex.Item>
159− <Grid.Item full>
238+ Residents{' '}
160− <Flex align="center">
239+ <span className="gray3">{matchBlinky(residentCount)}</span>
161− <Flex.Item>
162− {acceptedInvites.getOrElse(0)} / {_totalInvites}
163− </Flex.Item>
240+ </Flex.Item>
164− {_pendingInvites > 0 && (
241+ {_requestCount !== 0 && (
165− <Flex.Item as={Chip} className="bg-yellow1 yellow4">
242+ <Flex.Item className="f7 bg-gray2 h3 pv1 ph2 br-full r-full text-center">
166− {_pendingInvites} pending
243+ {_requestCount} PENDING
167− </Flex.Item>
168− )}
169− </Flex>
170− </Grid.Item>
171−
172− {showInvites && (
173− <>
174− <Grid.Item
175− full
176− as={BarGraph}
177− available={availableInvites}
178− sent={sentInvites}
179− accepted={acceptedInvites}
180− />
181− <Grid.Item
182− full
183− as={InviteSigilList}
184− pendingPoints={pendingPoints}
185− acceptedPoints={acceptedPoints}
186− />
187− </>
188− )}
189− {!showInvites && (
190− <>
191− <Grid.Item full className="b-gray4 b-dotted b1 self-center">
192− <div className="p4 pv8 t-center gray4">
193− Start your invite group by adding members
194− </div>
195− </Grid.Item>
196− </>
197− )}
198− {!showInviteForm && availableInvites.getOrElse(0) > 0 && isOwner && (
199− <Grid.Item
200− full
201− solid
202− as={Button}
203− center
204− onClick={() => setShowInviteForm(true)}>
205− Add Members
206− </Grid.Item>
244+ </Flex.Item>
207 )}
245 )}
208− {showInviteForm && <Inviter />}
209− <Grid.Item full className="mb2" />
246+ </Grid.Item>
210− </>
247+ )}
248+ {isPlanet && hasInvites && (
249+ <InviteForm
250+ showInviteForm={showInviteForm}
251+ setShowInviteForm={setShowInviteForm}
252+ acceptedInvites={acceptedInvites}
253+ acceptedPoints={acceptedPoints}
254+ availableInvites={availableInvites}
255+ goCohort={goCohort}
256+ pendingPoints={pendingPoints}
257+ sentInvites={sentInvites}
258+ showInvites={showInvites}
259+ />
211 )}
260 )}
212 {!loadedInvites && isPlanet && (
261 {!loadedInvites && isPlanet && (
213 <Grid.Item className="mv2" full>
262 <Grid.Item className="mv2" full>
@@ -216,20 +265,28 @@ export default function Point() {
216 )}
265 )}
217 </Grid>
266 </Grid>
218 <Grid className="pt2">
267 <Grid className="pt2">
268+ <Grid.Divider />
219 {inviteButton}
269 {inviteButton}
270+ <Grid.Item
271+ full
220− <Grid.Item full as={ForwardButton} onClick={goSigil}>
272+ as={ForwardButton}
221− Sigil
273+ disabled={!canManage}
274+ onClick={goUrbitID}
275+ className="mt1"
276+ detail="Identity and security settings">
277+ ID
222 </Grid.Item>
278 </Grid.Item>
223 <Grid.Divider />
279 <Grid.Divider />
224 <Grid.Item
280 <Grid.Item
225 full
281 full
226 as={ForwardButton}
282 as={ForwardButton}
227 disabled={!canManage}
283 disabled={!canManage}
284+ className="mt1"
285+ detail="Urbit OS Settings"
228− onClick={goAdmin}>
286+ onClick={goUrbitOS}>
229− Admin
287+ OS
230 </Grid.Item>
288 </Grid.Item>
231 <Grid.Divider />
289 <Grid.Divider />
232−
233 {isParent && (
290 {isParent && (
234 <>
291 <>
235 <Grid.Item
292 <Grid.Item
@@ -243,18 +300,6 @@ export default function Point() {
243 </>
300 </>
244 )}
301 )}
245 {senateButton}
302 {senateButton}
246− <Grid.Item full as={BootUrbitOSButton} />
247− <Grid.Divider />
248− <Grid.Item
249− full
250− as={ForwardButton}
251− accessory={code && <CopyButton text={code} />}
252− detail={code || notice}
253− disabled={!code}
254− detailClassName="mono">
255− Login Code
256− </Grid.Item>
257− <Grid.Divider />
258 </Grid>
303 </Grid>
259 </View>
304 </View>
260 );
305 );
src/views/Points.js+2−2
@@ -231,7 +231,7 @@ export default function Points() {
231 !starReleasing)
231 !starReleasing)
232 ) {
232 ) {
233 return (
233 return (
234− <View inset pop={pop}>
234+ <View inset>
235 <Grid>
235 <Grid>
236 <Grid.Item full as={HelpText} className="mt8 t-center">
236 <Grid.Item full as={HelpText} className="mt8 t-center">
237 <Blinky /> Loading...
237 <Blinky /> Loading...
@@ -242,7 +242,7 @@ export default function Points() {
242 }
242 }
243
243
244 return (
244 return (
245− <View pop={pop} inset>
245+ <View inset pop={pop} hideBack>
246 <NavHeader>
246 <NavHeader>
247 <CopiableAddress
247 <CopiableAddress
248 text={address}
248 text={address}
src/views/Release.js+9−4
@@ -15,6 +15,7 @@ import CopiableAddress from 'components/CopiableAddress';
15
15
16import Active from 'views/Release/Active';
16import Active from 'views/Release/Active';
17import Locked from 'views/Release/Locked';
17import Locked from 'views/Release/Locked';
18+import NavHeader from 'components/NavHeader';
18
19
19const NAMES = {
20const NAMES = {
20 ACTIVE: 'ACTIVE',
21 ACTIVE: 'ACTIVE',
@@ -54,11 +55,15 @@ export default function ReleaseView() {
54
55
55 return (
56 return (
56 <View pop={pop} inset>
57 <View pop={pop} inset>
58+ <NavHeader>
59+ <CopiableAddress
60+ className="f6 mono gray4 us-none pointer"
61+ text={address}>
62+ {address}
63+ </CopiableAddress>
64+ </NavHeader>
57 <Grid>
65 <Grid>
58− <Grid.Item full as={Crumbs} />
66+ <Grid.Item full as={H1}></Grid.Item>
59− <Grid.Item full as={H1} className="f6 mono gray4 us-none pointer">
60− <CopiableAddress text={address}>{address}</CopiableAddress>
61− </Grid.Item>
62
67
63 <Grid.Item
68 <Grid.Item
64 full
69 full
src/views/Residents.jsnew file+230
@@ -0,0 +1,230 @@
1+import React, { useState, useEffect, useCallback, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, H4, HelpText, Flex } from 'indigo-react';
4+import { Just, Nothing } from 'folktale/maybe';
5+import * as ob from 'urbit-ob';
6+
7+import View from 'components/View';
8+import Tabs from 'components/Tabs';
9+import Crumbs from 'components/Crumbs';
10+import Sigil from 'components/Sigil';
11+import Blinky from 'components/Blinky';
12+import NavHeader from 'components/NavHeader';
13+
14+import { useLocalRouter } from 'lib/LocalRouter';
15+import useCurrentPointName from 'lib/useCurrentPointName';
16+import * as need from 'lib/need';
17+
18+import { usePointCursor } from 'store/pointCursor';
19+import { usePointCache } from 'store/pointCache';
20+import Footer from 'components/Footer';
21+
22+const NAMES = {
23+ REQUESTS: 'REQUESTS',
24+ ALL: 'ALL',
25+};
26+
27+const OPTIONS = [
28+ { text: 'Requests', value: NAMES.REQUESTS },
29+ { text: 'All', value: NAMES.ALL },
30+];
31+
32+const VIEWS = {
33+ [NAMES.REQUESTS]: Tab,
34+ [NAMES.ALL]: Tab,
35+};
36+
37+const PAGE_SIZE = 10;
38+
39+function Tab({ className, points, onAccept, onDecline, page, setPage }) {
40+ const start = page * PAGE_SIZE;
41+ const end = page * PAGE_SIZE + PAGE_SIZE;
42+ const pointsCount = points.map(ps => ps.length).getOrElse(0);
43+ const maxPage = Math.ceil(pointsCount / PAGE_SIZE) - 1;
44+ const hasNext = page < maxPage;
45+ const hasPrev = page > 0;
46+ const _points = points.map(ps => ps.slice(start, end)).getOrElse([]);
47+
48+ const onNext = useCallback(() => {
49+ setPage(p => p + 1);
50+ }, [setPage]);
51+
52+ const onPrev = useCallback(() => {
53+ setPage(p => p - 1);
54+ }, [setPage]);
55+
56+ if (Nothing.hasInstance(points)) {
57+ return <Grid className={className}></Grid>;
58+ }
59+
60+ return (
61+ <Grid className={cn('mt2', className)}>
62+ {_points.length === 0 && (
63+ <Grid.Item full as={HelpText} className="mt8 t-center">
64+ {Just.hasInstance(points) ? (
65+ 'No points to display'
66+ ) : (
67+ <>
68+ <Blinky /> Loading...
69+ </>
70+ )}
71+ </Grid.Item>
72+ )}
73+
74+ {_points.map(point => (
75+ <Resident
76+ key={point}
77+ point={point}
78+ onAccept={onAccept}
79+ onDecline={onDecline}
80+ />
81+ ))}
82+ <Grid.Item as={Footer}>
83+ <Grid className="pb9">
84+ {hasPrev && (
85+ <Grid.Item
86+ className="pointer underline mt4"
87+ fourth={1}
88+ onClick={onPrev}>
89+ {'<-'}
90+ </Grid.Item>
91+ )}
92+ {maxPage > 0 && (
93+ <Grid.Item className="mt4 t-center gray3" cols={[4, 10]}>
94+ <span className="black">Page {page + 1}</span> of {maxPage + 1}
95+ </Grid.Item>
96+ )}
97+ {hasNext && (
98+ <Grid.Item
99+ className="pointer underline t-right mt4"
100+ fourth={4}
101+ onClick={onNext}>
102+ {'->'}
103+ </Grid.Item>
104+ )}
105+ </Grid>
106+ </Grid.Item>
107+ </Grid>
108+ );
109+}
110+
111+function Resident({ point, onAccept, onDecline }) {
112+ const patp = ob.patp(point);
113+ const sigilSize = 50;
114+
115+ const isRequest = onAccept && onDecline;
116+
117+ return (
118+ <>
119+ <Grid.Item className="flex-row justify-center align-center" cols={[1, 3]}>
120+ <div
121+ style={{
122+ display: 'inline-block',
123+ height: `${sigilSize}px`,
124+ width: `${sigilSize}px`,
125+ }}>
126+ <Sigil patp={patp} size={25} colors={['#FFFFFF', '#000000']} />
127+ </div>
128+ </Grid.Item>
129+ <Grid.Item className="flex-row align-center mono" cols={[3, 7]}>
130+ {patp}{' '}
131+ </Grid.Item>
132+ {isRequest && (
133+ <>
134+ <Grid.Item
135+ className="flex-row-r align-center underline green3 pointer"
136+ cols={[7, 10]}
137+ onClick={() => onAccept(point)}>
138+ Accept
139+ </Grid.Item>
140+ <Grid.Item
141+ className="flex-row-r align-center underline red4 pointer"
142+ cols={[10, 13]}
143+ onClick={() => onDecline(point)}>
144+ Decline
145+ </Grid.Item>
146+ </>
147+ )}
148+ <Grid.Divider />
149+ </>
150+ );
151+}
152+
153+export default function Residents() {
154+ const { pop, push, names } = useLocalRouter();
155+ const name = useCurrentPointName();
156+
157+ const { pointCursor } = usePointCursor();
158+ const point = need.point(pointCursor);
159+ const { getResidents, syncResidents } = usePointCache();
160+
161+ useEffect(() => {
162+ syncResidents(point);
163+ }, [syncResidents, point]);
164+
165+ const { residents, requests } = getResidents(point);
166+
167+ const [currentTab, _setCurrentTab] = useState(NAMES.ALL);
168+ const [page, setPage] = useState(0);
169+
170+ const setCurrentTab = useCallback(
171+ tab => {
172+ setPage(0);
173+ _setCurrentTab(tab);
174+ },
175+ [setPage, _setCurrentTab]
176+ );
177+
178+ const isRequests = useMemo(() => currentTab === NAMES.REQUESTS, [currentTab]);
179+
180+ const _onAccept = useCallback(
181+ adoptee =>
182+ push(names.ADOPT, {
183+ adoptee,
184+ denied: false,
185+ }),
186+ [push, names]
187+ );
188+ const onAccept = isRequests && _onAccept;
189+
190+ const _onDecline = useCallback(
191+ adoptee =>
192+ push(names.ADOPT, {
193+ adoptee,
194+ denied: true,
195+ }),
196+ [push, names]
197+ );
198+ const onDecline = isRequests && _onDecline;
199+
200+ const points = isRequests ? requests : residents;
201+
202+ return (
203+ <View pop={pop} inset>
204+ <NavHeader>
205+ <Crumbs routes={[{ text: name, action: pop }, { text: 'Residents' }]} />
206+ </NavHeader>
207+ <Grid>
208+ <Grid.Item full as={H4} className="mt4">
209+ Residents
210+ </Grid.Item>
211+ <Grid.Item
212+ full
213+ className="mt1"
214+ as={Tabs}
215+ // Tabs
216+ views={VIEWS}
217+ options={OPTIONS}
218+ currentTab={currentTab}
219+ onTabChange={setCurrentTab}
220+ // Props for tab
221+ points={points}
222+ onAccept={onAccept}
223+ onDecline={onDecline}
224+ page={page}
225+ setPage={setPage}
226+ />
227+ </Grid>
228+ </View>
229+ );
230+}
src/views/Senate.js+1−2
@@ -10,8 +10,6 @@ import useEthereumTransaction from 'lib/useEthereumTransaction';
10import { GAS_LIMITS } from 'lib/constants';
10import { GAS_LIMITS } from 'lib/constants';
11import { useLocalRouter } from 'lib/LocalRouter';
11import { useLocalRouter } from 'lib/LocalRouter';
12
12
13−import { keccak256 } from 'lib/wallet';
14−
15import ViewHeader from 'components/ViewHeader';
13import ViewHeader from 'components/ViewHeader';
16import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
14import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
17import View from 'components/View';
15import View from 'components/View';
@@ -83,6 +81,7 @@ export default function Senate() {
83 return;
81 return;
84 }, [_contracts, _point]);
82 }, [_contracts, _point]);
85
83
84+ // eslint-disable-next-line
86 const [open, closed] = useMemo(() => {
85 const [open, closed] = useMemo(() => {
87 let open = [];
86 let open = [];
88 let closed = [];
87 let closed = [];
src/views/UrbitID.jsnew file+88
@@ -0,0 +1,88 @@
1+import React from 'react';
2+import { Grid } from 'indigo-react';
3+
4+import { useHistory } from 'store/history';
5+
6+import useCurrentPointName from 'lib/useCurrentPointName';
7+import useRouter from 'lib/useRouter';
8+import { LocalRouterProvider } from 'lib/LocalRouter';
9+
10+import View from 'components/View';
11+import Crumbs from 'components/Crumbs';
12+import NavHeader from 'components/NavHeader';
13+
14+import UrbitIDHome from './UrbitID/Home';
15+import SigilGenerator from './UrbitID/SigilGenerator';
16+import DownloadKeys from './UrbitID/DownloadKeys';
17+import SetProxy from './UrbitID/SetProxy';
18+import ResetKeys from './UrbitID/ResetKeys';
19+import Transfer from './UrbitID/Transfer';
20+
21+const NAMES = {
22+ HOME: 'HOME',
23+ SIGIL_GENERATOR: 'SIGIL_GENERATOR',
24+ DOWNLOAD_KEYS: 'DOWNLOAD_KEYS',
25+ SET_PROXY: 'SET_PROXY',
26+ RESET_KEYS: 'RESET_KEYS',
27+ TRANSFER: 'TRANSFER',
28+};
29+
30+const VIEWS = {
31+ [NAMES.HOME]: UrbitIDHome,
32+ [NAMES.SIGIL_GENERATOR]: SigilGenerator,
33+ [NAMES.DOWNLOAD_KEYS]: DownloadKeys,
34+ [NAMES.RESET_KEYS]: ResetKeys,
35+ [NAMES.SET_PROXY]: SetProxy,
36+ [NAMES.TRANSFER]: Transfer,
37+};
38+
39+const humanizeName = name => {
40+ switch (name) {
41+ case NAMES.SIGIL_GENERATOR:
42+ return 'Sigil';
43+ case NAMES.DOWNLOAD_KEYS:
44+ return 'Download Keys';
45+ case NAMES.SET_PROXY:
46+ return 'Set Key';
47+ case NAMES.RESET_KEYS:
48+ return 'Reset Keys';
49+ case NAMES.TRANSFER:
50+ return 'Transfer';
51+ default:
52+ return undefined;
53+ }
54+};
55+
56+export default function UrbitID() {
57+ const history = useHistory();
58+ const name = useCurrentPointName();
59+
60+ const { Route, ...router } = useRouter({
61+ names: NAMES,
62+ views: VIEWS,
63+ initialRoutes: [{ key: NAMES.HOME }],
64+ });
65+
66+ const last = router.peek().key;
67+ const homeAction = last === NAMES.HOME ? undefined : () => history.pop();
68+ const lastCrumb = homeAction ? [{ text: humanizeName(last) }] : [];
69+
70+ return (
71+ <View pop={router.pop} inset>
72+ <NavHeader>
73+ <Crumbs
74+ routes={[
75+ { text: name, action: () => history.pop() },
76+ { text: 'Urbit ID', action: homeAction },
77+ ...lastCrumb,
78+ ]}
79+ />
80+ </NavHeader>
81+
82+ <LocalRouterProvider value={router}>
83+ <Grid className="mb4"></Grid>
84+ <Route />
85+ </LocalRouterProvider>
86+ </View>
87+ );
88+}
src/views/Admin/AdminRedownload.js → src/views/UrbitID/DownloadKeys.jsrenamed

The content is the same.

src/views/UrbitID/Home.jsnew file+164
@@ -0,0 +1,164 @@
1+import React, { useCallback } from 'react';
2+import { Grid, B } from 'indigo-react';
3+import { azimuth } from 'azimuth-js';
4+import { Just } from 'folktale/maybe';
5+
6+import { ForwardButton } from 'components/Buttons';
7+import CopyButton from 'components/CopyButton';
8+
9+import { useLocalRouter } from 'lib/LocalRouter';
10+import { PROXY_TYPE, proxyTypeToHuman } from 'lib/proxy';
11+import * as need from 'lib/need';
12+import capitalize from 'lib/capitalize';
13+import useCurrentPermissions from 'lib/useCurrentPermissions';
14+import convertToInt from 'lib/convertToInt';
15+
16+import { usePointCursor } from 'store/pointCursor';
17+import { usePointCache } from 'store/pointCache';
18+import { useWallet } from 'store/wallet';
19+
20+export default function UrbitIDHome() {
21+ const { push, names } = useLocalRouter();
22+ const { pointCursor } = usePointCursor();
23+ const { getDetails } = usePointCache();
24+ const { urbitWallet } = useWallet();
25+
26+ const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
27+
28+ const goDownloadKeys = useCallback(() => push(names.DOWNLOAD_KEYS), [
29+ push,
30+ names,
31+ ]);
32+
33+ const goResetKeys = useCallback(() => push(names.RESET_KEYS), [push, names]);
34+
35+ const isMasterTicket = Just.hasInstance(urbitWallet);
36+ const point = need.point(pointCursor);
37+ const details = need.details(getDetails(point));
38+ const { isOwner } = useCurrentPermissions();
39+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
40+
41+ const pointSize = azimuth.getPointSize(point);
42+ const isParent = pointSize !== azimuth.PointSize.Planet;
43+ const isSenate = pointSize === azimuth.PointSize.Galaxy;
44+
45+ const goSetProxy = useCallback(
46+ proxyType => push(names.SET_PROXY, { proxyType }),
47+ [push, names]
48+ );
49+
50+ const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
51+
52+ const renderProxyAction = useCallback(
53+ (proxyType, address) => {
54+ const disabled =
55+ !isOwner ||
56+ (proxyType === PROXY_TYPE.SPAWN ? networkRevision === 0 : false);
57+ return (
58+ <>
59+ <Grid.Item
60+ full
61+ as={ForwardButton}
62+ disabled={disabled}
63+ onClick={() => goSetProxy(proxyType)}>
64+ Edit {capitalize(proxyTypeToHuman(proxyType))} Key
65+ </Grid.Item>
66+ </>
67+ );
68+ },
69+ [goSetProxy, isOwner, networkRevision]
70+ );
71+ return (
72+ <Grid>
73+ <Grid.Item full className="mb7 f5">
74+ Urbit ID
75+ </Grid.Item>
76+ <Grid.Divider />
77+ <Grid.Item full as={ForwardButton} onClick={goSigil}>
78+ Sigil
79+ </Grid.Item>
80+ <Grid.Divider />
81+ <Grid.Item
82+ full
83+ as={ForwardButton}
84+ onClick={goDownloadKeys}
85+ disabled={!isMasterTicket}
86+ disabledDetail={
87+ <B className="wrap ws-normal">
88+ · Master Key Authentication required.
89+ </B>
90+ }>
91+ Download Keys
92+ </Grid.Item>
93+ <Grid.Divider />
94+ <Grid.Item
95+ full
96+ as={ForwardButton}
97+ onClick={goResetKeys}
98+ disabled={!isOwner}
99+ disabledDetail={
100+ <B className="wrap ws-normal"> · Ownership key required</B>
101+ }
102+ detail={
103+ isMasterTicket
104+ ? 'Reset Master Key and all other keys'
105+ : 'Transfer to Master Ticket, resetting all keys'
106+ }>
107+ {isMasterTicket ? 'Reset Keys' : 'Transfer to Master Ticket'}
108+ </Grid.Item>
109+
110+ <Grid.Divider />
111+
112+ <Grid.Item full className="mv7 f5 gray4">
113+ Advanced
114+ </Grid.Item>
115+ <Grid.Divider />
116+ <Grid.Item
117+ full
118+ as={ForwardButton}
119+ accessory={<CopyButton text={details.owner} />}
120+ detail={<div className="mt1 mono black">{details.owner}</div>}
121+ disabled={!details.owner}>
122+ Ownership Address
123+ </Grid.Item>
124+ <Grid.Divider />
125+
126+ {renderProxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy)}
127+ <Grid.Divider />
128+
129+ {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
130+
131+ {isParent && networkRevision !== 0 && <Grid.Divider />}
132+
133+ {isParent && networkRevision === 0 && (
134+ <>
135+ <Grid.Item
136+ full
137+ as={ForwardButton}
138+ className="f6"
139+ accessory={<span className="underline pointer">Set Keys</span>}>
140+ Network Keys Required
141+ </Grid.Item>
142+ <Grid.Divider />
143+ </>
144+ )}
145+
146+ {isSenate && (
147+ <>
148+ {renderProxyAction(PROXY_TYPE.VOTING, details.votingProxy)}{' '}
149+ <Grid.Divider />
150+ </>
151+ )}
152+ <Grid.Item
153+ full
154+ as={ForwardButton}
155+ onClick={goTransfer}
156+ disabled={!isOwner}
157+ disabledDetail={
158+ <B className="wrap ws-normal"> · Ownership key required</B>
159+ }>
160+ Transfer this point
161+ </Grid.Item>
162+ </Grid>
163+ );
164+}
src/views/Admin/AdminReticket.js → src/views/UrbitID/ResetKeys.jsrenamed+10−10
@@ -7,10 +7,10 @@ import useRouter from 'lib/useRouter';
7
7
8import Steps from 'components/Steps';
8import Steps from 'components/Steps';
9
9
10−import ReticketConfirm from './Reticket/ReticketConfirm';
10+import ResetConfirm from './ResetKeys/ResetConfirm';
11−import ReticketDownload from './Reticket/ReticketDownload';
11+import ResetDownload from './ResetKeys/ResetDownload';
12−import ReticketVerify from './Reticket/ReticketVerify';
12+import ResetVerify from './ResetKeys/ResetVerify';
13−import ReticketExecute from './Reticket/ReticketExecute';
13+import ResetExecute from './ResetKeys/ResetExecute';
14
14
15const NAMES = {
15const NAMES = {
16 CONFIRM: 'CONFIRM',
16 CONFIRM: 'CONFIRM',
@@ -20,15 +20,15 @@ const NAMES = {
20};
20};
21
21
22const VIEWS = {
22const VIEWS = {
23− CONFIRM: ReticketConfirm,
23+ CONFIRM: ResetConfirm,
24− DOWNLOAD: ReticketDownload,
24+ DOWNLOAD: ResetDownload,
25− VERIFY: ReticketVerify,
25+ VERIFY: ResetVerify,
26− EXECUTE: ReticketExecute,
26+ EXECUTE: ResetExecute,
27};
27};
28
28
29−const STEPS = [NAMES.CONFIRM, NAMES.DOWNLOAD, NAMES.VERIFY, NAMES.RETICKET];
29+const STEPS = [NAMES.CONFIRM, NAMES.DOWNLOAD, NAMES.VERIFY, NAMES.EXECUTE];
30
30
31−export default function AdminReticket() {
31+export default function ResetKeys() {
32 const { Route, ...router } = useRouter({
32 const { Route, ...router } = useRouter({
33 names: NAMES,
33 names: NAMES,
34 views: VIEWS,
34 views: VIEWS,
src/views/Admin/Reticket/ReticketConfirm.js → src/views/UrbitID/ResetKeys/ResetConfirm.jsrenamed+1−1
@@ -16,7 +16,7 @@ import WarningBox from 'components/WarningBox';
16import { blinkIf } from 'components/Blinky';
16import { blinkIf } from 'components/Blinky';
17import useCurrentPointName from 'lib/useCurrentPointName';
17import useCurrentPointName from 'lib/useCurrentPointName';
18
18
19−export default function ReticketConfirm({ newWallet, setNewWallet }) {
19+export default function ResetConfirm({ newWallet, setNewWallet }) {
20 const { push, names } = useLocalRouter();
20 const { push, names } = useLocalRouter();
21 const { pointCursor } = usePointCursor();
21 const { pointCursor } = usePointCursor();
22 const point = need.point(pointCursor);
22 const point = need.point(pointCursor);
src/views/Admin/Reticket/ReticketDownload.js → src/views/UrbitID/ResetKeys/ResetDownload.jsrenamed

The content is the same.

src/views/Admin/Reticket/ReticketExecute.js → src/views/UrbitID/ResetKeys/ResetExecute.jsrenamed+3−3
@@ -41,7 +41,7 @@ const labelForProgress = progress => {
41 }
41 }
42};
42};
43
43
44−export default function ReticketExecute({ newWallet, setNewWallet }) {
44+export default function ResetExecute({ newWallet, setNewWallet }) {
45 const { popTo, names, reset } = useHistory();
45 const { popTo, names, reset } = useHistory();
46 const { web3, contracts, networkType } = useNetwork();
46 const { web3, contracts, networkType } = useNetwork();
47 const {
47 const {
@@ -148,7 +148,7 @@ export default function ReticketExecute({ newWallet, setNewWallet }) {
148
148
149 return (
149 return (
150 <Grid.Item full as={WarningBox} className="mt4">
150 <Grid.Item full as={WarningBox} className="mt4">
151− Never give your Master Ticket to anyone
151+ Never give your Master Key to anyone
152 </Grid.Item>
152 </Grid.Item>
153 );
153 );
154 };
154 };
@@ -172,7 +172,7 @@ export default function ReticketExecute({ newWallet, setNewWallet }) {
172 className="mt4"
172 className="mt4"
173 accessory="↺"
173 accessory="↺"
174 onClick={loginAndGoHome}>
174 onClick={loginAndGoHome}>
175− Login with New Master Ticket
175+ Login with New Master Key
176 </Grid.Item>
176 </Grid.Item>
177 </>
177 </>
178 ) : (
178 ) : (
src/views/Admin/Reticket/ReticketVerify.js → src/views/UrbitID/ResetKeys/ResetVerify.jsrenamed+1−1
@@ -13,7 +13,7 @@ import FormError from 'form/FormError';
13
13
14const STUB_VERIFY_TICKET = isDevelopment;
14const STUB_VERIFY_TICKET = isDevelopment;
15
15
16−export default function ReticketVerify({ newWallet }) {
16+export default function ResetVerify({ newWallet }) {
17 const { push, names } = useLocalRouter();
17 const { push, names } = useLocalRouter();
18
18
19 const ticket = newWallet.value.wallet.ticket;
19 const ticket = newWallet.value.wallet.ticket;
src/views/Admin/AdminSetProxy.js → src/views/UrbitID/SetProxy.jsrenamed+25−36
@@ -1,6 +1,6 @@
1import React, { useCallback, useMemo } from 'react';
1import React, { useCallback, useMemo } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import { Grid, Text, Flex, ToggleInput } from 'indigo-react';
3+import { Grid, Text, Flex } from 'indigo-react';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5
5
6import { useNetwork } from 'store/network';
6import { useNetwork } from 'store/network';
@@ -18,7 +18,6 @@ import { ETH_ZERO_ADDR, eqAddr, isZeroAddress } from 'lib/wallet';
18import capitalize from 'lib/capitalize';
18import capitalize from 'lib/capitalize';
19import useEthereumTransaction from 'lib/useEthereumTransaction';
19import useEthereumTransaction from 'lib/useEthereumTransaction';
20
20
21−import ViewHeader from 'components/ViewHeader';
22import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
21import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
23import { GAS_LIMITS } from 'lib/constants';
22import { GAS_LIMITS } from 'lib/constants';
24import { AddressInput } from 'form/Inputs';
23import { AddressInput } from 'form/Inputs';
@@ -29,7 +28,6 @@ import {
29} from 'form/validators';
28} from 'form/validators';
30import BridgeForm from 'form/BridgeForm';
29import BridgeForm from 'form/BridgeForm';
31import FormError from 'form/FormError';
30import FormError from 'form/FormError';
32−import CopiableAddress from 'components/CopiableAddress';
33
31
34const proxyFromDetails = (details, contracts, proxyType) => {
32const proxyFromDetails = (details, contracts, proxyType) => {
35 switch (proxyType) {
33 switch (proxyType) {
@@ -94,7 +92,7 @@ function useSetProxy(proxyType) {
94 };
92 };
95}
93}
96
94
97−export default function AdminSetProxy() {
95+export default function SetProxy() {
98 const { data, pop } = useLocalRouter();
96 const { data, pop } = useLocalRouter();
99 const { getDetails } = usePointCache();
97 const { getDetails } = usePointCache();
100 const { pointCursor } = usePointCursor();
98 const { pointCursor } = usePointCursor();
@@ -154,28 +152,32 @@ export default function AdminSetProxy() {
154 const proxyAddress = proxyFromDetails(_details, _contracts, data.proxyType);
152 const proxyAddress = proxyFromDetails(_details, _contracts, data.proxyType);
155 const isProxySet = !isZeroAddress(proxyAddress);
153 const isProxySet = !isZeroAddress(proxyAddress);
156
154
157− const proxyAddressLabel = `${
155+ const header = useMemo(() => {
158− completed ? 'New' : 'Current'
156+ const pfix = completed ? 'New' : 'Edit';
159− } ${properProxyType} Address`;
157+ return `${pfix} ${properProxyType} Key`;
158+ }, [completed, properProxyType]);
160
159
161 return (
160 return (
162 <Grid>
161 <Grid>
163− <Grid.Item full as={ViewHeader}>
162+ <Grid.Item full className="f5 mb2">
164− {properProxyType} Address
163+ {header}
165 </Grid.Item>
164 </Grid.Item>
166
165
166+ {!completed && (
167+ <>
167− <Grid.Item full as={Text} className="mb4 f5">
168+ <Grid.Item full as={Text} className="mb4 f5 gray4">
168− {proxyTypeToHumanDescription(data.proxyType)}
169+ {proxyTypeToHumanDescription(data.proxyType)}
169− </Grid.Item>
170+ </Grid.Item>
170−
171− <Grid.Item
171+ <Grid.Item
172− full
172+ full
173− as={Text}
173+ as={Text}
174− className={cn('f6 mb1', {
174+ className={cn('f6', {
175− green3: completed,
175+ green3: completed,
176− })}>
176+ })}>
177− {proxyAddressLabel}
177+ Current
178− </Grid.Item>
178+ </Grid.Item>
179+ </>
180+ )}
179
181
180 <BridgeForm
182 <BridgeForm
181 validate={validate}
183 validate={validate}
@@ -187,26 +189,13 @@ export default function AdminSetProxy() {
187 <Flex.Item
189 <Flex.Item
188 flex
190 flex
189 as={Text}
191 as={Text}
190− className={cn('mono', {
192+ className={cn('mono mv3 gray4', {
191 black: !completed && isProxySet,
193 black: !completed && isProxySet,
192 gray4: !completed && !isProxySet,
194 gray4: !completed && !isProxySet,
193 green3: completed,
195 green3: completed,
194 })}>
196 })}>
195− {isProxySet ? (
197+ {isProxySet ? proxyAddress : 'Unset'}
196− <CopiableAddress>{proxyAddress}</CopiableAddress>
197− ) : (
198− 'Unset'
199− )}
200 </Flex.Item>
198 </Flex.Item>
201− {!completed && isProxySet && (
202− <Flex.Item
203− as={ToggleInput}
204− name="unset"
205− label="Unset"
206− inverseLabel="Specify"
207− disabled={inputsLocked}
208− />
209− )}
210 </Grid.Item>
199 </Grid.Item>
211
200
212 {completed ? (
201 {completed ? (
src/views/SigilGenerator.js → src/views/UrbitID/SigilGenerator.jsrenamed+3−6
@@ -6,11 +6,9 @@ import { colors } from 'indigo-tokens';
6
6
7import { usePointCursor } from 'store/pointCursor';
7import { usePointCursor } from 'store/pointCursor';
8
8
9−import View from 'components/View';
10import Sigil from 'components/Sigil';
9import Sigil from 'components/Sigil';
11
10
12import * as need from 'lib/need';
11import * as need from 'lib/need';
13−import { useLocalRouter } from 'lib/LocalRouter';
14import useSigilDownloader from 'lib/useSigilDownloader';
12import useSigilDownloader from 'lib/useSigilDownloader';
15
13
16import { composeValidator, buildNumberValidator } from 'form/validators';
14import { composeValidator, buildNumberValidator } from 'form/validators';
@@ -40,7 +38,6 @@ const BG_COLORS = [
40const FG_COLORS = [colors.black, colors.white];
38const FG_COLORS = [colors.black, colors.white];
41
39
42export default function SigilGenerator() {
40export default function SigilGenerator() {
43− const { pop } = useLocalRouter();
44 const { pointCursor } = usePointCursor();
41 const { pointCursor } = usePointCursor();
45
42
46 const validate = composeValidator({ size: buildNumberValidator(16) });
43 const validate = composeValidator({ size: buildNumberValidator(16) });
@@ -62,7 +59,7 @@ export default function SigilGenerator() {
62 [point, downloadSigil]
59 [point, downloadSigil]
63 );
60 );
64 return (
61 return (
65− <View pop={pop}>
62+ <>
66 <BridgeForm
63 <BridgeForm
67 validate={validate}
64 validate={validate}
68 initialValues={{
65 initialValues={{
@@ -73,7 +70,7 @@ export default function SigilGenerator() {
73 onSubmit={onSubmit}>
70 onSubmit={onSubmit}>
74 {({ handleSubmit, values }) => (
71 {({ handleSubmit, values }) => (
75 <Grid gap={6}>
72 <Grid gap={6}>
76− <Grid.Item full className="f7">
73+ <Grid.Item full className="f5">
77 Sigil
74 Sigil
78 </Grid.Item>
75 </Grid.Item>
79 <Grid.Item fourth={1}>
76 <Grid.Item fourth={1}>
@@ -121,6 +118,6 @@ export default function SigilGenerator() {
121 </BridgeForm>
118 </BridgeForm>
122
119
123 <canvas style={{ display: 'none' }} ref={canvasRef} />
120 <canvas style={{ display: 'none' }} ref={canvasRef} />
124− </View>
121+ </>
125 );
122 );
126}
123}
src/views/Admin/AdminTransfer.js → src/views/UrbitID/Transfer.jsrenamed+20−11
@@ -67,7 +67,7 @@ export default function AdminTransfer() {
67 );
67 );
68
68
69 return (
69 return (
70− <Grid>
70+ <Grid gap={1}>
71 <Grid.Item full as={ViewHeader}>
71 <Grid.Item full as={ViewHeader}>
72 Transfer Point
72 Transfer Point
73 </Grid.Item>
73 </Grid.Item>
@@ -75,16 +75,25 @@ export default function AdminTransfer() {
75 <BridgeForm validate={validate} onValues={onValues}>
75 <BridgeForm validate={validate} onValues={onValues}>
76 {({ handleSubmit, values }) => (
76 {({ handleSubmit, values }) => (
77 <>
77 <>
78− <Grid.Item
78+ {!completed && (
79− full
80− as={Text}
81− className={cn('f5 wrap', {
79+ <Grid.Item full as={Text} className={cn('f5 wrap')}>
82− green3: completed,
80+ Transfer {name} to a new owner.
81+ </Grid.Item>
83− })}>
82+ )}
84− {completed
83+ {completed && (
85− ? `${values.address} is now the Transfer Proxy for ${name} and can accept the transfer by logging into Bridge themselves. Until they accept your transfer, you will still have ownership over ${name}.`
84+ <>
86− : `Transfer ${name} to a new owner.`}
85+ <Grid.Item full as={Text} className="wrap">
86+ Started transferring <span className="mono">{name}</span> to{' '}
87+ </Grid.Item>
88+ <Grid.Item full as={Text}>
89+ <span className="mono gray4">{values.address}</span>
90+ </Grid.Item>
91+ <Grid.Item full as={Text} className="mt6">
92+ Until they accept the transfer, you still own{' '}
93+ <span className="mono">{name}</span>
87− </Grid.Item>
94+ </Grid.Item>
95+ </>
96+ )}
88
97
89 {!completed && (
98 {!completed && (
90 <Grid.Item
99 <Grid.Item
src/views/UrbitOS.jsnew file+61
@@ -0,0 +1,61 @@
1+import React, { useState } from 'react';
2+import { Grid } from 'indigo-react';
3+
4+import { useHistory } from 'store/history';
5+
6+import useCurrentPointName from 'lib/useCurrentPointName';
7+import useRouter from 'lib/useRouter';
8+import { LocalRouterProvider } from 'lib/LocalRouter';
9+
10+import View from 'components/View';
11+import Crumbs from 'components/Crumbs';
12+import NavHeader from 'components/NavHeader';
13+
14+import UrbitOSHome from './UrbitOS/Home';
15+import UrbitOSNetworkingKeys from './UrbitOS/NetworkingKeys';
16+import UrbitOSChangeSponsor from './UrbitOS/ChangeSponsor';
17+
18+const NAMES = {
19+ HOME: 'HOME',
20+ CHANGE_SPONSOR: 'CHANGE_SPONSOR',
21+ NETWORKING_KEYS: 'NETWORKING_KEYS',
22+};
23+
24+const VIEWS = {
25+ [NAMES.HOME]: UrbitOSHome,
26+ [NAMES.CHANGE_SPONSOR]: UrbitOSChangeSponsor,
27+ [NAMES.NETWORKING_KEYS]: UrbitOSNetworkingKeys,
28+};
29+
30+export default function UrbitOS() {
31+ const history = useHistory();
32+ const name = useCurrentPointName();
33+
34+ const { Route, ...router } = useRouter({
35+ names: NAMES,
36+ views: VIEWS,
37+ initialRoutes: [{ key: NAMES.HOME }],
38+ });
39+
40+ const [manualNetworkSeed, setManualNetworkSeed] = useState();
41+
42+ return (
43+ <View pop={router.pop} inset>
44+ <NavHeader>
45+ <Crumbs
46+ routes={[
47+ { text: name, action: () => history.pop() },
48+ { text: 'Urbit OS' },
49+ ]}
50+ />
51+ </NavHeader>
52+ <LocalRouterProvider value={router}>
53+ <Grid className="mb4"></Grid>
54+ <Route
55+ manualNetworkSeed={manualNetworkSeed}
56+ setManualNetworkSeed={setManualNetworkSeed}
57+ />
58+ </LocalRouterProvider>
59+ </View>
60+ );
61+}
src/views/UrbitOS/ChangeSponsor.jsnew file+199
@@ -0,0 +1,199 @@
1+import React, { useState, useMemo, useCallback, useEffect } from 'react';
2+import { Grid } from 'indigo-react';
3+import * as ob from 'urbit-ob';
4+import { azimuth, ecliptic } from 'azimuth-js';
5+
6+import { useNetwork } from 'store/network';
7+import { usePointCache } from 'store/pointCache';
8+
9+import { PointInput } from 'form/Inputs';
10+import FormError from 'form/FormError';
11+import BridgeForm from 'form/BridgeForm';
12+
13+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
14+import ViewHeader from 'components/ViewHeader';
15+import {
16+ composeValidator,
17+ buildPointValidator,
18+ hasErrors,
19+} from 'form/validators';
20+
21+import * as need from 'lib/need';
22+import { validateMinimumPatpByteLength } from 'lib/validators';
23+import { usePointCursor } from 'store/pointCursor';
24+import useEthereumTransaction from 'lib/useEthereumTransaction';
25+
26+function useChangeSponsor() {
27+ const { contracts } = useNetwork();
28+ const { pointCursor } = usePointCursor();
29+ const { syncOwnedPoint } = usePointCache();
30+
31+ const _contracts = need.contracts(contracts);
32+ const point = need.point(pointCursor);
33+
34+ return useEthereumTransaction(
35+ useCallback(sponsor => ecliptic.escape(_contracts, point, sponsor), [
36+ point,
37+ _contracts,
38+ ]),
39+ useCallback(() => {
40+ syncOwnedPoint(point);
41+ }, [syncOwnedPoint, point])
42+ );
43+}
44+
45+function ChangeSponsor({ onDone }) {
46+ const { contracts } = useNetwork();
47+ const { pointCursor } = usePointCursor();
48+
49+ const _contracts = need.contracts(contracts);
50+ const point = need.point(pointCursor);
51+ const { construct, unconstruct, inputsLocked, bind } = useChangeSponsor();
52+
53+ const validateFormAsync = useCallback(
54+ async values => {
55+ const sponsor = ob.patp2dec(values.sponsor);
56+ const details = await azimuth.getPoint(_contracts, sponsor);
57+
58+ if (details.keyRevisionNumber === 0) {
59+ return { sponsor: 'Sponsor is not online' };
60+ }
61+ },
62+ [_contracts]
63+ );
64+
65+ const validateForm = useCallback(
66+ (values, errors) => {
67+ if (hasErrors(errors)) {
68+ return errors;
69+ }
70+ return validateFormAsync(values);
71+ },
72+ [validateFormAsync]
73+ );
74+
75+ const validate = useMemo(() => {
76+ const sponsorSize =
77+ azimuth.getPointSize(point) === azimuth.PointSize.Star ? 1 : 2;
78+ return composeValidator(
79+ {
80+ sponsor: buildPointValidator(sponsorSize, [
81+ validateMinimumPatpByteLength(sponsorSize),
82+ ]),
83+ },
84+ validateForm
85+ );
86+ }, [validateForm, point]);
87+
88+ const onValues = useCallback(
89+ ({ valid, values }) => {
90+ if (valid) {
91+ construct(ob.patp2dec(values.sponsor));
92+ } else {
93+ unconstruct();
94+ }
95+ },
96+ [construct, unconstruct]
97+ );
98+ return (
99+ <Grid>
100+ <Grid.Item full as={ViewHeader}>
101+ Change Sponsor
102+ </Grid.Item>
103+ <BridgeForm validate={validate} onValues={onValues}>
104+ {() => (
105+ <>
106+ <Grid.Item
107+ full
108+ as={PointInput}
109+ name="sponsor"
110+ disabled={inputsLocked}
111+ label="New sponsor"
112+ />
113+ <Grid.Item full as={FormError} />
114+ <Grid.Item
115+ full
116+ as={InlineEthereumTransaction}
117+ {...bind}
118+ onReturn={onDone}
119+ label="Request"
120+ />
121+ </>
122+ )}
123+ </BridgeForm>
124+ </Grid>
125+ );
126+}
127+
128+function useCancelEscape() {
129+ const { contracts } = useNetwork();
130+ const { pointCursor } = usePointCursor();
131+ const { syncOwnedPoint } = usePointCache();
132+
133+ const _contracts = need.contracts(contracts);
134+ const point = need.point(pointCursor);
135+
136+ return useEthereumTransaction(
137+ useCallback(() => ecliptic.cancelEscape(_contracts, point), [
138+ point,
139+ _contracts,
140+ ]),
141+ useCallback(() => syncOwnedPoint(point), [point, syncOwnedPoint])
142+ );
143+}
144+
145+function CurrentEscape({ onDone }) {
146+ const { construct, bind } = useCancelEscape();
147+
148+ const { pointCursor } = usePointCursor();
149+ const { getDetails } = usePointCache();
150+
151+ const point = need.point(pointCursor);
152+
153+ const details = need.details(getDetails(point));
154+
155+ const newSponsor = useMemo(() => ob.patp(details.escapeRequestedTo), [
156+ details,
157+ ]);
158+
159+ useEffect(() => {
160+ construct();
161+ }, [construct]);
162+ return (
163+ <Grid>
164+ <Grid.Item full as={ViewHeader}>
165+ Currently requesting <span className="mono">{newSponsor}</span> as a new
166+ sponsor
167+ </Grid.Item>
168+ <Grid.Item
169+ full
170+ as={InlineEthereumTransaction}
171+ label="Cancel Request"
172+ {...bind}
173+ onReturn={onDone}
174+ />
175+ </Grid>
176+ );
177+}
178+
179+export default function UrbitOSChangeSponsor() {
180+ const { pointCursor } = usePointCursor();
181+
182+ const point = need.point(pointCursor);
183+
184+ const { getDetails } = usePointCache();
185+
186+ const details = need.details(getDetails(point));
187+
188+ const [requested, setRequested] = useState(details.escapeRequested);
189+
190+ const onDone = useCallback(() => {
191+ setRequested(details.escapeRequested);
192+ }, [details, setRequested]);
193+
194+ return requested ? (
195+ <CurrentEscape onDone={onDone} />
196+ ) : (
197+ <ChangeSponsor onDone={onDone} />
198+ );
199+}
src/views/UrbitOS/Home.jsnew file+273
@@ -0,0 +1,273 @@
1+import React, { useCallback, useState, useEffect, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, Button, SelectInput, Flex } from 'indigo-react';
4+import * as ob from 'urbit-ob';
5+import { azimuth } from 'azimuth-js';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
9+
10+import useCurrentPointName from 'lib/useCurrentPointName';
11+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
12+import * as need from 'lib/need';
13+
14+import { OutButton, ForwardButton } from 'components/Buttons';
15+import { CopyButtonWide } from 'components/CopyButton';
16+import NetworkingKeys from 'components/NetworkingKeys';
17+import ProgressButton from 'components/ProgressButton';
18+import LoginButton from 'components/LoginButton';
19+
20+import BridgeForm from 'form/BridgeForm';
21+import { useLocalRouter } from 'lib/LocalRouter';
22+import { useHosting } from 'store/hosting';
23+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
24+import useLifecycle from 'lib/useLifecycle';
25+import useLocalHosting from 'lib/useLocaHosting';
26+
27+export default function UrbitOSHome({ manualNetworkSeed }) {
28+ const { pointCursor } = usePointCursor();
29+ const { getDetails } = usePointCache();
30+
31+ const { push, names } = useLocalRouter();
32+
33+ const point = need.point(pointCursor);
34+ const details = need.details(getDetails(point));
35+
36+ const sponsor = ob.patp(details.sponsor);
37+
38+ const [showKeys, setShowKeys] = useState(false);
39+
40+ const showSponsor = azimuth.getPointSize(point) !== azimuth.PointSize.Galaxy;
41+ const toggleShowKeys = useCallback(() => setShowKeys(s => !s), [setShowKeys]);
42+
43+ const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
44+ names,
45+ push,
46+ ]);
47+
48+ const goChangeSponsor = useCallback(() => push(names.CHANGE_SPONSOR), [
49+ push,
50+ names,
51+ ]);
52+ return (
53+ <>
54+ <Hosting manualNetworkSeed={manualNetworkSeed} />
55+
56+ <Grid>
57+ <Grid.Item full className="mv7 f5">
58+ Network
59+ </Grid.Item>
60+ {showSponsor && (
61+ <>
62+ <Grid.Divider />
63+ <Grid.Item
64+ full
65+ as={ForwardButton}
66+ detail="A sponsor finds new peers in your network"
67+ accessory={<u>Change</u>}
68+ onClick={goChangeSponsor}>
69+ <span className="mono">{sponsor}</span>
70+ <span className="f7 bg-black white p1 ml2 r4">SPONSOR</span>
71+ </Grid.Item>
72+ </>
73+ )}
74+
75+ <Grid.Divider />
76+ <Grid.Item full as={ForwardButton} onClick={goNetworkingKeys}>
77+ Reset Networking Keys
78+ </Grid.Item>
79+ <Grid.Divider />
80+ <Grid.Item
81+ full
82+ as={ForwardButton}
83+ accessory={showKeys ? '▼' : '▲'}
84+ onClick={toggleShowKeys}>
85+ View Networking Keys
86+ </Grid.Item>
87+ {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
88+ </Grid>
89+ </>
90+ );
91+}
92+
93+// eslint-disable-next-line
94+function Hosting({ manualNetworkSeed }) {
95+ const bind = useKeyfileGenerator(manualNetworkSeed);
96+ const { keyfile, code } = bind;
97+ const ship = useHosting();
98+
99+ const {
100+ syncStatus,
101+ url,
102+ unknown,
103+ bootProgress,
104+ bootMessage,
105+ disabled,
106+ } = ship;
107+
108+ const { running: localRunning, url: localUrl } = useLocalHosting();
109+
110+ let running = useMemo(() => localRunning || ship.running, [
111+ ship,
112+ localRunning,
113+ ]);
114+
115+ useLifecycle(() => {
116+ syncStatus();
117+ });
118+
119+ const createShip = useCallback(() => ship.create(keyfile), [keyfile, ship]);
120+
121+ const name = useCurrentPointName();
122+ const options = [{ text: 'Tlon', value: 'tlon' }];
123+
124+ const renderMain = useCallback(() => {
125+ if (localRunning) {
126+ return (
127+ <Grid.Item
128+ full
129+ as={LoginButton}
130+ solid
131+ success
132+ url={localUrl}
133+ code={code}>
134+ Open OS
135+ </Grid.Item>
136+ );
137+ }
138+ if (ship.running) {
139+ return (
140+ <>
141+ <Grid.Item
142+ cols={[1, 9]}
143+ as={LoginButton}
144+ solid
145+ success
146+ url={url}
147+ code={code}>
148+ Open OS
149+ </Grid.Item>
150+ {/* Unsupported for now */}
151+ {/* <Grid.Item cols={[9, 13]} as={Button} className="b-black b1" center> */}
152+ {/* Disconnect */}
153+ {/* </Grid.Item> */}
154+ </>
155+ );
156+ }
157+ if (ship.missing) {
158+ return (
159+ <Grid.Item
160+ full
161+ as={ForwardButton}
162+ solid
163+ disabled={!keyfile}
164+ onClick={createShip}>
165+ Connect
166+ </Grid.Item>
167+ );
168+ }
169+ if (ship.pending) {
170+ return (
171+ <Grid.Item
172+ full
173+ as={ProgressButton}
174+ success
175+ disabled
176+ progress={bootProgress}>
177+ {bootMessage || 'Connecting'}
178+ </Grid.Item>
179+ );
180+ }
181+ }, [
182+ localRunning,
183+ ship.missing,
184+ ship.running,
185+ ship.pending,
186+ bootProgress,
187+ bootMessage,
188+ keyfile,
189+ createShip,
190+ url,
191+ ]);
192+
193+ const renderDetails = useCallback(() => {
194+ if (ship.running) {
195+ return (
196+ <>
197+ <Grid.Item cols={[1, 9]} className="gray4">
198+ <span className="mono">{name}</span> is connected to Tlon
199+ </Grid.Item>
200+ </>
201+ );
202+ }
203+ if (!keyfile && ship.missing) {
204+ return (
205+ <Grid.Item full className="gray4">
206+ Please reset your networking keys in order to use hosting
207+ </Grid.Item>
208+ );
209+ }
210+ }, [name, ship, keyfile]);
211+
212+ return (
213+ <>
214+ {disabled && (
215+ <Grid gap={4}>
216+ <Grid.Item full className="f5">
217+ Urbit OS
218+ </Grid.Item>
219+ <Grid.Divider />
220+ </Grid>
221+ )}
222+ {!disabled && (
223+ <Grid gap={4}>
224+ <Grid.Item full className="f5" as={Flex}>
225+ <Flex.Item>Urbit OS </Flex.Item>
226+ <Flex.Item
227+ className={cn({ green3: running, gray4: !running }, 'ml3')}>
228+ {running ? 'Connected' : 'Disconnected'}
229+ </Flex.Item>
230+ </Grid.Item>
231+ <BridgeForm initialValues={{ provider: 'tlon' }}>
232+ {() => (
233+ <>
234+ {renderDetails()}
235+ {renderMain()}
236+ {!localRunning && (
237+ <Grid.Item
238+ full
239+ as={SelectInput}
240+ name="provider"
241+ label="Host Provider"
242+ options={options}
243+ disabled
244+ />
245+ )}
246+ </>
247+ )}
248+ </BridgeForm>
249+ </Grid>
250+ )}
251+ <Grid>
252+ <Grid.Item
253+ full
254+ as={DownloadKeyfileButton}
255+ {...bind}
256+ className="mt2"
257+ detail="A keyfile authenticates your Urbit ID to Urbit OS"
258+ />
259+ <Grid.Divider />
260+ {code && (
261+ <Grid.Item
262+ full
263+ className="mt2"
264+ as={CopyButtonWide}
265+ detail={code}
266+ detailClassName="mono">
267+ Login Code
268+ </Grid.Item>
269+ )}
270+ </Grid>
271+ </>
272+ );
273+}
src/views/UrbitOS/NetworkingKeys.jsnew file+285
@@ -0,0 +1,285 @@
1+import React, { useCallback, useMemo, useRef } from 'react';
2+import { Just } from 'folktale/maybe';
3+import { Grid, ToggleInput, CheckboxInput } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
5+import { randomHex } from 'web3-utils';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
9+import { useNetwork } from 'store/network';
10+import { useWallet } from 'store/wallet';
11+
12+import { useLocalRouter } from 'lib/LocalRouter';
13+import * as need from 'lib/need';
14+import {
15+ attemptNetworkSeedDerivation,
16+ deriveNetworkKeys,
17+ CRYPTO_SUITE_VERSION,
18+} from 'lib/keys';
19+import useEthereumTransaction from 'lib/useEthereumTransaction';
20+import { GAS_LIMITS } from 'lib/constants';
21+import { addHexPrefix } from 'lib/wallet';
22+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
23+
24+import ViewHeader from 'components/ViewHeader';
25+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
26+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
27+import NoticeBox from 'components/NoticeBox';
28+
29+import { HexInput } from 'form/Inputs';
30+import {
31+ composeValidator,
32+ buildCheckboxValidator,
33+ buildHexValidator,
34+} from 'form/validators';
35+import BridgeForm from 'form/BridgeForm';
36+import Condition from 'form/Condition';
37+import FormError from 'form/FormError';
38+import convertToInt from 'lib/convertToInt';
39+
40+function useSetKeys(manualNetworkSeed, setManualNetworkSeed) {
41+ const { urbitWallet, wallet, authMnemonic } = useWallet();
42+ const { pointCursor } = usePointCursor();
43+ const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
44+ const { contracts } = useNetwork();
45+
46+ const _point = need.point(pointCursor);
47+ const _contracts = need.contracts(contracts);
48+ const _details = need.details(getDetails(_point));
49+
50+ const networkRevision = convertToInt(_details.keyRevisionNumber, 10);
51+ const randomSeed = useRef();
52+
53+ const {
54+ available: keyfileAvailable,
55+ generating: keyfileGenerating,
56+ filename,
57+ bind: keyfileBind,
58+ } = useKeyfileGenerator(manualNetworkSeed);
59+
60+ const buildNetworkSeed = useCallback(
61+ async manualSeed => {
62+ if (manualSeed !== undefined) {
63+ setManualNetworkSeed(manualSeed);
64+ return manualSeed;
65+ } else {
66+ const newNetworkRevision = networkRevision + 1;
67+ console.log(`deriving seed with revision ${newNetworkRevision}`);
68+
69+ const networkSeed = await attemptNetworkSeedDerivation({
70+ urbitWallet,
71+ wallet,
72+ authMnemonic,
73+ details: _details,
74+ revision: newNetworkRevision,
75+ });
76+
77+ if (Just.hasInstance(networkSeed)) {
78+ return networkSeed.value;
79+ }
80+
81+ randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
82+ setManualNetworkSeed(randomSeed.current);
83+
84+ return randomSeed.current;
85+ }
86+ },
87+ [
88+ _details,
89+ authMnemonic,
90+ networkRevision,
91+ setManualNetworkSeed,
92+ urbitWallet,
93+ wallet,
94+ ]
95+ );
96+
97+ const { completed: _completed, ...rest } = useEthereumTransaction(
98+ useCallback(
99+ async (manualSeed, isDiscontinuity) => {
100+ const seed = await buildNetworkSeed(manualSeed);
101+ const pair = deriveNetworkKeys(seed);
102+
103+ return azimuth.ecliptic.configureKeys(
104+ _contracts,
105+ _point,
106+ addHexPrefix(pair.crypt.public),
107+ addHexPrefix(pair.auth.public),
108+ CRYPTO_SUITE_VERSION,
109+ isDiscontinuity
110+ );
111+ },
112+ [_contracts, _point, buildNetworkSeed]
113+ ),
114+ useCallback(
115+ () => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
116+ [_point, syncDetails, syncRekeyDate]
117+ ),
118+ GAS_LIMITS.CONFIGURE_KEYS
119+ );
120+
121+ // only treat the transaction as completed once we also have keys to download
122+ const completed = _completed && keyfileAvailable && !keyfileGenerating;
123+
124+ return {
125+ completed,
126+ filename,
127+ keyfileBind,
128+ ...rest,
129+ };
130+}
131+
132+export default function UrbitOSNetworkingKeys({
133+ manualNetworkSeed,
134+ setManualNetworkSeed,
135+}) {
136+ const { pop } = useLocalRouter();
137+ const { pointCursor } = usePointCursor();
138+ const { getDetails } = usePointCache();
139+
140+ const point = need.point(pointCursor);
141+ const details = getDetails(point);
142+
143+ const hasKeys = details.matchWith({
144+ Nothing: () => false,
145+ Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
146+ // we actually don't mind the default NaN behavior of parseInt here,
147+ // since NaN > 0 === false and that's a reasonable result
148+ });
149+
150+ const {
151+ construct,
152+ unconstruct,
153+ completed,
154+ inputsLocked,
155+ bind,
156+ keyfileBind,
157+ } = useSetKeys(manualNetworkSeed, setManualNetworkSeed);
158+
159+ const validateForm = useCallback((values, errors) => {
160+ if (values.useNetworkSeed && errors.networkSeed) {
161+ return errors;
162+ }
163+ }, []);
164+
165+ const validate = useMemo(
166+ () =>
167+ composeValidator(
168+ {
169+ useNetworkSeed: buildCheckboxValidator(),
170+ networkSeed: buildHexValidator(64), // 64 chars
171+ useDiscontinuity: buildCheckboxValidator(),
172+ },
173+ validateForm
174+ ),
175+ [validateForm]
176+ );
177+
178+ const onValues = useCallback(
179+ ({ valid, values, form }) => {
180+ if (valid) {
181+ construct(
182+ values.useNetworkSeed ? values.networkSeed : undefined,
183+ values.useDiscontinuity
184+ );
185+ } else {
186+ unconstruct();
187+ }
188+
189+ if (!values.useNetworkSeed && values.networkSeed) {
190+ form.change('networkSeed', '');
191+ }
192+ },
193+ [construct, unconstruct]
194+ );
195+
196+ const initialValues = useMemo(
197+ () => ({
198+ useNetworkSeed: false,
199+ useDiscontinuity: false,
200+ }),
201+ []
202+ );
203+
204+ const usageMessage =
205+ 'You need this to authenticate with Arvo. Please download';
206+
207+ return (
208+ <>
209+ <Grid>
210+ <Grid.Item full as={ViewHeader}>
211+ Set Networking Keys
212+ </Grid.Item>
213+ <Grid.Item className="gray4 f6" full>
214+ Network Keys solidify the handshake between ID and OS
215+ </Grid.Item>
216+ {completed && (
217+ <>
218+ <Grid.Item
219+ full
220+ className="mv3"
221+ as={DownloadKeyfileButton}
222+ solid
223+ {...keyfileBind}
224+ />
225+ <Grid.Item full className="mb3 bg-red3 white t-center p3">
226+ {usageMessage}
227+ </Grid.Item>
228+ </>
229+ )}
230+ <BridgeForm
231+ validate={validate}
232+ onValues={onValues}
233+ initialValues={initialValues}>
234+ {() => (
235+ <>
236+ {!completed && (
237+ <>
238+ <Grid.Item
239+ full
240+ as={ToggleInput}
241+ name="useNetworkSeed"
242+ label="Use Custom Network Seed"
243+ inverseLabel="Back to Derived Network Seed"
244+ disabled={inputsLocked}
245+ />
246+ <Condition when="useNetworkSeed" is={true}>
247+ <Grid.Item full as={NoticeBox} className="mb2">
248+ When using a custom network seed, you'll need to download
249+ your Arvo keyfile immediately after this transaction as
250+ Bridge does not store your seed.
251+ </Grid.Item>
252+ <Grid.Item
253+ full
254+ as={HexInput}
255+ name="networkSeed"
256+ label="Network Seed (32 bytes)"
257+ disabled={inputsLocked}
258+ />
259+ </Condition>
260+ <Grid.Item
261+ full
262+ as={CheckboxInput}
263+ name="useDiscontinuity"
264+ label="Breach Continuity"
265+ disabled={inputsLocked}
266+ />
267+
268+ <Grid.Item full as={FormError} />
269+ </>
270+ )}
271+
272+ <Grid.Item
273+ full
274+ as={InlineEthereumTransaction}
275+ {...bind}
276+ label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
277+ onReturn={() => pop()}
278+ />
279+ </>
280+ )}
281+ </BridgeForm>
282+ </Grid>
283+ </>
284+ );
285+}