on master help sign in
Merge pull request #187 from urbit/feat/indigo-admin

WIP: Indigo-ify Admin Views

commit0415772ac35eb67a32a924b35eb42aed33af6599 authorMatt Condon <mattgcondon@gmail.com> dateThu 18 Jul 2019, 17:44 (7 years ago) parent6f80e867efdf70
106 files changed+4058−3956
package-lock.json+24−34 package.json+2−1 src/Bridge.js+6−1 src/components/Blinky.js+15 src/components/Buttons.js+16−6 src/components/Crumbs.js+1−1 src/components/DownloadKeyfileButton.js+33 src/components/InlineEthereumTransaction.js+219 src/components/MiniBackButton.js+1−1 src/components/NoticeBox.js+11 src/components/Passport.js+6−11 src/components/Steps.js+2−4 src/components/View.js+6−2 src/components/ViewHeader.js+7 src/components/old/Base.js−2 src/components/old/Passport.js−235 src/components/old/StatelessTransaction.js−598 src/indigo-react/components/Button.js+16−10 src/indigo-react/components/CheckboxInput.js+9−6 src/indigo-react/components/IconButton.js+5−2 src/indigo-react/components/Input.js+12−14 src/indigo-react/components/LinkButton.js+35 src/indigo-react/components/SelectInput.js+1−1 src/indigo-react/components/ToggleInput.js+70 src/indigo-react/index.js+2 src/indigo-react/indigo.css+2−7 src/indigo-react/lib/useForm.js+39−23 src/lib/capitalize.js+1 src/lib/constants.js+9 src/lib/dateFormat.js+5 src/lib/error.js+5−2 src/lib/explorer.js+28 src/lib/invite.js+2−262 src/lib/keys.js+122−58 src/lib/need.js+8−1 src/lib/proxy.js+33−13 src/lib/reticket.js+215 src/lib/routeNames.js+1−1 src/lib/router.js+17−40 src/lib/tank.js+8−14 src/lib/transformers.js+9−7 src/lib/txn.js+20−56 src/lib/useCurrentPermissions.js+17 src/lib/useDeepEqualReference.js+22 src/lib/useEthereumTransaction.js+270 src/lib/useInputs.js+67−15 src/lib/useKeyfileGenerator.js+118 src/lib/usePermissionsForPoint.js+97 src/lib/useRouter.js+54−18 src/lib/useSetState.js+3−3 src/lib/validators.js+41−14 src/lib/wallet.js+5−22 src/lib/walletgen.js+3−2 src/store/Provider.js−2 src/store/lib/useBirthdaysStore.js+1−1 src/store/lib/useInvitesStore.js+3−11 src/store/lib/usePointStore.js+7−3 src/store/lib/useRekeyDateStore.js+15−9 src/store/txnConfirmations.js−44 src/store/wallet.js+1 src/style/bridge.scss+54 src/views/AcceptTransfer.js−121 src/views/Activate/ActivateCode.js+5−7 src/views/Activate/ActivatePassport.js+4−4 src/views/Activate/PassportDownload.js+33−32 src/views/Activate/PassportTransfer.js+53−33 src/views/Activate/PassportVerify.js+23−25 src/views/Activate/PassportView.js+27 src/views/Admin.js+61−69 src/views/Admin/AdminCancelTransfer.js+87 src/views/Admin/AdminEditPermissions.js+122 src/views/Admin/AdminHome.js+108 src/views/Admin/AdminNetworkingKeys.js+381 src/views/Admin/AdminRedownload.js+74 src/views/Admin/AdminReticket.js+64 src/views/Admin/AdminSetProxy.js+205 src/views/Admin/AdminTransfer.js+94 src/views/Admin/Reticket/ReticketConfirm.js+90 src/views/Admin/Reticket/ReticketDownload.js+38 src/views/Admin/Reticket/ReticketExecute.js+185 src/views/Admin/Reticket/ReticketVerify.js+2−2 src/views/CancelTransfer.js−81 src/views/CreateGalaxy.js+154−190 src/views/GenKeyfile.js+1−1 src/views/Invite.js+9−7 src/views/Invite/InviteEmail.js+25−19 src/views/InvitesManage.js−185 src/views/IssueChild.js+165−243 src/views/Login.js+39−20 src/views/Login/Ticket.js+8−13 src/views/Party/PartySetPoolSize.js+133 src/views/Permissions.js−153 src/views/Point.js+49−40 src/views/Point/Actions.js+2−24 src/views/Point/KeysAndMetadata.js+2−1 src/views/Points.js+14−12 src/views/Redownload.js−52 src/views/Reticket.js−97 src/views/Reticket/Confirm.js−63 src/views/Reticket/DoReticket.js−128 src/views/Reticket/Download.js−35 src/views/Reticket/Verify.js−41 src/views/SentTransaction.js−183 src/views/SetKeys.js−210 src/views/SetProxy.js−181 src/views/Transfer.js−127
package-lock.json+24−34
@@ -11477,9 +11477,9 @@
1147711477 }
1147811478 },
1147911479 "lodash": {
11480− "version": "4.17.11",
11481− "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
11482− "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
11480+ "version": "4.17.14",
11481+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
11482+ "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
1148311483 },
1148411484 "lodash._reinterpolate": {
1148511485 "version": "3.0.0",
@@ -11893,9 +11893,9 @@
1189311893 }
1189411894 },
1189511895 "mixin-deep": {
11896− "version": "1.3.1",
11897− "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
11898− "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
11896+ "version": "1.3.2",
11897+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
11898+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
1189911899 "requires": {
1190011900 "for-in": "^1.0.2",
1190111901 "is-extendable": "^1.0.1"
@@ -14449,6 +14449,17 @@
1444914449 }
1445014450 }
1445114451 },
14452+ "react-dom": {
14453+ "version": "16.8.6",
14454+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
14455+ "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
14456+ "requires": {
14457+ "loose-envify": "^1.1.0",
14458+ "object-assign": "^4.1.1",
14459+ "prop-types": "^15.6.2",
14460+ "scheduler": "^0.13.6"
14461+ }
14462+ },
1445214463 "react-error-overlay": {
1445314464 "version": "5.1.6",
1445414465 "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
@@ -15531,9 +15542,9 @@
1553115542 "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
1553215543 },
1553315544 "set-value": {
15534− "version": "2.0.0",
15535− "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
15536− "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
15545+ "version": "2.0.1",
15546+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
15547+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
1553715548 "requires": {
1553815549 "extend-shallow": "^2.0.1",
1553915550 "is-extendable": "^0.1.1",
@@ -17311,35 +17322,14 @@
1731117322 }
1731217323 },
1731317324 "union-value": {
17314− "version": "1.0.0",
17315− "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
17316− "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
17325+ "version": "1.0.1",
17326+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
17327+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
1731717328 "requires": {
1731817329 "arr-union": "^3.1.0",
1731917330 "get-value": "^2.0.6",
1732017331 "is-extendable": "^0.1.1",
17321− "set-value": "^0.4.3"
17322− },
17323− "dependencies": {
17324− "extend-shallow": {
17325− "version": "2.0.1",
17326− "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
17327− "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
17328− "requires": {
17329− "is-extendable": "^0.1.0"
17330− }
17331− },
17332− "set-value": {
17333− "version": "0.4.3",
17334− "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
17335− "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
17336− "requires": {
17337− "extend-shallow": "^2.0.1",
17338− "is-extendable": "^0.1.1",
17339− "is-plain-object": "^2.0.1",
17340− "to-object-path": "^0.3.0"
17341− }
17342− }
17332+ "set-value": "^2.0.1"
1734317333 }
1734417334 },
1734517335 "uniq": {
package.json+2−1
@@ -32,11 +32,12 @@
3232 "jszip": "^3.1.5",
3333 "keccak": "^1.4.0",
3434 "keythereum": "^1.0.4",
35− "lodash": "^4.17.11",
35+ "lodash": "^4.17.14",
3636 "more-entropy": "^0.0.7",
3737 "prop-types": "^15.6.2",
3838 "react": "^16.8.6",
3939 "react-app-rewire-hot-loader": "^2.0.1",
40+ "react-dom": "^16.8.6",
4041 "react-hot-loader": "^4.12.0",
4142 "react-scripts": "3.0.1",
4243 "react-teleporter": "^1.1.0",
src/Bridge.js+6−1
@@ -24,7 +24,12 @@ const INITIAL_NETWORK_TYPE = isDevelopment
2424const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
2525const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
2626const INITIAL_ROUTES = IS_STUBBED
27− ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.LOGIN }]
27+ ? [
28+ { key: ROUTE_NAMES.LANDING },
29+ { key: ROUTE_NAMES.LOGIN },
30+ { key: ROUTE_NAMES.POINTS },
31+ { key: ROUTE_NAMES.POINT },
32+ ]
2833 : hasDisclaimed()
2934 ? [{ key: ROUTE_NAMES.LANDING }]
3035 : [{ key: ROUTE_NAMES.DISCLAIMER }];
src/components/Blinky.js+15
@@ -1,18 +1,33 @@
11import React, { useState, useRef } from 'react';
2+import { times } from 'lodash';
23
34import useInterval from 'lib/useInterval';
5+import { formatDots } from 'lib/dateFormat';
46
57// TODO: make these characters display as the same width
68export const LOADING_CHARACTER = '▓';
79export const INTERSTITIAL_CHARACTER = '░';
810const BLINK_AFTER_MS = 2500; // ms
911
12+const buildDate = char =>
13+ [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
14+const DATE_A = buildDate(LOADING_CHARACTER);
15+const DATE_B = buildDate(INTERSTITIAL_CHARACTER);
16+
1017export const matchBlinky = obj =>
1118 obj.matchWith({
1219 Nothing: () => <Blinky delayed />,
1320 Just: p => p.value,
1421 });
1522
23+export const matchBlinkyDate = obj =>
24+ obj.matchWith({
25+ Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
26+ Just: p => formatDots(p.value),
27+ });
28+
29+export const blinkIf = (test, right) => (test ? <Blinky /> : right);
30+
1631export default function Blinky({
1732 a = LOADING_CHARACTER,
1833 b = INTERSTITIAL_CHARACTER,
src/components/Buttons.js+16−6
@@ -1,9 +1,19 @@
11import React from 'react';
22import { Button } from 'indigo-react';
33
4+import { blinkIf } from './Blinky';
5+
46// NOTE: the -> is correct because inter recognizes the pair
5−export const ForwardButton = props => <Button accessory="->" {...props} />;
6−export const DownloadButton = props => <Button accessory="↓" {...props} />;
7+export const ForwardButton = ({ loading, ...props }) => (
8+ <Button accessory={blinkIf(loading, '->')} {...props} />
9+);
10+export const DownloadButton = ({ loading, ...props }) => (
11+ <Button accessory={blinkIf(loading, '↓')} {...props} />
12+);
13+export const RestartButton = props => <Button accessory="↺" {...props} />;
14+export const GenerateButton = ({ loading, ...props }) => (
15+ <Button accessory={blinkIf(loading, '○')} solid {...props} />
16+);
717export const OutButton = props => (
818 <Button as="a" target="_blank" accessory="↗" {...props} />
919);
@@ -14,8 +24,8 @@ export const OfflineButton = props => (
1424 </OutButton>
1525);
1626
17−export const RestartButton = props => (
18− <Button accessory="↺" solid {...props}>
19− Restart
20− </Button>
27+export const BootArvoButton = props => (
28+ <ForwardButton detail="Boot your computer" {...props}>
29+ Boot Arvo
30+ </ForwardButton>
2131);
src/components/Crumbs.js+1−1
@@ -20,7 +20,7 @@ export default function Crumbs({ className, routes = [] }) {
2020 {route.text}
2121 </Flex.Item>
2222 {i !== lastIndex && (
23− <Flex.Item as={Breadcrumb} className={cn(textStyle, 'mh4')}>
23+ <Flex.Item as={Breadcrumb} className={cn(textStyle, 'mh2')}>
2424 /
2525 </Flex.Item>
2626 )}
src/components/DownloadKeyfileButton.jsnew file+33
@@ -0,0 +1,33 @@
1+import React from 'react';
2+import { B } from 'indigo-react';
3+
4+import { DownloadButton } from 'components/Buttons';
5+
6+export default function DownloadKeyfileButton({
7+ // useKeyfileGenerator.bind
8+ generating,
9+ available,
10+ downloaded,
11+ download,
12+ notice,
13+
14+ // from caller
15+ className,
16+ children = 'Download Arvo Keyfile',
17+ ...rest
18+}) {
19+ return (
20+ <DownloadButton
21+ as="span"
22+ className={className}
23+ disabled={downloaded || !available}
24+ disabledDetail={
25+ !available && <B className="wrap ws-normal">· {notice}</B>
26+ }
27+ loading={generating}
28+ onClick={download}
29+ {...rest}>
30+ {children}
31+ </DownloadButton>
32+ );
33+}
src/components/InlineEthereumTransaction.jsnew file+219
@@ -0,0 +1,219 @@
1+import React, { useEffect } from 'react';
2+import cn from 'classnames';
3+import {
4+ Grid,
5+ ToggleInput,
6+ ErrorText,
7+ Flex,
8+ LinkButton,
9+ H5,
10+ Text,
11+ HelpText,
12+} from 'indigo-react';
13+import { fromWei } from 'web3-utils';
14+
15+import { useCheckboxInput } from 'lib/useInputs';
16+import { useExploreTxUrl } from 'lib/explorer';
17+import { hexify } from 'lib/txn';
18+
19+import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
20+import WarningBox from './WarningBox';
21+
22+export default function InlineEthereumTransaction({
23+ // from useEthereumTransaction.bind
24+ initializing,
25+ canSign,
26+ generateAndSign,
27+ signed,
28+ broadcast,
29+ broadcasted,
30+ confirmed,
31+ completed,
32+ reset,
33+ error,
34+ gasPrice,
35+ setGasPrice,
36+ resetGasPrice,
37+ txHash,
38+ nonce,
39+ chainId,
40+ needFunds,
41+ signedTransaction,
42+
43+ // additional from parent
44+ label = 'Generate & Sign Transaction',
45+ className,
46+ onReturn,
47+}) {
48+ // show receipt after successful broadcast
49+ const showReceipt = broadcasted || confirmed || completed;
50+ // show configure controls pre-broadcast
51+ const showConfigureInput = !(signed || broadcasted || confirmed || completed);
52+ // show the send/loading button while signed, broadcasting, or confirme
53+ const showBroadcastButton = signed || broadcasted || confirmed;
54+ const canBroadcast = signed;
55+ const isLoading = broadcasted || confirmed;
56+ // show signed tx only when signing (for offline usage)
57+ const showSignedTx = signed;
58+
59+ const exploreTxUrl = useExploreTxUrl(txHash);
60+ const [advancedInput, { data: advancedOpen }] = useCheckboxInput({
61+ name: 'advanced',
62+ label: 'Advanced Configuration',
63+ inverseLabel: 'Cancel Advanced Configuration',
64+ disabled: !showConfigureInput,
65+ });
66+
67+ const [
68+ viewSignedTransaction,
69+ { data: signedTransactionOpen },
70+ ] = useCheckboxInput({
71+ name: 'viewsigned',
72+ label: 'View Signed Transaction',
73+ inverseLabel: 'Hide Signed Transaction',
74+ disabled: !showSignedTx,
75+ });
76+
77+ // reset gas price when closing advanced configuration
78+ useEffect(() => {
79+ if (!advancedOpen) {
80+ resetGasPrice();
81+ }
82+ }, [advancedOpen, resetGasPrice]);
83+
84+ const renderPrimaryButton = () => {
85+ if (error) {
86+ return (
87+ <Grid.Item full as={RestartButton} solid onClick={() => reset()}>
88+ Reset Transaction
89+ </Grid.Item>
90+ );
91+ } else if (completed) {
92+ return (
93+ <>
94+ <Grid.Divider />
95+ <Grid.Item full as={RestartButton} onClick={onReturn}>
96+ Return
97+ </Grid.Item>
98+ </>
99+ );
100+ } else if (showBroadcastButton) {
101+ return (
102+ <Grid.Item
103+ full
104+ as={ForwardButton}
105+ solid
106+ success
107+ disabled={!canBroadcast}
108+ loading={isLoading}
109+ onClick={() => broadcast()}>
110+ Send Transaction
111+ </Grid.Item>
112+ );
113+ } else {
114+ return (
115+ <Grid.Item
116+ full
117+ as={GenerateButton}
118+ onClick={generateAndSign}
119+ disabled={!canSign}
120+ loading={!canSign && initializing}>
121+ {label}
122+ </Grid.Item>
123+ );
124+ }
125+ };
126+
127+ return (
128+ <Grid className={cn(className, 'mt1')}>
129+ {renderPrimaryButton()}
130+
131+ {error && (
132+ <Grid.Item full as={ErrorText} className="mt1">
133+ {error.message}
134+ </Grid.Item>
135+ )}
136+
137+ {needFunds && (
138+ <Grid.Item full as={WarningBox} className="mt3">
139+ The address {needFunds.address} needs at least{' '}
140+ {fromWei(needFunds.minBalance)} ETH and currently has{' '}
141+ {fromWei(needFunds.balance)} ETH. Waiting until the account has enough
142+ funds.
143+ </Grid.Item>
144+ )}
145+
146+ {showConfigureInput && (
147+ <>
148+ <Grid.Item full as={ToggleInput} {...advancedInput} />
149+ {advancedOpen && (
150+ <>
151+ <Grid.Item full as={Flex} row justify="between" className="mt2">
152+ <Flex.Item as={H5}>Gas Price</Flex.Item>
153+ <Flex.Item as={H5}>{gasPrice} Gwei</Flex.Item>
154+ </Grid.Item>
155+ {/* TODO(shrugs): move to indigo/RangeInput */}
156+ <Grid.Item
157+ full
158+ as="input"
159+ type="range"
160+ min="1"
161+ max="100"
162+ value={gasPrice}
163+ onChange={e => setGasPrice(parseInt(e.target.value, 10))}
164+ />
165+ <Grid.Item
166+ full
167+ as={Flex}
168+ row
169+ justify="between"
170+ className="f6 mt1">
171+ <Flex.Item as={Text}>Cheap</Flex.Item>
172+ <Flex.Item as={Text}>Fast</Flex.Item>
173+ </Grid.Item>
174+ <Grid.Item full as={HelpText} className="mt3">
175+ Nonce: {nonce}
176+ </Grid.Item>
177+ <Grid.Item full as={HelpText} className="mt1">
178+ Chain ID: {chainId}
179+ </Grid.Item>
180+ <Grid.Divider className="mt3" />
181+ </>
182+ )}
183+ </>
184+ )}
185+
186+ {showSignedTx && (
187+ <>
188+ <Grid.Item full as={ToggleInput} {...viewSignedTransaction} />
189+ {signedTransactionOpen && (
190+ <Grid.Item full as="code" className="f6 mono gray4 wrap">
191+ {hexify(signedTransaction.serialize())}
192+ </Grid.Item>
193+ )}
194+ </>
195+ )}
196+
197+ {showReceipt && (
198+ <>
199+ <Grid.Divider />
200+ <Grid.Item full as={Flex} col className="pv4">
201+ <Flex.Item as={Flex} row justify="between">
202+ <Flex.Item as={H5}>Transaction Hash</Flex.Item>
203+ <Flex.Item as={LinkButton} href={exploreTxUrl}>
204+ Etherscan↗
205+ </Flex.Item>
206+ </Flex.Item>
207+ <Flex.Item as={Flex}>
208+ <Flex.Item flex as="code" className="f6 mono gray4 wrap">
209+ {txHash}
210+ </Flex.Item>
211+ <Flex.Item flex />
212+ </Flex.Item>
213+ </Grid.Item>
214+ <Grid.Divider />
215+ </>
216+ )}
217+ </Grid>
218+ );
219+}
src/components/MiniBackButton.js+1−1
@@ -5,7 +5,7 @@ import { IconButton } from 'indigo-react';
55import { ReactComponent as Back } from 'assets/back.svg';
66
77export default ({ className, ...rest }) => (
8− <IconButton {...rest}>
8+ <IconButton {...rest} flush>
99 <Back className={cn('black', className)} />
1010 </IconButton>
1111);
src/components/NoticeBox.jsnew file+11
@@ -0,0 +1,11 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Flex, Text } from 'indigo-react';
4+
5+export default function NoticeBox({ className, children }) {
6+ return (
7+ <Flex className={cn('bg-gray2 pv3 ph4', className)} align="center">
8+ <Text className="f6 fw-bold gray4">{children}</Text>
9+ </Flex>
10+ );
11+}
src/components/Passport.js+6−11
@@ -6,16 +6,14 @@ import { Grid, Flex, Text } from 'indigo-react';
66import { times } from 'lodash';
77
88import usePointBirthday from 'lib/usePointBirthday';
9−import { formatDots } from 'lib/dateFormat';
109
11−import Blinky, { LOADING_CHARACTER, INTERSTITIAL_CHARACTER } from './Blinky';
10+import {
11+ LOADING_CHARACTER,
12+ INTERSTITIAL_CHARACTER,
13+ matchBlinkyDate,
14+} from './Blinky';
1215import MaybeSigil from './MaybeSigil';
1316
14−const buildDate = char =>
15− [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
16−const DATE_A = buildDate(LOADING_CHARACTER);
17−const DATE_B = buildDate(INTERSTITIAL_CHARACTER);
18−
1917const TICKET_CHAR_LENGTH = 30;
2018const TICKET = times(TICKET_CHAR_LENGTH, i =>
2119 i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
@@ -40,10 +38,7 @@ export default function Passport({
4038 mini = false,
4139}) {
4240 const birthday = usePointBirthday(point.getOrElse(null));
43− const visualBirthday = birthday.matchWith({
44− Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
45− Just: p => formatDots(p.value),
46− });
41+ const visualBirthday = matchBlinkyDate(birthday);
4742
4843 const name = point.matchWith({
4944 Nothing: () => Nothing(),
src/components/Steps.js+2−4
@@ -2,11 +2,9 @@ import React from 'react';
22import cn from 'classnames';
33import { Text } from 'indigo-react';
44
5−export default function Steps({ num = 1, total = 3, className }) {
6− // NOTE: the 48px must match the `mt8` class on Passport for things to line
7− // up nicely
5+export default function Steps({ num = 1, total = 3, className, ...rest }) {
86 return (
9− <Text className={cn('f5 gray3', className)} style={{ height: '48px' }}>
7+ <Text className={cn('f5 gray3', className)} {...rest}>
108 <Text className="f5 black">Step {num}</Text> of {total}
119 </Text>
1210 );
src/components/View.js+6−2
@@ -3,9 +3,13 @@ import cn from 'classnames';
33import { Flex } from 'indigo-react';
44
55import Footer from './Footer';
6+import useBreakpoints from 'lib/useBreakpoints';
67
78// View is a top-level component that all Views must render to inherit styling
89function View({ className, children, inset = false, full = false, ...rest }) {
10+ const isMobile = useBreakpoints([true, false, false]);
11+ const shouldInset = inset && !isMobile;
12+
913 return (
1014 <Flex
1115 col
@@ -21,8 +25,8 @@ function View({ className, children, inset = false, full = false, ...rest }) {
2125 {...rest}>
2226 <Flex.Item
2327 className={cn('pb5', {
24− pt10: inset,
25− pt5: !inset,
28+ pt10: shouldInset,
29+ pt5: !shouldInset,
2630 })}>
2731 {children}
2832 </Flex.Item>
src/components/ViewHeader.jsnew file+7
@@ -0,0 +1,7 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { H5 } from 'indigo-react';
4+
5+export default function ViewHeader({ children, className }) {
6+ return <H5 className={cn(className, 'mv4')}>{children}</H5>;
7+}
src/components/old/Base.js−2
@@ -63,7 +63,6 @@ import Warning from './Warning';
6363import ValidatedSigil from './ValidatedSigil';
6464import ValidatedBlockie from './ValidatedBlockie';
6565import ShowBlockie from './ShowBlockie';
66−import Passport from './Passport';
6766
6867// HOC validated form components
6968
@@ -178,5 +177,4 @@ export {
178177 P,
179178 Code,
180179 Warning,
181− Passport,
182180};
src/components/old/Passport.jsdeleted−235
@@ -1,235 +0,0 @@
1−import React from 'react';
2−import { pour } from 'sigil-js';
3−import * as ob from 'urbit-ob';
4−import ReactSVGComponents from '../ReactSVGComponents';
5−import PropTypes from 'prop-types';
6−import { Button } from './Base';
7−import PaperRenderer from 'components/PaperRenderer';
8−
9−import { WALLET_STATES } from '../../lib/invite';
10−import { downloadWallet } from '../../lib/invite';
11−import { compose } from '../../lib/lib';
12−import { withWallet } from '../../store/wallet';
13−
14−class Passport extends React.Component {
15− constructor(props) {
16− super(props);
17−
18− this.state = {
19− paper: null,
20− };
21−
22− this.masterCensor = this.generateCensor();
23− this.managementCensor = this.generateCensor();
24−
25− this.download = this.download.bind(this);
26− }
27−
28− download() {
29− downloadWallet(this.state.paper);
30− this.props.pushWalletState(WALLET_STATES.DOWNLOADED);
31− }
32−
33− generateCensor() {
34− let blocks = [];
35−
36− new Uint32Array(86).forEach((_, i) => {
37− let rand = Math.round(Math.random());
38− let filled = rand || i === 0 || i === 43;
39−
40− let className = filled ? 'passport-block bg-gray-30' : 'passport-block';
41−
42− blocks.push(<div key={i} data-key={i} className={className} />);
43− });
44−
45− return <div className="flex flex-wrap passport-censor">{blocks}</div>;
46− }
47−
48− getCurrentDate() {
49− let date = new Date();
50− const fil = n => (n >= 10 ? n : `0${n}`);
51− let fmtDate =
52− `${date.getUTCFullYear()}.` +
53− `${date.getUTCMonth() + 1}.` +
54− `${fil(date.getUTCDate())}`;
55−
56− return fmtDate;
57− }
58−
59− getWalletProgress() {
60− let { walletStates } = this.props;
61−
62− if (!walletStates.includes(WALLET_STATES.UNLOCKING)) return null;
63− if (walletStates.includes(WALLET_STATES.TRANSACTIONS)) return null;
64−
65− let progressTimes = {
66− [WALLET_STATES.UNLOCKING]: { ms: '2813', pct: '44%' },
67− [WALLET_STATES.GENERATING]: { ms: '2433', pct: '38%' },
68− [WALLET_STATES.RENDERING]: { ms: '986', pct: '15%' },
69− };
70−
71− const progressBars = Object.keys(progressTimes).map(state => {
72− let progressIndex = walletStates.indexOf(state);
73− let progressDone =
74− progressIndex !== -1 && progressIndex < walletStates.length - 1;
75−
76− let progressStyle = {
77− width: progressTimes[state].pct,
78− };
79−
80− return (
81− <div className="passport-wallet-progress" style={progressStyle}>
82− {progressDone && <div className="passport-wallet-progress-fill" />}
83− </div>
84− );
85− });
86−
87− return <div className="flex space-between mt-3">{progressBars}</div>;
88− }
89−
90− getWalletStates() {
91− let { walletStates } = this.props;
92−
93− if (walletStates.includes(WALLET_STATES.TRANSACTIONS)) return null;
94−
95− let stateElems = walletStates.map(state => {
96− if (state === WALLET_STATES.DOWNLOADED) return null;
97−
98− const lastIndex = walletStates.indexOf(state) === walletStates.length - 1;
99− const checkmark = lastIndex ? null : (
100− <div className="text-700 text-sm lh-6 green-dark">✓</div>
101− );
102−
103− return (
104− <div className="flex justify-between">
105− <div className="text-mono text-sm lh-6 green-dark">{state}</div>
106− {checkmark}
107− </div>
108− );
109− });
110−
111− return <div className="mt-3">{stateElems}</div>;
112− }
113−
114− render() {
115− let pointVal = this.props.point.matchWith({
116− Just: p => p.value,
117− Nothing: () => '',
118− });
119−
120− let patp = pointVal === '' ? '' : ob.patp(pointVal);
121−
122− let wallet = this.props.wallet.matchWith({
123− Just: w => w.value,
124− Nothing: () => null,
125− });
126−
127− let ownershipAddress = this.props.wallet.matchWith({
128− Just: w => w.value.ownership.keys.address,
129− Nothing: () => null,
130− });
131−
132− const sigil =
133− patp !== '' ? (
134− pour({
135− patp: patp,
136− renderer: ReactSVGComponents,
137− size: 128,
138− })
139− ) : (
140− <div className="passport-sigil-blank" />
141− );
142−
143− let currentDate = this.getCurrentDate();
144− let walletStates = this.getWalletStates();
145− let walletProgress = this.getWalletProgress();
146− let paperReady =
147− this.state.paper &&
148− this.props.walletStates.includes(WALLET_STATES.PAPER_READY);
149− let downloadColor = paperReady ? 'green' : 'black';
150−
151− let downloadBtn = (
152− <Button
153− disabled={!paperReady}
154− prop-size={'lg wide'}
155− prop-color={downloadColor}
156− onClick={this.download}>
157− {'Download'}
158− </Button>
159− );
160−
161− let paperCollateral = wallet ? (
162− <PaperRenderer
163− point={pointVal}
164− wallet={wallet}
165− callback={data => {
166− this.setState({ paper: data });
167− this.props.pushWalletState(WALLET_STATES.PAPER_READY);
168− }}
169− />
170− ) : (
171− <div />
172− );
173−
174− let walletAddresses = (
175− <div className="mt-8">
176− <div className="passport-label text-mono">Ethereum Address</div>
177− <div className="passport-value">
178− {ownershipAddress || 'Ungenerated'}
179− </div>
180−
181− <div className="passport-label gray-50 mt-3">Master Ticket</div>
182− <div className="passport-value passport-censor overflow-hidden">
183− {this.masterCensor}
184− </div>
185−
186− <div className="passport-label gray-50 mt-5">Management Seed</div>
187− <div className="passport-value passport-censor overflow-hidden">
188− {this.managementCensor}
189− </div>
190− </div>
191− );
192−
193− let walletMetadata = (
194− <div className="flex">
195− <div className="passport-sigil">{sigil}</div>
196− <div className="passport-metadata ml-4">
197− <div className="passport-label text-mono">Point</div>
198− <div className="text-mono">{patp || '-'}</div>
199−
200− <div className="passport-label gray-50 mt-6">Created on</div>
201− <div className="passport-value">{patp ? currentDate : '-'}</div>
202−
203− <div className="passport-label gray-50 mt-2">Invited by</div>
204− <div className="passport-value">{this.props.invitedBy || '-'}</div>
205− </div>
206− </div>
207− );
208−
209− return (
210− <div>
211− <div className="passport-border">
212− <div className={`passport ${paperReady && 'passport-filled'}`}>
213− {walletMetadata}
214− {walletAddresses}
215− {paperCollateral}
216− </div>
217− {downloadBtn}
218− </div>
219−
220− {walletProgress}
221− {walletStates}
222− </div>
223− );
224− }
225−}
226−
227−Passport.propTypes = {
228− wallet: PropTypes.object,
229−};
230−
231−Passport.defaultProps = {
232− wallet: null,
233−};
234−
235−export default compose(withWallet)(Passport);
src/components/old/StatelessTransaction.jsdeleted−598
@@ -1,598 +0,0 @@
1−import { Nothing, Just } from 'folktale/maybe';
2−import { Ok } from 'folktale/result';
3−import React from 'react';
4−import * as need from '../../lib/need';
5−
6−import { Code, H3 } from './Base';
7−import { Button } from './Base';
8−import { CheckboxButton, Input, InnerLabel, InnerLabelDropdown } from './Base';
9−import { Warning } from './Base';
10−
11−import { GAS_LIMITS } from '../../lib/constants';
12−import { BRIDGE_ERROR } from '../../lib/error';
13−import { ROUTE_NAMES } from '../../lib/routeNames';
14−import { compose } from '../../lib/lib';
15−import * as tank from '../../lib/tank';
16−import {
17− sendSignedTransaction,
18− fromWei,
19− toWei,
20− hexify,
21− renderSignedTx,
22− signTransaction,
23−} from '../../lib/txn';
24−
25−import { withTxnConfirmations } from '../../store/txnConfirmations';
26−import { withNetwork } from '../../store/network';
27−import { withHistory } from '../../store/history';
28−import { withWallet } from '../../store/wallet';
29−import { withTxnCursor } from '../../store/txnCursor';
30−
31−const SUBMISSION_STATES = {
32− PROMPT: 'Send transaction',
33− PREPARING: 'Preparing...',
34− FUNDING: 'Finding transaction funding...',
35− SENDING: 'Sending transaction...',
36−};
37−
38−class StatelessTransaction extends React.Component {
39− constructor(props) {
40− super(props);
41−
42− this.state = {
43− gasPrice: '5',
44− gasLimit: GAS_LIMITS.DEFAULT.toString(),
45− showGasDetails: false,
46− userApproval: false,
47− chainId: '1',
48− customChain: false,
49− nonce: '0',
50− stx: Nothing(),
51− txn: Nothing(),
52− txStatus: SUBMISSION_STATES.PROMPT,
53− txError: Nothing(),
54− };
55−
56− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
57− this.setUserApproval = this.setUserApproval.bind(this);
58− this.setTxn = this.setTxn.bind(this);
59− this.setStx = this.setStx.bind(this);
60− this.sendTxn = this.sendTxn.bind(this);
61− this.setNonce = this.setNonce.bind(this);
62− this.setChainId = this.setChainId.bind(this);
63− this.setGasPrice = this.setGasPrice.bind(this);
64− this.setGasLimit = this.setGasLimit.bind(this);
65− this.rangeChange = this.rangeChange.bind(this);
66− this.toggleGasDetails = this.toggleGasDetails.bind(this);
67− this.handleChainUpdate = this.handleChainUpdate.bind(this);
68− }
69−
70− componentDidMount() {
71− const { props } = this;
72−
73− const addr = need.addressFromWallet(props.wallet);
74−
75− props.web3.matchWith({
76− Nothing: () => {},
77− Just: w3 => {
78− const validWeb3 = w3.value;
79−
80− const getTxMetadata = [
81− validWeb3.eth.getTransactionCount(addr),
82− validWeb3.eth.net.getId(),
83− validWeb3.eth.getGasPrice(),
84− ];
85−
86− Promise.all(getTxMetadata).then(r => {
87− const txMetadata = {
88− nonce: r[0],
89− chainId: r[1],
90− gasPrice: fromWei(r[2], 'gwei'),
91− };
92−
93− this.setState({ ...txMetadata });
94− });
95− },
96− });
97− }
98−
99− handleChainUpdate(chainId) {
100− if (chainId === 'custom') {
101− this.setState({
102− customChain: true,
103− });
104− } else {
105− this.setState({
106− customChain: false,
107− chainId,
108− });
109− }
110−
111− this.clearStx();
112− }
113−
114− setUserApproval() {
115− const { state } = this;
116− this.setState({ userApproval: !state.userApproval });
117− }
118−
119− toggleGasDetails() {
120− this.setState({
121− showGasDetails: !this.state.showGasDetails,
122− });
123− }
124−
125− setStx(stx) {
126− this.setState({
127− stx,
128− userApproval: false,
129− });
130− }
131−
132− setTxn(txn) {
133− this.setState({ txn });
134− }
135−
136− createUnsignedTxn() {
137− const txn = this.props.createUnsignedTxn();
138−
139− this.setState({ txn });
140− }
141−
142− clearTxn() {
143− this.setState({
144− userApproval: false,
145− txn: Nothing(),
146− stx: Nothing(),
147− });
148− }
149−
150− setNonce(nonce) {
151− this.setState({ nonce });
152− this.clearStx();
153− }
154−
155− setChainId(chainId) {
156− this.setState({ chainId });
157− this.clearStx();
158− }
159−
160− setGasPrice(gasPrice) {
161− this.setState({ gasPrice });
162− this.clearStx();
163− }
164−
165− rangeChange(e) {
166− this.setGasPrice(e.target.value);
167− }
168−
169− setGasLimit(gasLimit) {
170− this.setState({ gasLimit });
171− this.clearStx();
172− }
173−
174− clearStx() {
175− this.setState({
176− userApproval: false,
177− stx: Nothing(),
178− });
179− }
180−
181− async sendTxn() {
182− const { props, state } = this;
183− const web3 = props.web3.value;
184−
185− const stx = state.stx.matchWith({
186− Just: tx => tx.value,
187− Nothing: () => {
188− throw new Error(BRIDGE_ERROR.MISSING_TXN);
189− },
190− });
191−
192− if (props.beforeSend) {
193− try {
194− this.setState({ txStatus: SUBMISSION_STATES.PREPARING });
195− const res = await props.beforeSend(stx);
196− if (res === false) throw new Error('beforeSend disallowed sending');
197− } catch (e) {
198− console.log('beforeSend error', e);
199− this.setState({
200− txStatus: SUBMISSION_STATES.PROMPT,
201− txError: Just('Something went wrong!'),
202− });
203− return;
204− }
205− }
206−
207− this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
208−
209− const rawTx = hexify(stx.serialize());
210− const cost = state.gasLimit * toWei(state.gasPrice, 'gwei');
211−
212− const address = need.addressFromWallet(props.wallet);
213− let balance = await web3.eth.getBalance(address);
214− let hasBalance = balance >= cost;
215−
216− let usedTank = false;
217− // if we need to, try and fund the transaction
218− if (!hasBalance) {
219− usedTank = await this.ensureFundsFor(web3, address, cost, [rawTx]);
220− hasBalance = usedTank;
221− }
222−
223− // if we still don't have sufficient balance, fail and tell the user
224− if (!hasBalance) {
225− this.setState({
226− txStatus: SUBMISSION_STATES.PROMPT,
227− txError: Just(`Insufficient funds.
228− Address ${address} needs at least ${fromWei(cost.toString())} ETH,
229− currently has ${fromWei(balance.toString())} ETH.`),
230− });
231−
232− // if we have the balance, proceed with submission
233− } else {
234− this.setState({ txStatus: SUBMISSION_STATES.SENDING });
235−
236− sendSignedTransaction(web3, stx, usedTank, props.setTxnConfirmations)
237− .then(txHash => {
238− props.setTxnCursor(Just(Ok(txHash)));
239−
240− let routeData = {};
241− if (props.newNetworkSeed) {
242− props.setNetworkSeed(Just(props.newNetworkSeed));
243− routeData.promptKeyfile = true;
244− }
245−
246− if (props.newRevision) {
247− props.setNetworkRevision(Just(props.newRevision));
248− }
249−
250− props.history.popAndPush(ROUTE_NAMES.SENT_TRANSACTION, routeData);
251− })
252− .catch(err => {
253− this.setState({
254− txStatus: SUBMISSION_STATES.PROMPT,
255− txError: Just(err),
256− });
257− });
258− }
259− }
260−
261− // uses the gas tank to ensure funds, but if we have to ask the user
262− // for funding, just resolve instead of waiting
263− // returns true if the gas tank was used, false otherwise
264− ensureFundsFor(web3, address, cost, signedTxs) {
265− return new Promise((resolve, reject) => {
266− tank
267− .ensureFundsFor(web3, null, address, cost, signedTxs, () => {
268− resolve(false);
269− })
270− .then(res => {
271− resolve(res);
272− });
273− });
274− }
275−
276− getChainTitle(chainId) {
277− let map = {
278− '1': 'Mainnet - 1',
279− '2': 'Morden - 2',
280− '3': 'Ropsten - 3',
281− '4': 'Goerli - 4',
282− '42': 'Kovan - 42',
283− '1337': 'Geth private chains - 1337',
284− custom: 'Custom',
285− };
286−
287− return map[chainId];
288− }
289−
290− getChainOptions() {
291− return [
292− {
293− title: this.getChainTitle('1'),
294− value: '1',
295− },
296− {
297− title: this.getChainTitle('2'),
298− value: '2',
299− },
300− {
301− title: this.getChainTitle('3'),
302− value: '3',
303− },
304− {
305− title: this.getChainTitle('4'),
306− value: '4',
307− },
308− {
309− title: this.getChainTitle('42'),
310− value: '42',
311− },
312− {
313− title: this.getChainTitle('1337'),
314− value: '1337',
315− },
316− {
317− title: this.getChainTitle('custom'),
318− value: 'custom',
319− },
320− ];
321− }
322−
323− render() {
324− const { web3, canGenerate } = this.props;
325− const {
326− gasPrice,
327− gasLimit,
328− nonce,
329− chainId,
330− txn,
331− stx,
332− userApproval,
333− showGasDetails,
334− customChain,
335− txStatus,
336− } = this.state;
337−
338− const {
339− setNonce,
340− setChainId,
341− setGasLimit,
342− setGasPrice,
343− toggleGasDetails,
344− setUserApproval,
345− sendTxn,
346− createUnsignedTxn,
347− handleChainUpdate,
348− } = this;
349−
350− const { state } = this;
351−
352− const canSign = Just.hasInstance(txn);
353− const canApprove = Just.hasInstance(stx);
354− const canSend = Just.hasInstance(stx) && userApproval === true;
355−
356− const chainOptions = this.getChainOptions();
357−
358− const generateButtonColor = Nothing.hasInstance(txn) ? 'blue' : 'green';
359−
360− const signerButtonColor = Nothing.hasInstance(stx) ? 'blue' : 'green';
361−
362− const generateTxnButton = (
363− <Button
364− className={'mt-8'}
365− disabled={!canGenerate}
366− prop-color={generateButtonColor}
367− prop-size={'lg wide'}
368− onClick={createUnsignedTxn}>
369− {'Generate Transaction'}
370− </Button>
371− );
372−
373− const unsignedTxnDisplay = txn.matchWith({
374− Nothing: _ => '',
375− Just: tx => (
376− <React.Fragment>
377− <H3 className={'mt-8'}>{'Unsigned Transaction'}</H3>
378− <Code>{JSON.stringify(tx.value, null, 2)}</Code>
379− </React.Fragment>
380− ),
381− });
382−
383− const gasPriceRangeDialogue = (
384− <React.Fragment>
385− <div className="mt-12 flex space-between align-baseline">
386− <div>
387− <span>Gas Price:</span>
388− <span className="ml-4 text-700 text-sm">{gasPrice} gwei</span>
389− </div>
390− <div className="text-sm">
391− <span>Max transaction fee: </span>
392− <span className="text-700">
393− {(gasPrice * gasLimit) / 1000000000} eth
394− </span>
395− </div>
396− </div>
397−
398− <input
399− className="mt-4"
400− type="range"
401− min="2"
402− max="20"
403− list="gweiVals"
404− value={gasPrice}
405− onChange={this.rangeChange}
406− />
407−
408− <div className="flex space-between text-sm mb-8">
409− <div>Cheap</div>
410− <div>Fast</div>
411− </div>
412− </React.Fragment>
413− );
414−
415− const toggleGasDetailsDialogue = (
416− <span className="text-link" onClick={toggleGasDetails}>
417− Gas Details
418− </span>
419− );
420−
421− const gasPriceDialogue = (
422− <Input
423− className={'mono mt-4'}
424− prop-size={'md'}
425− prop-format={'innerLabel'}
426− value={gasPrice}
427− onChange={setGasPrice}>
428− <InnerLabel>{'Gas Price (gwei)'}</InnerLabel>
429− </Input>
430− );
431−
432− const gasLimitDialogue = (
433− <Input
434− className={'mono mt-4'}
435− prop-size={'md'}
436− prop-format={'innerLabel'}
437− value={gasLimit}
438− onChange={setGasLimit}>
439− <InnerLabel>{'Gas Limit'}</InnerLabel>
440− </Input>
441− );
442−
443− const nonceDialogue = (
444− <Input
445− className={'mono mt-4 mb-4'}
446− prop-size={'md'}
447− prop-format={'innerLabel'}
448− value={nonce}
449− onChange={setNonce}>
450− <InnerLabel>{'Nonce'}</InnerLabel>
451− </Input>
452− );
453−
454− const chainDialogueTitle = customChain
455− ? 'Custom'
456− : this.getChainTitle(chainId);
457− const chainDialogue = (
458− <InnerLabelDropdown
459− className={'mt-6'}
460− fullWidth={true}
461− title={'Chain ID'}
462− options={chainOptions}
463− handleUpdate={handleChainUpdate}
464− currentSelectionTitle={chainDialogueTitle}
465− />
466− );
467−
468− const customChainDialogue = !customChain ? null : (
469− <Input
470− className={'mono mt-4 mb-8'}
471− prop-size={'md'}
472− prop-format={'innerLabel'}
473− value={chainId}
474− onChange={setChainId}>
475− <InnerLabel>{'Chain ID'}</InnerLabel>
476− </Input>
477− );
478−
479− const onlineParamsDialogue = web3.matchWith({
480− Just: _ => <div />,
481− Nothing: _ => (
482− <React.Fragment>
483− {nonceDialogue}
484− {chainDialogue}
485− {customChainDialogue}
486− </React.Fragment>
487− ),
488− });
489−
490− const signTxnButton = (
491− <Button
492− disabled={!canSign}
493− className={'mt-8'}
494− prop-size={'lg wide'}
495− prop-color={signerButtonColor}
496− onClick={() =>
497− signTransaction({
498− ...this.props,
499− ...this.state,
500− setStx: this.setStx,
501− wallet: this.props.wallet.value,
502− txn: txn.value,
503− })
504− }>
505− {'Sign Transaction'}
506− </Button>
507− );
508−
509− const signedTxnDisplay = stx.matchWith({
510− Nothing: _ => '',
511− Just: tx => (
512− <React.Fragment>
513− <H3 className={'mt-8'}>{'Signed Transaction'}</H3>
514− <Code>{JSON.stringify(renderSignedTx(tx.value), null, 2)}</Code>
515− </React.Fragment>
516− ),
517− });
518−
519− const confirmButton = (
520− <CheckboxButton
521− className={'mt-8'}
522− disabled={!canApprove}
523− onToggle={setUserApproval}
524− state={userApproval}
525− label="I approve this transaction and wish to send."
526− />
527− );
528−
529− const sending = txStatus !== SUBMISSION_STATES.PROMPT;
530− const sendTxnSpinner = sending ? '' : 'hide';
531− const sendTxnButton = (
532− <Button
533− prop-size={'xl wide'}
534− className={'mt-8'}
535− disabled={!canSend || sending}
536− onClick={sendTxn}>
537− <span className="relative">
538− <span className={`btn-spinner ${sendTxnSpinner}`} />
539− {txStatus}
540− </span>
541− </Button>
542− );
543−
544− const sendDialogue = web3.matchWith({
545− Nothing: _ => '',
546− Just: _ => (
547− <React.Fragment>
548− {confirmButton}
549− {sendTxnButton}
550− </React.Fragment>
551− ),
552− });
553−
554− const txnErrorDialogue = Nothing.hasInstance(state.txError) ? (
555− ''
556− ) : (
557− <Warning className={'mt-8'}>
558− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
559− {'There was an error sending your transaction.'}
560− </H3>
561− {state.txError.value}
562− </Warning>
563− );
564−
565− return (
566− <React.Fragment>
567− {generateTxnButton}
568− {unsignedTxnDisplay}
569−
570− {gasPriceRangeDialogue}
571− {toggleGasDetailsDialogue}
572−
573− {showGasDetails && (
574− <div>
575− {gasPriceDialogue}
576− {gasLimitDialogue}
577− </div>
578− )}
579− {onlineParamsDialogue}
580−
581− {signTxnButton}
582−
583− {signedTxnDisplay}
584− {sendDialogue}
585−
586− {txnErrorDialogue}
587− </React.Fragment>
588− );
589− }
590−}
591−
592−export default compose(
593− withNetwork,
594− withTxnConfirmations,
595− withHistory,
596− withWallet,
597− withTxnCursor
598−)(StatelessTransaction);
src/indigo-react/components/Button.js+16−10
@@ -10,6 +10,7 @@ export default function Button({
1010 solid = false,
1111 success = false,
1212 disabled = false,
13+ disabledDetail,
1314 detail,
1415 className,
1516 accessory = '→',
@@ -17,17 +18,12 @@ export default function Button({
1718 children,
1819 ...rest
1920}) {
20− const textColor = {
21− white: solid,
22− black: !solid && !disabled,
23− gray4: !solid && disabled,
24− };
2521 return (
2622 <Grid
2723 as={As}
2824 gap={1}
2925 className={cn(
30− 'pointer pv4 truncate flex-row justify-between',
26+ 'pointer pv4 truncate flex-row justify-between us-none',
3127 {
3228 p4: solid,
3329 },
@@ -38,6 +34,11 @@ export default function Button({
3834 'bg-gray3': !success && solid && disabled,
3935 'bg-transparent': !success && !solid,
4036 },
37+ {
38+ white: solid,
39+ black: !solid && !disabled,
40+ gray4: !solid && disabled,
41+ },
4142 className
4243 )}
4344 style={{
@@ -48,15 +49,20 @@ export default function Button({
4849 }}
4950 onClick={!disabled && onClick ? onClick : undefined}
5051 {...rest}>
51− <Grid.Item as={Flex} justify="between" full>
52− <span className={cn(textColor)}>{children}</span>
53− <div className={cn('pl4', textColor)}>{accessory}</div>
52+ <Grid.Item full as={Flex} justify="between">
53+ <span>{children}</span>
54+ <div className={cn('pl4')}>{accessory}</div>
5455 </Grid.Item>
5556 {detail && (
56− <Grid.Item as={HelpText} full>
57+ <Grid.Item full as={HelpText}>
5758 {detail}
5859 </Grid.Item>
5960 )}
61+ {disabled && disabledDetail && (
62+ <Grid.Item full className="f6 black mt1">
63+ {disabledDetail}
64+ </Grid.Item>
65+ )}
6066 </Grid>
6167 );
6268}
src/indigo-react/components/CheckboxInput.js+9−6
@@ -3,7 +3,6 @@ import cn from 'classnames';
33
44import Flex from './Flex';
55
6−// TODO: make this look like screens
76export default function CheckboxInput({
87 // visuals
98 name,
@@ -11,12 +10,12 @@ export default function CheckboxInput({
1110 className,
1211
1312 // callbacks
14− onValue,
1513 onEnter,
1614
1715 // state from hook
1816 focused,
1917 pass,
18+ syncPass,
2019 visiblyPassed,
2120 error,
2221 hintError,
@@ -35,9 +34,12 @@ export default function CheckboxInput({
3534}) {
3635 return (
3736 <Flex
38− className={cn(className, 'mv2')}
3937 row
4038 align="center"
39+ className={cn(className, 'mv2', {
40+ black: !disabled,
41+ gray4: disabled,
42+ })}
4143 style={{
4244 ...(disabled && {
4345 pointerEvents: 'none',
@@ -62,9 +64,10 @@ export default function CheckboxInput({
6264 <Flex
6365 justify="center"
6466 align="center"
65− className={cn('b-black b1 p1 mr3', {
66− 'bg-black white': data,
67− 'bg-white black': !data,
67+ className={cn('b1 p1 mr3', {
68+ 'bg-gray1': disabled,
69+ 'bg-black white b-black': !disabled && data,
70+ 'bg-white black b-black': !disabled && !data,
6871 })}
6972 style={{
7073 height: '14px',
src/indigo-react/components/IconButton.js+5−2
@@ -10,6 +10,7 @@ export default function IconButton({
1010 disabled = false,
1111 secondary = false,
1212 solid = false,
13+ flush = false, // if true, remove horizontal padding
1314 ...rest
1415}) {
1516 return (
@@ -18,7 +19,10 @@ export default function IconButton({
1819 align="center"
1920 justify="center"
2021 className={cn(
21− 'pointer p2',
22+ 'pointer pv2',
23+ {
24+ ph2: !flush,
25+ },
2226 {
2327 'bg-black': solid && !secondary && !disabled,
2428 'bg-gray3': (solid && disabled) || (solid && secondary),
@@ -37,7 +41,6 @@ export default function IconButton({
3741 height: '1rem',
3842 fontSize: '1rem',
3943 fontWeight: 'bold',
40− // TODO: ^ replace with 1:1 svgs and remove explicit sizing
4144 ...(disabled && {
4245 pointerEvents: 'none',
4346 cursor: 'not-allowed',
src/indigo-react/components/Input.js+12−14
@@ -1,10 +1,10 @@
1−import React, { useCallback, useEffect } from 'react';
1+import React, { useCallback } from 'react';
22import cn from 'classnames';
33
44import Flex from './Flex';
55import { ErrorText } from './Typography';
66
7−export default function Input({
7+export default React.memo(function Input({
88 // visuals
99 name,
1010 label,
@@ -13,12 +13,12 @@ export default function Input({
1313 mono = false,
1414
1515 // callbacks
16− onValue,
1716 onEnter,
1817
1918 // state from hook
2019 focused,
2120 pass,
21+ syncPass,
2222 visiblyPassed,
2323 error,
2424 hintError,
@@ -46,14 +46,6 @@ export default function Input({
4646 [disabled, pass] // eslint-disable-line react-hooks/exhaustive-deps
4747 );
4848
49− // notify parent of value only when passing
50− // TODO: deprecate/remove this
51− // keeping it around for now to avoid refactoring InviteEmail.js, but this
52− // feels like a hack
53− useEffect(() => {
54− pass && onValue && onValue(data);
55− }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
56−
5749 return (
5850 <Flex
5951 className={cn(className, 'mb1')}
@@ -64,7 +56,13 @@ export default function Input({
6456 cursor: 'not-allowed',
6557 }),
6658 }}>
67− <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
59+ <Flex.Item
60+ as="label"
61+ className={cn('f6 lh-tall', {
62+ black: !disabled,
63+ gray4: disabled,
64+ })}
65+ htmlFor={name}>
6866 {label}
6967 </Flex.Item>
7068 <Flex.Item as={Flex} className="rel" row>
@@ -86,7 +84,7 @@ export default function Input({
8684 'b-green3': visiblyPassed,
8785 'b-black': focused && !visiblyPassed,
8886 'b-yellow3': !focused && hintError,
89− 'b-gray3': !focused && !hintError && !visiblyPassed,
87+ 'b-gray2': !focused && !hintError && !visiblyPassed,
9088 }
9189 // TODO: inputClassName ?
9290 )}
@@ -117,4 +115,4 @@ export default function Input({
117115 )}
118116 </Flex>
119117 );
120−}
118+});
src/indigo-react/components/LinkButton.jsnew file+35
@@ -0,0 +1,35 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+export default function LinkButton({
5+ as: As = 'a',
6+ disabled = false,
7+ className,
8+ onClick,
9+ children,
10+ ...rest
11+}) {
12+ return (
13+ <As
14+ onClick={!disabled && onClick ? onClick : undefined}
15+ style={{
16+ ...(disabled && {
17+ pointerEvents: 'none',
18+ cursor: 'not-allowed',
19+ }),
20+ }}
21+ className={cn(
22+ 'pointer underline',
23+ {
24+ black: !disabled,
25+ gray4: disabled,
26+ },
27+ className
28+ )}
29+ target="_blank"
30+ rel="noopener noreferrer"
31+ {...rest}>
32+ {children}
33+ </As>
34+ );
35+}
src/indigo-react/components/SelectInput.js+1−1
@@ -17,12 +17,12 @@ export default function SelectInput({
1717 placeholder,
1818
1919 // callbacks
20− onValue,
2120 onEnter,
2221
2322 // state from hook
2423 focused,
2524 pass,
25+ syncPass,
2626 visiblyPassed,
2727 error,
2828 hintError,
src/indigo-react/components/ToggleInput.jsnew file+70
@@ -0,0 +1,70 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import Flex from './Flex';
5+import LinkButton from './LinkButton';
6+
7+export default function ToggleInput({
8+ // visuals
9+ name,
10+ label,
11+ inverseLabel,
12+ className,
13+
14+ // callbacks
15+ onEnter,
16+
17+ // state from hook
18+ focused,
19+ pass,
20+ syncPass,
21+ visiblyPassed,
22+ error,
23+ hintError,
24+ data,
25+ bind,
26+ autoFocus,
27+ disabled,
28+
29+ // ignored
30+ initialValue,
31+ validators,
32+ transformers,
33+
34+ ...rest
35+}) {
36+ return (
37+ <Flex
38+ className={cn(className)}
39+ row
40+ align="center"
41+ style={{
42+ ...(disabled && {
43+ pointerEvents: 'none',
44+ cursor: 'not-allowed',
45+ }),
46+ }}>
47+ {/* we totally hide the checkbox itself */}
48+ <Flex.Item
49+ as="input"
50+ {...rest}
51+ className={cn('super-hidden')}
52+ id={name}
53+ name={name}
54+ {...bind}
55+ />
56+ {/* and then display a prettier one in its stead */}
57+ <Flex.Item
58+ flex
59+ as="label"
60+ className={cn('f6 pv2 lh-tall us-none flex-row align-center', {
61+ clickable: !disabled,
62+ })}
63+ htmlFor={name}>
64+ <LinkButton disabled={disabled} className="f5">
65+ {bind.checked ? inverseLabel : label}
66+ </LinkButton>
67+ </Flex.Item>
68+ </Flex>
69+ );
70+}
src/indigo-react/index.js+2
@@ -4,9 +4,11 @@ export { default as Flex } from './components/Flex';
44export { default as Input } from './components/Input';
55export { default as SelectInput } from './components/SelectInput';
66export { default as CheckboxInput } from './components/CheckboxInput';
7+export { default as ToggleInput } from './components/ToggleInput';
78export { default as AccessoryIcon } from './components/AccessoryIcon';
89export { default as Button } from './components/Button';
910export { default as IconButton } from './components/IconButton';
11+export { default as LinkButton } from './components/LinkButton';
1012
1113export * from './components/Typography';
1214
src/indigo-react/indigo.css+2−7
@@ -13,7 +13,7 @@
1313 * git+https://github.com/urbit/Indigo.git
1414 * Bugs: https://github.com/urbit/Indigo/issues
1515 * License: MIT
16− * Timestamp: Mon Jul 01 2019 12:26:43 GMT-0700 (Pacific Daylight Time)
16+ * Timestamp: Tue Jul 09 2019 13:26:50 GMT-0700 (Pacific Daylight Time)
1717 *******************************************************************************/
1818/**********************
1919Color variables
@@ -323,7 +323,7 @@ button {
323323* {
324324 -webkit-font-smoothing: antialiased; }
325325
326−* {
326+body {
327327 font-family: "Inter UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
328328 color: #000000; }
329329
@@ -1918,11 +1918,6 @@ caption,
19181918 grid-template-columns: repeat(12, [col-start] 1fr);
19191919 grid-auto-rows: auto; }
19201920
1921−.grid8 {
1922− display: grid;
1923− grid-template-columns: repeat(8, [col-start] 1fr);
1924− grid-auto-rows: auto; }
1925−
19261921.full {
19271922 grid-area: auto / 1 / auto / 13; }
19281923
src/indigo-react/lib/useForm.js+39−23
@@ -1,10 +1,10 @@
1−import { useCallback, useMemo, useState } from 'react';
2−import { isEqual, keyBy, get, every, some } from 'lodash';
1+import { useCallback, useMemo } from 'react';
2+import { keyBy, get, every, some } from 'lodash';
33
44import { compose } from 'lib/lib';
55import { kDefaultValidator } from 'lib/validators';
66import useSetState from 'lib/useSetState';
7−import usePreviousValue from 'lib/usePreviousValue';
7+import useDeepEqualReference from 'lib/useDeepEqualReference';
88
99// interface InputConfig {
1010// name: string;
@@ -26,18 +26,12 @@ const defaultsFor = (configs, mapper) =>
2626 * useForm manages a set of inputs for rendering them in a loop
2727 */
2828export default function useForm(inputConfigs = []) {
29− // track the old value of the config set
30− const previousConfigs = usePreviousValue(inputConfigs);
31− // then compare equality
32− const configsAreEqual = isEqual(inputConfigs, previousConfigs);
33− // if equality changes, give `configs` a new identity
34− // eslint-disable-next-line react-hooks/exhaustive-deps
35− const configs = useMemo(() => inputConfigs, [configsAreEqual]);
29+ const configs = useDeepEqualReference(inputConfigs);
3630
3731 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
3832
3933 // track values
40− const [values, _setValues] = useState(() =>
34+ const [values, _setValues, clearValues] = useSetState(() =>
4135 defaultsFor(configs, config =>
4236 config.initialValue === undefined ? '' : config.initialValue
4337 )
@@ -50,7 +44,7 @@ export default function useForm(inputConfigs = []) {
5044 // until there's a traditional state update.
5145
5246 // track focused states
53− const [focused, setFocused] = useSetState(() =>
47+ const [focused, setFocused, clearFocused] = useSetState(() =>
5448 defaultsFor(configs, config => config.autoFocus && !config.disabled)
5549 );
5650 const focuses = useMemo(
@@ -59,10 +53,14 @@ export default function useForm(inputConfigs = []) {
5953 );
6054
6155 // track whether or not input has been focused
62− const [hasBeenFocused, setHasBeenFocused] = useSetState({});
56+ const [hasBeenFocused, setHasBeenFocused, clearHasBeenFocused] = useSetState(
57+ {}
58+ );
6359
6460 // track whether or not the input has been touched
65− const [hasBeenTouched, setHasBeenTouched] = useSetState({});
61+ const [hasBeenTouched, setHasBeenTouched, clearHasBeenTouched] = useSetState(
62+ {}
63+ );
6664
6765 // build fn that transforms a value by input name
6866 const transform = useCallback(
@@ -72,11 +70,18 @@ export default function useForm(inputConfigs = []) {
7270
7371 // set a value (and transform it)
7472 const setValue = useCallback(
75− (name, value) =>
76− _setValues(values => ({ ...values, [name]: transform(name, value) })),
73+ (name, value) => _setValues({ [name]: transform(name, value) }),
7774 [_setValues, transform]
7875 );
7976
77+ // reset the form
78+ const reset = useCallback(() => {
79+ clearFocused();
80+ clearHasBeenFocused();
81+ clearHasBeenTouched();
82+ clearValues();
83+ }, [clearFocused, clearHasBeenFocused, clearHasBeenTouched, clearValues]);
84+
8085 // build fn that validates a value by input name
8186 const validate = useCallback(
8287 (name, value) =>
@@ -130,6 +135,10 @@ export default function useForm(inputConfigs = []) {
130135
131136 // memo-compute the set of (perhaps changed by validation) data
132137 const datas = useMemo(() => validations.map(v => v.data), [validations]);
138+ const syncPasses = useMemo(
139+ () => validations.map(v => v.pass, [validations]),
140+ [validations]
141+ );
133142
134143 // the input has errored if it
135144 // 1) did not pass validation and has an error text
@@ -192,12 +201,18 @@ export default function useForm(inputConfigs = []) {
192201 configs.map(
193202 ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => {
194203 const value =
195− values[name] || (initialValue === undefined ? '' : initialValue);
204+ datas[i] !== undefined
205+ ? datas[i]
206+ : initialValue !== undefined
207+ ? initialValue
208+ : '';
196209 return {
197210 // Input props
198211 name,
199− data: datas[i],
212+ value: value,
213+ data: passes[i] ? datas[i] : undefined,
200214 pass: passes[i],
215+ syncPass: syncPasses[i],
201216 visiblyPassed: visiblePasses[i],
202217 error: errors[i],
203218 hintError: hintErrors[i],
@@ -205,10 +220,10 @@ export default function useForm(inputConfigs = []) {
205220 autoFocus: autoFocus && !disabled,
206221 disabled,
207222 ...rest,
208− // dom properties below:
223+ // DOM Properties for <input />
209224 bind: {
210225 value,
211− checked: !!values[name],
226+ checked: !!datas[i],
212227 onChange: onChange(name),
213228 onFocus: onFocus(name),
214229 onBlur: onBlur(name),
@@ -220,15 +235,15 @@ export default function useForm(inputConfigs = []) {
220235 [
221236 configs,
222237 datas,
223− visiblePasses,
224238 passes,
239+ syncPasses,
240+ visiblePasses,
225241 errors,
226242 hintErrors,
227243 focuses,
228− values,
229− onBlur,
230244 onChange,
231245 onFocus,
246+ onBlur,
232247 ]
233248 );
234249
@@ -242,5 +257,6 @@ export default function useForm(inputConfigs = []) {
242257 pass,
243258 error,
244259 setValue,
260+ reset,
245261 };
246262}
src/lib/capitalize.jsnew file+1
@@ -0,0 +1 @@
1+export default str => str.charAt(0).toUpperCase() + str.slice(1);
src/lib/constants.js+9
@@ -1,3 +1,8 @@
1+import { isDevelopment } from './flags';
2+
3+const CHECK_BLOCK_EVERY_MS = isDevelopment ? 1000 : 5000;
4+const DEFAULT_GAS_PRICE_GWEI = 20;
5+
16const MIN_GALAXY = 0;
27const MAX_GALAXY = 255;
38const MIN_STAR = 256;
@@ -28,6 +33,8 @@ const GAS_LIMITS = {
2833 //
2934 GIFT_PLANET: 400000, //NOTE low sample size
3035 //
36+ SEND_ETH: 21000,
37+ //
3138 DEFAULT: 550000,
3239};
3340
@@ -63,6 +70,8 @@ const PROFILE_STATES = {
6370};
6471
6572export {
73+ CHECK_BLOCK_EVERY_MS,
74+ DEFAULT_GAS_PRICE_GWEI,
6675 GAS_LIMITS,
6776 GEN_STATES,
6877 BUTTON_STATES,
src/lib/dateFormat.js+5
@@ -6,3 +6,8 @@ export const formatDots = date => {
66
77 return [year, month, day].join('.');
88};
9+
10+export const formatTime = date => date.toLocaleTimeString();
11+
12+export const formatDotsWithTime = date =>
13+ `${formatDots(date)} ${formatTime(date)}`;
src/lib/error.js+5−2
@@ -1,3 +1,5 @@
1+import { fromWei } from 'web3-utils';
2+
13const BRIDGE_ERROR = {
24 MISSING_WEB3: 'no web3 object found',
35 MISSING_WALLET: 'no wallet found',
@@ -7,6 +9,7 @@ const BRIDGE_ERROR = {
79 MISSING_MNEMONIC: 'no authentication mnemonic found',
810 MISSING_URBIT_WALLET: 'no urbit wallet found',
911 MISSING_POINT: 'no point found',
12+ MISSING_POINT_DETAILS: 'no details of point found',
1013 INVALID_ROUTE: 'invalid route',
1114 INVALID_NETWORK_TYPE: 'invalid network type',
1215 INVALID_WALLET_TYPE: 'invalid wallet type',
@@ -15,13 +18,13 @@ const BRIDGE_ERROR = {
1518const renderTxnError = (web3, msg) => {
1619 const txnCost = (web3, msg) => {
1720 const costWei = msg.match(/upfront cost is: ([0-9]+)/);
18− const costEth = web3.utils.fromWei(costWei[1], 'ether');
21+ const costEth = fromWei(costWei[1], 'ether');
1922 return parseFloat(costEth).toFixed(4);
2023 };
2124
2225 const acctBalance = (web3, msg) => {
2326 const balWei = msg.match(/sender's account only has: ([0-9]+)/);
24− const balEth = web3.utils.fromWei(balWei[1], 'ether');
27+ const balEth = fromWei(balWei[1], 'ether');
2528 return parseFloat(balEth).toFixed(4);
2629 };
2730
src/lib/explorer.jsnew file+28
@@ -0,0 +1,28 @@
1+import { useNetwork } from 'store/network';
2+import { NETWORK_TYPES } from './network';
3+
4+function useEtherscanDomain() {
5+ const { networkType } = useNetwork();
6+
7+ switch (networkType) {
8+ case NETWORK_TYPES.ROPSTEN:
9+ return 'https://ropsten.etherscan.io';
10+ case NETWORK_TYPES.OFFLINE:
11+ case NETWORK_TYPES.MAINNET:
12+ case NETWORK_TYPES.LOCAL:
13+ default:
14+ return 'https://etherscan.io';
15+ }
16+}
17+
18+export function useExploreTxUrl(txHash) {
19+ const domain = useEtherscanDomain();
20+
21+ return `${domain}/tx/${txHash}`;
22+}
23+
24+export function useExploreAddressUrl(address) {
25+ const domain = useEtherscanDomain();
26+
27+ return `${domain}/address/${address}`;
28+}
src/lib/invite.js+2−262
@@ -1,70 +1,16 @@
1−import { Just } from 'folktale/maybe';
2−import Tx from 'ethereumjs-tx';
3−
4−import * as azimuth from 'azimuth-js';
5−import * as kg from 'urbit-key-generation/dist';
61import * as wg from './walletgen';
7−import * as tank from './tank';
82
93import JSZip from 'jszip';
104import saveAs from 'file-saver';
115
12−import { sendAndAwaitAll, hexify } from './txn';
13−import { attemptNetworkSeedDerivation } from './keys';
14−import { addHexPrefix, WALLET_TYPES } from './wallet';
15−import { GAS_LIMITS } from './constants';
16−
17−const INVITE_STAGES = {
18− INVITE_LOGIN: 'invite login',
19− INVITE_WALLET: 'invite wallet',
20− INVITE_VERIFY: 'invite verify',
21− INVITE_TRANSACTIONS: 'invite transactions',
22−};
23−
24−const WALLET_STATES = {
25− UNLOCKING: 'Unlocking invite wallet',
26− GENERATING: 'Generating your wallet',
27− RENDERING: 'Creating paper collateral',
28− PAPER_READY: 'Download your wallet',
29− DOWNLOADED: 'Wallet downloaded',
30− TRANSACTIONS: 'Sending transactions',
31−};
32−
33−const TRANSACTION_STATES = {
34− GENERATING: {
35− label: 'Generating Transactions...',
36− progress: 0.0,
37− },
38− SIGNING: {
39− label: 'Signing Transactions...',
40− progress: 0.15,
41− },
42− FUNDING: {
43− label: 'Funding Transactions...',
44− progress: 0.3,
45− },
46− CLAIMING: {
47− label: 'Claiming Invite...',
48− progress: 0.55,
49− },
50− CLEANING: {
51− label: 'Cleaning Up...',
52− progress: 0.95,
53− },
54− DONE: {
55− label: 'Done',
56− progress: 1.0,
57− },
58−};
59−
60−async function generateWallet(point) {
6+export async function generateWallet(point) {
617 const ticket = await wg.makeTicket(point);
628 const wallet = await wg.generateWallet(point, ticket);
639 return wallet;
6410}
6511
6612//TODO should be moved to lib/walletgen
67−async function downloadWallet(paper) {
13+export async function downloadWallet(paper) {
6814 return new Promise((resolve, reject) => {
6915 const zip = new JSZip();
7016 //TODO the categories here aren't explained in bridge at all...
@@ -93,209 +39,3 @@ async function downloadWallet(paper) {
9339 });
9440 });
9541}
96−
97−async function claimPointFromInvite({
98− inviteWallet,
99− wallet,
100− point,
101− web3,
102− contracts,
103− onUpdate,
104− transferEth = false,
105−}) {
106− const askForFunding = (address, minBalance, balance) =>
107− onUpdate({
108− type: 'askFunding',
109− value: { address, minBalance, balance },
110− });
111−
112− const gotFunding = () => onUpdate({ type: 'gotFunding' });
113− const progress = state => onUpdate({ type: 'progress', state });
114−
115− const inviteAddress = inviteWallet.address;
116−
117− //
118− // generate transactions
119− //
120−
121− progress(TRANSACTION_STATES.GENERATING);
122−
123− // transfer to invite wallet, so it can configure
124− //NOTE doing this in the invite wallet instead of the final wallet makes
125− // failure cases "solvable" by just telling the user to fully retry.
126− // in other words, the invite flow is effectively idempotent.
127−
128− //TODO no harm done if we already owned it, but should still get a bool arg
129− // for skipping this, if it isn't too big a burden for callers
130− const transferTmpTx = azimuth.ecliptic.transferPoint(
131− contracts,
132− point,
133− inviteAddress,
134− false
135− );
136− transferTmpTx.gas = GAS_LIMITS.TRANSFER;
137−
138− // configure networking public keys
139−
140− //TODO feels like more of this logic should live in a lib?
141− const seed = await attemptNetworkSeedDerivation(true, {
142− walletType: WALLET_TYPES.TICKET,
143− urbitWallet: Just(wallet),
144− pointCursor: Just(point),
145− pointCache: { [point]: { keyRevisionNumber: 0 } },
146− });
147−
148− const networkSeed = seed.matchWith({
149− Nothing: () => {
150− throw new Error('network seed not derived');
151− },
152− Just: p => p.value,
153− });
154−
155− const networkKeys = kg.deriveNetworkKeys(networkSeed);
156−
157− const keysTx = azimuth.ecliptic.configureKeys(
158− contracts,
159− point,
160− addHexPrefix(networkKeys.crypt.public),
161− addHexPrefix(networkKeys.auth.public),
162− 1,
163− false
164− );
165− keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
166−
167− // configure management proxy
168−
169− const managementTx = azimuth.ecliptic.setManagementProxy(
170− contracts,
171− point,
172− wallet.management.keys.address
173− );
174− managementTx.gas = GAS_LIMITS.SET_PROXY;
175−
176− // set spawn & voting proxies situationally
177−
178− let txs = [transferTmpTx, keysTx, managementTx];
179− if (
180− azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
181− ) {
182− const spawnTx = azimuth.ecliptic.setSpawnProxy(
183− contracts,
184− point,
185− wallet.spawn.keys.address
186− );
187− spawnTx.gas = GAS_LIMITS.SET_PROXY;
188− txs.push(spawnTx);
189−
190− if (
191− azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
192− ) {
193− const votingTx = azimuth.ecliptic.setVotingProxy(
194− contracts,
195− point,
196− wallet.voting.keys.address
197− );
198− votingTx.gas = GAS_LIMITS.SET_PROXY;
199− txs.push(votingTx);
200− }
201− }
202−
203− // transfer configured point to user's new wallet
204−
205− const transferFinalTx = azimuth.ecliptic.transferPoint(
206− contracts,
207− point,
208− wallet.ownership.keys.address,
209− false
210− );
211− transferFinalTx.gas = GAS_LIMITS.TRANSFER;
212− txs.push(transferFinalTx);
213−
214− //
215− // finalizing & signing transactions
216− //
217−
218− progress(TRANSACTION_STATES.SIGNING);
219−
220− let totalCost = 0;
221− let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
222− txs = txs.map(tx => {
223− tx.from = inviteAddress;
224− tx.nonce = inviteNonce++;
225− tx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
226− totalCost = totalCost + tx.gasPrice * tx.gas;
227− return tx;
228− });
229−
230− let txPairs = txs.map(tx => {
231− let stx = new Tx(tx);
232− stx.sign(Buffer.from(inviteWallet.privateKey, 'hex'));
233− return {
234− raw: hexify(stx.serialize()),
235− signed: stx,
236− };
237− });
238−
239− //
240− // ensuring funding for transactions
241− //
242−
243− progress(TRANSACTION_STATES.FUNDING);
244−
245− const usedTank = await tank.ensureFundsFor(
246− web3,
247− point,
248− inviteAddress,
249− totalCost,
250− txPairs.map(p => p.raw),
251− askForFunding,
252− gotFunding
253− );
254−
255− //
256− // sending and awaiting transactions
257− //
258−
259− progress(TRANSACTION_STATES.CLAIMING);
260−
261− await sendAndAwaitAll(web3, txPairs.map(p => p.signed), usedTank);
262−
263− //
264− // move leftover eth
265− //
266−
267− progress(TRANSACTION_STATES.CLEANING);
268−
269− // if non-trivial eth left in invite wallet, transfer to new ownership
270− let balance = await web3.eth.getBalance(inviteAddress);
271− const gasPrice = 20000000000;
272− const gasLimit = 21000;
273− const sendEthCost = gasPrice * gasLimit;
274− if (transferEth && balance > sendEthCost) {
275− const value = balance - sendEthCost;
276− const tx = {
277− to: wallet.ownership.keys.address,
278− value: value,
279− gasPrice: gasPrice,
280− gas: gasLimit,
281− nonce: inviteNonce++,
282− };
283− let stx = new Tx(tx);
284− stx.sign(inviteWallet.privateKey);
285− const rawTx = hexify(stx.serialize());
286− web3.eth.sendSignedTransaction(rawTx).catch(err => {
287− console.log('error sending value tx, who cares', err);
288− });
289− }
290−
291− progress(TRANSACTION_STATES.DONE);
292−}
293−
294−export {
295− generateWallet,
296− downloadWallet,
297− claimPointFromInvite,
298− INVITE_STAGES,
299− WALLET_STATES,
300− TRANSACTION_STATES,
301−};
src/lib/keys.js+122−58
@@ -1,13 +1,19 @@
11import BN from 'bn.js';
22import { Just, Nothing } from 'folktale/maybe';
3−import * as need from './need';
43
54import * as noun from '../nockjs/noun';
65import * as serial from '../nockjs/serial';
76import * as kg from 'urbit-key-generation/dist';
87
9−import { BRIDGE_ERROR } from './error';
10−import { WALLET_TYPES, eqAddr } from './wallet';
8+import { eqAddr, addHexPrefix } from './wallet';
9+
10+// the curve param for the network keys
11+export const NETWORK_KEY_CURVE_PARAMETER = '42';
12+// the current crypto suite version
13+export const CRYPTO_SUITE_VERSION = 1;
14+
15+export const CURVE_ZERO_ADDR =
16+ '0x0000000000000000000000000000000000000000000000000000000000000000';
1117
1218// ctsy joemfb
1319const b64 = buf => {
@@ -50,9 +56,20 @@ const jam = seed => {
5056 return Buffer.from(pad, 'hex').reverse();
5157};
5258
53−const genKey = (networkSeed, point, revision) => {
54− const pair = kg.deriveNetworkKeys(networkSeed);
55− const bnsec = new BN(pair.crypt.private + pair.auth.private + '42', 'hex'); // '42' is curve parameter
59+// simple alias to avoid importing kg into component scope
60+export const deriveNetworkKeys = seed => kg.deriveNetworkKeys(seed);
61+
62+/**
63+ * @param {object} pair
64+ * @param {number} point
65+ * @param {number} revision
66+ * @return {string}
67+ */
68+export const compileNetworkingKey = (pair, point, revision) => {
69+ const bnsec = new BN(
70+ pair.crypt.private + pair.auth.private + NETWORK_KEY_CURVE_PARAMETER,
71+ 'hex'
72+ );
5673
5774 const sed = noun.dwim(
5875 noun.Atom.fromInt(point),
@@ -64,68 +81,115 @@ const genKey = (networkSeed, point, revision) => {
6481 return b64(jam(sed));
6582};
6683
67−// 'next' refers to setting the next set of keys
68−// if false, we use revision - 1
69−const attemptNetworkSeedDerivation = async (next, args) => {
70− const { walletType, urbitWallet, authMnemonic } = args;
71−
72− // NB (jtobin):
73− //
74− // following code is intentionally verbose for sake of clarity
75−
76− const point = need.point(args.pointCursor);
77− const pointDetails = need.fromPointCache(args.pointCache, point);
84+/**
85+ * @param {object} urbitWallet
86+ * @param {number} revision
87+ * @return {Promise<Maybe<string>>}
88+ */
89+export const deriveNetworkSeedFromUrbitWallet = async (
90+ urbitWallet,
91+ revision = 1
92+) => {
93+ return await deriveNetworkSeedFromMnemonic(
94+ urbitWallet.management.seed,
95+ urbitWallet.meta.passphrase,
96+ revision
97+ );
98+};
7899
79− const revision =
80− next === true
81− ? parseInt(pointDetails.keyRevisionNumber)
82− : parseInt(pointDetails.keyRevisionNumber) - 1;
100+/**
101+ * @param {Maybe<any>} wallet
102+ * @param {string} authMnemonic
103+ * @param {object} details
104+ * @param {number} revision
105+ * @return {Promise<Maybe<string>>}
106+ */
107+export const deriveNetworkSeedFromManagementMnemonic = async (
108+ wallet,
109+ authMnemonic,
110+ details,
111+ revision = 1
112+) => {
113+ const isManagementProxy = eqAddr(wallet.address, details.managementProxy);
114+
115+ // the network seed is derivable iff this mnemonic is the management proxy
116+ if (isManagementProxy) {
117+ return await deriveNetworkSeedFromMnemonic(
118+ authMnemonic,
119+ wallet.passphrase,
120+ revision
121+ );
122+ }
83123
84− let managementSeed = '';
124+ return Nothing();
125+};
85126
86− const ticketLike = [WALLET_TYPES.TICKET, WALLET_TYPES.SHARDS];
127+/**
128+ * @param {string} mnemonic
129+ * @param {string} passphrase
130+ * @param {number} revision
131+ * @return {Promise<Maybe<string>>}
132+ */
133+const deriveNetworkSeedFromMnemonic = async (
134+ mnemonic,
135+ passphrase,
136+ revision
137+) => {
138+ //NOTE revision is the point's on-chain revision number. since common uhdw
139+ // usage derives the first key at revision/index 0, we need to decrement
140+ // the on-chain revision number by one to get the number to derive with.
141+ return Just(await kg.deriveNetworkSeed(mnemonic, passphrase, revision - 1));
142+};
87143
88− if (ticketLike.includes(walletType)) {
89− const uwal = urbitWallet.matchWith({
90− Just: uw => uw.value,
91− Nothing: _ => {
92− throw new Error(BRIDGE_ERROR.MISSING_URBIT_WALLET);
93− },
94− });
144+/**
145+ * @return {Promise<Maybe<string>>}
146+ */
147+export const attemptNetworkSeedDerivation = async ({
148+ urbitWallet,
149+ wallet,
150+ authMnemonic,
151+ details,
152+ revision,
153+}) => {
154+ if (Just.hasInstance(urbitWallet)) {
155+ return await deriveNetworkSeedFromUrbitWallet(urbitWallet.value, revision);
156+ }
95157
96− managementSeed = uwal.management.seed;
97− } else if (walletType === WALLET_TYPES.MNEMONIC) {
98− const walProxy = need.addressFromWallet(args.wallet);
158+ if (Just.hasInstance(wallet) && Just.hasInstance(authMnemonic)) {
159+ return await deriveNetworkSeedFromManagementMnemonic(
160+ wallet.value,
161+ authMnemonic.value,
162+ details,
163+ revision
164+ );
165+ }
99166
100− const mnemonic = authMnemonic.matchWith({
101− Just: mnem => mnem.value,
102− Nothing: _ => {
103− throw new Error(BRIDGE_ERROR.MISSING_MNEMONIC);
104− },
105− });
167+ return Nothing();
168+};
106169
107− const chainProxy = pointDetails.managementProxy;
170+/**
171+ *
172+ * @param {object} pair
173+ * @param {object} details
174+ * @return {boolean}
175+ */
176+export const keysMatchChain = (pair, details) => {
177+ const { crypt, auth } = pair;
178+ const { encryptionKey, authenticationKey } = details;
108179
109− // the network seed is only derivable from mnemonic if the derived
110− // management seed equals the record we have on chain
111− const networkSeedDerivable = eqAddr(walProxy, chainProxy);
180+ return (
181+ encryptionKey === addHexPrefix(crypt.public) &&
182+ authenticationKey === addHexPrefix(auth.public)
183+ );
184+};
112185
113− if (networkSeedDerivable === true) {
114− managementSeed = mnemonic;
115− }
186+export const segmentNetworkKey = hex => {
187+ if (hex === CURVE_ZERO_ADDR) {
188+ return null;
116189 }
117190
118− if (managementSeed !== '') {
119− const seed = await kg.deriveNetworkSeed(managementSeed, '', revision);
191+ const sl = i => hex.slice(i, i + 4);
192+ const rowFrom = i => `${sl(i)}.${sl(i + 4)}.${sl(i + 8)}.${sl(i + 12)}`;
120193
121− if (seed === '') {
122− return Nothing();
123− }
124−
125− return Just(seed);
126− }
127−
128− return Nothing();
194+ return [rowFrom(2), rowFrom(18), rowFrom(34), rowFrom(50)];
129195};
130−
131−export { genKey, attemptNetworkSeedDerivation };
src/lib/need.js+8−1
@@ -14,8 +14,15 @@ const needBuilder = fn => obj => {
1414 });
1515};
1616
17+// simpler function for inline need.value(thing, () => {})
18+export const value = (obj, fn) => needBuilder(fn)(obj);
19+
20+export const details = needBuilder(() => {
21+ throw new Error(BRIDGE_ERROR.MISSING_POINT_DETAILS);
22+});
23+
1724export const web3 = needBuilder(() => {
18− throw new Error(BRIDGE_ERROR.MISSING_WEB3.message);
25+ throw new Error(BRIDGE_ERROR.MISSING_WEB3);
1926});
2027
2128export const contracts = needBuilder(() => {
src/lib/proxy.js+33−13
@@ -1,16 +1,36 @@
1−const PROXY_TYPE = {
2− MANAGEMENT_PROXY: Symbol('MANAGEMENT_PROXY'),
3− SPAWN_PROXY: Symbol('SPAWN_PROXY'),
4− TRANSFER_PROXY: Symbol('TRANSFER_PROXY'),
1+export const PROXY_TYPE = {
2+ MANAGEMENT: 'MANAGEMENT',
3+ SPAWN: 'SPAWN',
4+ TRANSFER: 'TRANSFER',
5+ VOTING: 'VOTING',
56};
67
7−const renderProxyType = proxyType =>
8− proxyType === PROXY_TYPE.MANAGEMENT_PROXY
9− ? 'management'
10− : proxyType === PROXY_TYPE.SPAWN_PROXY
11− ? 'spawn'
12− : proxyType === PROXY_TYPE.TRANSFER_PROXY
13− ? 'transfer'
14− : 'proxy';
8+export const proxyTypeToHuman = proxyType => {
9+ switch (proxyType) {
10+ case PROXY_TYPE.MANAGEMENT:
11+ return 'management';
12+ case PROXY_TYPE.SPAWN:
13+ return 'spawn';
14+ case PROXY_TYPE.TRANSFER:
15+ return 'transfer';
16+ case PROXY_TYPE.VOTING:
17+ return 'voting';
18+ default:
19+ throw new Error(`Unknown proxyType: ${proxyType}`);
20+ }
21+};
1522
16−export { PROXY_TYPE, renderProxyType };
23+export const proxyTypeToHumanDescription = proxyType => {
24+ switch (proxyType) {
25+ case PROXY_TYPE.MANAGEMENT:
26+ return 'The management proxy is allowed to manage networking keys.';
27+ case PROXY_TYPE.SPAWN:
28+ return 'The spawn proxy is allowed to spawn points.';
29+ case PROXY_TYPE.TRANSFER:
30+ return 'The transfer proxy is allowed to transfer this point.';
31+ case PROXY_TYPE.VOTING:
32+ return "The voting proxy is allowed to vote on this point's behalf.";
33+ default:
34+ throw new Error(`Unknown proxyType: ${proxyType}`);
35+ }
36+};
src/lib/reticket.jsnew file+215
@@ -0,0 +1,215 @@
1+import Tx from 'ethereumjs-tx';
2+import * as azimuth from 'azimuth-js';
3+import * as tank from './tank';
4+
5+import { hexify } from './txn';
6+import {
7+ deriveNetworkSeedFromUrbitWallet,
8+ deriveNetworkKeys,
9+ CRYPTO_SUITE_VERSION,
10+} from './keys';
11+import { addHexPrefix } from './wallet';
12+import { sendAndAwaitAll } from './txn';
13+import { GAS_LIMITS, DEFAULT_GAS_PRICE_GWEI } from './constants';
14+import { toWei } from 'web3-utils';
15+
16+// the initial network key revision is always 1
17+const INITIAL_NETWORK_KEY_REVISION = 1;
18+
19+export const TRANSACTION_PROGRESS = {
20+ GENERATING: 0.01,
21+ SIGNING: 0.15,
22+ FUNDING: 0.3,
23+ TRANSFERRING: 0.55,
24+ CLEANING: 0.95,
25+ DONE: 1.0,
26+};
27+
28+export async function reticketPointBetweenWallets({
29+ fromWallet,
30+ toWallet,
31+ point,
32+ web3,
33+ contracts,
34+ onUpdate,
35+ transferEth = false,
36+ nextRevision = INITIAL_NETWORK_KEY_REVISION,
37+}) {
38+ const askForFunding = (address, minBalance, balance) =>
39+ onUpdate({
40+ type: 'askFunding',
41+ value: { address, minBalance, balance },
42+ });
43+
44+ const gotFunding = () => onUpdate({ type: 'gotFunding' });
45+ const progress = state => onUpdate({ type: 'progress', state });
46+
47+ //
48+ // generate transactions
49+ //
50+
51+ progress(TRANSACTION_PROGRESS.GENERATING);
52+
53+ // transfer to invite wallet, so it can configure
54+ //NOTE doing this in the invite wallet instead of the final wallet makes
55+ // failure cases "solvable" by just telling the user to fully retry.
56+ // in other words, the invite flow is effectively idempotent.
57+
58+ //TODO no harm done if we already owned it, but should still get a bool arg
59+ // for skipping this, if it isn't too big a burden for callers
60+ const transferTmpTx = azimuth.ecliptic.transferPoint(
61+ contracts,
62+ point,
63+ fromWallet.address,
64+ false
65+ );
66+ transferTmpTx.gas = GAS_LIMITS.TRANSFER;
67+
68+ // configure networking public keys
69+ const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
70+
71+ const networkSeed = seed.matchWith({
72+ Nothing: () => {
73+ throw new Error('network seed not derived');
74+ },
75+ Just: p => p.value,
76+ });
77+
78+ const networkKeys = deriveNetworkKeys(networkSeed);
79+
80+ const keysTx = azimuth.ecliptic.configureKeys(
81+ contracts,
82+ point,
83+ addHexPrefix(networkKeys.crypt.public),
84+ addHexPrefix(networkKeys.auth.public),
85+ CRYPTO_SUITE_VERSION,
86+ /* discontinuous */ false
87+ );
88+ keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
89+
90+ // configure management proxy
91+
92+ const managementTx = azimuth.ecliptic.setManagementProxy(
93+ contracts,
94+ point,
95+ toWallet.management.keys.address
96+ );
97+ managementTx.gas = GAS_LIMITS.SET_PROXY;
98+
99+ // set spawn & voting proxies situationally
100+
101+ let txs = [transferTmpTx, keysTx, managementTx];
102+ if (
103+ azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
104+ ) {
105+ const spawnTx = azimuth.ecliptic.setSpawnProxy(
106+ contracts,
107+ point,
108+ toWallet.spawn.keys.address
109+ );
110+ spawnTx.gas = GAS_LIMITS.SET_PROXY;
111+ txs.push(spawnTx);
112+
113+ if (
114+ azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
115+ ) {
116+ const votingTx = azimuth.ecliptic.setVotingProxy(
117+ contracts,
118+ point,
119+ toWallet.voting.keys.address
120+ );
121+ votingTx.gas = GAS_LIMITS.SET_PROXY;
122+ txs.push(votingTx);
123+ }
124+ }
125+
126+ // transfer configured point to user's new wallet
127+
128+ const transferFinalTx = azimuth.ecliptic.transferPoint(
129+ contracts,
130+ point,
131+ toWallet.ownership.keys.address,
132+ false
133+ );
134+ transferFinalTx.gas = GAS_LIMITS.TRANSFER;
135+ txs.push(transferFinalTx);
136+
137+ //
138+ // finalizing & signing transactions
139+ //
140+
141+ progress(TRANSACTION_PROGRESS.SIGNING);
142+
143+ let totalCost = 0;
144+ let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
145+ txs = txs.map(tx => {
146+ tx.from = fromWallet.address;
147+ tx.nonce = inviteNonce++;
148+ tx.gasPrice = parseInt(toWei(DEFAULT_GAS_PRICE_GWEI.toFixed(), 'gwei'), 10);
149+ totalCost = totalCost + tx.gasPrice * tx.gas;
150+ return tx;
151+ });
152+
153+ let txPairs = txs.map(tx => {
154+ let stx = new Tx(tx);
155+ stx.sign(Buffer.from(fromWallet.privateKey, 'hex'));
156+ return {
157+ raw: hexify(stx.serialize()),
158+ signed: stx,
159+ };
160+ });
161+
162+ //
163+ // ensuring funding for transactions
164+ //
165+
166+ progress(TRANSACTION_PROGRESS.FUNDING);
167+
168+ const usedTank = await tank.ensureFundsFor(
169+ web3,
170+ point,
171+ fromWallet.address,
172+ totalCost,
173+ txPairs.map(p => p.raw),
174+ askForFunding,
175+ gotFunding
176+ );
177+
178+ //
179+ // sending and awaiting transactions
180+ //
181+
182+ progress(TRANSACTION_PROGRESS.TRANSFERRING);
183+
184+ await sendAndAwaitAll(web3, txPairs.map(p => p.signed), usedTank);
185+
186+ //
187+ // move leftover eth
188+ //
189+
190+ progress(TRANSACTION_PROGRESS.CLEANING);
191+
192+ // if non-trivial eth left in invite wallet, transfer to new ownership
193+ let balance = await web3.eth.getBalance(fromWallet.address);
194+ const gasPrice = parseInt(toWei(DEFAULT_GAS_PRICE_GWEI.toFixed(), 'gwei'));
195+ const gasLimit = GAS_LIMITS.SEND_ETH;
196+ const sendEthCost = gasPrice * gasLimit;
197+ if (transferEth && balance > sendEthCost) {
198+ const value = balance - sendEthCost;
199+ const tx = {
200+ to: toWallet.ownership.keys.address,
201+ value: value,
202+ gasPrice: gasPrice,
203+ gas: gasLimit,
204+ nonce: inviteNonce++,
205+ };
206+ let stx = new Tx(tx);
207+ stx.sign(fromWallet.privateKey);
208+ const rawTx = hexify(stx.serialize());
209+ web3.eth.sendSignedTransaction(rawTx).catch(err => {
210+ console.log('error sending value tx, who cares', err);
211+ });
212+ }
213+
214+ progress(TRANSACTION_PROGRESS.DONE);
215+}
src/lib/routeNames.js+1−1
@@ -26,6 +26,6 @@ export const ROUTE_NAMES = {
2626 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
2727 SET_KEYS: Symbol('SET_KEYS'),
2828 TRANSFER: Symbol('TRANSFER'),
29− SENT_TRANSACTION: Symbol('SENT_TRANSACTION'),
3029 GEN_KEYFILE: Symbol('GEN_KEYFILE'),
30+ PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
3131};
src/lib/router.js+17−40
@@ -1,55 +1,32 @@
1−import Landing from '../views/Landing';
2−import Disclaimer from '../views/Disclaimer';
3−import Login from '../views/Login.js';
4−import InvitesManage from '../views/InvitesManage.js';
5−import Admin from '../views/Admin';
6−import Redownload from '../views/Redownload';
7−import Reticket from '../views/Reticket';
8−import Permissions from '../views/Permissions';
9−import AcceptTransfer from '../views/AcceptTransfer';
10−import CancelTransfer from '../views/CancelTransfer';
11−import CreateGalaxy from '../views/CreateGalaxy';
12−import GenKeyfile from '../views/GenKeyfile';
13−import IssueChild from '../views/IssueChild';
14−import ViewPoint from '../views/ViewPoint';
15−import SentTransaction from '../views/SentTransaction';
16−import SetKeys from '../views/SetKeys';
17−import {
18− SetManagementProxy,
19− SetSpawnProxy,
20− SetTransferProxy,
21−} from '../views/SetProxy';
22−import Point from '../views/Point';
23−import Points from '../views/Points';
24−import Transfer from '../views/Transfer';
1+import Landing from 'views/Landing';
2+import Disclaimer from 'views/Disclaimer';
3+import Login from 'views/Login.js';
4+import Admin from 'views/Admin';
5+import CreateGalaxy from 'views/CreateGalaxy';
6+import IssueChild from 'views/IssueChild';
7+import ViewPoint from 'views/ViewPoint';
8+
9+import Point from 'views/Point';
10+import Points from 'views/Points';
2511import Invite from 'views/Invite';
2612import Activate from 'views/Activate';
2713
14+import PartySetPoolSize from 'views/Party/PartySetPoolSize';
15+
2816import { ROUTE_NAMES } from './routeNames';
2917
3018export const ROUTES = {
3119 [ROUTE_NAMES.LANDING]: Landing,
3220 [ROUTE_NAMES.DISCLAIMER]: Disclaimer,
3321 [ROUTE_NAMES.ACTIVATE]: Activate,
34− [ROUTE_NAMES.INVITE]: Invite,
35− [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
36− [ROUTE_NAMES.LOGIN]: Login,
37− [ROUTE_NAMES.ADMIN]: Admin,
38− [ROUTE_NAMES.REDOWNLOAD]: Redownload,
39− [ROUTE_NAMES.RETICKET]: Reticket,
40− [ROUTE_NAMES.PERMISSIONS]: Permissions,
41− [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
42− [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
4322 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
23+ [ROUTE_NAMES.LOGIN]: Login,
4424 [ROUTE_NAMES.POINTS]: Points,
4525 [ROUTE_NAMES.POINT]: Point,
46− [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
47− [ROUTE_NAMES.SET_SPAWN_PROXY]: SetSpawnProxy,
48− [ROUTE_NAMES.SET_TRANSFER_PROXY]: SetTransferProxy,
4926 [ROUTE_NAMES.CREATE_GALAXY]: CreateGalaxy,
5027 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
51− [ROUTE_NAMES.SET_KEYS]: SetKeys,
52− [ROUTE_NAMES.TRANSFER]: Transfer,
53− [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
54− [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
28+ [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
29+ [ROUTE_NAMES.INVITE]: Invite,
30+ [ROUTE_NAMES.ADMIN]: Admin,
31+ // TODO: nest this route under the Party.js router
5532};
src/lib/tank.js+8−14
@@ -1,6 +1,6 @@
11// /lib/tank: functions for funding transactions
22
3−import * as retry from 'async-retry';
3+import retry from 'async-retry';
44import { RETRY_OPTIONS, waitForTransactionConfirm } from './txn';
55
66//NOTE if accessing this in a localhost configuration fails with "CORS request
@@ -95,24 +95,18 @@ const ensureFundsFor = async (
9595
9696// returns a promise that resolves when address has at least minBalance
9797function waitForBalance(web3, address, minBalance, askForFunding, gotFunding) {
98− let oldBalance = null;
9998 return retry(async (bail, n) => {
10099 const balance = await web3.eth.getBalance(address);
101100 if (balance >= minBalance) {
102− // if we ever asked for funding, retract that request now
103− if (gotFunding && oldBalance !== null) {
104− gotFunding();
105− }
101+ gotFunding && gotFunding();
106102 return;
107103 } else {
108− if (balance !== oldBalance) {
109− // TODO: minBalance is a `number` type
110− // but we want to display ETH, which will never accept a number
111− // but instead wants string or BN/BigNumber.
112− // this will be heavily refactored to correctly do BN math, but until
113− // then we'll manually convert this number to an integer string
114− askForFunding(address, minBalance.toFixed(), balance);
115− }
104+ // TODO: minBalance is a `number` type
105+ // but we want to display ETH, which will never accept a number
106+ // but instead wants string or BN/BigNumber.
107+ // this will be heavily refactored to correctly do BN math, but until
108+ // then we'll manually convert this number to an integer string
109+ askForFunding(address, minBalance.toFixed(), balance);
116110 throw new Error('retrying');
117111 }
118112 }, RETRY_OPTIONS);
src/lib/transformers.js+9−7
@@ -1,6 +1,14 @@
11// import { fill } from './lib'
22
3−const prependSig = s => (s.charAt(0) !== '~' ? `~${s}` : s);
3+export const prependSig = s => (s.charAt(0) !== '~' ? `~${s}` : s);
4+
5+export const convertToNumber = s => {
6+ try {
7+ return parseInt(s, 10);
8+ } catch {
9+ return 0;
10+ }
11+};
412
513// const hideAllButLast = s => {
614// const ll = s[s.length -1];
@@ -9,9 +17,3 @@ const prependSig = s => (s.charAt(0) !== '~' ? `~${s}` : s);
917// };
1018//
1119// const hideAll = s => fill(s.length, '•');
12−
13−export {
14− prependSig,
15− // hideAllButLast,
16− // hideAll,
17−};
src/lib/txn.js+20−56
@@ -1,16 +1,13 @@
11import * as ob from 'urbit-ob';
2−import { Just } from 'folktale/maybe';
32import Tx from 'ethereumjs-tx';
43import { toWei, fromWei, toHex } from 'web3-utils';
5−import * as retry from 'async-retry';
4+import retry from 'async-retry';
65
76import { NETWORK_TYPES } from './network';
87import { ledgerSignTransaction } from './ledger';
98import { trezorSignTransaction } from './trezor';
109import { WALLET_TYPES, addHexPrefix } from './wallet';
11−
12−const CHECK_BLOCK_EVERY_MS =
13− process.env.NODE_ENV === 'development' ? 1000 : 5000;
10+import { CHECK_BLOCK_EVERY_MS } from './constants';
1411
1512const RETRY_OPTIONS = {
1613 retries: 99999,
@@ -19,51 +16,18 @@ const RETRY_OPTIONS = {
1916 randomize: false,
2017};
2118
22−const TXN_PURPOSE = {
23− SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
24− SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
25− SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
26− CREATE_GALAXY: Symbol('CREATE_GALAXY'),
27− ISSUE_CHILD: Symbol('ISSUE_CHILD'),
28− SET_KEYS: Symbol('SET_KEYS'),
29− TRANSFER: Symbol('TRANSFER'),
30− CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
31−};
32−
33−const renderTxnPurpose = purpose =>
34− purpose === TXN_PURPOSE.SET_MANAGEMENT_PROXY
35− ? 'set this management proxy'
36− : purpose === TXN_PURPOSE.SET_SPAWN_PROXY
37− ? 'set this spawn proxy'
38− : purpose === TXN_PURPOSE.SET_TRANSFER_PROXY
39− ? 'set this transfer proxy'
40− : purpose === TXN_PURPOSE.CREATE_GALAXY
41− ? 'create this galaxy'
42− : purpose === TXN_PURPOSE.ISSUE_CHILD
43− ? 'issue this point'
44− : purpose === TXN_PURPOSE.SET_KEYS
45− ? 'set these network keys'
46− : purpose === TXN_PURPOSE.TRANSFER
47− ? 'transfer this point'
48− : purpose === TXN_PURPOSE.CANCEL_TRANSFER
49− ? 'cancel this transfer'
50− : 'perform this transaction';
51−
52−const signTransaction = async config => {
53− let {
54− wallet,
55− walletType,
56− walletHdPath,
57− networkType,
58− txn,
59− setStx,
60− nonce,
61− chainId,
62− gasPrice,
63− gasLimit,
64− } = config;
65−
66− //TODO require these in txn object
19+const signTransaction = async ({
20+ wallet,
21+ walletType,
22+ walletHdPath,
23+ networkType,
24+ txn,
25+ nonce, // number
26+ chainId, // number
27+ gasPrice, // string, in gwei
28+ gasLimit, // string | number
29+}) => {
30+ // TODO: require these in txn object
6731 nonce = toHex(nonce);
6832 chainId = toHex(chainId);
6933 gasPrice = toHex(toWei(gasPrice, 'gwei'));
@@ -129,7 +93,6 @@ const signTransaction = async config => {
12993 stx.sign(wallet.privateKey);
13094 }
13195
132− setStx(Just(stx));
13396 return stx;
13497};
13598
@@ -186,6 +149,9 @@ const sendAndAwaitAll = (web3, stxs, doubtNonceError) => {
186149 );
187150};
188151
152+const sendTransactionsAndAwaitConfirm = async (web3, signedTxs, usedTank) =>
153+ Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
154+
189155const hexify = val => addHexPrefix(val.toString('hex'));
190156
191157const renderSignedTx = stx => ({
@@ -200,6 +166,7 @@ const getTxnInfo = async (web3, addr) => {
200166 let nonce = await web3.eth.getTransactionCount(addr);
201167 let chainId = await web3.eth.net.getId();
202168 let gasPrice = await web3.eth.getGasPrice();
169+
203170 return {
204171 nonce: nonce,
205172 chainId: chainId,
@@ -207,6 +174,7 @@ const getTxnInfo = async (web3, addr) => {
207174 };
208175};
209176
177+// TODO(shrugs): deprecate, unifiy with other callsites
210178const canDecodePatp = p => {
211179 try {
212180 ob.patp2dec(p);
@@ -221,14 +189,10 @@ export {
221189 signTransaction,
222190 sendSignedTransaction,
223191 waitForTransactionConfirm,
192+ sendTransactionsAndAwaitConfirm,
224193 sendAndAwaitAll,
225− TXN_PURPOSE,
226194 getTxnInfo,
227− renderTxnPurpose,
228195 hexify,
229196 renderSignedTx,
230− toHex,
231− toWei,
232− fromWei,
233197 canDecodePatp,
234198};
src/lib/useCurrentPermissions.jsnew file+17
@@ -0,0 +1,17 @@
1+import usePermissionsForPoint from './usePermissionsForPoint';
2+import { useWallet } from 'store/wallet';
3+import { usePointCursor } from 'store/pointCursor';
4+
5+export default function useCurrentPermissions() {
6+ const { wallet } = useWallet();
7+ const { pointCursor } = usePointCursor();
8+
9+ const _address = wallet.matchWith({
10+ Nothing: () => '',
11+ Just: p => p.value.address,
12+ });
13+
14+ const _point = pointCursor.getOrElse(null);
15+
16+ return usePermissionsForPoint(_address, _point);
17+}
src/lib/useDeepEqualReference.jsnew file+22
@@ -0,0 +1,22 @@
1+import { useRef } from 'react';
2+import { isEqual } from 'lodash';
3+
4+import usePreviousValue from './usePreviousValue';
5+
6+// changes the referential identity of value on deep equality change
7+export default function useDeepEqualReference(value) {
8+ const ref = useRef(value);
9+
10+ // track the old value of the config set
11+ const prevValue = usePreviousValue(value);
12+
13+ // then compare equality
14+ const areEqual = isEqual(value, prevValue);
15+
16+ // if equality changes, give value a new identity
17+ if (!areEqual) {
18+ ref.current = value;
19+ }
20+
21+ return ref.current;
22+}
src/lib/useEthereumTransaction.jsnew file+270
@@ -0,0 +1,270 @@
1+import { useCallback, useEffect, useState } from 'react';
2+import { Just } from 'folktale/maybe';
3+import { fromWei, toWei } from 'web3-utils';
4+
5+import { useNetwork } from 'store/network';
6+import { useWallet } from 'store/wallet';
7+import { usePointCursor } from 'store/pointCursor';
8+
9+import { GAS_LIMITS } from './constants';
10+import {
11+ signTransaction,
12+ sendSignedTransaction,
13+ waitForTransactionConfirm,
14+ hexify,
15+} from './txn';
16+import * as need from './need';
17+import useLifecycle from './useLifecycle';
18+import { ensureFundsFor } from './tank';
19+import useDeepEqualReference from './useDeepEqualReference';
20+
21+const STATE = {
22+ NONE: 'NONE',
23+ SIGNED: 'SIGNED',
24+ BROADCASTED: 'BROADCASTED',
25+ CONFIRMED: 'CONFIRMED',
26+ COMPLETED: 'COMPLETED',
27+};
28+
29+/**
30+ * manage the state around sending and confirming an ethereum transaction
31+ * @param {Transaction Function()} transactionBuilder
32+ * @param {Promise<any> Function()} refetch async function called after completion
33+ * @param {number} initialGasLimit gas limit
34+ * @param {number} initialGasPrice gas price in gwei
35+ */
36+export default function useEthereumTransaction(
37+ transactionBuilder,
38+ refetch,
39+ initialGasLimit = GAS_LIMITS.DEFAULT,
40+ initialGasPrice = 20
41+) {
42+ const { wallet, walletType, walletHdPath } = useWallet();
43+ const { web3, networkType } = useNetwork();
44+ const { pointCursor } = usePointCursor();
45+
46+ const _web3 = need.web3(web3);
47+ const _wallet = need.wallet(wallet);
48+
49+ const [error, _setError] = useState();
50+ const setError = useCallback(
51+ error => {
52+ _setError(error);
53+ if (error) {
54+ console.error(error);
55+ }
56+ },
57+ [_setError]
58+ );
59+ const [state, setState] = useState(STATE.NONE);
60+ const [chainId, setChainId] = useState();
61+ const [nonce, setNonce] = useState();
62+ const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
63+ const [suggestedGasPrice, setSuggestedGasPrice] = useState(gasPrice); // gwei
64+ const [gasLimit] = useState(initialGasLimit);
65+ const [unsignedTransaction, setUnsignedTransaction] = useState();
66+ const [signedTransaction, setSignedTransaction] = useState();
67+ const [txHash, setTxHash] = useState();
68+ const [needFunds, setNeedFunds] = useState();
69+
70+ const initializing = nonce === undefined || chainId === undefined;
71+ const constructed = !!unsignedTransaction;
72+ const isDefaultState = state === STATE.NONE;
73+ const signed = state === STATE.SIGNED;
74+ const broadcasted = state === STATE.BROADCASTED;
75+ const confirmed = state === STATE.CONFIRMED;
76+ const completed = state === STATE.COMPLETED;
77+
78+ // lock inputs once we're out of the default state
79+ const inputsLocked = !isDefaultState;
80+
81+ // we can sign when:
82+ const canSign = !initializing && constructed && isDefaultState;
83+
84+ const construct = useCallback(
85+ (...args) => setUnsignedTransaction(transactionBuilder(...args)),
86+ [transactionBuilder]
87+ );
88+
89+ const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
90+ setUnsignedTransaction,
91+ ]);
92+
93+ const generateAndSign = useCallback(async () => {
94+ try {
95+ setError(undefined);
96+
97+ const txn = await signTransaction({
98+ wallet: _wallet,
99+ walletType,
100+ walletHdPath,
101+ networkType,
102+ txn: unsignedTransaction,
103+ nonce,
104+ chainId,
105+ gasPrice: gasPrice.toFixed(0),
106+ gasLimit,
107+ });
108+
109+ setSignedTransaction(txn);
110+ setState(STATE.SIGNED);
111+ } catch (error) {
112+ setError(error);
113+ }
114+ }, [
115+ _wallet,
116+ chainId,
117+ gasLimit,
118+ gasPrice,
119+ networkType,
120+ nonce,
121+ setError,
122+ unsignedTransaction,
123+ walletHdPath,
124+ walletType,
125+ ]);
126+
127+ const broadcast = useCallback(async () => {
128+ try {
129+ setError(undefined);
130+
131+ const rawTx = hexify(signedTransaction.serialize());
132+ const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
133+
134+ let usedTank = false;
135+ // if this ethereum transaction is being executed by a specific point
136+ // see if we can use the tank
137+ if (Just.hasInstance(pointCursor)) {
138+ usedTank = await ensureFundsFor(
139+ _web3,
140+ pointCursor.value,
141+ _wallet.address,
142+ cost,
143+ [rawTx],
144+ (address, minBalance, balance) =>
145+ setNeedFunds({ address, minBalance, balance }),
146+ () => setNeedFunds(undefined)
147+ );
148+ }
149+
150+ const txHash = await sendSignedTransaction(
151+ _web3,
152+ signedTransaction,
153+ /* doubtNonceError */ usedTank
154+ );
155+
156+ setState(STATE.BROADCASTED);
157+ setTxHash(txHash);
158+
159+ await waitForTransactionConfirm(_web3, txHash);
160+
161+ setState(STATE.CONFIRMED);
162+ } catch (error) {
163+ setError(error);
164+ }
165+ }, [
166+ _wallet.address,
167+ _web3,
168+ gasLimit,
169+ gasPrice,
170+ pointCursor,
171+ setError,
172+ signedTransaction,
173+ ]);
174+
175+ const reset = useCallback(() => {
176+ setUnsignedTransaction(undefined);
177+ setTxHash(undefined);
178+ setSignedTransaction(undefined);
179+ setGasPrice(suggestedGasPrice);
180+ setState(STATE.NONE);
181+ setError(undefined);
182+ setNeedFunds(undefined);
183+ }, [suggestedGasPrice, setError]);
184+
185+ const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
186+ setGasPrice,
187+ suggestedGasPrice,
188+ ]);
189+
190+ useLifecycle(() => {
191+ (async () => {
192+ try {
193+ setError(undefined);
194+ const [nonce, chainId, estimatedGasPrice] = await Promise.all([
195+ _web3.eth.getTransactionCount(_wallet.address),
196+ _web3.eth.net.getId(),
197+ _web3.eth.getGasPrice(),
198+ ]);
199+
200+ setNonce(nonce);
201+ setChainId(chainId);
202+ const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
203+ setSuggestedGasPrice(gasPrice);
204+ setGasPrice(gasPrice);
205+ } catch (error) {
206+ setError(error);
207+ }
208+ })();
209+ });
210+
211+ useEffect(() => {
212+ let cancelled = false;
213+
214+ if (confirmed) {
215+ (async () => {
216+ if (refetch) {
217+ try {
218+ await refetch();
219+ } catch (error) {
220+ // log the original
221+ console.error(error);
222+ // track a user-friendly error
223+ setError(
224+ new Error(
225+ 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
226+ )
227+ );
228+ }
229+ }
230+
231+ if (cancelled) {
232+ return;
233+ }
234+
235+ setState(STATE.COMPLETED);
236+ })();
237+ }
238+
239+ return () => (cancelled = true);
240+ }, [confirmed, refetch, completed, setError]);
241+
242+ const values = useDeepEqualReference({
243+ isDefaultState,
244+ initializing,
245+ construct,
246+ unconstruct,
247+ constructed,
248+ canSign,
249+ generateAndSign,
250+ signed,
251+ broadcast,
252+ broadcasted,
253+ confirmed,
254+ completed,
255+ reset,
256+ error,
257+ inputsLocked,
258+ //
259+ txHash,
260+ signedTransaction,
261+ gasPrice,
262+ setGasPrice,
263+ resetGasPrice,
264+ nonce,
265+ chainId,
266+ needFunds,
267+ });
268+
269+ return { ...values, bind: values };
270+}
src/lib/useInputs.js+67−15
@@ -1,4 +1,4 @@
1−import React, { useCallback, useState, useMemo } from 'react';
1+import React, { useCallback, useMemo } from 'react';
22import { AccessoryIcon, useForm } from 'indigo-react';
33import { identity } from 'lodash';
44import * as bip39 from 'bip39';
@@ -14,8 +14,11 @@ import {
1414 validateHexString,
1515 validateOneOf,
1616 validateMaximumPatpByteLength,
17+ validateEthereumAddress,
18+ validateNotNullAddress,
19+ validateGreaterThan,
1720} from 'lib/validators';
18−import { prependSig } from 'lib/transformers';
21+import { prependSig, convertToNumber } from 'lib/transformers';
1922
2023import InputSigil from 'components/InputSigil';
2124
@@ -68,7 +71,7 @@ export function useHexInput({ length, ...rest }) {
6871 {
6972 type: 'text', // or password
7073 autoComplete: 'off',
71− placeholder: EXAMPLE_PRIVATE_KEY,
74+ placeholder: `e.g. ${EXAMPLE_PRIVATE_KEY}`,
7275 validators: useMemo(
7376 () => [
7477 validateHexString,
@@ -142,31 +145,34 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
142145 );
143146}
144147
145−const kPointValidators = [
146− validatePoint,
147− validateMaximumPatpByteLength(4),
148− validateNotEmpty,
149−];
150148const kPointTransformers = [prependSig];
151−export function usePointInput(rest) {
152− const [lastValidPoint, setLastValidPoint] = useState('');
149+export function usePointInput({ size = 4, validators = [], ...rest }) {
150+ const _validators = useMemo(
151+ () => [
152+ ...validators,
153+ validatePoint,
154+ validateMaximumPatpByteLength(size),
155+ validateNotEmpty,
156+ ],
157+ [size, validators]
158+ );
159+
153160 return useFirstOf(
154161 useForm([
155162 {
156163 type: 'text',
157164 label: 'Point',
158165 placeholder: 'e.g. ~zod',
159− validators: kPointValidators,
166+ validators: _validators,
160167 transformers: kPointTransformers,
161168 mono: true,
162− onValue: setLastValidPoint,
163169 ...rest,
164170 },
165171 ]),
166− ({ error, pass, focused }) => ({
167− accessory: lastValidPoint ? (
172+ ({ error, pass, focused, value }) => ({
173+ accessory: value ? (
168174 <InputSigil
169− patp={lastValidPoint}
175+ patp={value}
170176 size={44}
171177 margin={8}
172178 pass={pass}
@@ -178,6 +184,14 @@ export function usePointInput(rest) {
178184 );
179185}
180186
187+export function useGalaxyInput(props) {
188+ return usePointInput({
189+ label: 'Galaxy Name',
190+ size: 1,
191+ ...props,
192+ });
193+}
194+
181195export function useCheckboxInput({ initialValue, ...rest }) {
182196 return useFirstOf(
183197 useForm([
@@ -211,6 +225,44 @@ export function useSelectInput({ initialValue, options, ...rest }) {
211225 );
212226}
213227
228+const kAddressValidators = [
229+ validateEthereumAddress,
230+ validateNotNullAddress,
231+ validateNotEmpty,
232+];
233+export function useAddressInput({ ...rest }) {
234+ return useFirstOf(
235+ useForm([
236+ {
237+ type: 'string',
238+ label: 'Ethereum Address',
239+ placeholder: 'e.g. 0x6DEfFb0caFDB11D175F123F6891AA64F01c24F7d',
240+ autoComplete: 'off',
241+ validators: kAddressValidators,
242+ mono: true,
243+ ...rest,
244+ },
245+ ])
246+ );
247+}
248+
249+const kNumberTransformers = [convertToNumber];
250+const kNumberValidators = [validateGreaterThan(0)];
251+export function useNumberInput({ ...rest }) {
252+ return useFirstOf(
253+ useForm([
254+ {
255+ type: 'number',
256+ label: 'Number',
257+ autoComplete: 'off',
258+ transformers: kNumberTransformers,
259+ validators: kNumberValidators,
260+ ...rest,
261+ },
262+ ])
263+ );
264+}
265+
214266const kEmailValidators = [validateEmail, validateNotEmpty];
215267export const buildEmailInputConfig = extra => ({
216268 type: 'email',
src/lib/useKeyfileGenerator.jsnew file+118
@@ -0,0 +1,118 @@
1+import { useCallback, useEffect, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import saveAs from 'file-saver';
4+import * as ob from 'urbit-ob';
5+
6+import { usePointCache } from 'store/pointCache';
7+import { useWallet } from 'store/wallet';
8+import { usePointCursor } from 'store/pointCursor';
9+
10+import * as need from 'lib/need';
11+
12+import {
13+ attemptNetworkSeedDerivation,
14+ keysMatchChain,
15+ deriveNetworkKeys,
16+ compileNetworkingKey,
17+} from './keys';
18+import useCurrentPermissions from './useCurrentPermissions';
19+
20+export default function useKeyfileGenerator(manualNetworkSeed) {
21+ const { urbitWallet, wallet, authMnemonic } = useWallet();
22+ const { pointCursor } = usePointCursor();
23+ const { getDetails } = usePointCache();
24+
25+ const [notice, setNotice] = useState('Deriving networking keys...');
26+ const [downloaded, setDownloaded] = useState(false);
27+ const [generating, setGenerating] = useState(true);
28+ const [keyfile, setKeyfile] = useState(false);
29+
30+ const _point = need.point(pointCursor);
31+ const _details = need.details(getDetails(_point));
32+
33+ const networkRevision = parseInt(_details.keyRevisionNumber, 10);
34+ const { isOwner, isManagementProxy } = useCurrentPermissions();
35+
36+ const hasNetworkingKeys = networkRevision > 0;
37+ const available =
38+ (isOwner || isManagementProxy) && hasNetworkingKeys && !!keyfile;
39+
40+ const generate = useCallback(async () => {
41+ if (!hasNetworkingKeys) {
42+ setGenerating(false);
43+ setNotice('Networking keys not yet set.');
44+ console.log(
45+ `no networking keys available for revision ${networkRevision}`
46+ );
47+ return;
48+ }
49+
50+ const networkSeed = manualNetworkSeed
51+ ? Just(manualNetworkSeed)
52+ : await attemptNetworkSeedDerivation({
53+ urbitWallet,
54+ wallet,
55+ authMnemonic,
56+ details: _details,
57+ revision: networkRevision,
58+ });
59+
60+ if (Nothing.hasInstance(networkSeed)) {
61+ setGenerating(false);
62+ setNotice(
63+ 'Custom or nondeterministic networking keys cannot be re-downloaded.'
64+ );
65+ console.log(`seed is nondeterminable for revision ${networkRevision}`);
66+ return;
67+ }
68+
69+ const _networkSeed = networkSeed.value;
70+
71+ const pair = deriveNetworkKeys(_networkSeed);
72+
73+ if (!keysMatchChain(pair, _details)) {
74+ setGenerating(false);
75+ setNotice('Derived networking keys do not match on-chain details.');
76+ console.log(`keys do not match details for revision ${networkRevision}`);
77+ return;
78+ }
79+
80+ setNotice();
81+ setKeyfile(compileNetworkingKey(pair, _point, networkRevision));
82+ setGenerating(false);
83+ }, [
84+ networkRevision,
85+ hasNetworkingKeys,
86+ manualNetworkSeed,
87+ urbitWallet,
88+ wallet,
89+ authMnemonic,
90+ _details,
91+ _point,
92+ ]);
93+
94+ const download = useCallback(() => {
95+ saveAs(
96+ new Blob([keyfile], {
97+ type: 'text/plain;charset=utf-8',
98+ }),
99+ `${ob.patp(_point).slice(1)}-${networkRevision}.key`
100+ // TODO: ^ unifiy "remove tilde" calls
101+ );
102+ setDownloaded(true);
103+ }, [_point, keyfile, networkRevision]);
104+
105+ useEffect(() => {
106+ generate();
107+ }, [generate]);
108+
109+ const values = {
110+ generating,
111+ available,
112+ downloaded,
113+ download,
114+ notice,
115+ };
116+
117+ return { ...values, bind: values };
118+}
src/lib/usePermissionsForPoint.jsnew file+97
@@ -0,0 +1,97 @@
1+import { azimuth } from 'azimuth-js';
2+
3+import { usePointCache } from 'store/pointCache';
4+import { useNetwork } from 'store/network';
5+
6+import * as need from 'lib/need';
7+
8+import { eqAddr, isZeroAddress } from './wallet';
9+
10+const NULL_PERMISSIONS = {
11+ isPlanet: false,
12+ isStar: false,
13+ isGalaxy: false,
14+ //
15+ isOwner: false,
16+ isActiveOwner: false,
17+ isTransferProxy: false,
18+ isManagementProxy: false,
19+ //
20+ isManagementProxySet: false,
21+ isSpawnProxySet: false,
22+ isVotingProxySet: false,
23+ isTransferProxySet: false,
24+ //
25+ canManage: false,
26+ canTransfer: false,
27+ canSpawn: false,
28+ canVote: false,
29+ //
30+ spawnIsDelegatedSending: false,
31+};
32+
33+/**
34+ * @param {string} address
35+ * @param {number} point
36+ */
37+export default function usePermissionsForPoint(address, point) {
38+ const { getDetails } = usePointCache();
39+ const { contracts } = useNetwork();
40+
41+ return getDetails(point).matchWith({
42+ Nothing: () => NULL_PERMISSIONS,
43+ Just: ({ value: details }) => {
44+ const _contracts = need.contracts(contracts);
45+
46+ const pointSize = azimuth.getPointSize(point);
47+ const isPlanet = pointSize === azimuth.PointSize.Planet;
48+ const isStar = pointSize === azimuth.PointSize.Star;
49+ const isGalaxy = pointSize === azimuth.PointSize.Galaxy;
50+
51+ const isOwner = eqAddr(address, details.owner);
52+ const isActiveOwner = isOwner && details.active;
53+ const isManagementProxy = eqAddr(address, details.managementProxy);
54+ const isSpawnProxy = eqAddr(address, details.spawnProxy);
55+ const isVotingProxy = eqAddr(address, details.votingProxy);
56+ const isTransferProxy = eqAddr(address, details.transferProxy);
57+
58+ const isManagementProxySet = !isZeroAddress(details.managementProxy);
59+ const isSpawnProxySet = !isZeroAddress(details.spawnProxy);
60+ const isVotingProxySet = !isZeroAddress(details.votingProxy);
61+ const isTransferProxySet = !isZeroAddress(details.transferProxy);
62+
63+ const canManage = isOwner || isManagementProxy;
64+ const canTransfer = isOwner || isTransferProxy;
65+ const canSpawn = (isStar || isGalaxy) && (isOwner || isSpawnProxy);
66+ const canVote = isGalaxy && (isOwner || isVotingProxy);
67+
68+ const spawnIsDelegatedSending = eqAddr(
69+ _contracts.delegatedSending.address,
70+ details.spawnProxy
71+ );
72+
73+ return {
74+ isPlanet,
75+ isStar,
76+ isGalaxy,
77+ //
78+ isOwner,
79+ isActiveOwner,
80+ isTransferProxy,
81+ isManagementProxy,
82+ //
83+ isManagementProxySet,
84+ isSpawnProxySet,
85+ isVotingProxySet,
86+ isTransferProxySet,
87+ //
88+ canManage,
89+ canTransfer,
90+ canSpawn,
91+ canVote,
92+ //
93+ spawnIsDelegatedSending,
94+ };
95+ },
96+ });
97+}
src/lib/useRouter.js+54−18
@@ -1,5 +1,7 @@
1−import { useCallback, useMemo, useState, useEffect } from 'react';
2−import { last, includes as _includes } from 'lodash';
1+import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2+import { last, includes as _includes, findIndex } from 'lodash';
3+
4+const NULL_DATA = {};
35
46/**
57 * @param primary whether or not this is the top-level router
@@ -15,6 +17,9 @@ export default function useRouter({
1517}) {
1618 const [routes, setRoutes] = useState(initialRoutes);
1719
20+ const oldPopState = useRef();
21+
22+ const size = routes.length;
1823 const push = useCallback(
1924 (key, data) => {
2025 if (!views[key]) {
@@ -46,12 +51,25 @@ export default function useRouter({
4651 [setRoutes, views]
4752 );
4853 const pop = useCallback(
49− (count = 1) =>
50− setRoutes(routes =>
51− routes.length > 1 ? [...routes.slice(0, routes.length - count)] : routes
52− ),
53− // ^ can't pop root route
54− [setRoutes]
54+ (count = 1) => {
55+ if (size > 1) {
56+ // pop as expected
57+ return setRoutes(routes => [...routes.slice(0, routes.length - count)]);
58+ }
59+
60+ // if we are at the root, pass this event to our parent
61+ if (oldPopState.current) {
62+ window.history.back();
63+ }
64+ },
65+ [size, setRoutes, oldPopState]
66+ );
67+ const popTo = useCallback(
68+ name => {
69+ const index = findIndex(routes, route => route.key === name);
70+ return pop(routes.length - 1 - index);
71+ },
72+ [pop, routes]
5573 );
5674 const peek = useCallback(() => last(routes), [routes]);
5775 const replaceWith = useCallback(routes => setRoutes(() => routes), [
@@ -64,30 +82,47 @@ export default function useRouter({
6482 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
6583 routes,
6684 ]);
67− const size = routes.length;
68− const data = useMemo(() => last(routes).data, [routes]);
85+ const data = useMemo(() => last(routes).data || NULL_DATA, [routes]);
6986 const Route = useMemo(() => views[peek().key], [views, peek]);
7087
7188 // Scroll to top of page with each route transition
7289 useEffect(() => {
7390 window.scrollTo(0, 0);
74− }, [routes, primary]);
91+ }, [routes]);
7592
76− // capture browser pop in primary router
7793 useEffect(() => {
78− if (!primary) {
79− return;
80− }
81−
94+ // on router mount, register new state with browser
8295 window.history.pushState(null, null, null);
96+ }, []);
97+
98+ // capture browser pop in primary router
99+ useEffect(() => {
100+ // store the previous onpopstate handler
101+ oldPopState.current = window.onpopstate;
83102
103+ // construct new onpopstate handler
84104 window.onpopstate = e => {
105+ if (size <= 1 && oldPopState.current) {
106+ // if this is the root route and there's a parent handler,
107+ // give the event to the handler
108+ return oldPopState.current(e);
109+ }
110+
111+ // on pop, tell the browser of a new state to avoid giving the user
112+ // the ability to go forward
113+ // TODO: allow the user to go forward by storing our data in history
114+ // and using the url for other state
85115 window.history.pushState(null, null, null);
116+
117+ // then update our local state for rendering
86118 pop();
87119 };
88120
89− // TODO: disposer functon
90− }, [pop, primary]);
121+ return () => {
122+ // reset the onpopstate to the previous version
123+ window.onpopstate = oldPopState.current;
124+ };
125+ }, [size, pop, oldPopState]);
91126
92127 return {
93128 Route,
@@ -96,6 +131,7 @@ export default function useRouter({
96131 push,
97132 popAndPush,
98133 pop,
134+ popTo,
99135 replaceWith,
100136 reset,
101137 peek,
src/lib/useSetState.js+3−3
@@ -1,15 +1,15 @@
1−import { useCallback, useState } from 'react';
1+import { useCallback, useRef, useState } from 'react';
22
33export default function useSetState(initialState = {}) {
4+ const initialStateRef = useRef(initialState);
45 const [state, _setState] = useState(initialState);
56 const setState = useCallback(
67 items => _setState(state => ({ ...state, ...items })),
78 [_setState]
89 );
910
10− const reset = useCallback(() => _setState(initialState), [
11+ const reset = useCallback(() => _setState(initialStateRef.current), [
1112 _setState,
12− initialState,
1313 ]);
1414
1515 return [state, setState, reset];
src/lib/validators.js+41−14
@@ -2,9 +2,10 @@ import * as bip39 from 'bip39';
22import * as ob from 'urbit-ob';
33import { identity, includes } from 'lodash';
44
5−import { isValidAddress } from './wallet';
5+import { isValidAddress, ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT } from './wallet';
66import patp2dec from './patp2dec';
77import { patpStringLength } from './lib';
8+import { MIN_GALAXY, MAX_GALAXY } from './constants';
89
910// NOTE: do not use the /g modifier for these regexes
1011// https://stackoverflow.com/a/21373261
@@ -74,7 +75,7 @@ export const validateNotEmpty = m =>
7475 prevMessage: m,
7576 validator: d => {
7677 try {
77− return d.length > 1;
78+ return d.length > 0;
7879 } catch {
7980 return false;
8081 }
@@ -90,12 +91,12 @@ export const validateGalaxy = m =>
9091 let point;
9192 try {
9293 point = patp2dec(d);
93− return point >= 0 && point < 256;
94+ return point >= MIN_GALAXY && point <= MAX_GALAXY;
9495 } catch (e) {
9596 return false;
9697 }
9798 },
98− error: 'This is not a valid galaxy',
99+ error: 'This is not a valid galaxy.',
99100 });
100101
101102export const validatePoint = m =>
@@ -108,7 +109,7 @@ export const validatePoint = m =>
108109 return false;
109110 }
110111 },
111− error: 'This is not a valid point',
112+ error: 'This is not a valid point.',
112113 });
113114
114115export const validateTicket = m =>
@@ -121,7 +122,7 @@ export const validateTicket = m =>
121122 return false;
122123 }
123124 },
124− error: 'This is not a valid ticket',
125+ error: 'This is not a valid ticket.',
125126 });
126127
127128export const validateShard = m =>
@@ -134,7 +135,7 @@ export const validateShard = m =>
134135 return false;
135136 }
136137 },
137− error: 'This is not a valid shard',
138+ error: 'This is not a valid shard.',
138139 });
139140
140141export const validateOneOf = (options = []) => m =>
@@ -148,7 +149,7 @@ export const validateHexString = m =>
148149 simpleValidatorWrapper({
149150 prevMessage: m,
150151 validator: d => isHexString.test(d),
151− error: 'This is not a valid hex string',
152+ error: 'This is not a valid hex string.',
152153 });
153154
154155// @deprecate
@@ -156,7 +157,7 @@ export const validateNetworkKey = m =>
156157 simpleValidatorWrapper({
157158 prevMessage: m,
158159 validator: d => plain64CharHexValue.test(d),
159− error: 'This is not a valid network key',
160+ error: 'This is not a valid network key.',
160161 });
161162
162163// @deprecate
@@ -164,7 +165,7 @@ export const validateNetworkSeed = m =>
164165 simpleValidatorWrapper({
165166 prevMessage: m,
166167 validator: d => plain64CharHexValue.test(d),
167− error: 'This is not a valid network seed',
168+ error: 'This is not a valid network seed.',
168169 });
169170
170171// Checks if a string is a valid ethereum address
@@ -172,14 +173,14 @@ export const validateEthereumAddress = m =>
172173 simpleValidatorWrapper({
173174 prevMessage: m,
174175 validator: d => isValidAddress(d),
175− error: 'This is not a valid Ethereum address',
176+ error: 'This is not a valid Ethereum address.',
176177 });
177178
178179export const validateEmail = m =>
179180 simpleValidatorWrapper({
180181 prevMessage: m,
181182 validator: d => emailRegExp.test(d),
182− error: 'This is not a valid email address',
183+ error: 'This is not a valid email address.',
183184 });
184185
185186export const validateInt = m =>
@@ -194,7 +195,7 @@ export const validateInt = m =>
194195 return false;
195196 }
196197 },
197− error: 'This is not a valid number',
198+ error: 'This is not a valid number.',
198199 });
199200
200201export const validateExactly = (value, error) => m =>
@@ -204,6 +205,13 @@ export const validateExactly = (value, error) => m =>
204205 error,
205206 });
206207
208+export const validateNotAny = (values = []) => m =>
209+ simpleValidatorWrapper({
210+ prevMessage: m,
211+ validator: d => !values.includes(d),
212+ error: `Cannot be ${m.data}.`,
213+ });
214+
207215export const validateLength = l => m =>
208216 simpleValidatorWrapper({
209217 prevMessage: m,
@@ -214,7 +222,7 @@ export const validateLength = l => m =>
214222 return false;
215223 }
216224 },
217− error: `Must be exactly ${l} characters`,
225+ error: `Must be exactly ${l} characters.`,
218226 });
219227
220228export const validateMaximumLength = l => m =>
@@ -230,6 +238,20 @@ export const validateMaximumLength = l => m =>
230238 error: `Must be ${l} characters or fewer.`,
231239 });
232240
241+export const validateGreaterThan = l => m =>
242+ simpleValidatorWrapper({
243+ prevMessage: m,
244+ validator: d => d > l,
245+ error: `Must be at least ${l}.`,
246+ });
247+
248+export const validateInSet = (set, error) => m =>
249+ simpleValidatorWrapper({
250+ prevMessage: m,
251+ validator: d => set.has(d),
252+ error,
253+ });
254+
233255export const validatePatpByteLength = byteLength => {
234256 return validateLength(patpStringLength(byteLength));
235257};
@@ -237,3 +259,8 @@ export const validatePatpByteLength = byteLength => {
237259export const validateMaximumPatpByteLength = byteLength => {
238260 return validateMaximumLength(patpStringLength(byteLength));
239261};
262+
263+export const validateNotNullAddress = validateNotAny([
264+ ETH_ZERO_ADDR,
265+ ETH_ZERO_ADDR_SHORT,
266+]);
src/lib/wallet.js+5−22
@@ -4,14 +4,12 @@ import keccak from 'keccak';
44import { reduce } from 'lodash';
55import { Just, Nothing } from 'folktale/maybe';
66import * as secp256k1 from 'secp256k1';
7−import * as ob from 'urbit-ob';
87import * as kg from 'urbit-key-generation/dist';
98import { isAddress } from 'web3-utils';
109
1110export const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
1211export const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
13−export const CURVE_ZERO_ADDR =
14− '0x0000000000000000000000000000000000000000000000000000000000000000';
12+export const ETH_ZERO_ADDR_SHORT = '0x0';
1513
1614export const WALLET_TYPES = {
1715 MNEMONIC: Symbol('MNEMONIC'),
@@ -42,6 +40,7 @@ export const addressFromSecp256k1Public = pub => {
4240 return toChecksumAddress(addr);
4341};
4442
43+// TODO: move all of these generic crypto helpers to another file
4544export const addHexPrefix = hex =>
4645 hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
4746
@@ -55,7 +54,8 @@ export const keccak256 = str =>
5554
5655export const isValidAddress = a => '0x0' === a || isAddress(a);
5756
58−export const isZeroAddress = a => ETH_ZERO_ADDR === a;
57+export const isZeroAddress = a =>
58+ a === ETH_ZERO_ADDR || a === ETH_ZERO_ADDR_SHORT;
5959
6060export const toChecksumAddress = address => {
6161 const addr = stripHexPrefix(address).toLowerCase();
@@ -73,11 +73,6 @@ export const eqAddr = (addr0, addr1) =>
7373 toChecksumAddress(addr0) === toChecksumAddress(addr1);
7474
7575export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
76− if (typeof point === 'string') {
77− //TODO why not in kg?
78− point = ob.patp2dec(point);
79− }
80−
8176 return await kg.generateWallet({
8277 ticket: ticket,
8378 ship: point,
@@ -85,19 +80,6 @@ export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
8580 });
8681};
8782
88−export const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
89− if (typeof point === 'string') {
90− //TODO why not in kg?
91− point = ob.patp2dec(point);
92− }
93−
94− return await kg.generateOwnershipWallet({
95− ticket,
96− ship: point,
97− passphrase,
98− });
99−};
100−
10183export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
10284 const seed = bip39.validateMnemonic(mnemonic)
10385 ? Just(bip39.mnemonicToSeed(mnemonic, passphrase))
@@ -109,6 +91,7 @@ export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
10991 const hd = bip32.fromSeed(sd);
11092 wal = hd.derivePath(path);
11193 wal.address = addressFromSecp256k1Public(wal.publicKey);
94+ wal.passphrase = passphrase || '';
11295 wal = Just(wal);
11396 } catch (_) {
11497 wal = Nothing();
src/lib/walletgen.js+3−2
@@ -55,12 +55,12 @@ export const generateWallet = async (point, ticket, boot) => {
5555 boot: boot,
5656 };
5757
58− const wallet = await kg.generateWallet(config);
59−
6058 // This is here to notify anyone who opens console because the thread
6159 // hangs, blocking UI updates so this cannot be done in the UI
6260 console.log('Generating Wallet for point address: ', point);
6361
62+ const wallet = await kg.generateWallet(config);
63+
6464 return wallet;
6565};
6666
@@ -71,6 +71,7 @@ export const generateOwnershipWallet = (ship, ticket) =>
7171// so we always use ~zod as the wallet point.
7272export const generateTemporaryOwnershipWallet = ticket =>
7373 generateOwnershipWallet(ZOD, ticket);
74+
7475export const generateTemporaryTicketAndWallet = async point => {
7576 const ticket = await makeTicket(point);
7677 const owner = await generateOwnershipWallet(ZOD, ticket);
src/store/Provider.js−2
@@ -1,7 +1,6 @@
11import nest from 'lib/nest';
22
33import { HistoryProvider } from './history';
4−import { TxnConfirmationsProvider } from './txnConfirmations';
54import { OnlineProvider } from './online';
65import { NetworkProvider } from './network';
76import { WalletProvider } from './wallet';
@@ -16,7 +15,6 @@ export default nest([
1615 OnlineProvider,
1716 NetworkProvider,
1817 WalletProvider,
19− TxnConfirmationsProvider,
2018 PointCursorProvider,
2119 PointCacheProvider,
2220 TxnCursorProvider,
src/store/lib/useBirthdaysStore.js+1−1
@@ -45,7 +45,7 @@ export default function useBirthdayStore() {
4545 [point]: Just(new Date(block.timestamp * 1000)),
4646 });
4747 } else {
48− // TODO: better encoding for "no birthday" state?
48+ // TODO: better encoding for "no birthday" state using Result.Ok/Error
4949 // if there's no birthday, just use today
5050 addToBirthdayCache({
5151 [point]: Just(new Date()),
src/store/lib/useInvitesStore.js+3−11
@@ -1,8 +1,9 @@
1−import { useCallback, useState } from 'react';
1+import { useCallback } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
55import { useNetwork } from '../network';
6+import useSetState from 'lib/useSetState';
67
78// the default value of a point's invites
89const EMPTY_INVITES = {
@@ -13,16 +14,7 @@ const EMPTY_INVITES = {
1314
1415export default function useInvitesStore() {
1516 const { contracts } = useNetwork();
16− const [invitesCache, _setInvitesCache] = useState({});
17−
18− const addToInvitesCache = useCallback(
19− entry =>
20− _setInvitesCache(cache => ({
21− ...cache,
22− ...entry,
23− })),
24− [_setInvitesCache]
25− );
17+ const [invitesCache, addToInvitesCache] = useSetState({});
2618
2719 const getInvites = useCallback(
2820 point => invitesCache[point] || EMPTY_INVITES,
src/store/lib/usePointStore.js+7−3
@@ -12,11 +12,11 @@ export default function usePointStore() {
1212 const { syncBirthday, ...birthdays } = useBirthdaysStore();
1313 const { syncRekeyDate, ...rekeyDates } = useRekeyDateStore();
1414 const { syncInvites, ...invites } = useInvitesStore();
15− const ecliptic = useEclipticOwnerStore();
1615 const {
1716 syncControlledPoints,
1817 ...controlledPoints
1918 } = useControlledPointsStore();
19+ const ecliptic = useEclipticOwnerStore();
2020
2121 // sync all of the on-chain info required to display a known point
2222 const syncKnownPoint = useCallback(
@@ -25,17 +25,19 @@ export default function usePointStore() {
2525 );
2626
2727 // sync all of the on-chain info required to display a foreign point
28− const syncForeignPoint = useCallback(async point => Promise.all([]), []);
28+ const syncForeignPoint = useCallback(async point => {}, []);
2929
3030 // sync all of the on-chain info required for a point that the user owns
3131 const syncOwnedPoint = useCallback(
3232 async point =>
3333 Promise.all([
34+ syncForeignPoint(point),
3435 syncKnownPoint(point),
36+ //
3537 syncDetails(point),
3638 syncInvites(point),
3739 ]),
38− [syncKnownPoint, syncDetails, syncInvites]
40+ [syncForeignPoint, syncKnownPoint, syncDetails, syncInvites]
3941 );
4042
4143 return {
@@ -45,6 +47,8 @@ export default function usePointStore() {
4547 ...invites,
4648 ...controlledPoints,
4749 ...ecliptic,
50+ syncDetails,
51+ syncRekeyDate,
4852 syncInvites,
4953 syncControlledPoints,
5054 syncKnownPoint,
src/store/lib/useRekeyDateStore.js+15−9
@@ -1,5 +1,6 @@
11import { useCallback, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3+import Result from 'folktale/result';
34
45import { useNetwork } from '../network';
56
@@ -16,6 +17,10 @@ export default function useRekeyDateStore() {
1617 [_setRekeyDateCache]
1718 );
1819
20+ /**
21+ * @param {number} point
22+ * @return {Maybe<Result<Date, String>>}
23+ */
1924 const getRekeyDate = useCallback(
2025 point => rekeyDateCache[point] || Nothing(),
2126 [rekeyDateCache]
@@ -24,11 +29,8 @@ export default function useRekeyDateStore() {
2429 const syncRekeyDate = useCallback(
2530 async point => {
2631 const _web3 = web3.getOrElse(null);
27− if (!_web3) {
28− return;
29− }
3032 const _contracts = contracts.getOrElse(null);
31− if (!_contracts) {
33+ if (!_web3 || !_contracts) {
3234 return;
3335 }
3436
@@ -37,26 +39,30 @@ export default function useRekeyDateStore() {
3739 const logs = await _contracts.azimuth.getPastEvents('ChangedKeys', {
3840 fromBlock: 0,
3941 toBlock: 'latest',
40− filter: { point: [point] },
42+ filter: {
43+ point: [point],
44+ },
4145 });
4246
4347 if (logs.length === 0) {
4448 //TODO: better encoding for "no rekeyDate" state?
4549 addToRekeyDateCache({
46− [point]: Just('unknown'),
50+ [point]: Just(Result.Error('No rekey date available.')),
4751 });
4852 } else {
4953 // last log is most recent
5054 const blockNumber = logs[logs.length - 1].blockNumber;
5155 const block = await _web3.eth.getBlock(blockNumber);
5256 addToRekeyDateCache({
53− //TODO date format
54− [point]: Just(new Date(block.timestamp * 1000).toString()),
57+ [point]: Just(Result.Ok(new Date(block.timestamp * 1000))),
5558 });
5659 }
5760 },
5861 [web3, contracts, addToRekeyDateCache]
5962 );
6063
61− return { getRekeyDate, syncRekeyDate };
64+ return {
65+ getRekeyDate,
66+ syncRekeyDate,
67+ };
6268}
src/store/txnConfirmations.jsdeleted−44
@@ -1,44 +0,0 @@
1−import React, { createContext, forwardRef, useContext, useState } from 'react';
2−
3−export const TxnConfirmationsContext = createContext(null);
4−
5−function _useTxnConfirmations() {
6− const [confirmations, setConfirmations] = useState({});
7−
8− const setTxnConfirmations = (txnHash, txnConfirmations) => {
9− setConfirmations({
10− ...confirmations,
11− [txnHash]: txnConfirmations,
12− });
13− };
14−
15− return {
16− txnConfirmations: confirmations,
17− setTxnConfirmations,
18− };
19−}
20−
21−export function TxnConfirmationsProvider({ children }) {
22− const value = _useTxnConfirmations();
23−
24− return (
25− <TxnConfirmationsContext.Provider value={value}>
26− {children}
27− </TxnConfirmationsContext.Provider>
28− );
29−}
30−
31−// HOC version
32−export const withTxnConfirmations = Component =>
33− forwardRef((props, ref) => (
34− <TxnConfirmationsContext.Consumer>
35− {txnConfirmations => (
36− <Component ref={ref} {...txnConfirmations} {...props} />
37− )}
38− </TxnConfirmationsContext.Consumer>
39− ));
40−
41−// Hook version
42−export function useTxnConfirmations() {
43− return useContext(TxnConfirmationsContext);
44−}
src/store/wallet.js+1
@@ -55,6 +55,7 @@ function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
5555 const setWallet = useCallback(
5656 wallet => {
5757 // force that public addresses are derived for each wallet
58+ // NOTE wallet is Maybe<> and .map is Maybe#map
5859 wallet.map(wal => {
5960 wal.address = wal.address || addressFromSecp256k1Public(wal.publicKey);
6061 return wal;
src/style/bridge.scss+54
@@ -26,3 +26,57 @@ body {
2626#root {
2727 min-height: 100vh;
2828}
29+
30+// range slider
31+
32+input[type='range'] {
33+ overflow: hidden;
34+ -webkit-appearance: none;
35+ background-color: #e6e6e6;
36+
37+ /* Chrome */
38+ &::-webkit-slider-runnable-track {
39+ height: 24px;
40+ -webkit-appearance: none;
41+ background-color: #e6e6e6;
42+ margin-top: -1px;
43+ }
44+
45+ &::-webkit-slider-thumb {
46+ -webkit-appearance: none;
47+ width: 24px;
48+ height: 100%;
49+ background-color: #000;
50+ border: none;
51+ border-radius: 0;
52+ cursor: ew-resize;
53+ }
54+
55+ /* FF */
56+ &::-moz-range-progress {
57+ height: 100%;
58+ background-color: #000;
59+ }
60+
61+ &::-moz-range-track {
62+ background-color: #e6e6e6;
63+ }
64+
65+ &::-moz-range-thumb {
66+ width: 24px;
67+ height: 100%;
68+ background-color: #000;
69+ border: none;
70+ border-radius: 0;
71+ cursor: ew-resize;
72+ }
73+
74+ /* IE */
75+ &::-ms-fill-lower {
76+ background-color: #000;
77+ }
78+
79+ &::-ms-fill-upper {
80+ background-color: #e6e6e6;
81+ }
82+}
src/views/AcceptTransfer.jsdeleted−121
@@ -1,121 +0,0 @@
1−import { Just } from 'folktale/maybe';
2−import React from 'react';
3−import { H1, P } from '../components/old/Base';
4−import {
5− InnerLabel,
6− AddressInput,
7− ShowBlockie,
8− Anchor,
9−} from '../components/old/Base';
10−import * as azimuth from 'azimuth-js';
11−import * as ob from 'urbit-ob';
12−import * as need from '../lib/need';
13−
14−import StatelessTransaction from '../components/old/StatelessTransaction';
15−import { NETWORK_TYPES } from '../lib/network';
16−import { isValidAddress } from '../lib/wallet';
17−import { withNetwork } from '../store/network';
18−import { compose } from '../lib/lib';
19−import { withWallet } from '../store/wallet';
20−import { withPointCursor } from '../store/pointCursor';
21−import View from 'components/View';
22−
23−class AcceptTransfer extends React.Component {
24− constructor(props) {
25− super(props);
26−
27− const receivingAddress = need.addressFromWallet(props.wallet);
28− const incomingPoint = need.point(props.pointCursor);
29−
30− this.state = {
31− receivingAddress: receivingAddress,
32− incomingPoint: incomingPoint,
33− };
34−
35− this.handleAddressInput = this.handleAddressInput.bind(this);
36− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
37− this.statelessRef = React.createRef();
38− }
39−
40− handleAddressInput(proxyAddress) {
41− this.setState({ proxyAddress });
42− this.statelessRef.current.clearTxn();
43− }
44−
45− createUnsignedTxn() {
46− const { state, props } = this;
47−
48− const validContracts = need.contracts(props.contracts);
49− const validPoint = need.point(props.pointCursor);
50− //TODO state.incomingPoint ?
51−
52− return Just(
53− azimuth.ecliptic.transferPoint(
54− validContracts,
55− validPoint,
56− state.receivingAddress,
57− true
58− )
59− );
60− }
61−
62− render() {
63− const { state, props } = this;
64− const validAddress = isValidAddress(state.receivingAddress);
65− const canGenerate = validAddress === true;
66−
67− const esvisible =
68− props.networkType === NETWORK_TYPES.ROPSTEN ||
69− props.networkType === NETWORK_TYPES.MAINNET;
70−
71− const esdomain =
72− props.networkType === NETWORK_TYPES.ROPSTEN
73− ? 'ropsten.etherscan.io'
74− : 'etherscan.io';
75−
76− return (
77− <View>
78− <H1>
79− {'Accept Transfer of '}{' '}
80− <code>{` ${ob.patp(state.incomingPoint)} `}</code>
81− </H1>
82−
83− <P>
84− {"By default, the recipient is the address you're logged in " +
85− 'as. But you can transfer to any address you like.'}
86− </P>
87−
88− <AddressInput
89− className="mono mt-8"
90− prop-size="lg"
91− prop-format="innerLabel"
92− value={state.receivingAddress}
93− onChange={v => this.handleAddressInput(v)}>
94− <InnerLabel>{'Receiving Address'}</InnerLabel>
95− <ShowBlockie className={'mt-1'} address={state.receivingAddress} />
96− </AddressInput>
97−
98− <Anchor
99− className={'mt-1 sm'}
100− prop-size="sm"
101− prop-disabled={!isValidAddress(state.receivingAddress) || !esvisible}
102− target={'_blank'}
103− href={`https://${esdomain}/address/${state.receivingAddress}`}>
104− {'View on Etherscan ↗'}
105− </Anchor>
106−
107− <StatelessTransaction
108− canGenerate={canGenerate}
109− createUnsignedTxn={this.createUnsignedTxn}
110− ref={this.statelessRef}
111− />
112− </View>
113− );
114− }
115−}
116−
117−export default compose(
118− withNetwork,
119− withWallet,
120− withPointCursor
121−)(AcceptTransfer);
src/views/Activate/ActivateCode.js+5−7
@@ -14,7 +14,7 @@ import * as need from 'lib/need';
1414import { ROUTE_NAMES } from 'lib/routeNames';
1515import { useSyncKnownPoints } from 'lib/useSyncPoints';
1616import FooterButton from 'components/FooterButton';
17−import Blinky from 'components/Blinky';
17+import { blinkIf } from 'components/Blinky';
1818import { DEFAULT_HD_PATH, walletFromMnemonic } from 'lib/wallet';
1919import { useNetwork } from 'store/network';
2020import { generateWallet } from 'lib/invite';
@@ -38,7 +38,7 @@ export default function ActivateCode() {
3838 setDerivedPoint,
3939 } = useActivateFlow();
4040
41− const [ticketInput, { pass: validTicket }] = useTicketInput({
41+ const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
4242 name: 'ticket',
4343 label: 'Code',
4444 initialValue: impliedTicket || '',
@@ -62,9 +62,7 @@ export default function ActivateCode() {
6262 setDeriving(true);
6363 // when the ticket becomes valid, derive the point
6464 (async () => {
65− const { seed } = await generateTemporaryOwnershipWallet(
66− ticketInput.data
67− );
65+ const { seed } = await generateTemporaryOwnershipWallet(ticket);
6866
6967 //TODO isn't all this accessible in the ownership object?
7068 const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
@@ -114,7 +112,7 @@ export default function ActivateCode() {
114112 }, [
115113 validTicket,
116114 contracts,
117− ticketInput.data,
115+ ticket,
118116 setDerivedPoint,
119117 setDerivedWallet,
120118 setInviteWallet,
@@ -140,7 +138,7 @@ export default function ActivateCode() {
140138 as={ForwardButton}
141139 className="mt4"
142140 disabled={!pass || deriving}
143− accessory={deriving ? <Blinky /> : undefined}
141+ accessory={blinkIf(deriving)}
144142 onClick={goToPassport}
145143 solid
146144 full>
src/views/Activate/ActivatePassport.js+4−4
@@ -64,8 +64,8 @@ export default function ActivatePassport() {
6464 );
6565
6666 return (
67− <LocalRouterProvider value={router}>
68− <View full={fullView}>
67+ <View full={fullView}>
68+ <LocalRouterProvider value={router}>
6969 <Grid gap={gap} className="mt8 mb10">
7070 <Grid.Item half={half && 1} full={full}>
7171 <Passport
@@ -82,7 +82,7 @@ export default function ActivatePassport() {
8282 resetActivateRouter={resetActivateRouter}
8383 />
8484 </Grid>
85− </View>
86− </LocalRouterProvider>
85+ </LocalRouterProvider>
86+ </View>
8787 );
8888}
src/views/Activate/PassportDownload.js+33−32
@@ -1,16 +1,16 @@
11import React, { useState, useEffect, useCallback } from 'react';
2−import cn from 'classnames';
32import { Just, Nothing } from 'folktale/maybe';
4−import { Grid, H4, P } from 'indigo-react';
3+import { Grid, P } from 'indigo-react';
54
65import * as need from 'lib/need';
7−
8−import { useActivateFlow } from './ActivateFlow';
96import { downloadWallet } from 'lib/invite';
7+import { useLocalRouter } from 'lib/LocalRouter';
8+
109import { DownloadButton, ForwardButton } from 'components/Buttons';
1110import PaperRenderer from 'components/PaperRenderer';
12−import { useLocalRouter } from 'lib/LocalRouter';
13−import Steps from 'components/Steps';
11+
12+import { useActivateFlow } from './ActivateFlow';
13+import PassportView from './PassportView';
1414
1515export default function PassportDownload({ className }) {
1616 const {
@@ -51,36 +51,37 @@ export default function PassportDownload({ className }) {
5151 const goToVerify = useCallback(() => push(names.VERIFY), [push, names]);
5252
5353 return (
54− <Grid className={cn(className, 'auto-rows-min')}>
55− <Grid.Item full as={Steps} num={1} total={3} />
56− <Grid.Item full as={H4}>
57− Passport
58− </Grid.Item>
59− <Grid.Item full>
60− <P>
61− After you’ve downloaded your passport, back up the ticket manually or
62− store on a trusted device.
63− </P>
64− <P>
65− What is digital identity? A passport is your digital identity. You
66− will use your passport to access your true computer, send payments,
67− and administer your identity. So naturally, you must keep this secure.
68− </P>
69− </Grid.Item>
70− <Grid.Item
71− full
72− as={!downloaded ? DownloadButton : ForwardButton}
73− disabled={!generated}
74− onClick={!downloaded ? download : goToVerify}
75− success={downloaded}
76− solid>
77− {!downloaded ? 'Download Passport' : 'Continue'}
78− </Grid.Item>
54+ <>
55+ <PassportView className={className} header="Passport" step={1}>
56+ <Grid>
57+ <Grid.Item full>
58+ <P>
59+ After you’ve downloaded your passport, back up the ticket manually
60+ or store on a trusted device.
61+ </P>
62+ <P>
63+ What is digital identity? A passport is your digital identity. You
64+ will use your passport to access your true computer, send
65+ payments, and administer your identity. So naturally, you must
66+ keep this secure.
67+ </P>
68+ </Grid.Item>
69+ <Grid.Item
70+ full
71+ as={!downloaded ? DownloadButton : ForwardButton}
72+ disabled={!generated}
73+ onClick={!downloaded ? download : goToVerify}
74+ success={downloaded}
75+ solid>
76+ {!downloaded ? 'Download Passport' : 'Continue'}
77+ </Grid.Item>
78+ </Grid>
79+ </PassportView>
7980 <PaperRenderer
8081 point={pointAsString}
8182 wallet={wallet}
8283 callback={data => setPaper(Just(data))}
8384 />
84− </Grid>
85+ </>
8586 );
8687}
src/views/Activate/PassportTransfer.js+53−33
@@ -1,6 +1,6 @@
11import React, { useState, useCallback } from 'react';
2−import cn from 'classnames';
3−import { Grid, H4, Text, ErrorText } from 'indigo-react';
2+import { Grid, Text, ErrorText } from 'indigo-react';
3+import { fromWei } from 'web3-utils';
44
55import { useNetwork } from 'store/network';
66import { useWallet } from 'store/wallet';
@@ -9,17 +9,37 @@ import { usePointCursor } from 'store/pointCursor';
99
1010import * as need from 'lib/need';
1111import useLifecycle from 'lib/useLifecycle';
12−import { claimPointFromInvite } from 'lib/invite';
12+import {
13+ reticketPointBetweenWallets,
14+ TRANSACTION_PROGRESS,
15+} from 'lib/reticket';
1316import timeout from 'lib/timeout';
14−import { fromWei } from 'lib/txn';
1517
16−import Steps from 'components/Steps';
1718import WarningBox from 'components/WarningBox';
1819import LoadingBar from 'components/LoadingBar';
1920import Highlighted from 'components/Highlighted';
21+import { RestartButton } from 'components/Buttons';
2022
2123import { useActivateFlow } from './ActivateFlow';
22−import { RestartButton } from 'components/Buttons';
24+import PassportView from './PassportView';
25+
26+const labelForProgress = progress => {
27+ if (progress <= 0) {
28+ return 'Verify Passport';
29+ } else if (progress <= TRANSACTION_PROGRESS.GENERATING) {
30+ return 'Generating Transactions...';
31+ } else if (progress <= TRANSACTION_PROGRESS.SIGNING) {
32+ return 'Signing Transactions...';
33+ } else if (progress <= TRANSACTION_PROGRESS.FUNDING) {
34+ return 'Funding Transactions...';
35+ } else if (progress <= TRANSACTION_PROGRESS.TRANSFERRING) {
36+ return 'Transferring Point...';
37+ } else if (progress <= TRANSACTION_PROGRESS.CLEANING) {
38+ return 'Cleaning Up...';
39+ } else if (progress <= TRANSACTION_PROGRESS.DONE) {
40+ return 'Done';
41+ }
42+};
2343
2444export default function PassportTransfer({ className, resetActivateRouter }) {
2545 const { replaceWith, names } = useHistory();
@@ -33,10 +53,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
3353 reset: resetActivateFlow,
3454 } = useActivateFlow();
3555 const [generalError, setGeneralError] = useState();
36− const [{ label, progress }, setState] = useState({
37− label: 'Verify Passport',
38− progress: 0,
39− });
56+ const [progress, setProcesss] = useState(0);
4057 const [needFunds, setNeedFunds] = useState();
4158
4259 const goToLogin = useCallback(
@@ -70,7 +87,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
7087 ({ type, state, value }) => {
7188 switch (type) {
7289 case 'progress':
73− return setState(state);
90+ return setProcesss(state);
7491 case 'askFunding':
7592 return setNeedFunds(value);
7693 case 'gotFunding':
@@ -79,7 +96,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
7996 console.error(`Unknown update: ${type}`);
8097 }
8198 },
82− [setState, setNeedFunds]
99+ [setProcesss, setNeedFunds]
83100 );
84101
85102 const claimPoint = useCallback(async () => {
@@ -92,9 +109,9 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
92109 const _web3 = need.web3(web3);
93110 const _contracts = need.contracts(contracts);
94111
95− await claimPointFromInvite({
96− inviteWallet: _inviteWallet,
97− wallet: _wallet,
112+ await reticketPointBetweenWallets({
113+ fromWallet: _inviteWallet,
114+ toWallet: _wallet,
98115 point: _point,
99116 web3: _web3,
100117 contracts: _contracts,
@@ -140,8 +157,10 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
140157 full
141158 className="mt3"
142159 as={RestartButton}
143− onClick={goToRestart}
144− />
160+ solid
161+ onClick={goToRestart}>
162+ Restart
163+ </Grid.Item>
145164 </>
146165 );
147166 }
@@ -159,7 +178,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
159178 );
160179 }
161180
162− if (progress < 100) {
181+ if (progress < 1.0) {
163182 return (
164183 <Grid.Item full as={WarningBox} className="mt8">
165184 Never give your Master Ticket to anyone
@@ -171,22 +190,23 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
171190 };
172191
173192 return (
174− <Grid className={cn(className, 'auto-rows-min')}>
175− <Grid.Item full as={Steps} num={3} total={3} />
176− <Grid.Item full as={H4}>
177− {label}
178− </Grid.Item>
179− <Grid.Item full as={Grid} className="mt3" gap={3}>
180− <Grid.Item full as={LoadingBar} progress={progress} />
181− <Grid.Item full>
182− <Text className="f5 green4">
183− This process can take up to 5 minutes to complete. Don't leave this
184− page until the process is complete.
185− </Text>
193+ <PassportView
194+ className={className}
195+ header={labelForProgress(progress)}
196+ step={3}>
197+ <Grid>
198+ <Grid.Item full as={Grid} className="mt3" gap={3}>
199+ <Grid.Item full as={LoadingBar} progress={progress} />
200+ <Grid.Item full>
201+ <Text className="f5 green4">
202+ This process can take up to 5 minutes to complete. Don't leave
203+ this page until the process is complete.
204+ </Text>
205+ </Grid.Item>
186206 </Grid.Item>
187− </Grid.Item>
188207
189− {renderAdditionalInfo()}
190− </Grid>
208+ {renderAdditionalInfo()}
209+ </Grid>
210+ </PassportView>
191211 );
192212}
src/views/Activate/PassportVerify.js+23−25
@@ -1,18 +1,18 @@
11import React, { useCallback, useMemo } from 'react';
2−import cn from 'classnames';
3−import { Grid, H4, Input, P } from 'indigo-react';
2+import { Grid, Input, P } from 'indigo-react';
43
54import * as need from 'lib/need';
65import { useLocalRouter } from 'lib/LocalRouter';
76import { useTicketInput } from 'lib/useInputs';
87import { validateExactly } from 'lib/validators';
8+import { isDevelopment } from 'lib/flags';
99
1010import { ForwardButton } from 'components/Buttons';
11−import Steps from 'components/Steps';
1211
1312import { useActivateFlow } from './ActivateFlow';
13+import PassportView from './PassportView';
1414
15−const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
15+const STUB_VERIFY_TICKET = isDevelopment;
1616
1717export default function PassportVerify({ className }) {
1818 const { push, names } = useLocalRouter();
@@ -33,26 +33,24 @@ export default function PassportVerify({ className }) {
3333 });
3434
3535 return (
36− <Grid className={cn(className, 'auto-rows-min')}>
37− <Grid.Item full as={Steps} num={2} total={3} />
38− <Grid.Item full as={H4}>
39− Verify Passport
40− </Grid.Item>
41− <Grid.Item full as={P}>
42− After you download your passport, verify your custody. Your passport
43− should be a folder of image files. One of them is your Master Ticket.
44− Open it and enter the 4 word phrase below (with hyphens).
45− </Grid.Item>
46− <Grid.Item full as={Input} {...ticketInput} />
47− <Grid.Item
48− full
49− className="mt3"
50− as={ForwardButton}
51− disabled={!pass}
52− onClick={goToTransfer}
53− solid>
54− Verify
55− </Grid.Item>
56− </Grid>
36+ <PassportView header="Verify Passport" step={2} className={className}>
37+ <Grid>
38+ <Grid.Item full as={P}>
39+ After you download your passport, verify your custody. Your passport
40+ should be a folder of image files. One of them is your Master Ticket.
41+ Open it and enter the 4 word phrase below (with hyphens).
42+ </Grid.Item>
43+ <Grid.Item full as={Input} {...ticketInput} />
44+ <Grid.Item
45+ full
46+ className="mt3"
47+ as={ForwardButton}
48+ disabled={!pass}
49+ onClick={goToTransfer}
50+ solid>
51+ Verify
52+ </Grid.Item>
53+ </Grid>
54+ </PassportView>
5755 );
5856}
src/views/Activate/PassportView.jsnew file+27
@@ -0,0 +1,27 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Grid, H4 } from 'indigo-react';
4+
5+import Steps from 'components/Steps';
6+
7+// NOTE: the 48px matches the `mt8` class on Passport so things line up nicely
8+// TODO: there's probably a clever grid/flex way to do this instead
9+export default function PassportView({ className, children, header, step }) {
10+ return (
11+ <>
12+ <Grid className={cn(className, 'auto-rows-min')}>
13+ <Grid.Item
14+ full
15+ as={Steps}
16+ num={step}
17+ total={3}
18+ style={{ height: '48px' }}
19+ />
20+ <Grid.Item full as={H4}>
21+ {header}
22+ </Grid.Item>
23+ <Grid.Item full>{children}</Grid.Item>
24+ </Grid>
25+ </>
26+ );
27+}
src/views/Admin.js+61−69
@@ -1,84 +1,76 @@
1−import React, { useCallback } from 'react';
2−import { Just } from 'folktale/maybe';
1+import React from 'react';
32import { Grid } from 'indigo-react';
4−import * as need from 'lib/need';
5−
6−import View from 'components/View';
7−import { ForwardButton } from 'components/Buttons';
8−
9−import { ROUTE_NAMES } from 'lib/routeNames';
10−import { eqAddr } from 'lib/wallet';
113
124import { useHistory } from 'store/history';
13−import { useWallet } from 'store/wallet';
14−import { usePointCursor } from 'store/pointCursor';
15−import { usePointCache } from 'store/pointCache';
16−import FooterButton from 'components/FooterButton';
175
18−export default function Admin() {
19− const history = useHistory();
20− const { urbitWallet, wallet } = useWallet();
21− const { pointCursor } = usePointCursor();
22− const { pointCache } = usePointCache();
6+import useCurrentPointName from 'lib/useCurrentPointName';
7+import useRouter from 'lib/useRouter';
8+import { LocalRouterProvider } from 'lib/LocalRouter';
239
24− const point = need.point(pointCursor);
25− const pointDetails = need.fromPointCache(pointCache, point);
26− const address = need.addressFromWallet(wallet);
10+import View from 'components/View';
11+import Crumbs from 'components/Crumbs';
2712
28− const goRedownload = useCallback(() => history.push(ROUTE_NAMES.REDOWNLOAD), [
29− history,
30− ]);
13+import AdminHome from './Admin/AdminHome';
14+import AdminEditPermissions from './Admin/AdminEditPermissions';
15+import AdminRedownload from './Admin/AdminRedownload';
16+import AdminReticket from './Admin/AdminReticket';
17+import AdminSetProxy from './Admin/AdminSetProxy';
18+import AdminTransfer from './Admin/AdminTransfer';
19+import AdminCancelTransfer from './Admin/AdminCancelTransfer';
20+import AdminNetworkingKeys from './Admin/AdminNetworkingKeys';
3121
32− const goReticket = useCallback(() => history.push(ROUTE_NAMES.RETICKET), [
33− history,
34− ]);
22+const NAMES = {
23+ HOME: 'HOME',
24+ EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
25+ REDOWNLOAD: 'REDOWNLOAD',
26+ RETICKET: 'RETICKET',
27+ SET_PROXY: 'SET_PROXY',
28+ TRANSFER: 'TRANSFER',
29+ CANCEL_TRANSFER: 'CANCEL_TRANSFER',
30+ NETWORKING_KEYS: 'NETWORKING_KEYS',
31+};
3532
36− const goEditPerms = useCallback(() => history.push(ROUTE_NAMES.PERMISSIONS), [
37− history,
38− ]);
33+const VIEWS = {
34+ [NAMES.HOME]: AdminHome,
35+ [NAMES.EDIT_PERMISSIONS]: AdminEditPermissions,
36+ [NAMES.REDOWNLOAD]: AdminRedownload,
37+ [NAMES.RETICKET]: AdminReticket,
38+ [NAMES.SET_PROXY]: AdminSetProxy,
39+ [NAMES.TRANSFER]: AdminTransfer,
40+ [NAMES.CANCEL_TRANSFER]: AdminCancelTransfer,
41+ [NAMES.NETWORKING_KEYS]: AdminNetworkingKeys,
42+};
3943
40− const goTransfer = useCallback(() => history.push(ROUTE_NAMES.TRANSFER), [
41− history,
42− ]);
44+export default function Admin() {
45+ const history = useHistory();
46+ const name = useCurrentPointName();
4347
44− const canDownloadPassport = Just.hasInstance(urbitWallet);
45− const isOwner = eqAddr(address, pointDetails.owner);
48+ const { Route, ...router } = useRouter({
49+ names: NAMES,
50+ views: VIEWS,
51+ initialRoutes: [{ key: NAMES.HOME }],
52+ });
4653
4754 return (
48− <View>
49− <Grid className="pt2">
50− <Grid.Item
51− full
52− as={ForwardButton}
53− disabled={!canDownloadPassport}
54− onClick={goRedownload}
55− detail="Re-download your paper wallet">
56− Download Passport
57− </Grid.Item>
58− <Grid.Divider />
59− <Grid.Item
60− full
61− as={ForwardButton}
62− disabled={!isOwner}
63− onClick={goReticket}
64− detail="Move to brand new wallet, re-setting all permissions">
65− Reticket
66− </Grid.Item>
67− <Grid.Item
68− full
69− as={ForwardButton}
70− onClick={goEditPerms}
71− detail="Management, networking keys, etc.">
72− Edit permissions
73− </Grid.Item>
74− </Grid>
75−
76− <FooterButton
77− detail="Transfer this identity to a new owner"
78− disabled={!isOwner}
79− onClick={goTransfer}>
80− Transfer
81− </FooterButton>
55+ <View inset>
56+ <LocalRouterProvider value={router}>
57+ <Grid className="mb4">
58+ <Grid.Item
59+ full
60+ as={Crumbs}
61+ routes={[
62+ {
63+ text: name,
64+ action: () => history.pop(),
65+ },
66+ {
67+ text: 'Admin',
68+ },
69+ ]}
70+ />
71+ </Grid>
72+ <Route />
73+ </LocalRouterProvider>
8274 </View>
8375 );
8476}
src/views/Admin/AdminCancelTransfer.jsnew file+87
@@ -0,0 +1,87 @@
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';
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 { 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 MiniBackButton from 'components/MiniBackButton';
20+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
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+export default 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={MiniBackButton} onClick={() => pop()} />
63+
64+ <Grid.Item full as={ViewHeader}>
65+ Cancel Outgoing Transfer
66+ </Grid.Item>
67+
68+ <Grid.Item
69+ full
70+ as={Text}
71+ className={cn('f5', {
72+ green3: completed,
73+ })}>
74+ {completed
75+ ? `The outgoing transfer of ${name} has been cancelled.`
76+ : `Cancel the outgoing transfer of ${name} to ${_details.transferProxy}.`}
77+ </Grid.Item>
78+
79+ <Grid.Item
80+ full
81+ as={InlineEthereumTransaction}
82+ {...bind}
83+ onReturn={() => pop()}
84+ />
85+ </Grid>
86+ );
87+}
src/views/Admin/AdminEditPermissions.jsnew file+122
@@ -0,0 +1,122 @@
1+import React, { useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid } 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 MiniBackButton from 'components/MiniBackButton';
22+
23+export default function AdminEditPermissions() {
24+ const { pop, push, names } = useLocalRouter();
25+ const { contracts } = useNetwork();
26+ const { pointCursor } = usePointCursor();
27+ const { getDetails, getRekeyDate } = usePointCache();
28+
29+ const _contracts = need.contracts(contracts);
30+
31+ const point = need.point(pointCursor);
32+ const pointSize = azimuth.getPointSize(point);
33+ const isParent = pointSize !== azimuth.PointSize.Planet;
34+ const isSenate = pointSize === azimuth.PointSize.Galaxy;
35+
36+ const details = need.details(getDetails(point));
37+ const networkRevision = parseInt(details.keyRevisionNumber, 10);
38+ const { canManage, isOwner } = useCurrentPermissions();
39+
40+ const goSetProxy = useCallback(
41+ proxyType => push(names.SET_PROXY, { proxyType }),
42+ [push, names]
43+ );
44+
45+ const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
46+ push,
47+ names,
48+ ]);
49+
50+ const renderProxyAction = (proxyType, address) => {
51+ if (isZeroAddress(address)) {
52+ address = 'Not set';
53+ } else if (eqAddr(address, _contracts.delegatedSending.address)) {
54+ address = `${address} (invites contract)`;
55+ }
56+
57+ return (
58+ <>
59+ <Grid.Item full>
60+ <ForwardButton
61+ disabled={!isOwner}
62+ onClick={() => goSetProxy(proxyType)}
63+ detail={address}>
64+ {capitalize(proxyTypeToHuman(proxyType))} Proxy Address
65+ </ForwardButton>
66+ </Grid.Item>
67+ <Grid.Divider />
68+ </>
69+ );
70+ };
71+
72+ const renderNetworkKeysDetail = () => {
73+ if (networkRevision === 0) {
74+ return 'Not yet configured.';
75+ }
76+
77+ const renderRevision = maybeDate => (
78+ <>
79+ Revision #{networkRevision}, since {matchBlinkyDate(maybeDate)}
80+ </>
81+ );
82+
83+ return getRekeyDate(point).matchWith({
84+ Nothing: () => renderRevision(Nothing()),
85+ Just: p =>
86+ p.value.matchWith({
87+ Ok: r => renderRevision(Just(r.value)),
88+ Error: 'Unknown',
89+ }),
90+ });
91+ };
92+
93+ return (
94+ <Grid>
95+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
96+ <Grid.Item full as={ViewHeader}>
97+ Permissions
98+ </Grid.Item>
99+
100+ <Grid.Item full as={ForwardButton} detail={details.owner} disabled>
101+ Ownership Address
102+ </Grid.Item>
103+ <Grid.Divider />
104+
105+ {renderProxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy)}
106+
107+ {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
108+
109+ {isSenate && renderProxyAction(PROXY_TYPE.VOTING, details.votingProxy)}
110+
111+ <Grid.Item
112+ full
113+ as={ForwardButton}
114+ disabled={!canManage}
115+ onClick={goNetworkingKeys}
116+ detail={renderNetworkKeysDetail()}>
117+ Networking Keys
118+ </Grid.Item>
119+ <Grid.Divider />
120+ </Grid>
121+ );
122+}
src/views/Admin/AdminHome.jsnew file+108
@@ -0,0 +1,108 @@
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+import MiniBackButton from 'components/MiniBackButton';
15+
16+export default function AdminHome() {
17+ const { push, names, pop } = useLocalRouter();
18+ const { urbitWallet } = useWallet();
19+
20+ const { bind: keyfileBind } = useKeyfileGenerator();
21+
22+ const { isOwner, canTransfer, isTransferProxySet } = useCurrentPermissions();
23+
24+ const goRedownload = useCallback(() => push(names.REDOWNLOAD), [push, names]);
25+ const goReticket = useCallback(() => push(names.RETICKET), [push, names]);
26+ const goEditPermissions = useCallback(() => push(names.EDIT_PERMISSIONS), [
27+ push,
28+ names,
29+ ]);
30+ const goCancelTransfer = useCallback(() => push(names.CANCEL_TRANSFER), [
31+ names,
32+ push,
33+ ]);
34+ const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
35+
36+ const canDownloadPassport = Just.hasInstance(urbitWallet);
37+
38+ return (
39+ <>
40+ <Grid>
41+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
42+ <Grid.Item full as={ViewHeader}>
43+ Admin
44+ </Grid.Item>
45+
46+ <Grid.Item
47+ full
48+ as={ForwardButton}
49+ disabled={!canDownloadPassport}
50+ onClick={goRedownload}
51+ detail="Re-download your paper wallet"
52+ disabledDetail={
53+ <B className="wrap ws-normal">
54+ · Master Ticket Authentication required.
55+ </B>
56+ }>
57+ Download Passport
58+ </Grid.Item>
59+ <Grid.Divider />
60+ <Grid.Item
61+ full
62+ as={DownloadKeyfileButton}
63+ {...keyfileBind}
64+ detail="Download your Arvo Keyfile">
65+ Redownload Arvo Keyfile
66+ </Grid.Item>
67+ <Grid.Divider />
68+ <Grid.Item
69+ full
70+ as={ForwardButton}
71+ disabled={!isOwner}
72+ onClick={goReticket}
73+ detail="Move to brand new wallet, resetting all permissions">
74+ Reticket
75+ </Grid.Item>
76+ <Grid.Divider />
77+ <Grid.Item
78+ full
79+ as={ForwardButton}
80+ onClick={goEditPermissions}
81+ detail="Proxies, networking keys, etc.">
82+ Edit Permissions
83+ </Grid.Item>
84+ <Grid.Divider />
85+ {isTransferProxySet ? (
86+ <Grid.Item
87+ full
88+ as={ForwardButton}
89+ onClick={goCancelTransfer}
90+ detail="Cancel outgoing transfer"
91+ disabled={!canTransfer}>
92+ Cancel Outgoing Transfer
93+ </Grid.Item>
94+ ) : (
95+ <Grid.Item
96+ full
97+ as={ForwardButton}
98+ onClick={goTransfer}
99+ detail="Transfer ownership of this point"
100+ disabled={!canTransfer}>
101+ Transfer
102+ </Grid.Item>
103+ )}
104+ <Grid.Divider />
105+ </Grid>
106+ </>
107+ );
108+}
src/views/Admin/AdminNetworkingKeys.jsnew file+381
@@ -0,0 +1,381 @@
1+import React, { useCallback, useEffect, useRef, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import {
4+ Grid,
5+ Text,
6+ H5,
7+ Flex,
8+ ToggleInput,
9+ Input,
10+ CheckboxInput,
11+} from 'indigo-react';
12+import * as azimuth from 'azimuth-js';
13+import { randomHex } from 'web3-utils';
14+
15+import { usePointCursor } from 'store/pointCursor';
16+import { usePointCache } from 'store/pointCache';
17+import { useNetwork } from 'store/network';
18+import { useWallet } from 'store/wallet';
19+
20+import { useLocalRouter } from 'lib/LocalRouter';
21+import * as need from 'lib/need';
22+import {
23+ segmentNetworkKey,
24+ attemptNetworkSeedDerivation,
25+ deriveNetworkKeys,
26+ CRYPTO_SUITE_VERSION,
27+ CURVE_ZERO_ADDR,
28+} from 'lib/keys';
29+import { formatDotsWithTime } from 'lib/dateFormat';
30+import useEthereumTransaction from 'lib/useEthereumTransaction';
31+import { GAS_LIMITS } from 'lib/constants';
32+import { addHexPrefix } from 'lib/wallet';
33+
34+import ViewHeader from 'components/ViewHeader';
35+import MiniBackButton from 'components/MiniBackButton';
36+import { BootArvoButton } from 'components/Buttons';
37+import FooterButton from 'components/FooterButton';
38+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
39+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
40+import NoticeBox from 'components/NoticeBox';
41+import { useHexInput, useCheckboxInput } from 'lib/useInputs';
42+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
43+
44+const chainKeyProp = name => d =>
45+ d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
46+
47+const renderNetworkKey = key => {
48+ const segments = segmentNetworkKey(key);
49+
50+ if (!segments) {
51+ return 'Unset';
52+ }
53+
54+ return segments.join('\n');
55+};
56+
57+function useSetKeys() {
58+ const { urbitWallet, wallet, authMnemonic } = useWallet();
59+ const { pointCursor } = usePointCursor();
60+ const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
61+ const { contracts } = useNetwork();
62+
63+ const _point = need.point(pointCursor);
64+ const _contracts = need.contracts(contracts);
65+ const _details = need.details(getDetails(_point));
66+
67+ const networkRevision = parseInt(_details.keyRevisionNumber, 10);
68+ const randomSeed = useRef();
69+
70+ // NOTE: nd = 'nondeterministic' (can also be a 'manual' seed)
71+ const [ndNetworkSeed, setNdNetworkSeed] = useState();
72+
73+ const {
74+ available: keyfileAvailable,
75+ bind: keyfileBind,
76+ } = useKeyfileGenerator(ndNetworkSeed);
77+
78+ const buildNetworkSeed = useCallback(
79+ async manualSeed => {
80+ if (manualSeed !== undefined) {
81+ setNdNetworkSeed(manualSeed);
82+ return manualSeed;
83+ } else {
84+ const newNetworkRevision = networkRevision + 1;
85+ console.log(`deriving seed with revision ${newNetworkRevision}`);
86+
87+ const networkSeed = await attemptNetworkSeedDerivation({
88+ urbitWallet,
89+ wallet,
90+ authMnemonic,
91+ details: _details,
92+ revision: newNetworkRevision,
93+ });
94+
95+ if (Just.hasInstance(networkSeed)) {
96+ return networkSeed.value;
97+ }
98+
99+ randomSeed.current = randomSeed.current || randomHex(64);
100+ setNdNetworkSeed(randomSeed.current);
101+
102+ return randomSeed.current;
103+ }
104+ },
105+ [_details, authMnemonic, networkRevision, urbitWallet, wallet]
106+ );
107+
108+ const { completed: _completed, ...rest } = useEthereumTransaction(
109+ useCallback(
110+ async (manualSeed, isDiscontinuity) => {
111+ const seed = await buildNetworkSeed(manualSeed);
112+ const pair = deriveNetworkKeys(seed);
113+
114+ return azimuth.ecliptic.configureKeys(
115+ _contracts,
116+ _point,
117+ addHexPrefix(pair.crypt.public),
118+ addHexPrefix(pair.auth.public),
119+ CRYPTO_SUITE_VERSION,
120+ isDiscontinuity
121+ );
122+ },
123+ [_contracts, _point, buildNetworkSeed]
124+ ),
125+ useCallback(
126+ () => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
127+ [_point, syncDetails, syncRekeyDate]
128+ ),
129+ GAS_LIMITS.CONFIGURE_KEYS
130+ );
131+
132+ // only treat the transaction as completed once we also have keys to download
133+ const completed = _completed && keyfileAvailable;
134+
135+ return {
136+ completed,
137+ ndNetworkSeed,
138+ keyfileBind,
139+ ...rest,
140+ };
141+}
142+
143+export default function AdminNetworkingKeys() {
144+ const { push, names, pop } = useLocalRouter();
145+ const { pointCursor } = usePointCursor();
146+ const { getDetails, getRekeyDate } = usePointCache();
147+
148+ const point = need.point(pointCursor);
149+ const details = getDetails(point);
150+
151+ const hasKeys = details.matchWith({
152+ Nothing: () => false,
153+ Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
154+ });
155+
156+ const {
157+ isDefaultState,
158+ construct,
159+ unconstruct,
160+ broadcasting,
161+ completed,
162+ inputsLocked,
163+ bind,
164+ keyfileBind,
165+ } = useSetKeys();
166+
167+ const [showNetworkSeedInput, { data: showNetworkSeed }] = useCheckboxInput({
168+ name: 'shownetworkseed',
169+ label: 'Use Custom Network Seed',
170+ inverseLabel: 'Back to Derived Network Seed',
171+ initialValue: false,
172+ disabled: inputsLocked,
173+ });
174+
175+ const [
176+ networkSeedInput,
177+ { pass: validNetworkSeed, data: networkSeed },
178+ { reset: resetNetworkSeed },
179+ ] = useHexInput({
180+ name: 'networkseed',
181+ label: 'Network Seed (64 bytes)',
182+ length: 32, // 64 bytes
183+ disabled: inputsLocked,
184+ });
185+
186+ const [
187+ discontinuityInput,
188+ { pass: validDiscontinuity, data: isDiscontinuity },
189+ ] = useCheckboxInput({
190+ name: 'discontinuity',
191+ label: 'Trigger New Continuity Era',
192+ initialValue: false,
193+ disabled: inputsLocked,
194+ });
195+
196+ useEffect(() => {
197+ const nothingOrValidSeed =
198+ !showNetworkSeed || (showNetworkSeed && validNetworkSeed);
199+ if (nothingOrValidSeed && validDiscontinuity) {
200+ construct(networkSeed, isDiscontinuity);
201+ } else {
202+ unconstruct();
203+ }
204+ }, [
205+ construct,
206+ unconstruct,
207+ isDiscontinuity,
208+ networkSeed,
209+ showNetworkSeed,
210+ validDiscontinuity,
211+ validNetworkSeed,
212+ ]);
213+
214+ useEffect(() => {
215+ if (!showNetworkSeed) {
216+ resetNetworkSeed();
217+ }
218+ }, [resetNetworkSeed, showNetworkSeed]);
219+
220+ const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
221+
222+ const renderTitle = () => {
223+ if (completed) {
224+ return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
225+ }
226+
227+ if (broadcasting) {
228+ return 'Setting Network Keys...';
229+ }
230+
231+ if (!hasKeys) {
232+ return 'Networking keys have not yet been set.';
233+ }
234+
235+ return 'Networking';
236+ };
237+
238+ const renderNetworkKeySection = (title, key) => (
239+ <>
240+ <Grid.Item full as={H5} className="mt3 gray4">
241+ {title}
242+ </Grid.Item>
243+ {key.matchWith({
244+ Nothing: () => (
245+ <Grid.Item full as="code" className="f5 gray4">
246+ Unset
247+ </Grid.Item>
248+ ),
249+ Just: ({ value: key }) => (
250+ <>
251+ <Grid.Item full as="code" className="f5 wrap">
252+ 0x
253+ </Grid.Item>
254+ <Grid.Item full as="code" className="f5 wrap">
255+ {renderNetworkKey(key)}
256+ </Grid.Item>
257+ </>
258+ ),
259+ })}
260+ </>
261+ );
262+
263+ const renderDetail = (title, value) => (
264+ <Flex.Item as={Flex} col>
265+ <Flex.Item as={H5} className="gray4">
266+ {title}
267+ </Flex.Item>
268+ {value.matchWith({
269+ Nothing: () => (
270+ <Flex.Item as={Text} className="f5 gray4">
271+ Unset
272+ </Flex.Item>
273+ ),
274+ Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
275+ })}
276+ </Flex.Item>
277+ );
278+
279+ const renderDetails = () => {
280+ return (
281+ <>
282+ {renderNetworkKeySection(
283+ 'Authentication',
284+ details.chain(chainKeyProp('authenticationKey'))
285+ )}
286+ {renderNetworkKeySection(
287+ 'Encryption',
288+ details.chain(chainKeyProp('encryptionKey'))
289+ )}
290+ <Grid.Item full as={Flex} row justify="between" className="mt3">
291+ {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
292+ {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
293+ {renderDetail(
294+ 'Crypto Suite Ver.',
295+ details.map(d => d.cryptoSuiteVersion)
296+ )}
297+ </Grid.Item>
298+ {hasKeys && (
299+ <Grid.Item full as={Flex} row justify="between" className="mt3">
300+ {renderDetail(
301+ 'Last Set',
302+ getRekeyDate(point).map(date =>
303+ date.matchWith({
304+ Ok: r => formatDotsWithTime(r.value),
305+ Error: () => Nothing(),
306+ })
307+ )
308+ )}
309+ </Grid.Item>
310+ )}
311+ </>
312+ );
313+ };
314+
315+ return (
316+ <>
317+ <Grid>
318+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
319+
320+ <Grid.Item full as={ViewHeader}>
321+ {renderTitle()}
322+ </Grid.Item>
323+
324+ <Grid.Item full as={Text} className="mb3">
325+ {hasKeys
326+ ? 'Here are your public keys that authenticate your Arvo.'
327+ : 'Set your networking keys to authenticate with Arvo.'}
328+ </Grid.Item>
329+
330+ {completed && (
331+ <>
332+ <Grid.Item full as={NoticeBox} className="mb3">
333+ You need this keyfile to authenticate with Arvo.
334+ </Grid.Item>
335+ <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
336+ </>
337+ )}
338+
339+ {!completed && (
340+ <>
341+ <Grid.Item full as={ToggleInput} {...showNetworkSeedInput} />
342+ {showNetworkSeed && (
343+ <>
344+ <Grid.Item full as={NoticeBox} className="mb2">
345+ When using a custom network seed, you'll need to download your
346+ Arvo keyfile immediately after this transaction is
347+ completed—Multipass does not store your seed.
348+ </Grid.Item>
349+ <Grid.Item full as={Input} {...networkSeedInput} />
350+ </>
351+ )}
352+ <Grid.Item full as={CheckboxInput} {...discontinuityInput} />
353+ </>
354+ )}
355+
356+ <Grid.Item
357+ full
358+ as={InlineEthereumTransaction}
359+ {...bind}
360+ label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
361+ onReturn={() => pop()}
362+ />
363+
364+ {isDefaultState && renderDetails()}
365+
366+ {completed && (
367+ <>
368+ <Grid.Item full as={BootArvoButton} disabled />
369+ <Grid.Divider />
370+ </>
371+ )}
372+ </Grid>
373+
374+ {isDefaultState && (
375+ <FooterButton onClick={goRelocate} disabled>
376+ Relocate
377+ </FooterButton>
378+ )}
379+ </>
380+ );
381+}
src/views/Admin/AdminRedownload.jsnew file+74
@@ -0,0 +1,74 @@
1+import React, { useCallback, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import * as need from 'lib/need';
4+
5+import { usePointCursor } from 'store/pointCursor';
6+
7+import { DownloadButton, RestartButton } from 'components/Buttons';
8+import PaperRenderer from 'components/PaperRenderer';
9+
10+import { downloadWallet } from 'lib/invite';
11+
12+import { useWallet } from 'store/wallet';
13+import { Grid, P } from 'indigo-react';
14+import ViewHeader from 'components/ViewHeader';
15+import { useLocalRouter } from 'lib/LocalRouter';
16+import MiniBackButton from 'components/MiniBackButton';
17+
18+export default function AdminRedownload() {
19+ const { urbitWallet } = useWallet();
20+ const { pointCursor } = usePointCursor();
21+ const { pop } = useLocalRouter();
22+
23+ const _urbitWallet = need.wallet(urbitWallet);
24+ const point = need.point(pointCursor);
25+
26+ const [paper, setPaper] = useState(Nothing());
27+ const [downloaded, setDownloaded] = useState(false);
28+
29+ const doDownload = useCallback(() => {
30+ downloadWallet(paper.value);
31+ setDownloaded(true);
32+ }, [paper, setDownloaded]);
33+
34+ const goBack = useCallback(() => pop(), [pop]);
35+
36+ return (
37+ <>
38+ <Grid>
39+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
40+ <Grid.Item full as={ViewHeader}>
41+ Download Passport
42+ </Grid.Item>
43+ <Grid.Item full as={P}>
44+ After you’ve downloaded your passport, back up the ticket manually or
45+ store on a trusted device.
46+ </Grid.Item>
47+ {downloaded ? (
48+ <Grid.Item full as={RestartButton} solid onClick={goBack}>
49+ Done
50+ </Grid.Item>
51+ ) : (
52+ <Grid.Item
53+ full
54+ as={DownloadButton}
55+ solid
56+ disabled={Nothing.hasInstance(paper)}
57+ onClick={doDownload}>
58+ {paper.matchWith({
59+ Nothing: () => 'Printing and folding...',
60+ Just: _ => 'Download paper wallet',
61+ })}
62+ </Grid.Item>
63+ )}
64+ </Grid>
65+ <PaperRenderer
66+ point={point}
67+ wallet={_urbitWallet}
68+ callback={paper => {
69+ setPaper(Just(paper));
70+ }}
71+ />
72+ </>
73+ );
74+}
src/views/Admin/AdminReticket.jsnew file+64
@@ -0,0 +1,64 @@
1+import React, { useCallback, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, Flex } from 'indigo-react';
4+
5+import { LocalRouterProvider } from 'lib/LocalRouter';
6+import useRouter from 'lib/useRouter';
7+
8+import MiniBackButton from 'components/MiniBackButton';
9+import Steps from 'components/Steps';
10+
11+import ReticketConfirm from './Reticket/ReticketConfirm';
12+import ReticketDownload from './Reticket/ReticketDownload';
13+import ReticketVerify from './Reticket/ReticketVerify';
14+import ReticketExecute from './Reticket/ReticketExecute';
15+
16+const NAMES = {
17+ CONFIRM: 'CONFIRM',
18+ DOWNLOAD: 'DOWNLOAD',
19+ VERIFY: 'VERIFY',
20+ EXECUTE: 'EXECUTE',
21+};
22+
23+const VIEWS = {
24+ CONFIRM: ReticketConfirm,
25+ DOWNLOAD: ReticketDownload,
26+ VERIFY: ReticketVerify,
27+ EXECUTE: ReticketExecute,
28+};
29+
30+const STEPS = [NAMES.CONFIRM, NAMES.DOWNLOAD, NAMES.VERIFY, NAMES.RETICKET];
31+
32+export default function AdminReticket() {
33+ const { Route, ...router } = useRouter({
34+ names: NAMES,
35+ views: VIEWS,
36+ initialRoutes: [{ key: NAMES.CONFIRM }],
37+ });
38+
39+ const [newWallet, _setNewWallet] = useState(Nothing());
40+
41+ const setNewWallet = useCallback(
42+ (wallet, paper) => {
43+ _setNewWallet(
44+ Just({
45+ wallet,
46+ paper,
47+ })
48+ );
49+ },
50+ [_setNewWallet]
51+ );
52+
53+ return (
54+ <LocalRouterProvider value={router}>
55+ <Grid>
56+ <Grid.Item full as={Flex} row justify="between" align="center">
57+ <Flex.Item as={MiniBackButton} onClick={() => router.pop()} />
58+ <Flex.Item as={Steps} num={router.size} total={STEPS.length} />
59+ </Grid.Item>
60+ </Grid>
61+ <Route newWallet={newWallet} setNewWallet={setNewWallet} />
62+ </LocalRouterProvider>
63+ );
64+}
src/views/Admin/AdminSetProxy.jsnew file+205
@@ -0,0 +1,205 @@
1+import React, { useCallback, useEffect } from 'react';
2+import cn from 'classnames';
3+import { Grid, Text, Input, Flex, ToggleInput } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
5+
6+import { useNetwork } from 'store/network';
7+import { usePointCache } from 'store/pointCache';
8+import { usePointCursor } from 'store/pointCursor';
9+
10+import {
11+ PROXY_TYPE,
12+ proxyTypeToHuman,
13+ proxyTypeToHumanDescription,
14+} from 'lib/proxy';
15+import * as need from 'lib/need';
16+import { useLocalRouter } from 'lib/LocalRouter';
17+import { ETH_ZERO_ADDR, eqAddr, isZeroAddress } from 'lib/wallet';
18+import capitalize from 'lib/capitalize';
19+import { useAddressInput, useCheckboxInput } from 'lib/useInputs';
20+import useEthereumTransaction from 'lib/useEthereumTransaction';
21+
22+import ViewHeader from 'components/ViewHeader';
23+import MiniBackButton from 'components/MiniBackButton';
24+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
25+import { GAS_LIMITS } from 'lib/constants';
26+
27+const proxyFromDetails = (details, contracts, proxyType) => {
28+ switch (proxyType) {
29+ case PROXY_TYPE.MANAGEMENT:
30+ return details.managementProxy;
31+ case PROXY_TYPE.SPAWN:
32+ return details.spawnProxy;
33+ case PROXY_TYPE.TRANSFER:
34+ return details.transferProxy;
35+ case PROXY_TYPE.VOTING:
36+ if (eqAddr(details.votingProxy, contracts.delegatedSending.address)) {
37+ return `${details.votingProxy} (invites contract)`;
38+ } else {
39+ return details.votingProxy;
40+ }
41+ default:
42+ throw new Error(`Unknown proxyType: ${proxyType}`);
43+ }
44+};
45+
46+function useSetProxy(proxyType) {
47+ const { contracts } = useNetwork();
48+ const { pointCursor } = usePointCursor();
49+ const { syncDetails } = usePointCache();
50+
51+ const _contracts = need.contracts(contracts);
52+ const _point = need.point(pointCursor);
53+
54+ const { construct, ...rest } = useEthereumTransaction(
55+ useCallback(
56+ address => {
57+ const txArgs = [_contracts, _point, address];
58+
59+ switch (proxyType) {
60+ case PROXY_TYPE.MANAGEMENT:
61+ return azimuth.ecliptic.setManagementProxy(...txArgs);
62+ case PROXY_TYPE.SPAWN:
63+ return azimuth.ecliptic.setSpawnProxy(...txArgs);
64+ case PROXY_TYPE.TRANSFER:
65+ return azimuth.ecliptic.setTransferProxy(...txArgs);
66+ case PROXY_TYPE.VOTING:
67+ return azimuth.ecliptic.setVotingProxy(...txArgs);
68+ default:
69+ throw new Error(`Unknown proxyType ${proxyType}`);
70+ }
71+ },
72+ [_contracts, _point, proxyType]
73+ ),
74+ useCallback(() => syncDetails(_point), [_point, syncDetails]),
75+ GAS_LIMITS.SET_PROXY
76+ );
77+
78+ // force-unset
79+ const unset = useCallback(() => {
80+ construct(ETH_ZERO_ADDR);
81+ }, [construct]);
82+
83+ return {
84+ construct,
85+ unset,
86+ ...rest,
87+ };
88+}
89+
90+export default function AdminSetProxy() {
91+ const { data, pop } = useLocalRouter();
92+ const { getDetails } = usePointCache();
93+ const { pointCursor } = usePointCursor();
94+ const { contracts } = useNetwork();
95+
96+ const _point = need.point(pointCursor);
97+ const _details = need.details(getDetails(_point));
98+ const _contracts = need.contracts(contracts);
99+
100+ const properProxyType = capitalize(proxyTypeToHuman(data.proxyType));
101+
102+ const {
103+ construct,
104+ unconstruct,
105+ unset,
106+ inputsLocked,
107+ completed,
108+ bind,
109+ } = useSetProxy(data.proxyType);
110+
111+ const [unsetInput, { data: isUnsetting }] = useCheckboxInput({
112+ name: 'unset',
113+ label: 'Unset',
114+ inverseLabel: 'Specify',
115+ initialValue: false,
116+ disabled: inputsLocked,
117+ });
118+ const [
119+ addressInput,
120+ { pass: validAddress, data: address },
121+ { reset: resetAddress },
122+ ] = useAddressInput({
123+ name: 'address',
124+ label: `New ${properProxyType} Address`,
125+ disabled: inputsLocked || isUnsetting,
126+ });
127+
128+ useEffect(() => {
129+ if (isUnsetting) {
130+ unset();
131+ resetAddress();
132+ } else if (validAddress) {
133+ construct(address);
134+ } else {
135+ unconstruct();
136+ }
137+ }, [
138+ validAddress,
139+ address,
140+ construct,
141+ isUnsetting,
142+ unset,
143+ unconstruct,
144+ resetAddress,
145+ ]);
146+
147+ const proxyAddress = proxyFromDetails(_details, _contracts, data.proxyType);
148+ const isProxySet = !isZeroAddress(proxyAddress);
149+
150+ const proxyAddressLabel = `${
151+ completed ? 'New' : 'Current'
152+ } ${properProxyType} Address`;
153+
154+ return (
155+ <Grid>
156+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
157+
158+ <Grid.Item full as={ViewHeader}>
159+ {properProxyType} Address
160+ </Grid.Item>
161+
162+ <Grid.Item full as={Text} className="mb4 f5">
163+ {proxyTypeToHumanDescription(data.proxyType)}
164+ </Grid.Item>
165+
166+ <Grid.Item
167+ full
168+ as={Text}
169+ className={cn('f6 mb1', {
170+ green3: completed,
171+ })}>
172+ {proxyAddressLabel}
173+ </Grid.Item>
174+
175+ <Grid.Item full as={Flex} row justify="between" align="center">
176+ <Flex.Item
177+ flex
178+ as={Text}
179+ className={cn('mono', {
180+ black: !completed && isProxySet,
181+ gray4: !completed && !isProxySet,
182+ green3: completed,
183+ })}>
184+ {isProxySet ? proxyAddress : 'Unset'}
185+ </Flex.Item>
186+ {!completed && isProxySet && (
187+ <Flex.Item as={ToggleInput} {...unsetInput} />
188+ )}
189+ </Grid.Item>
190+
191+ {completed ? (
192+ <Grid.Item full className="mb4" />
193+ ) : (
194+ <Grid.Item full as={Input} {...addressInput} className="mv4" />
195+ )}
196+
197+ <Grid.Item
198+ full
199+ as={InlineEthereumTransaction}
200+ {...bind}
201+ onReturn={() => pop()}
202+ />
203+ </Grid>
204+ );
205+}
src/views/Admin/AdminTransfer.jsnew file+94
@@ -0,0 +1,94 @@
1+import React, { useCallback, useEffect } from 'react';
2+import cn from 'classnames';
3+import { Grid, Text, Input } from 'indigo-react';
4+import * as 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 { useLocalRouter } from 'lib/LocalRouter';
12+import { useAddressInput } from 'lib/useInputs';
13+import useCurrentPointName from 'lib/useCurrentPointName';
14+import useEthereumTransaction from 'lib/useEthereumTransaction';
15+import { GAS_LIMITS } from 'lib/constants';
16+
17+import ViewHeader from 'components/ViewHeader';
18+import MiniBackButton from 'components/MiniBackButton';
19+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
20+
21+function useTransfer() {
22+ const { contracts } = useNetwork();
23+ const { pointCursor } = usePointCursor();
24+ const { syncDetails } = usePointCache();
25+
26+ const _contracts = need.contracts(contracts);
27+ const _point = need.point(pointCursor);
28+
29+ return useEthereumTransaction(
30+ useCallback(
31+ address => azimuth.ecliptic.setTransferProxy(_contracts, _point, address),
32+ [_contracts, _point]
33+ ),
34+ useCallback(() => syncDetails(_point), [_point, syncDetails]),
35+ GAS_LIMITS.SET_PROXY
36+ );
37+}
38+
39+export default function AdminTransfer() {
40+ const { pop } = useLocalRouter();
41+ const name = useCurrentPointName();
42+
43+ const {
44+ construct,
45+ unconstruct,
46+ completed,
47+ inputsLocked,
48+ bind,
49+ } = useTransfer();
50+
51+ const [addressInput, { pass, data: address }] = useAddressInput({
52+ name: 'address',
53+ label: `Ethereum Address`,
54+ disabled: inputsLocked,
55+ });
56+
57+ useEffect(() => {
58+ if (pass) {
59+ construct(address);
60+ } else {
61+ unconstruct();
62+ }
63+ }, [pass, address, construct, unconstruct]);
64+
65+ return (
66+ <Grid>
67+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
68+ <Grid.Item full as={ViewHeader}>
69+ Transfer Point
70+ </Grid.Item>
71+ <Grid.Item
72+ full
73+ as={Text}
74+ className={cn('f5', {
75+ green3: completed,
76+ })}>
77+ {completed
78+ ? `${address} is now the Transfer Proxy for ${name} and can accept the transfer by logging into Multipass themselves. Until they accept your transfer, you will still have ownership over ${name}.`
79+ : `Transfer ${name} to a new owner.`}
80+ </Grid.Item>
81+
82+ {!completed && (
83+ <Grid.Item full as={Input} {...addressInput} className="mv4" />
84+ )}
85+
86+ <Grid.Item
87+ full
88+ as={InlineEthereumTransaction}
89+ {...bind}
90+ onReturn={() => pop()}
91+ />
92+ </Grid>
93+ );
94+}
src/views/Admin/Reticket/ReticketConfirm.jsnew file+90
@@ -0,0 +1,90 @@
1+import React, { useState, useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, Text } from 'indigo-react';
4+import * as need from 'lib/need';
5+
6+import { usePointCursor } from 'store/pointCursor';
7+
8+import useLifecycle from 'lib/useLifecycle';
9+import { generateWallet } from 'lib/invite';
10+import { useLocalRouter } from 'lib/LocalRouter';
11+import { isDevelopment } from 'lib/flags';
12+
13+import { ForwardButton } from 'components/Buttons';
14+import PaperRenderer from 'components/PaperRenderer';
15+import WarningBox from 'components/WarningBox';
16+import { blinkIf } from 'components/Blinky';
17+import useCurrentPointName from 'lib/useCurrentPointName';
18+
19+export default function ReticketConfirm({ newWallet, setNewWallet }) {
20+ const { push, names } = useLocalRouter();
21+ const { pointCursor } = usePointCursor();
22+ const point = need.point(pointCursor);
23+ const name = useCurrentPointName();
24+
25+ const [generatedWallet, setGeneratedWallet] = useState(Nothing());
26+
27+ // generating new wallet on mount, conveniently using it to 'force' users
28+ // to read the disclaimer text
29+ //TODO also check we can pay for the transactions?
30+ useLifecycle(() => {
31+ let mounted = true;
32+ (async () => {
33+ if (Just.hasInstance(newWallet)) {
34+ // skip if already computed (hitting the back button for example)
35+ return;
36+ }
37+
38+ const wal = await generateWallet(point);
39+ if (isDevelopment) {
40+ console.log(`The new ticket for ${name} is ${wal.ticket}`);
41+ }
42+ if (!mounted) {
43+ return;
44+ }
45+
46+ setGeneratedWallet(Just(wal));
47+ })();
48+
49+ return () => (mounted = false);
50+ });
51+
52+ const goDownload = useCallback(() => push(names.DOWNLOAD), [push, names]);
53+
54+ const paperRenderer = generatedWallet.matchWith({
55+ Nothing: () => null,
56+ Just: ({ value: wallet }) => (
57+ <PaperRenderer
58+ point={point}
59+ wallet={wallet}
60+ callback={paper => {
61+ setNewWallet(wallet, paper);
62+ }}
63+ />
64+ ),
65+ });
66+
67+ return (
68+ <Grid gap={4} className="mt4">
69+ <Grid.Item full as={Text}>
70+ Reticketing is the process of generating a completely fresh wallet and
71+ transferring ownership of your point to that wallet.
72+ </Grid.Item>
73+ <Grid.Item full as={WarningBox}>
74+ Beware, this resets your proxy addresses; if you're using smart
75+ contracts, this might break them! It will also change your networking
76+ keys!
77+ </Grid.Item>
78+ <Grid.Item
79+ full
80+ as={ForwardButton}
81+ solid
82+ accessory={blinkIf(Nothing.hasInstance(newWallet))}
83+ disabled={Nothing.hasInstance(newWallet)}
84+ onClick={goDownload}>
85+ I understand, continue
86+ </Grid.Item>
87+ {paperRenderer}
88+ </Grid>
89+ );
90+}
src/views/Admin/Reticket/ReticketDownload.jsnew file+38
@@ -0,0 +1,38 @@
1+import React, { useState, useCallback } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+import { Grid, Text } from 'indigo-react';
4+
5+import { useLocalRouter } from 'lib/LocalRouter';
6+import { downloadWallet } from 'lib/invite';
7+
8+import { ForwardButton, DownloadButton } from 'components/Buttons';
9+
10+export default function ReticketDownload({ newWallet }) {
11+ const { push, names } = useLocalRouter();
12+
13+ const [downloaded, setDownloaded] = useState(false);
14+
15+ const download = () => {
16+ downloadWallet(newWallet.value.paper);
17+ setDownloaded(true);
18+ };
19+
20+ const goVerify = useCallback(() => push(names.VERIFY), [push, names]);
21+
22+ return (
23+ <Grid gap={4} className="mt4">
24+ <Grid.Item full as={Text}>
25+ Download the thing, and keep it somewhere safe!
26+ </Grid.Item>
27+ <Grid.Item
28+ full
29+ as={!downloaded ? DownloadButton : ForwardButton}
30+ disabled={Nothing.hasInstance(newWallet)}
31+ onClick={!downloaded ? download : goVerify}
32+ success={downloaded}
33+ solid>
34+ {!downloaded ? 'Download Passport' : 'Continue'}
35+ </Grid.Item>
36+ </Grid>
37+ );
38+}
src/views/Admin/Reticket/ReticketExecute.jsnew file+185
@@ -0,0 +1,185 @@
1+import React, { useState, useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, H4, Text, ErrorText } from 'indigo-react';
4+import { fromWei } from 'web3-utils';
5+
6+import {
7+ reticketPointBetweenWallets,
8+ TRANSACTION_PROGRESS,
9+} from 'lib/reticket';
10+import * as need from 'lib/need';
11+import useLifecycle from 'lib/useLifecycle';
12+
13+import { useNetwork } from 'store/network';
14+import { useWallet } from 'store/wallet';
15+import { usePointCursor } from 'store/pointCursor';
16+import { usePointCache } from 'store/pointCache';
17+import { useHistory } from 'store/history';
18+
19+import { RestartButton, ForwardButton } from 'components/Buttons';
20+import WarningBox from 'components/WarningBox';
21+import LoadingBar from 'components/LoadingBar';
22+import Highlighted from 'components/Highlighted';
23+import { WALLET_TYPES } from 'lib/wallet';
24+
25+const labelForProgress = progress => {
26+ if (progress <= 0) {
27+ return 'Starting...';
28+ } else if (progress <= TRANSACTION_PROGRESS.GENERATING) {
29+ return 'Generating Transactions...';
30+ } else if (progress <= TRANSACTION_PROGRESS.SIGNING) {
31+ return 'Signing Transactions...';
32+ } else if (progress <= TRANSACTION_PROGRESS.FUNDING) {
33+ return 'Funding Transactions...';
34+ } else if (progress <= TRANSACTION_PROGRESS.TRANSFERRING) {
35+ return 'Transferring Point...';
36+ } else if (progress <= TRANSACTION_PROGRESS.CLEANING) {
37+ return 'Cleaning Up...';
38+ } else if (progress <= TRANSACTION_PROGRESS.DONE) {
39+ return 'Done';
40+ }
41+};
42+
43+export default function ReticketExecute({ newWallet, setNewWallet }) {
44+ const { popTo, names, reset } = useHistory();
45+ const { web3, contracts } = useNetwork();
46+ const { wallet, setWalletType, resetWallet, setUrbitWallet } = useWallet();
47+ const { pointCursor } = usePointCursor();
48+ const { getDetails } = usePointCache();
49+
50+ const [generalError, setGeneralError] = useState();
51+ const [progress, setProgress] = useState(0);
52+ const [needFunds, setNeedFunds] = useState();
53+ const isDone = progress >= 1.0;
54+
55+ // start reticketing transactions on mount
56+ useLifecycle(() => {
57+ (async () => {
58+ const point = need.point(pointCursor);
59+ const details = need.details(getDetails(point));
60+ const networkRevision = parseInt(details.keyRevisionNumber, 10);
61+ try {
62+ await reticketPointBetweenWallets({
63+ fromWallet: need.wallet(wallet),
64+ toWallet: newWallet.value.wallet,
65+ point: point,
66+ web3: need.web3(web3),
67+ contracts: need.contracts(contracts),
68+ onUpdate: handleUpdate,
69+ nextRevision: networkRevision + 1,
70+ });
71+ } catch (err) {
72+ setGeneralError(err);
73+ }
74+ })();
75+ });
76+
77+ const goToRestart = useCallback(() => {
78+ reset();
79+ setNewWallet(Nothing());
80+ }, [reset, setNewWallet]);
81+
82+ const loginAndGoHome = useCallback(() => {
83+ // set wallet state
84+ resetWallet();
85+ setWalletType(WALLET_TYPES.TICKET);
86+ setUrbitWallet(Just(newWallet.value.wallet));
87+ // (implicit) pointCursor stays the same
88+ // rediect to point
89+ popTo(names.POINT);
90+ }, [resetWallet, setWalletType, setUrbitWallet, newWallet, popTo, names]);
91+
92+ const handleUpdate = useCallback(
93+ ({ type, state, value }) => {
94+ switch (type) {
95+ case 'progress':
96+ return setProgress(state);
97+ case 'askFunding':
98+ return setNeedFunds(value);
99+ case 'gotFunding':
100+ return setNeedFunds(false);
101+ default:
102+ console.error(`Unknown update: ${type}`);
103+ }
104+ },
105+ [setProgress, setNeedFunds]
106+ );
107+
108+ const renderAdditionalInfo = () => {
109+ if (generalError) {
110+ return (
111+ <>
112+ <Grid.Item full>
113+ <ErrorText>{generalError.message.toString()}</ErrorText>
114+ </Grid.Item>
115+ <Grid.Item
116+ full
117+ className="mt3"
118+ as={RestartButton}
119+ solid
120+ onClick={goToRestart}>
121+ Restart
122+ </Grid.Item>
123+ </>
124+ );
125+ }
126+
127+ if (needFunds) {
128+ return (
129+ <Grid.Item full>
130+ <Highlighted>
131+ The address {needFunds.address} needs at least{' '}
132+ {fromWei(needFunds.minBalance)} ETH and currently has{' '}
133+ {fromWei(needFunds.balance)} ETH. Waiting until the account has
134+ enough funds.
135+ </Highlighted>
136+ </Grid.Item>
137+ );
138+ }
139+
140+ return (
141+ <Grid.Item full as={WarningBox}>
142+ Never give your Master Ticket to anyone
143+ </Grid.Item>
144+ );
145+ };
146+
147+ return (
148+ <Grid gap={4} className="mt4">
149+ <Grid.Item full as={H4}>
150+ {labelForProgress(progress)}
151+ </Grid.Item>
152+
153+ {isDone ? (
154+ <>
155+ <Grid.Item full as={Text}>
156+ Your changes are now reflected on-chain and you can use the new
157+ ticket to manage your point.
158+ </Grid.Item>
159+ <Grid.Item
160+ full
161+ as={ForwardButton}
162+ solid
163+ accessory="↺"
164+ onClick={loginAndGoHome}>
165+ Login with New Master Ticket
166+ </Grid.Item>
167+ </>
168+ ) : (
169+ <>
170+ <Grid.Item full as={Grid} className="mt3" gap={3}>
171+ <Grid.Item full as={LoadingBar} progress={progress} />
172+ <Grid.Item full>
173+ <Text className="f5 green4">
174+ This process can take up to 5 minutes to complete. Don't leave
175+ this page until the process is complete.
176+ </Text>
177+ </Grid.Item>
178+ </Grid.Item>
179+
180+ {renderAdditionalInfo()}
181+ </>
182+ )}
183+ </Grid>
184+ );
185+}
src/views/Admin/Reticket/ReticketVerify.js+2−2
@@ -1,13 +1,13 @@
11import React, { useCallback, useMemo } from 'react';
22import { Input, Text, Grid } from 'indigo-react';
33
4−import { ForwardButton } from 'components/Buttons';
5−
64import { useTicketInput } from 'lib/useInputs';
75import { useLocalRouter } from 'lib/LocalRouter';
86import { validateExactly } from 'lib/validators';
97import { isDevelopment } from 'lib/flags';
108
9+import { ForwardButton } from 'components/Buttons';
10+
1111const STUB_VERIFY_TICKET = isDevelopment;
1212
1313export default function ReticketVerify({ newWallet }) {
src/views/CancelTransfer.jsdeleted−81
@@ -1,81 +0,0 @@
1−import React from 'react';
2−import { Just } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−
6−import { H1, P } from '../components/old/Base';
7−import * as need from '../lib/need';
8−
9−import StatelessTransaction from '../components/old/StatelessTransaction';
10−import { ETH_ZERO_ADDR } from '../lib/wallet';
11−import { withNetwork } from '../store/network';
12−import { compose } from '../lib/lib';
13−import { withPointCursor } from '../store/pointCursor';
14−import { withPointCache } from '../store/pointCache';
15−import View from 'components/View';
16−
17−class CancelTransfer extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− const pointInTransfer = need.point(props.pointCursor);
22−
23− this.state = {
24− proxyAddress: '',
25− pointInTransfer: pointInTransfer,
26− };
27−
28− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
29− }
30−
31− createUnsignedTxn() {
32− const { props } = this;
33−
34− const validContracts = need.contracts(props.contracts);
35− const validPoint = need.point(props.pointCursor);
36− //TODO this.state.pointInTransfer ?
37−
38− const txn = azimuth.ecliptic.setTransferProxy(
39− validContracts,
40− validPoint,
41− ETH_ZERO_ADDR
42− );
43−
44− return Just(txn);
45− }
46−
47− render() {
48− const { props, state } = this;
49−
50− const online = Just.hasInstance(props.web3);
51−
52− const proxy = online
53− ? props.pointCache[state.pointInTransfer].transferProxy
54− : 'any outgoing addresses';
55−
56− // const canGenerate = validAddress === true
57−
58− const canGenerate = true;
59−
60− return (
61− <View>
62− <H1>
63− {'Cancel Transfer of '}{' '}
64− <code>{` ${ob.patp(state.pointInTransfer)} `}</code>
65− </H1>
66−
67− <P>{`This action will cancel the transfer to ${proxy}.`}</P>
68− <StatelessTransaction
69− canGenerate={canGenerate}
70− createUnsignedTxn={this.createUnsignedTxn}
71− />
72− </View>
73− );
74− }
75−}
76−
77−export default compose(
78− withNetwork,
79− withPointCursor,
80− withPointCache
81−)(CancelTransfer);
src/views/CreateGalaxy.js+154−190
@@ -1,198 +1,162 @@
1−import React from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
1+import React, { useCallback, useEffect, useState } from 'react';
2+import { Nothing, Just } from 'folktale/maybe';
3+import cn from 'classnames';
4+import { Grid, Text, Input } from 'indigo-react';
35import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−
7−import { Button } from '../components/old/Base';
8−import { H1, P, Anchor } from '../components/old/Base';
9−import {
10− InnerLabel,
11− GalaxyInput,
12− AddressInput,
13− ValidatedSigil,
14− ShowBlockie,
15−} from '../components/old/Base';
16−import StatelessTransaction from '../components/old/StatelessTransaction';
17−
18−import { NETWORK_TYPES } from '../lib/network';
19−import { canDecodePatp } from '../lib/txn';
20−
21−import { isValidAddress, isZeroAddress } from '../lib/wallet';
22−
23−import { isValidGalaxy, compose } from '../lib/lib';
24−import { withNetwork } from '../store/network';
25−import { withWallet } from '../store/wallet';
6+
7+import { useNetwork } from 'store/network';
8+import { useHistory } from 'store/history';
9+import { usePointCache } from 'store/pointCache';
10+
11+import * as need from 'lib/need';
12+import { useAddressInput, useGalaxyInput } from 'lib/useInputs';
13+import useEthereumTransaction from 'lib/useEthereumTransaction';
14+import { GAS_LIMITS } from 'lib/constants';
15+import patp2dec from 'lib/patp2dec';
16+
17+import ViewHeader from 'components/ViewHeader';
18+import MiniBackButton from 'components/MiniBackButton';
19+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
2620import View from 'components/View';
21+import { isZeroAddress } from 'lib/wallet';
22+
23+function useCreateGalaxy() {
24+ const { contracts } = useNetwork();
25+ const { syncKnownPoint } = usePointCache();
26+
27+ const _contracts = need.contracts(contracts);
28+
29+ const [galaxy, setGalaxy] = useState();
30+
31+ return useEthereumTransaction(
32+ useCallback(
33+ (galaxy, owner) => {
34+ setGalaxy(galaxy);
35+ return azimuth.ecliptic.createGalaxy(_contracts, galaxy, owner);
36+ },
37+ [_contracts]
38+ ),
39+ useCallback(() => syncKnownPoint(galaxy), [galaxy, syncKnownPoint]),
40+ GAS_LIMITS.DEFAULT
41+ );
42+}
2743
28−const buttonTriState = status => {
29− if (status === null) return 'blue';
30− if (status === false) return 'yellow';
31− if (status === true) return 'green';
32−};
33−
34−const buttonTriStateText = status => {
35− if (status === null) return 'Confirm Galaxy Availablility';
36− if (status === false) return 'Galaxy is Not Available';
37− if (status === true) return 'Galaxy is Available';
38−};
39−
40−class CreateGalaxy extends React.Component {
41− constructor(props) {
42− super(props);
43−
44− const galaxyOwner = need.addressFromWallet(props.wallet);
45−
46− this.state = {
47− galaxyOwner: galaxyOwner,
48− galaxyName: '',
49− isAvailable: null,
50− };
51−
52− this.handleAddressInput = this.handleAddressInput.bind(this);
53− this.handleGalaxyNameInput = this.handleGalaxyNameInput.bind(this);
54− this.confirmAvailability = this.confirmAvailability.bind(this);
55− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
56− this.statelessRef = React.createRef();
57− }
58−
59− handleGalaxyNameInput = galaxyName => {
60− this.setState({ galaxyName, isAvailable: null });
61− this.statelessRef.current.clearTxn();
62− };
63−
64− handleAddressInput = galaxyOwner => {
65− this.setState({ galaxyOwner });
66− this.statelessRef.current.clearTxn();
67− };
68−
69− createUnsignedTxn = () => {
70− const { state, props } = this;
71− if (isValidAddress(state.galaxyOwner) === false) return Nothing();
72− if (state.isAvailable === false) return Nothing();
73− if (canDecodePatp(state.galaxyName) === false) return Nothing();
74− if (isValidGalaxy(state.galaxyName) === false) return Nothing();
75−
76− const validContracts = need.contracts(props.contracts);
77−
78− const galaxyDec = ob.patp2dec(state.galaxyName);
79−
80− const txn = azimuth.ecliptic.createGalaxy(
81− validContracts,
82− galaxyDec,
83− state.galaxyOwner
84− );
85−
86− return Just(txn);
87− };
88−
89− confirmAvailability = async () => {
90− const { state, props } = this;
91−
92− if (canDecodePatp(state.galaxyName) === false) {
93− this.setState({ isAvailable: false });
44+export default function CreateGalaxy() {
45+ const { pop } = useHistory();
46+ const { contracts } = useNetwork();
47+ const _contracts = need.contracts(contracts);
48+
49+ const [error, setError] = useState();
50+ const [isAvailable, setIsAvailable] = useState(Nothing());
51+
52+ const {
53+ construct,
54+ unconstruct,
55+ completed,
56+ inputsLocked,
57+ bind,
58+ } = useCreateGalaxy();
59+
60+ const [
61+ galaxyNameInput,
62+ { pass: validGalaxyName, syncPass: syncValidGalaxyName, value: galaxyName },
63+ // ^ we use value: here so our effect runs onChange
64+ ] = useGalaxyInput({
65+ name: 'galaxy',
66+ disabled: inputsLocked,
67+ autoFocus: true,
68+ error:
69+ error ||
70+ isAvailable.matchWith({
71+ Nothing: () => 'Loading availability...', // TODO: make async loading?
72+ Just: p => (p.value ? undefined : 'This galaxy is already owned.'),
73+ }),
74+ });
75+
76+ const [ownerInput, { pass: validOwner, data: owner }] = useAddressInput({
77+ name: 'owner',
78+ label: `Ethereum Address`,
79+ disabled: inputsLocked,
80+ });
81+
82+ useEffect(() => {
83+ if (validGalaxyName && validOwner) {
84+ construct(patp2dec(galaxyName), owner);
85+ } else {
86+ unconstruct();
87+ }
88+ }, [owner, construct, unconstruct, validGalaxyName, validOwner, galaxyName]);
89+
90+ useEffect(() => {
91+ if (!syncValidGalaxyName || inputsLocked) {
9492 return;
9593 }
9694
97− const validContracts = need.contracts(props.contracts);
98−
99− const galaxyDec = ob.patp2dec(state.galaxyName);
100−
101− const currentOwner = await azimuth.azimuth.getOwner(
102− validContracts,
103− galaxyDec
104− );
105−
106− const available = isZeroAddress(currentOwner);
107−
108− this.setState({ isAvailable: available });
109− };
110−
111− render() {
112− const { props, state } = this;
113−
114− const validAddress = isValidAddress(state.galaxyOwner);
115− const validGalaxy = isValidGalaxy(state.galaxyName);
116−
117− const canGenerate =
118− validAddress === true &&
119− validGalaxy === true &&
120− state.isAvailable === true;
121−
122− const esvisible =
123− props.networkType === NETWORK_TYPES.ROPSTEN ||
124− props.networkType === NETWORK_TYPES.MAINNET;
125−
126− const esdomain =
127− props.networkType === NETWORK_TYPES.ROPSTEN
128− ? 'ropsten.etherscan.io'
129− : 'etherscan.io';
130−
131− return (
132− <View>
133− <H1> {'Create a Galaxy'} </H1>
134−
135− <P>
136− {'Enter the galaxy to create and the address that will own ' +
137− 'it (defaulting to this account, if not provided).'}
138− </P>
139−
140− <GalaxyInput
141− className="mono"
142− prop-size="lg"
143− prop-format="innerLabel"
144− autoFocus
145− placeholder="e.g. ~zod"
146− value={state.galaxyName}
147− onChange={v => this.handleGalaxyNameInput(v)}>
148− <InnerLabel>{'Galaxy Name'}</InnerLabel>
149− <ValidatedSigil
150− className="tr-0 mt-05 mr-0 abs"
151− patp={state.galaxyName}
152− size={68}
153− margin={8}
154− />
155− </GalaxyInput>
156−
157− <AddressInput
158− className="mono mt-8"
159− prop-size="lg"
160− prop-format="innerLabel"
161− value={state.galaxyOwner}
162− onChange={v => this.handleAddressInput(v)}>
163− <InnerLabel>{'Address that will own this galaxy'}</InnerLabel>
164− <ShowBlockie className={'mt-1'} address={state.galaxyOwner} />
165− </AddressInput>
166−
167− <Anchor
168− className={'mt-1'}
169− prop-size={'sm'}
170− prop-disabled={!validAddress || !esvisible}
171− target={'_blank'}
172− href={`https://${esdomain}/address/${state.galaxyOwner}`}>
173− {'View on Etherscan ↗'}
174− </Anchor>
175−
176− <Button
177− prop-size="lg wide"
178− className="mt-8"
179− prop-color={buttonTriState(state.isAvailable)}
180− disabled={!validGalaxy}
181− onClick={() => this.confirmAvailability()}>
182− {buttonTriStateText(state.isAvailable)}
183− </Button>
184−
185− <StatelessTransaction
186− ref={this.statelessRef}
187− canGenerate={canGenerate}
188− createUnsignedTxn={this.createUnsignedTxn}
95+ setError();
96+ setIsAvailable(Nothing());
97+
98+ let cancelled = false;
99+
100+ (async () => {
101+ try {
102+ const currentOwner = await azimuth.azimuth.getOwner(
103+ _contracts,
104+ patp2dec(galaxyName)
105+ );
106+
107+ const isAvailable = isZeroAddress(currentOwner);
108+
109+ if (cancelled) {
110+ return;
111+ }
112+
113+ setIsAvailable(Just(isAvailable));
114+ } catch (error) {
115+ console.error(error);
116+ setError(error.message);
117+ setIsAvailable(Just(false));
118+ }
119+ })();
120+
121+ return () => (cancelled = true);
122+ }, [
123+ _contracts,
124+ galaxyName,
125+ inputsLocked,
126+ setIsAvailable,
127+ syncValidGalaxyName,
128+ ]);
129+
130+ return (
131+ <View inset>
132+ <Grid>
133+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
134+
135+ <Grid.Item full as={ViewHeader}>
136+ Create a Galaxy
137+ </Grid.Item>
138+
139+ {completed && (
140+ <Grid.Item
141+ full
142+ as={Text}
143+ className={cn('f5', {
144+ green3: completed,
145+ })}>
146+ {galaxyName} has been created and can be claimed by {owner}.
147+ </Grid.Item>
148+ )}
149+
150+ <Grid.Item full as={Input} {...galaxyNameInput} className="mt4" />
151+ <Grid.Item full as={Input} {...ownerInput} className="mb4" />
152+
153+ <Grid.Item
154+ full
155+ as={InlineEthereumTransaction}
156+ {...bind}
157+ onReturn={() => pop()}
189158 />
190− </View>
191− );
192− }
159+ </Grid>
160+ </View>
161+ );
193162}
194−
195−export default compose(
196− withNetwork,
197− withWallet
198−)(CreateGalaxy);
src/views/GenKeyfile.js+1−1
@@ -33,7 +33,7 @@ class GenKeyfile extends React.Component {
3333 // in case we did SetKeys earlier this session, make sure to generate the
3434 // newer keyfile, rather than the one that will expire soon
3535 const revision = this.props.networkRevision.matchWith({
36− Nothing: () => parseInt(pointDetails.keyRevisionNumber),
36+ Nothing: () => parseInt(pointDetails.keyRevisionNumber, 10),
3737 Just: p => p.value,
3838 });
3939
src/views/Invite.js+9−7
@@ -1,6 +1,6 @@
11import React from 'react';
22import * as need from 'lib/need';
3−import { Grid, H5, Text } from 'indigo-react';
3+import { Grid, Text } from 'indigo-react';
44
55import { usePointCursor } from 'store/pointCursor';
66import { useHistory } from 'store/history';
@@ -18,6 +18,7 @@ import { LocalRouterProvider } from 'lib/LocalRouter';
1818
1919import InviteUrl from './Invite/InviteUrl';
2020import InviteEmail from './Invite/InviteEmail';
21+import ViewHeader from 'components/ViewHeader';
2122
2223const NAMES = {
2324 URL: 'URL',
@@ -52,10 +53,11 @@ export default function Invite() {
5253 const acceptedInvitesText = matchBlinky(acceptedInvites);
5354
5455 return (
55− <LocalRouterProvider value={router}>
56− <View>
56+ <View inset>
57+ <LocalRouterProvider value={router}>
5758 <Grid className="mb4">
5859 <Grid.Item
60+ full
5961 as={Crumbs}
6062 routes={[
6163 {
@@ -66,9 +68,9 @@ export default function Invite() {
6668 text: 'Invite',
6769 },
6870 ]}
69− full
71+ className="mb4"
7072 />
71− <Grid.Item as={H5} className="mv4" full>
73+ <Grid.Item full as={ViewHeader}>
7274 Invite
7375 </Grid.Item>
7476 <Grid.Item as={Text} className="f5" full>
@@ -81,7 +83,7 @@ export default function Invite() {
8183 </Grid.Item>
8284 </Grid>
8385 <Route />
84− </View>
85− </LocalRouterProvider>
86+ </LocalRouterProvider>
87+ </View>
8688 );
8789}
src/views/Invite/InviteEmail.js+25−19
@@ -13,6 +13,7 @@ import {
1313 useForm,
1414} from 'indigo-react';
1515import { uniq } from 'lodash';
16+import { fromWei, toWei } from 'web3-utils';
1617
1718import { usePointCursor } from 'store/pointCursor';
1819import { useNetwork } from 'store/network';
@@ -25,14 +26,12 @@ import {
2526 sendSignedTransaction,
2627 waitForTransactionConfirm,
2728 hexify,
28− fromWei,
29− toWei,
3029} from 'lib/txn';
3130import * as tank from 'lib/tank';
3231import { useLocalRouter } from 'lib/LocalRouter';
3332import useArray from 'lib/useArray';
3433import { buildEmailInputConfig } from 'lib/useInputs';
35−import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
34+import { MIN_PLANET, GAS_LIMITS, DEFAULT_GAS_PRICE_GWEI } from 'lib/constants';
3635import * as need from 'lib/need';
3736import * as wg from 'lib/walletgen';
3837import useSetState from 'lib/useSetState';
@@ -43,9 +42,11 @@ import MiniBackButton from 'components/MiniBackButton';
4342import LoadableButton from 'components/LoadableButton';
4443import Highlighted from 'components/Highlighted';
4544
46−const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
4745const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
48−const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
46+const INVITE_COST = toWei(
47+ (DEFAULT_GAS_PRICE_GWEI * GAS_LIMIT).toString(),
48+ 'gwei'
49+);
4950const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
5051
5152const STATUS = {
@@ -121,7 +122,7 @@ export default function InviteEmail() {
121122 const [hovered, setHovered] = useSetState();
122123 const [invites, addInvite, clearInvites] = useSetState();
123124 const [receipts, addReceipt, clearReceipts] = useSetState();
124− const [errors, addError, clearErrors] = useSetState();
125+ const [errors, addError] = useSetState();
125126
126127 // manage general state that affects the whole form
127128 const [status, setStatus] = useState(STATUS.INPUT);
@@ -252,11 +253,10 @@ export default function InviteEmail() {
252253 networkType,
253254 // TODO: ^ make a useTransactionSigner to encapsulate this logic
254255 txn: inviteTx,
255− gasPrice: GAS_PRICE_GWEI.toString(),
256+ gasPrice: DEFAULT_GAS_PRICE_GWEI.toString(),
256257 gasLimit: GAS_LIMIT.toString(),
257258 nonce: nonce + i,
258259 chainId,
259− setStx: () => {},
260260 });
261261 const rawTx = hexify(signedTx.serialize());
262262
@@ -277,18 +277,19 @@ export default function InviteEmail() {
277277 );
278278 }
279279 }, [
280− point,
281280 contracts,
282281 web3,
283− addInvite,
282+ wallet,
283+ inputs,
284+ point,
284285 clearInvites,
286+ getHasReceived,
287+ addError,
285288 syncInvites,
286− inputs,
287− networkType,
288− wallet,
289289 walletType,
290290 walletHdPath,
291− addError,
291+ networkType,
292+ addInvite,
292293 ]);
293294
294295 const sendInvites = useCallback(async () => {
@@ -307,7 +308,7 @@ export default function InviteEmail() {
307308 inputs.map(input => invites[input.name].rawTx),
308309 (address, minBalance, balance) =>
309310 setNeedFunds({ address, minBalance, balance }),
310− () => setNeedFunds(false)
311+ () => setNeedFunds(undefined)
311312 );
312313
313314 setStatus(STATUS.SENDING);
@@ -320,8 +321,7 @@ export default function InviteEmail() {
320321 const txHash = await sendSignedTransaction(
321322 _web3,
322323 invite.signedTx,
323− tankWasUsed,
324− () => {}
324+ tankWasUsed
325325 );
326326
327327 // TODO: waitForTransactionConfirm never rejects
@@ -415,6 +415,14 @@ export default function InviteEmail() {
415415 }
416416 }, [isDone, syncInvites, point]);
417417
418+ useEffect(() => {
419+ for (const input of inputs) {
420+ if (input.pass) {
421+ syncHasReceivedForEmail(input.data);
422+ }
423+ }
424+ }, [inputs, syncHasReceivedForEmail]);
425+
418426 return (
419427 <Grid gap={3}>
420428 <Grid.Item as={Grid} full>
@@ -465,8 +473,6 @@ export default function InviteEmail() {
465473 as={Input}
466474 cols={[1, 11]}
467475 {...input}
468− onValue={email => syncHasReceivedForEmail(email)}
469− // NB(shrugs): ^ this feels like a hack?
470476 accessory={accessoryFor(input.name)}
471477 />
472478 {!isFirst &&
src/views/InvitesManage.jsdeleted−185
@@ -1,185 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import * as ob from 'urbit-ob';
4−import * as azimuth from 'azimuth-js';
5−import * as need from '../lib/need';
6−
7−import {
8− Warning,
9− Input,
10− PointInput,
11− InnerLabel,
12− ValidatedSigil,
13−} from '../components/old/Base';
14−import StatelessTransaction from '../components/old/StatelessTransaction';
15−import { withNetwork } from '../store/network';
16−import { compose } from '../lib/lib';
17−import { withPointCursor } from '../store/pointCursor';
18−import View from 'components/View';
19−
20−class InvitesManage extends React.Component {
21− constructor(props) {
22− super(props);
23−
24− this.state = {
25− invitesWillWork: Nothing(),
26− planetInput: '',
27− targetPlanet: Nothing(),
28− currentPoolSize: Nothing(),
29− cachedPoolSizes: {},
30− targetPoolSize: 5,
31− };
32−
33− this.checkCurrentInvites = this.checkCurrentInvites.bind(this);
34− this.handlePointInput = this.handlePointInput.bind(this);
35− this.updatePoolSize = this.updatePoolSize.bind(this);
36− this.handlePoolSizeInput = this.handlePoolSizeInput.bind(this);
37− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
38− this.statelessRef = React.createRef();
39− }
40−
41− componentDidMount() {
42− this.point = need.point(this.props.pointCursor);
43− this.contracts = need.contracts(this.props.contracts);
44−
45− azimuth.azimuth
46− .isSpawnProxy(
47− this.contracts,
48− this.point,
49− this.contracts.delegatedSending.address
50− )
51− .then(isSpawnProxy => {
52− this.setState({ invitesWillWork: Just(isSpawnProxy) });
53− });
54− }
55−
56− componentDidUpdate(prevProps) {
57− //
58− }
59−
60− handlePointInput(targetPlanet) {
61− let newState = {
62− targetPlanet: Nothing(),
63− planetInput: targetPlanet,
64− canGenerate: false,
65− currentPoolSize: Nothing(),
66− };
67− if (targetPlanet.length === 14 && ob.isValidPatp(targetPlanet)) {
68− const target = ob.patp2dec(targetPlanet);
69− //TODO there must be a simpler way to do this check
70− const isChild = ob.sein(targetPlanet) === ob.patp(this.point);
71− if (isChild) {
72− newState.targetPlanet = Just(target);
73− newState.currentPoolSize = this.checkCurrentInvites(target);
74− newState.canGenerate = true;
75− }
76− }
77− this.setState(newState);
78− }
79−
80− checkCurrentInvites(who) {
81− const cachedPoolSizes = this.state.cachedPoolSizes;
82− if (typeof cachedPoolSizes[who] !== 'undefined') {
83− return Just(cachedPoolSizes[who]);
84− } else {
85− azimuth.delegatedSending.getPool(this.contracts, who).then(pool => {
86− azimuth.delegatedSending
87− .invitesInPool(this.contracts, pool, this.point)
88− .then(size => {
89− this.updatePoolSize(who, size);
90− });
91− });
92− return Nothing();
93− }
94− }
95−
96− updatePoolSize(who, size) {
97− const cache = this.state.cachedPoolSizes;
98− cache[who] = size;
99− this.setState({ cachedPoolSizes: cache });
100− const target = this.state.targetPlanet;
101− const match = Just.hasInstance(target) ? who === target.value : false;
102− if (match) {
103− this.setState({ currentPoolSize: Just(size) });
104− }
105− }
106−
107− handlePoolSizeInput(targetSize) {
108− this.setState({ targetPoolSize: targetSize });
109− }
110−
111− createUnsignedTxn() {
112− return Just(
113− azimuth.delegatedSending.setPoolSize(
114− this.contracts,
115− this.point,
116− this.state.targetPlanet.value,
117− this.state.targetPoolSize
118− )
119− );
120− }
121−
122− render() {
123− let spawnProxyWarning = null;
124− if (this.state.invitesWillWork.value === false) {
125− spawnProxyWarning = (
126− <Warning>
127− <h3 className={'mb-2'}>{'Warning'}</h3>
128− {'Planets under this star will not be able to send invites ' +
129− 'until the invite contract ('}
130− <code>{this.contracts.delegatedSending.address}</code>
131− {') is made spawn proxy for this star.'}
132− </Warning>
133− );
134− }
135−
136− let poolSizeText = this.state.currentPoolSize.matchWith({
137− Just: ps => `(currently ${ps.value})`,
138− Nothing: _ => '',
139− });
140−
141− return (
142− <View>
143− <p>
144− {
145− 'manage invites here, for stars. can only give invites to child planets'
146− }
147− </p>
148−
149− {spawnProxyWarning}
150−
151− <PointInput
152− prop-format="innerlabel"
153− prop-size="lg"
154− placeholder={'~sampel-sipnem'}
155− value={this.state.planetInput}
156− onChange={this.handlePointInput}>
157− <InnerLabel>{'Planet to set invites for'}</InnerLabel>
158− <ValidatedSigil
159− patp={this.state.targetPlanet}
160− validator={() => this.validatePoint}
161− />
162− </PointInput>
163−
164− <Input
165− prop-format="innerlabel"
166− prop-size="lg"
167− value={this.state.targetPoolSize}
168− onChange={this.handlePoolSizeInput}>
169− <InnerLabel>{`Available invites ${poolSizeText}`}</InnerLabel>
170− </Input>
171−
172− <StatelessTransaction
173− canGenerate={this.state.canGenerate}
174− createUnsignedTxn={this.createUnsignedTxn}
175− ref={this.statelessRef}
176− />
177− </View>
178− );
179− }
180−}
181−
182−export default compose(
183− withNetwork,
184− withPointCursor
185−)(InvitesManage);
src/views/IssueChild.js+165−243
@@ -1,254 +1,176 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { azimuth, ecliptic } from 'azimuth-js';
1+import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+import { Nothing, Just } from 'folktale/maybe';
3+import cn from 'classnames';
4+import { Grid, Text, Input } from 'indigo-react';
5+import * as azimuth from 'azimuth-js';
46import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−
7−import {
8− H1,
9− P,
10− Anchor,
11− ShowBlockie,
12− ValidatedSigil,
13− PointInput,
14− AddressInput,
15− InnerLabel,
16−} from '../components/old/Base';
17−
18−import StatelessTransaction from '../components/old/StatelessTransaction';
19−
20−import { NETWORK_TYPES } from '../lib/network';
21−import { getSpawnCandidate } from '../lib/child';
22−import { canDecodePatp } from '../lib/txn';
23−
24−import { isValidAddress } from '../lib/wallet';
25−import { withNetwork } from '../store/network';
26−import { compose } from '../lib/lib';
27−import { withPointCursor } from '../store/pointCursor';
28−import View from 'components/View';
29−
30−const setFind = (set, pred) => {
31− for (const e of set) {
32− if (pred(e)) {
33− return e;
34− }
35− }
36− return undefined;
37−};
38−
39−class IssueChild extends React.Component {
40− constructor(props) {
41− super(props);
42−
43− const issuingPoint = need.point(props.pointCursor);
44−
45− const getCandidate = () => ob.patp(getSpawnCandidate(issuingPoint));
46−
47− const suggestions = [
48− getCandidate(),
49− getCandidate(),
50− getCandidate(),
51− getCandidate(),
52− ];
53−
54− this.state = {
55− receivingAddress: '',
56− issuingPoint: issuingPoint,
57− desiredPoint: '',
58− isAvailable: Nothing(), // use Nothing to allow attempt when offline
59− suggestions: suggestions,
60− validChildren: new Set(),
61− autoComplete: '',
62− };
63−
64− this.handlePointInput = this.handlePointInput.bind(this);
65− this.handleAddressInput = this.handleAddressInput.bind(this);
66− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
67− this.statelessRef = React.createRef();
68− }
69−
70− handleAddressInput = receivingAddress => {
71− this.setState({ receivingAddress });
72− this.statelessRef.current.clearTxn();
73− };
74−
75− componentWillMount() {
76− const { issuingPoint } = this.state;
77−
78− const validContracts = need.contracts(this.props.contracts);
79−
80− azimuth
81− .getUnspawnedChildren(validContracts, parseInt(issuingPoint, 10))
82− .then(ps => this.setState({ validChildren: new Set(ps.map(ob.patp)) }));
83− }
84−
85− handlePointInput(desiredPoint) {
86− if (desiredPoint.length < 15) {
87− const suggestedPoint =
88− setFind(this.state.validChildren, e => e.startsWith(desiredPoint)) ||
89− '';
90− const rendered =
91− ' '.repeat(desiredPoint.length) +
92− suggestedPoint.substring(desiredPoint.length);
93−
94− const available = Just(this.state.validChildren.has(desiredPoint));
95−
96− this.setState({
97− desiredPoint,
98− isAvailable: available,
99− autoComplete: rendered,
100− });
101− this.statelessRef.current.clearTxn();
102− }
103− }
104−
105− createUnsignedTxn() {
106− const { state, props } = this;
107−
108− if (isValidAddress(state.receivingAddress) === false) return Nothing();
109− if (state.isAvailable === false) return Nothing();
110− if (canDecodePatp(state.desiredPoint) === false) return Nothing();
111−
112− const validContracts = need.contracts(props.contracts);
113−
114− const pointDec = ob.patp2dec(state.desiredPoint);
1157
116− const txn = ecliptic.spawn(
117− validContracts,
118− pointDec,
119− state.receivingAddress
120− );
8+import { useNetwork } from 'store/network';
9+import { useHistory } from 'store/history';
10+import { usePointCache } from 'store/pointCache';
12111
122− return Just(txn);
123− }
12+import * as need from 'lib/need';
13+import { useAddressInput, usePointInput } from 'lib/useInputs';
14+import useEthereumTransaction from 'lib/useEthereumTransaction';
15+import { GAS_LIMITS } from 'lib/constants';
16+import patp2dec from 'lib/patp2dec';
12417
125− validatePoint = patp => {
126− const point = this.point;
127−
128− let vpatp = false;
129− try {
130− vpatp = ob.isValidPatp(patp);
131− } catch (_) {}
132−
133− let vchild = false;
134− try {
135− vchild = ob.sein(patp) === ob.patp(point);
136− } catch (_) {}
18+import ViewHeader from 'components/ViewHeader';
19+import MiniBackButton from 'components/MiniBackButton';
20+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
21+import View from 'components/View';
22+import useLifecycle from 'lib/useLifecycle';
23+import { usePointCursor } from 'store/pointCursor';
24+import { validateInSet } from 'lib/validators';
25+import { getSpawnCandidate } from 'lib/child';
13726
138− return vpatp && vchild;
139− };
27+function useIssueChild() {
28+ const { contracts } = useNetwork();
29+ const { syncKnownPoint } = usePointCache();
14030
141− render() {
142− const { props, state } = this;
31+ const _contracts = need.contracts(contracts);
14332
144− const validAddress = isValidAddress(state.receivingAddress);
33+ const [spawnedPoint, setSpawnedPoint] = useState();
14534
146− const canGenerate = props.web3.matchWith({
147− Nothing: () => {
148− return validAddress === true;
149− },
150− Just: () => {
151− return validAddress === true && state.isAvailable.value === true;
35+ return useEthereumTransaction(
36+ useCallback(
37+ (spawnedPoint, owner) => {
38+ setSpawnedPoint(spawnedPoint);
39+ return azimuth.ecliptic.spawn(_contracts, spawnedPoint, owner);
15240 },
153− });
154−
155− const esvisible =
156− props.networkType === NETWORK_TYPES.ROPSTEN ||
157− props.networkType === NETWORK_TYPES.MAINNET;
158−
159− const esdomain =
160− props.networkType === NETWORK_TYPES.ROPSTEN
161− ? 'ropsten.etherscan.io'
162− : 'etherscan.io';
163−
164− return (
165− <View>
166− <H1>
167− {'Issue a Child From '}{' '}
168− <code>{`${ob.patp(state.issuingPoint)}`}</code>
169− </H1>
170−
171− <P>
172− {`Please enter the point you would like to issue, and specify the
173− receiving Ethereum address. If you need to create an address, you
174− can also use Wallet Generator.`}
175− </P>
176−
177− <P>
178− {`Your point can only issue children with particular names. Some
179− valid suggestions for `}
180− {<code>{ob.patp(state.issuingPoint)}</code>}
181− {' are '}
182− <code>{state.suggestions[0]}</code>
183− {', '}
184− <code>{state.suggestions[1]}</code>
185− {', and '}
186− <code>{state.suggestions[2]}</code>
187− {'.'}
188− </P>
189−
190− <PointInput
191− prop-size="lg"
192− prop-format="innerLabel"
193− className={'mono mt-8'}
194− placeholder={`e.g. ${state.suggestions[3]}`}
195− value={state.desiredPoint}
196− onChange={this.handlePointInput}>
197− <InnerLabel>{'Point to Issue'}</InnerLabel>
198− <div
199− style={{
200− marginTop: '8.8rem',
201− marginLeft: '4.6rem',
202− fontSize: '5rem',
203− color: '#757575',
204− whiteSpace: 'pre-wrap',
205− }}
206− className="abs tl-0 mono">
207− {state.autoComplete}
208− </div>
209− <ValidatedSigil
210− className={'tr-0 mt-05 mr-0 abs'}
211− patp={state.desiredPoint}
212− size={68}
213− margin={8}
214− validator={() => this.validatePoint(state.desiredPoint)}
215− />
216− </PointInput>
217−
218− <AddressInput
219− className="text-mono mt-8"
220− prop-size="lg"
221− prop-format="innerLabel"
222− placeholder={`e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f`}
223− value={state.receivingAddress}
224− disabled={
225− Nothing.hasInstance(state.isAvailable) || !state.isAvailable.value
226− }
227− onChange={v => this.handleAddressInput(v)}>
228− <InnerLabel>{'Receiving Address'}</InnerLabel>
229− <ShowBlockie className={'mt-1'} address={state.receivingAddress} />
230− </AddressInput>
231−
232− <Anchor
233− className={'mt-1'}
234− prop-size={'sm'}
235− prop-disabled={!isValidAddress(state.receivingAddress) || !esvisible}
236− target={'_blank'}
237− href={`https://${esdomain}/address/${state.receivingAddress}`}>
238− {'View on Etherscan ↗'}
239− </Anchor>
240−
241− <StatelessTransaction
242− canGenerate={canGenerate}
243− createUnsignedTxn={this.createUnsignedTxn}
244− ref={this.statelessRef}
245− />
246− </View>
247− );
248− }
41+ [_contracts]
42+ ),
43+ useCallback(() => syncKnownPoint(spawnedPoint), [
44+ spawnedPoint,
45+ syncKnownPoint,
46+ ]),
47+ GAS_LIMITS.DEFAULT
48+ );
24949}
25050
251−export default compose(
252− withNetwork,
253− withPointCursor
254−)(IssueChild);
51+export default function IssueChild() {
52+ const { pop } = useHistory();
53+ const { contracts } = useNetwork();
54+ const { pointCursor } = usePointCursor();
55+
56+ const _contracts = need.contracts(contracts);
57+ const _point = need.point(pointCursor);
58+
59+ const [availablePointNames, setAvailablePointNames] = useState(Nothing());
60+
61+ const candidates = useMemo(() => {
62+ const getCandidate = () => ob.patp(getSpawnCandidate(_point));
63+
64+ return [getCandidate(), getCandidate(), getCandidate(), getCandidate()];
65+ }, [_point]);
66+
67+ const {
68+ isDefaultState,
69+ construct,
70+ unconstruct,
71+ completed,
72+ inputsLocked,
73+ bind,
74+ } = useIssueChild();
75+
76+ const validators = useMemo(
77+ () => [
78+ validateInSet(
79+ availablePointNames.getOrElse(new Set()),
80+ 'This point cannot be spawned.'
81+ ),
82+ ],
83+ [availablePointNames]
84+ );
85+ const [
86+ pointNameInput,
87+ { pass: validPointName, value: pointName },
88+ // ^ we use value: here so our effect runs onChange
89+ ] = usePointInput({
90+ name: 'point',
91+ disabled: inputsLocked,
92+ autoFocus: true,
93+ validators,
94+ error: availablePointNames.matchWith({
95+ Nothing: () => 'Loading availability...',
96+ Just: () => undefined,
97+ }),
98+ });
99+
100+ const [ownerInput, { pass: validOwner, data: owner }] = useAddressInput({
101+ name: 'owner',
102+ label: `Ethereum Address`,
103+ disabled: inputsLocked,
104+ });
105+
106+ useEffect(() => {
107+ if (validPointName && validOwner) {
108+ construct(patp2dec(pointName), owner);
109+ } else {
110+ unconstruct();
111+ }
112+ }, [owner, construct, unconstruct, validPointName, validOwner, pointName]);
113+
114+ useLifecycle(() => {
115+ let mounted = true;
116+
117+ (async () => {
118+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
119+ _contracts,
120+ _point
121+ );
122+
123+ if (!mounted) {
124+ return;
125+ }
126+
127+ setAvailablePointNames(Just(new Set(availablePoints.map(ob.patp))));
128+ })();
129+
130+ return () => (mounted = false);
131+ });
132+
133+ return (
134+ <View inset>
135+ <Grid>
136+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
137+
138+ <Grid.Item full as={ViewHeader}>
139+ Issue Child Point
140+ </Grid.Item>
141+
142+ {isDefaultState && (
143+ <Grid.Item full as={Text}>
144+ Perhaps one of {candidates.slice(0, 3).join(', ')}, or{' '}
145+ {candidates[candidates.length - 1]}?
146+ </Grid.Item>
147+ )}
148+
149+ {completed && (
150+ <Grid.Item
151+ full
152+ as={Text}
153+ className={cn('f5', {
154+ green3: completed,
155+ })}>
156+ {pointName} has been spawned and can be claimed by {owner}.
157+ </Grid.Item>
158+ )}
159+
160+ {!completed && (
161+ <>
162+ <Grid.Item full as={Input} {...pointNameInput} className="mt4" />
163+ <Grid.Item full as={Input} {...ownerInput} className="mb4" />
164+ </>
165+ )}
166+
167+ <Grid.Item
168+ full
169+ as={InlineEthereumTransaction}
170+ {...bind}
171+ onReturn={() => pop()}
172+ />
173+ </Grid>
174+ </View>
175+ );
176+}
src/views/Login.js+39−20
@@ -1,27 +1,27 @@
11import React, { useCallback, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
4−
54import { H4, Grid } from 'indigo-react';
65
7−import Ticket from './Login/Ticket';
8−import Mnemonic from './Login/Mnemonic';
9−import Advanced from './Login/Advanced';
10−import Hardware from './Login/Hardware';
11−
12−import * as need from 'lib/need';
13−
146import { useHistory } from 'store/history';
157import { useNetwork } from 'store/network';
168import { useWallet } from 'store/wallet';
179import { usePointCursor } from 'store/pointCursor';
1810
11+import * as need from 'lib/need';
12+import { WALLET_TYPES } from 'lib/wallet';
13+
1914import View from 'components/View';
2015import Tabs from 'components/Tabs';
2116import Crumbs from 'components/Crumbs';
2217import Footer from 'components/Footer';
2318import { ForwardButton, OfflineButton } from 'components/Buttons';
2419
20+import Ticket from './Login/Ticket';
21+import Mnemonic from './Login/Mnemonic';
22+import Advanced from './Login/Advanced';
23+import Hardware from './Login/Hardware';
24+
2525const NAMES = {
2626 TICKET: 'TICKET',
2727 MNEMONIC: 'MNEMONIC',
@@ -43,15 +43,37 @@ const OPTIONS = [
4343 { text: 'Advanced', value: NAMES.ADVANCED },
4444];
4545
46+const walletTypeToViewName = walletType => {
47+ switch (walletType) {
48+ case WALLET_TYPES.MNEMONIC:
49+ return NAMES.MNEMONIC;
50+ case WALLET_TYPES.LEDGER:
51+ case WALLET_TYPES.TREZOR:
52+ return NAMES.HARDWARE;
53+ case WALLET_TYPES.KEYSTORE:
54+ case WALLET_TYPES.PRIVATE_KEY:
55+ return NAMES.ADVANCED;
56+ case WALLET_TYPES.TICKET:
57+ case WALLET_TYPES.SHARDS:
58+ return NAMES.TICKET;
59+ default:
60+ return NAMES.TICKET;
61+ }
62+};
63+
4664export default function Login() {
4765 // globals
4866 const { push, replaceWith, names } = useHistory();
4967 const { contracts } = useNetwork();
50− const { wallet } = useWallet();
68+ const { wallet, walletType } = useWallet();
5169 const { pointCursor, setPointCursor } = usePointCursor();
5270
71+ const [deducing, setDeducing] = useState(false);
72+
5373 // inputs
54− const [currentTab, setCurrentTab] = useState(NAMES.TICKET);
74+ const [currentTab, setCurrentTab] = useState(
75+ walletTypeToViewName(walletType)
76+ );
5577
5678 const goToActivate = useCallback(
5779 () => replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]),
@@ -71,8 +93,10 @@ export default function Login() {
7193 const _wallet = need.wallet(wallet);
7294 const _contracts = need.contracts(contracts);
7395
74− // if no point cursor set by login logic, try to deduce it
96+ setDeducing(true);
97+
7598 let deduced = pointCursor;
99+ // if no point cursor set by login logic, try to deduce it
76100 if (Nothing.hasInstance(deduced)) {
77101 const owned = await azimuth.azimuth.getOwnedPoints(
78102 _contracts,
@@ -80,17 +104,11 @@ export default function Login() {
80104 );
81105 if (owned.length === 1) {
82106 deduced = Just(owned[0]);
83− } else if (owned.length === 0) {
84− const canOwn = await azimuth.azimuth.getTransferringFor(
85− _contracts,
86− _wallet.address
87− );
88− if (canOwn.length === 1) {
89− deduced = Just(canOwn[0]);
90− }
91107 }
92108 }
93109
110+ setDeducing(false);
111+
94112 // if we have a deduced point or one in the global context,
95113 // navigate to that specific point, otherwise navigate to list of points
96114 if (Just.hasInstance(deduced)) {
@@ -124,7 +142,8 @@ export default function Login() {
124142 as={ForwardButton}
125143 solid
126144 className="mt2"
127− disabled={Nothing.hasInstance(wallet)}
145+ loading={deducing}
146+ disabled={Nothing.hasInstance(wallet) || deducing}
128147 onClick={doContinue}>
129148 Continue
130149 </Grid.Item>
src/views/Login/Ticket.js+8−13
@@ -41,7 +41,7 @@ export default function Ticket({ className }) {
4141 label: 'Wallet Passphrase',
4242 });
4343
44− const [hasPassphraseInput] = useCheckboxInput({
44+ const [hasPassphraseInput, { data: hasPassphrase }] = useCheckboxInput({
4545 name: 'has-passphrase',
4646 label: 'Passphrase',
4747 initialValue: false,
@@ -94,21 +94,17 @@ export default function Ticket({ className }) {
9494 }
9595
9696 const _contracts = need.contracts(contracts);
97− const pointNumber = ob.patp2dec(pointName);
98− const urbitWallet = await urbitWalletFromTicket(
99− ticket,
100− pointName,
101− passphrase
102− );
97+ const point = ob.patp2dec(pointName);
98+ const urbitWallet = await urbitWalletFromTicket(ticket, point, passphrase);
10399 const [isOwner, isTransferProxy] = await Promise.all([
104100 azimuth.azimuth.isOwner(
105101 _contracts,
106− pointNumber,
102+ point,
107103 urbitWallet.ownership.keys.address
108104 ),
109105 azimuth.azimuth.isTransferProxy(
110106 _contracts,
111− pointNumber,
107+ point,
112108 urbitWallet.ownership.keys.address
113109 ),
114110 ]);
@@ -140,7 +136,8 @@ export default function Ticket({ className }) {
140136
141137 try {
142138 const ticket = kg.combine([s1, s2, s3]);
143− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
139+ const point = ob.patp2dec(pointName);
140+ const uhdw = await urbitWalletFromTicket(ticket, point, passphrase);
144141 setUrbitWallet(Just(uhdw));
145142 } catch (_) {
146143 // do nothing
@@ -174,9 +171,7 @@ export default function Ticket({ className }) {
174171 <Grid.Item full as={Input} {...shard3Input} />
175172 </>
176173 )}
177− {hasPassphraseInput.data && (
178− <Grid.Item full as={Input} {...passphraseInput} />
179− )}
174+ {hasPassphrase && <Grid.Item full as={Input} {...passphraseInput} />}
180175
181176 <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
182177 <Grid.Item full as={CheckboxInput} {...shardsInput} />
src/views/Party/PartySetPoolSize.jsnew file+133
@@ -0,0 +1,133 @@
1+import React, { useCallback, useEffect } from 'react';
2+import cn from 'classnames';
3+import { Grid, Text, Input } from 'indigo-react';
4+import * as 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 { usePointInput, useNumberInput } from 'lib/useInputs';
12+import useEthereumTransaction from 'lib/useEthereumTransaction';
13+import { GAS_LIMITS } from 'lib/constants';
14+
15+import ViewHeader from 'components/ViewHeader';
16+import MiniBackButton from 'components/MiniBackButton';
17+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
18+import patp2dec from 'lib/patp2dec';
19+import { useHistory } from 'store/history';
20+import View from 'components/View';
21+import useCurrentPermissions from 'lib/useCurrentPermissions';
22+import WarningBox from 'components/WarningBox';
23+
24+function useSetPoolSize() {
25+ const { contracts } = useNetwork();
26+ const { pointCursor } = usePointCursor();
27+ const { syncOwnedPoint } = usePointCache();
28+
29+ const _contracts = need.contracts(contracts);
30+ const _point = need.point(pointCursor);
31+
32+ return useEthereumTransaction(
33+ useCallback(
34+ (poolOwner, poolSize) =>
35+ azimuth.delegatedSending.setPoolSize(
36+ _contracts,
37+ _point,
38+ poolOwner,
39+ poolSize
40+ ),
41+ [_contracts, _point]
42+ ),
43+ useCallback(() => syncOwnedPoint(_point), [_point, syncOwnedPoint]),
44+ GAS_LIMITS.DEFAULT // TODO: GAS_LIMITS.SET_POOL_SIZE
45+ );
46+}
47+
48+export default function PartySetPoolSize() {
49+ const { pop } = useHistory();
50+ const { contracts } = useNetwork();
51+
52+ const _contracts = need.contracts(contracts);
53+
54+ const { spawnIsDelegatedSending } = useCurrentPermissions();
55+
56+ const {
57+ construct,
58+ unconstruct,
59+ completed,
60+ inputsLocked,
61+ bind,
62+ } = useSetPoolSize();
63+
64+ const [poolOwnerInput, { pass: validPoint, data: poolOwner }] = usePointInput(
65+ {
66+ name: 'point',
67+ label: 'Point',
68+ disabled: inputsLocked,
69+ }
70+ );
71+
72+ const [
73+ poolSizeInput,
74+ { pass: validPoolSize, data: poolSize },
75+ ] = useNumberInput({
76+ name: 'poolsize',
77+ label: 'Pool Size',
78+ initialValue: 5,
79+ disabled: inputsLocked,
80+ });
81+
82+ useEffect(() => {
83+ if (validPoint && validPoolSize) {
84+ construct(patp2dec(poolOwner), poolSize);
85+ } else {
86+ unconstruct();
87+ }
88+ }, [poolOwner, construct, validPoint, validPoolSize, poolSize, unconstruct]);
89+
90+ return (
91+ <View inset>
92+ <Grid>
93+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
94+ <Grid.Item full as={ViewHeader}>
95+ Set Pool Size
96+ </Grid.Item>
97+ {!spawnIsDelegatedSending && (
98+ <Grid.Item full as={WarningBox} className="mb4">
99+ The spawn proxy must be set to{' '}
100+ <code className="mono f6">
101+ {_contracts.delegatedSending.address}
102+ </code>{' '}
103+ for invitations to be available.
104+ </Grid.Item>
105+ )}
106+ <Grid.Item
107+ full
108+ as={Text}
109+ className={cn('f5', {
110+ green3: completed,
111+ })}>
112+ {completed
113+ ? `${poolSize} invites have been allocated to ${poolOwner}`
114+ : `Allocate invites to a child point.`}
115+ </Grid.Item>
116+
117+ {!completed && (
118+ <>
119+ <Grid.Item full as={Input} {...poolOwnerInput} className="mt4" />
120+ <Grid.Item full as={Input} {...poolSizeInput} className="mb4" />
121+ </>
122+ )}
123+
124+ <Grid.Item
125+ full
126+ as={InlineEthereumTransaction}
127+ {...bind}
128+ onReturn={() => pop()}
129+ />
130+ </Grid>
131+ </View>
132+ );
133+}
src/views/Permissions.jsdeleted−153
@@ -1,153 +0,0 @@
1−import React, { useState, useCallback } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import * as need from 'lib/need';
4−import { azimuth } from 'azimuth-js';
5−import { Grid } from 'indigo-react';
6−
7−import View from 'components/View';
8−import { ForwardButton } from 'components/Buttons';
9−import FooterButton from 'components/FooterButton';
10−import { matchBlinky } from 'components/Blinky';
11−
12−import { ROUTE_NAMES } from 'lib/routeNames';
13−import { eqAddr } from 'lib/wallet';
14−
15−import useLifecycle from 'lib/useLifecycle';
16−import { useNetwork } from 'store/network';
17−import { useHistory } from 'store/history';
18−import { useWallet } from 'store/wallet';
19−import { usePointCursor } from 'store/pointCursor';
20−import { usePointCache } from 'store/pointCache';
21−
22−//TODO consolidate with azimuth-js' getActivationBlock
23−const getRekeyDate = async (web3, contracts, point) => {
24− const logs = await contracts.azimuth.getPastEvents('ChangedKeys', {
25− fromBlock: 0,
26− toBlock: 'latest',
27− filter: { point: [point] },
28− });
29− if (logs.length === 0) {
30− return null;
31− } else {
32− // last log is most recent
33− const block = await web3.eth.getBlock(logs[logs.length - 1].blockNumber);
34− return new Date(block.timestamp * 1000);
35− }
36−};
37−
38−export default function Permissions() {
39− const history = useHistory();
40− const { web3, contracts } = useNetwork();
41− const { wallet } = useWallet();
42− const { pointCursor } = usePointCursor();
43− const { pointCache } = usePointCache();
44−
45− const [rekeyDate, setRekeyDate] = useState(Nothing());
46−
47− const point = need.point(pointCursor);
48− const pointSize = azimuth.getPointSize(point);
49− const userAddress = need.wallet(wallet).address;
50−
51− useLifecycle(() => {
52− getRekeyDate(need.web3(web3), need.contracts(contracts), point).then(
53− res => {
54− if (res === null) setRekeyDate(Just('unknown'));
55− else setRekeyDate(Just(res.toString()));
56− }
57− );
58− });
59−
60− const goSetManagement = useCallback(
61− () => history.push(ROUTE_NAMES.SET_MANAGEMENT_PROXY),
62− [history]
63− );
64−
65− const goSetSpawn = useCallback(
66− () => history.push(ROUTE_NAMES.SET_SPAWN_PROXY),
67− [history]
68− );
69−
70− const goSetVoting = useCallback(
71− () => history.push(ROUTE_NAMES.SET_VOTING_PROXY),
72− [history]
73− );
74−
75− const goSetKeys = useCallback(() => history.push(ROUTE_NAMES.SET_KEYS), [
76− history,
77− ]);
78−
79− const pointDetails = need.fromPointCache(pointCache, point);
80−
81− const isOwner = eqAddr(userAddress, pointDetails.owner);
82− const canManage =
83− isOwner || eqAddr(userAddress, pointDetails.managementProxy);
84−
85− const proxyAction = (name, address, onClick) => {
86− if (address === '0x0000000000000000000000000000000000000000')
87− address = 'Unset';
88− return (
89− <Grid.Item full>
90− <ForwardButton disabled={!isOwner} onClick={onClick} detail={address}>
91− {`${name} proxy address`}
92− </ForwardButton>
93− </Grid.Item>
94− );
95− };
96−
97− let spawnProxyAction = null;
98− if (pointSize !== azimuth.PointSize.Planet) {
99− spawnProxyAction = proxyAction(
100− 'Spawn',
101− pointDetails.spawnProxy,
102− goSetSpawn
103− );
104− }
105−
106− let votingProxyAction = null;
107− if (pointSize === azimuth.PointSize.Galaxy) {
108− votingProxyAction = proxyAction(
109− 'Voting',
110− pointDetails.votingProxy,
111− goSetVoting
112− );
113− }
114−
115− const netKeysText =
116− pointDetails.keyRevisionNumber === 0 ? (
117− 'Not yet configured'
118− ) : (
119− <>
120− {`Revision #${pointDetails.keyRevisionNumber}, since `}
121− {matchBlinky(rekeyDate)}
122− </>
123− );
124−
125− return (
126− <View>
127− <Grid className="pt2">
128− <Grid.Item full as={ForwardButton} disabled detail={pointDetails.owner}>
129− Ownership address
130− </Grid.Item>
131− {proxyAction(
132− 'Management',
133− pointDetails.managementProxy,
134− goSetManagement
135− )}
136− {spawnProxyAction}
137− {votingProxyAction}
138− <Grid.Item
139− full
140− as={ForwardButton}
141− disabled={!canManage}
142− onClick={goSetKeys}
143− detail={netKeysText}>
144− Network keys
145− </Grid.Item>
146− </Grid>
147−
148− <FooterButton detail="Transfer this identity to a new owner" disabled>
149− Transfer
150− </FooterButton>
151− </View>
152− );
153−}
src/views/Point.js+49−40
@@ -2,60 +2,70 @@ import React, { useCallback } from 'react';
22import { Just } from 'folktale/maybe';
33import * as need from 'lib/need';
44import { Grid } from 'indigo-react';
5+import { azimuth } from 'azimuth-js';
56
67import { usePointCursor } from 'store/pointCursor';
7−import { usePointCache } from 'store/pointCache';
88import { useHistory } from 'store/history';
99
1010import View from 'components/View';
1111import Passport from 'components/Passport';
12−import { ForwardButton } from 'components/Buttons';
12+import { ForwardButton, BootArvoButton } from 'components/Buttons';
1313import FooterButton from 'components/FooterButton';
1414import { matchBlinky } from 'components/Blinky';
1515
1616import useInvites from 'lib/useInvites';
1717import { useSyncOwnedPoints } from 'lib/useSyncPoints';
18−import { ROUTE_NAMES } from 'lib/routeNames';
19−import { eqAddr } from 'lib/wallet';
20−
21−import Actions from './Point/Actions';
22−import { useWallet } from 'store/wallet';
18+import useCurrentPermissions from 'lib/useCurrentPermissions';
2319
2420export default function Point() {
25− const history = useHistory();
21+ const { push, names } = useHistory();
2622 const { pointCursor } = usePointCursor();
27− const { pointCache } = usePointCache();
28− const { wallet } = useWallet();
2923
3024 const point = need.point(pointCursor);
3125
32− let canAdmin = false;
33− if (point in pointCache) {
34− canAdmin = wallet.matchWith({
35− Nothing: () => false,
36− Just: wal => {
37− const pointDetails = need.fromPointCache(pointCache, point);
38− const address = need.addressFromWallet(wallet);
39− return (
40− eqAddr(address, pointDetails.owner) ||
41− eqAddr(address, pointDetails.managementProxy)
42− );
43− },
44− });
45− }
26+ const { isActiveOwner, canManage, canSpawn } = useCurrentPermissions();
4627
4728 // fetch the invites for the current cursor
4829 const { availableInvites } = useInvites(point);
49− const availableInvitesText = matchBlinky(availableInvites);
5030
51− const showActions = Just.hasInstance(wallet);
31+ const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
5232
53− const goAdmin = useCallback(() => history.push(ROUTE_NAMES.ADMIN), [history]);
33+ const goInvite = useCallback(() => push(names.INVITE), [push, names]);
5434
55− const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
56− history,
35+ const goPartiesSetPoolSize = useCallback(
36+ () => push(names.PARTY_SET_POOL_SIZE),
37+ [push, names]
38+ );
39+
40+ const goIssuePoint = useCallback(() => push(names.ISSUE_CHILD), [
41+ names.ISSUE_CHILD,
42+ push,
5743 ]);
5844
45+ const inviteButton = (() => {
46+ switch (azimuth.getPointSize(point)) {
47+ case azimuth.PointSize.Planet:
48+ const availableInvitesText = matchBlinky(availableInvites);
49+ return (
50+ <FooterButton disabled={!isActiveOwner} onClick={goInvite}>
51+ Invite <sup>{availableInvitesText} available</sup>
52+ </FooterButton>
53+ );
54+ //
55+ case azimuth.PointSize.Star:
56+ return (
57+ <FooterButton
58+ disabled={!isActiveOwner}
59+ onClick={goPartiesSetPoolSize}>
60+ Manage Parties
61+ </FooterButton>
62+ );
63+ //
64+ default:
65+ return null;
66+ }
67+ })();
68+
5969 // sync the current cursor
6070 useSyncOwnedPoints([point]);
6171
@@ -66,24 +76,23 @@ export default function Point() {
6676 <Grid.Item
6777 full
6878 as={ForwardButton}
69− disabled={!canAdmin}
79+ disabled={!canManage}
7080 onClick={goAdmin}>
7181 Admin
7282 </Grid.Item>
7383 <Grid.Divider />
74− <Grid.Item full as={ForwardButton} detail="Boot your computer" disabled>
75− Boot Arvo
76− </Grid.Item>
77− {showActions && (
78− <Grid.Item full>
79− <Actions />
80− </Grid.Item>
84+ {canSpawn && (
85+ <>
86+ <Grid.Item full as={ForwardButton} onClick={goIssuePoint}>
87+ Issue Point
88+ </Grid.Item>
89+ <Grid.Divider />
90+ </>
8191 )}
92+ <Grid.Item full as={BootArvoButton} disabled />
8293 </Grid>
8394
84− <FooterButton onClick={goInvite}>
85− Invite <sup>{availableInvitesText}</sup>
86− </FooterButton>
95+ {inviteButton}
8796 </View>
8897 );
8998}
src/views/Point/Actions.js+2−24
@@ -2,7 +2,7 @@ import React from 'react';
22import { azimuth } from 'azimuth-js';
33import * as need from '../../lib/need';
44
5−import { CURVE_ZERO_ADDR, isZeroAddress, eqAddr } from '../../lib/wallet';
5+import { isZeroAddress, eqAddr } from '../../lib/wallet';
66import { H2, P } from '../../components/old/Base';
77import { Button } from '../../components/old/Base';
88import { ROUTE_NAMES } from '../../lib/routeNames';
@@ -11,6 +11,7 @@ import { useWallet } from '../../store/wallet';
1111import { Grid } from 'indigo-react';
1212import { usePointCursor } from 'store/pointCursor';
1313import { usePointCache } from 'store/pointCache';
14+import { CURVE_ZERO_ADDR } from 'lib/keys';
1415
1516const isPlanet = point =>
1617 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
@@ -67,19 +68,6 @@ function Actions() {
6768 eqAddr(deets.value.owner, addr),
6869 });
6970
70− const canGenKeyfile = pointDetails.matchWith({
71− Nothing: () => false,
72− Just: deets => {
73− const hasPermission =
74− eqAddr(deets.value.owner, addr) ||
75− eqAddr(deets.value.managementProxy, addr);
76−
77− const isBooted = deets.value.keyRevisionNumber > 0;
78−
79− return hasPermission && isBooted;
80− },
81− });
82−
8371 const canAcceptTransfer = pointDetails.matchWith({
8472 Nothing: () => false,
8573 Just: deets => eqAddr(deets.value.transferProxy, addr),
@@ -142,16 +130,6 @@ function Actions() {
142130 }}>
143131 {'Cancel outgoing transfer'}
144132 </Button>
145−
146− <Button
147− prop-size={'sm'}
148− prop-type={'link'}
149− disabled={!canGenKeyfile}
150− onClick={() => {
151− history.push(ROUTE_NAMES.GEN_KEYFILE);
152− }}>
153− {'Generate Arvo keyfile'}
154− </Button>
155133 </Grid.Item>
156134 <Grid.Item third={2}>
157135 <Button
src/views/Point/KeysAndMetadata.js+2−1
@@ -1,8 +1,9 @@
11import React from 'react';
22import { H2, H4 } from '../../components/old/Base';
33
4−import { CURVE_ZERO_ADDR, isZeroAddress } from '../../lib/wallet';
4+import { isZeroAddress } from '../../lib/wallet';
55import { Grid } from 'indigo-react';
6+import { CURVE_ZERO_ADDR } from 'lib/keys';
67
78const renderAddress = addr =>
89 isZeroAddress(addr) ? (
src/views/Points.js+14−12
@@ -78,6 +78,7 @@ export default function Points() {
7878 </P>
7979
8080 <Grid>
81+ <Grid.Divider />
8182 <Grid.Item full>
8283 <ForwardButton
8384 detail="View a point on Azimuth"
@@ -85,20 +86,21 @@ export default function Points() {
8586 View a point
8687 </ForwardButton>
8788 </Grid.Item>
89+ <Grid.Divider />
90+ {isEclipticOwner && (
91+ <>
92+ <Grid.Item full>
93+ <ForwardButton
94+ detail="You have the authority to create a new Galaxy."
95+ onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
96+ Create a galaxy
97+ </ForwardButton>
98+ </Grid.Item>
99+ <Grid.Divider />
100+ </>
101+ )}
88102 </Grid>
89103
90− {isEclipticOwner && (
91− <Grid>
92− <Grid.Item full>
93− <ForwardButton
94− detail="You have the authority to create a new Galaxy."
95− onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
96− Create a galaxy
97− </ForwardButton>
98− </Grid.Item>
99− </Grid>
100− )}
101−
102104 {incoming.length > 0 && (
103105 <>
104106 <H2>Incoming Transfers</H2>
src/views/Redownload.jsdeleted−52
@@ -1,52 +0,0 @@
1−import React, { useState } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import * as need from 'lib/need';
4−
5−import { usePointCursor } from 'store/pointCursor';
6−
7−import View from 'components/View';
8−import { DownloadButton } from 'components/Buttons';
9−import PaperRenderer from 'components/PaperRenderer';
10−
11−import { downloadWallet } from 'lib/invite';
12−
13−import { useWallet } from 'store/wallet';
14−
15−export default function Redownload() {
16− const { urbitWallet } = useWallet();
17− const { pointCursor } = usePointCursor();
18− const point = need.point(pointCursor);
19−
20− const [paper, setPaper] = useState(Nothing());
21−
22− const doDownload = () => {
23− downloadWallet(paper.value);
24− };
25−
26− const paperRenderer = paper.matchWith({
27− Nothing: () => null,
28− Just: p => (
29− <PaperRenderer
30− point={point}
31− wallet={urbitWallet.value}
32− callback={paper => {
33− setPaper(Just(paper));
34− }}
35− />
36− ),
37− });
38−
39− return (
40− <View>
41− <DownloadButton
42− disabled={Nothing.hasInstance(paper)}
43− onClick={doDownload}>
44− {paper.matchWith({
45− Nothing: () => 'Printing and folding...',
46− Just: _ => 'Download paper wallet',
47− })}
48− </DownloadButton>
49− {paperRenderer}
50− </View>
51− );
52−}
src/views/Reticket.jsdeleted−97
@@ -1,97 +0,0 @@
1−import React, { useState } from 'react';
2−import { size } from 'lodash';
3−import * as need from 'lib/need';
4−import ob from 'urbit-ob';
5−import { Just, Nothing } from 'folktale/maybe';
6−
7−import { usePointCursor } from 'store/pointCursor';
8−
9−import View from 'components/View';
10−import MiniBackButton from 'components/MiniBackButton';
11−import { LocalRouterProvider } from 'lib/LocalRouter';
12−import Crumbs from 'components/Crumbs';
13−
14−import useRouter from 'lib/useRouter';
15−
16−import { useHistory } from 'store/history';
17−
18−import Confirm from './Reticket/Confirm';
19−import Download from './Reticket/Download';
20−import Verify from './Reticket/Verify';
21−import DoReticket from './Reticket/DoReticket';
22−
23−const NAMES = {
24− CONFIRM: 'CONFIRM',
25− DOWNLOAD: 'DOWNLOAD',
26− VERIFY: 'VERIFY',
27− RETICKET: 'RETICKET',
28−};
29−
30−const VIEWS = {
31− CONFIRM: Confirm,
32− DOWNLOAD: Download,
33− VERIFY: Verify,
34− RETICKET: DoReticket,
35−};
36−
37−const STEP_MAX = size(NAMES);
38−
39−export default function Reticket() {
40− const history = useHistory();
41− const { Route, ...innerRouter } = useRouter({
42− names: NAMES,
43− views: VIEWS,
44− initialRoutes: [{ key: NAMES.CONFIRM }],
45− });
46− const { pointCursor } = usePointCursor();
47− const point = need.point(pointCursor);
48− const name = ob.patp(point);
49−
50− const [newWallet, setNewWallet] = useState(Nothing());
51−
52− //TODO step # from Route?
53− const stepNumber = 1;
54−
55− const storeNewWallet = (wallet, paper) => {
56− setNewWallet(
57− Just({
58− wallet,
59− paper,
60− })
61− );
62− };
63−
64− const completed = () => {
65− history.pop();
66− };
67−
68− return (
69− <LocalRouterProvider value={innerRouter}>
70− <View>
71− <Crumbs
72− routes={[
73− {
74− text: name,
75− action: () => {
76− return history.pop(2);
77− },
78− },
79− {
80− text: 'Admin',
81− action: () => history.pop(),
82− },
83− ]}
84− />
85− <MiniBackButton onClick={innerRouter.pop} />
86− Step {stepNumber} of {STEP_MAX}
87− <Route
88− newWallet={newWallet}
89− //
90− storeNewWallet={storeNewWallet}
91− //
92− completed={completed}
93− />
94− </View>
95− </LocalRouterProvider>
96− );
97−}
src/views/Reticket/Confirm.jsdeleted−63
@@ -1,63 +0,0 @@
1−import React, { useState, useCallback } from 'react';
2−import * as need from 'lib/need';
3−import { Just, Nothing } from 'folktale/maybe';
4−
5−import { usePointCursor } from 'store/pointCursor';
6−
7−import View from 'components/View';
8−import { ForwardButton } from 'components/Buttons';
9−import PaperRenderer from 'components/PaperRenderer';
10−
11−import useLifecycle from 'lib/useLifecycle';
12−import { generateWallet } from 'lib/invite';
13−import { useLocalRouter } from 'lib/LocalRouter';
14−
15−import { isDevelopment } from 'lib/flags';
16−
17−export default function Confirm({ newWallet, storeNewWallet }) {
18− const { push, names } = useLocalRouter();
19− const { pointCursor } = usePointCursor();
20− const point = need.point(pointCursor);
21−
22− const [generatedWallet, setGeneratedWallet] = useState(Nothing());
23−
24− // start generating new wallet on mount
25− //TODO also check we can pay for the transactions?
26− useLifecycle(() => {
27− (async () => {
28− const wal = await generateWallet(point);
29− if (isDevelopment) {
30− console.log('ticket for', point, wal.ticket);
31− }
32− setGeneratedWallet(Just(wal));
33− })();
34− });
35−
36− const next = useCallback(() => push(names.DOWNLOAD), [push, names]);
37−
38− const paperRenderer = generatedWallet.matchWith({
39− Nothing: () => null,
40− Just: gw => (
41− <PaperRenderer
42− point={point}
43− wallet={gw.value}
44− callback={paper => {
45− storeNewWallet(gw.value, paper);
46− }}
47− />
48− ),
49− });
50−
51− return (
52− <View>
53− Maybe we should just duplicate the Login component here? Not everyone who
54− does this will be logged in with an existing ticket. Also, beware, this
55− changes your proxy addresses. If you're using smart contracts, this might
56− break them! It will also change your networking keys!
57− <ForwardButton disabled={Nothing.hasInstance(newWallet)} onClick={next}>
58− I understand, continue
59− </ForwardButton>
60− {paperRenderer}
61− </View>
62− );
63−}
src/views/Reticket/DoReticket.jsdeleted−128
@@ -1,128 +0,0 @@
1−import React, { useState, useCallback } from 'react';
2−import { Just } from 'folktale/maybe';
3−
4−import { Grid, H4, Text, ErrorText } from 'indigo-react';
5−import { RestartButton } from 'components/Buttons';
6−import WarningBox from 'components/WarningBox';
7−import LoadingBar from 'components/LoadingBar';
8−import Highlighted from 'components/Highlighted';
9−
10−import { claimPointFromInvite } from 'lib/invite';
11−import { fromWei } from 'lib/txn';
12−
13−import * as need from 'lib/need';
14−import { useNetwork } from 'store/network';
15−import { useWallet } from 'store/wallet';
16−import { usePointCursor } from 'store/pointCursor';
17−import useLifecycle from 'lib/useLifecycle';
18−
19−//TODO maybe we want to drop in PassportTransfer here?
20−export default function DoReticket({ newWallet, completed }) {
21− const { web3, contracts } = useNetwork();
22− const { wallet, setUrbitWallet } = useWallet();
23− const { pointCursor } = usePointCursor();
24−
25− const [generalError, setGeneralError] = useState();
26− const [{ label, progress }, setState] = useState({
27− label: 'Starting...',
28− progress: 0,
29− });
30− const [needFunds, setNeedFunds] = useState();
31−
32− // start reticketing transactions on mount
33− useLifecycle(() => {
34− (async () => {
35− try {
36− await claimPointFromInvite({
37− inviteWallet: need.wallet(wallet),
38− wallet: newWallet.value.wallet,
39− point: need.point(pointCursor),
40− web3: need.web3(web3),
41− contracts: need.contracts(contracts),
42− onUpdate: handleUpdate,
43− });
44− //TODO would it be better for this to happen in completed()?
45− setUrbitWallet(Just(newWallet.value.wallet));
46− completed(); //TODO don't auto-redirect
47− } catch (err) {
48− setGeneralError(err);
49− }
50− })();
51− });
52−
53− const handleUpdate = useCallback(
54− ({ type, state, value }) => {
55− switch (type) {
56− case 'progress':
57− return setState(state);
58− case 'askFunding':
59− return setNeedFunds(value);
60− case 'gotFunding':
61− return setNeedFunds(false);
62− default:
63− console.error(`Unknown update: ${type}`);
64− }
65− },
66− [setState, setNeedFunds]
67− );
68−
69− const renderAdditionalInfo = () => {
70− if (generalError) {
71− return (
72− <>
73− <Grid.Item full className="mt8">
74− <ErrorText>{generalError.message.toString()}</ErrorText>
75− </Grid.Item>
76− <Grid.Item
77− full
78− className="mt3"
79− as={RestartButton}
80− //TODO onClick={goToRestart}
81− />
82− </>
83− );
84− }
85−
86− if (needFunds) {
87− return (
88− <Grid.Item full className="mt8">
89− <Highlighted>
90− The address {needFunds.address} needs at least{' '}
91− {fromWei(needFunds.minBalance)} ETH and currently has{' '}
92− {fromWei(needFunds.balance)} ETH. Waiting until the account has
93− enough funds.
94− </Highlighted>
95− </Grid.Item>
96− );
97− }
98−
99− if (progress < 100) {
100− return (
101− <Grid.Item full as={WarningBox} className="mt8">
102− Never give your Master Ticket to anyone
103− </Grid.Item>
104− );
105− }
106−
107− return null;
108− };
109−
110− return (
111− <Grid className={'auto-rows-min'}>
112− <Grid.Item full as={H4}>
113− {label}
114− </Grid.Item>
115− <Grid.Item full as={Grid} className="mt3" gap={3}>
116− <Grid.Item full as={LoadingBar} progress={progress} />
117− <Grid.Item full>
118− <Text className="f5 green4">
119− This process can take up to 5 minutes to complete. Don't leave this
120− page until the process is complete.
121− </Text>
122− </Grid.Item>
123− </Grid.Item>
124−
125− {renderAdditionalInfo()}
126− </Grid>
127− );
128−}
src/views/Reticket/Download.jsdeleted−35
@@ -1,35 +0,0 @@
1−import React, { useState, useCallback } from 'react';
2−import { Nothing } from 'folktale/maybe';
3−
4−import View from 'components/View';
5−import { ForwardButton, DownloadButton } from 'components/Buttons';
6−
7−import { useLocalRouter } from 'lib/LocalRouter';
8−import { downloadWallet } from 'lib/invite';
9−
10−export default function Download({ newWallet }) {
11− const { push, names } = useLocalRouter();
12−
13− const [hasDownloaded, setHasDownloaded] = useState(false);
14−
15− const doDownload = () => {
16− downloadWallet(newWallet.value.paper);
17− setHasDownloaded(true);
18− };
19−
20− const next = useCallback(() => push(names.VERIFY), [push, names]);
21−
22− return (
23− <View>
24− Download the thing, and keep it somewhere safe!
25− <DownloadButton
26− disabled={Nothing.hasInstance(newWallet)}
27− onClick={doDownload}>
28− Download
29− </DownloadButton>
30− <ForwardButton disabled={!hasDownloaded} onClick={next}>
31− Continue
32− </ForwardButton>
33− </View>
34− );
35−}
src/views/Reticket/Verify.jsdeleted−41
@@ -1,41 +0,0 @@
1−import React, { useCallback, useMemo } from 'react';
2−import { Input } from 'indigo-react';
3−
4−import { useTicketInput } from 'lib/useInputs';
5−import { useLocalRouter } from 'lib/LocalRouter';
6−import { validateExactly } from 'lib/validators';
7−
8−import View from 'components/View';
9−import { ForwardButton } from 'components/Buttons';
10−
11−const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
12−
13−//TODO deduplicate with PassportVerify
14−export default function Verify({ newWallet }) {
15− const { push, names } = useLocalRouter();
16−
17− const ticket = newWallet.value.wallet.ticket;
18− const validators = useMemo(
19− () => [validateExactly(ticket, 'Does not match expected master ticket.')],
20− [ticket]
21− );
22− const [ticketInput, { pass }] = useTicketInput({
23− name: 'ticket',
24− label: 'New master ticket',
25− initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
26− autoFocus: true,
27− validators,
28− });
29−
30− const next = useCallback(() => push(names.RETICKET), [push, names]);
31−
32− return (
33− <View>
34− Prove that you downloaded the new secrets!
35− <Input {...ticketInput} />
36− <ForwardButton disabled={!pass} onClick={next}>
37− Reticket
38− </ForwardButton>
39− </View>
40− );
41−}
src/views/SentTransaction.jsdeleted−183
@@ -1,183 +0,0 @@
1−import Maybe from 'folktale/maybe';
2−import React from 'react';
3−import * as need from '../lib/need';
4−import { H1, H3, P, Warning, Anchor } from '../components/old/Base';
5−import { Button } from '../components/old/Base';
6−
7−import { ROUTE_NAMES } from '../lib/routeNames';
8−import { useHistory } from '../store/history';
9−import { BRIDGE_ERROR, renderTxnError } from '../lib/error';
10−import { NETWORK_TYPES } from '../lib/network';
11−import { useTxnConfirmations } from '../store/txnConfirmations';
12−import { useNetwork } from '../store/network';
13−import { useTxnCursor } from '../store/txnCursor';
14−import View from 'components/View';
15−import { Grid } from 'indigo-react';
16−
17−class Success extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− pending: '.',
23− interval: null,
24− };
25− }
26−
27− componentDidMount() {
28− const nextDot = { '.': '..', '..': '...', '...': '.' };
29−
30− const interval = setInterval(() => {
31− this.setState(({ pending }) => ({ pending: nextDot[pending] }));
32− }, 1000);
33− this.setState({ interval: interval });
34− }
35−
36− componentWillUnmount() {
37− clearInterval(this.state.interval);
38− }
39−
40− render() {
41− const { props, state } = this;
42− const { networkType, hash, txnConfirmations } = props;
43− const { pending } = state;
44−
45− const esvisible =
46− networkType === NETWORK_TYPES.ROPSTEN ||
47− networkType === NETWORK_TYPES.MAINNET;
48−
49− const esdomain =
50− networkType === NETWORK_TYPES.ROPSTEN
51− ? 'ropsten.etherscan.io'
52− : 'etherscan.io';
53−
54− const esmessage =
55− esvisible === true
56− ? 'If you’d like to keep track of it, click the Etherscan link below.'
57− : '';
58−
59− const esanchor =
60− esvisible === false ? null : (
61− <Anchor
62− className={'mb-4 mt-1'}
63− prop-size={'sm'}
64− target={'_blank'}
65− href={`https://${esdomain}/tx/${hash}`}>
66− {'View on Etherscan ↗'}
67− </Anchor>
68− );
69−
70− const confirmations = Maybe.fromNullable(txnConfirmations[hash]).getOrElse(
71− 0
72− );
73−
74− const requiredConfirmations = 1;
75−
76− const status =
77− confirmations < requiredConfirmations
78− ? `Pending${pending}`
79− : `Confirmed! (x${confirmations} confirmations)!`;
80−
81− return (
82− <Grid>
83− <Grid.Item full>
84− <H1>{'Your Transaction was Sent'}</H1>
85−
86− <P>
87− {`We sent your transaction to the chain. It can take some time to
88− execute, especially if the network is busy. ${esmessage}`}
89− </P>
90−
91− <H3>{'Transaction Hash'}</H3>
92− <P>{hash}</P>
93−
94− <H3>{'Transaction Status'}</H3>
95− <P>{status}</P>
96− <P>{esanchor}</P>
97− </Grid.Item>
98− </Grid>
99− );
100− }
101−}
102−
103−const Failure = props => (
104− <Grid>
105− <Grid.Item full>
106− <H1>{'Error!'}</H1>
107−
108− <Warning>
109− <H3>{'There was an error sending your transaction.'}</H3>
110− {renderTxnError(props.web3, props.message)}
111− </Warning>
112− </Grid.Item>
113− </Grid>
114−);
115−
116−function SentTransaction(props) {
117− const history = useHistory();
118− const { txnConfirmations } = useTxnConfirmations();
119− const { web3, networkType } = useNetwork();
120− const { txnCursor } = useTxnCursor();
121−
122− const promptKeyfile = history.data && history.data.promptKeyfile;
123−
124− const w3 = need.web3(web3);
125−
126− const result = txnCursor.matchWith({
127− Nothing: _ => {
128− throw new Error(BRIDGE_ERROR.MISSING_TXN);
129− },
130− Just: res => res.value,
131− });
132−
133− const body = result.matchWith({
134− Error: message => <Failure web3={w3} message={message.value} />,
135− Ok: hash => (
136− <Success
137− hash={hash.value}
138− networkType={networkType}
139− txnConfirmations={txnConfirmations}
140− />
141− ),
142− });
143−
144− const ok = (
145− <Grid>
146− <Grid.Item full>
147− <Button
148− prop-type={'link'}
149− onClick={() => {
150− history.pop();
151− }}>
152− {'Ok →'}
153− </Button>
154− </Grid.Item>
155− </Grid>
156− );
157−
158− let keyfile;
159−
160− if (promptKeyfile) {
161− keyfile = (
162− <Grid>
163− <Grid.Item full>
164− <Button
165− prop-type={'link'}
166− onClick={() => history.popAndPush(ROUTE_NAMES.GEN_KEYFILE)}>
167− {'Download Keyfile →'}
168− </Button>
169− </Grid.Item>
170− </Grid>
171− );
172− }
173−
174− return (
175− <View>
176− {body}
177− {ok}
178− {keyfile}
179− </View>
180− );
181−}
182−
183−export default SentTransaction;
src/views/SetKeys.jsdeleted−210
@@ -1,210 +0,0 @@
1−import React from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−import * as kg from 'urbit-key-generation/dist';
7−import { H1, P, Warning, CheckboxButton } from '../components/old/Base';
8−
9−import StatelessTransaction from '../components/old/StatelessTransaction';
10−import { attemptNetworkSeedDerivation } from '../lib/keys';
11−
12−import { addHexPrefix } from '../lib/wallet';
13−import { compose } from '../lib/lib';
14−
15−import { withNetwork } from '../store/network';
16−import { withWallet } from '../store/wallet';
17−import { withPointCursor } from '../store/pointCursor';
18−import { withPointCache } from '../store/pointCache';
19−import View from 'components/View';
20−
21−class SetKeys extends React.Component {
22− constructor(props) {
23− super(props);
24−
25− const point = need.point(props.pointCursor);
26−
27− this.state = {
28− auth: '',
29− encr: '',
30− newNetworkSeed: '',
31− nondeterministicSeed: false,
32− point: point,
33− cryptoSuiteVersion: 1,
34− discontinuity: false,
35− isManagementSeed: false,
36− };
37−
38− this.toggleDiscontinuity = this.toggleDiscontinuity.bind(this);
39− // Transaction
40− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
41− }
42−
43− toggleDiscontinuity() {
44− this.setState({ discontinuity: !this.state.discontinuity });
45− }
46−
47− componentDidMount() {
48− const { props } = this;
49−
50− this.deriveSeed();
51−
52− const addr = need.addressFromWallet(props.wallet);
53−
54− this.determineManagementSeed(props.contracts.value, addr);
55− }
56−
57− async determineManagementSeed(ctrcs, addr) {
58− const managing = await azimuth.azimuth.getManagerFor(ctrcs, addr);
59−
60− this.setState({
61− isManagementSeed: managing.length !== 0,
62− });
63− }
64−
65− //TODO use web3.utils.randomHex when it gets fixed, see web3.js#1490
66− randomHex(len) {
67− let hex = '';
68−
69− for (var i = 0; i < len; i++) {
70− hex =
71− hex +
72− [
73− '0',
74− '1',
75− '2',
76− '3',
77− '4',
78− '5',
79− '6',
80− '7',
81− '8',
82− '9',
83− 'A',
84− 'B',
85− 'C',
86− 'D',
87− 'E',
88− 'F',
89− ][Math.floor(Math.random() * 16)];
90− }
91−
92− return hex;
93− }
94−
95− async deriveSeed() {
96− const next = true;
97− let nondeterministicSeed = false;
98−
99− const seed = await attemptNetworkSeedDerivation(next, this.props);
100− const newNetworkSeed = seed.matchWith({
101− Nothing: () => {
102− nondeterministicSeed = true;
103− return this.randomHex(64);
104− },
105− Just: p => p.value,
106− });
107−
108− this.setState({
109− newNetworkSeed,
110− nondeterministicSeed,
111− });
112− }
113−
114− createUnsignedTxn() {
115− const { state, props } = this;
116−
117− const validContracts = need.contracts(props.contracts);
118− const validPoint = need.point(props.pointCursor);
119−
120− // TODO: move this to a lib for validating things
121− const hexRegExp = /[0-9A-Fa-f]{64}/g;
122−
123− if (hexRegExp.test(state.newNetworkSeed)) {
124− // derive network keys
125− const pair = kg.deriveNetworkKeys(state.newNetworkSeed);
126−
127− const pencr = addHexPrefix(pair.crypt.public);
128− const pauth = addHexPrefix(pair.auth.public);
129−
130− const txn = azimuth.ecliptic.configureKeys(
131− validContracts,
132− validPoint,
133− pencr,
134− pauth,
135− 1,
136− state.discontinuity
137− );
138−
139− return Just(txn);
140− }
141−
142− return Nothing();
143− }
144−
145− render() {
146− const { props, state } = this;
147−
148− const canGenerate =
149− state.newNetworkSeed.length === 64 && state.newNetworkSeed.length === 64;
150−
151− const pointDetails = need.fromPointCache(props.pointCache, state.point);
152−
153− return (
154− <View>
155− <H1>
156− {'Set Network Keys For '} <code>{`${ob.patp(state.point)}`}</code>
157− </H1>
158−
159− <P className="mt-10">
160− {`Set new authentication and encryption keys for your Arvo ship.`}
161− </P>
162−
163− {state.nondeterministicSeed && (
164− <Warning>
165− <h3 className={'mb-2'}>{'Warning'}</h3>
166− {`Your network seed could not be derived automatically. We've
167− generated a random one for you, so you must download your Arvo
168− keyfile during this session after setting your keys.`}
169− </Warning>
170− )}
171−
172− {pointDetails.keyRevisionNumber === 0 ? (
173− <Warning>
174− <h3 className={'mb-2'}>{'Warning'}</h3>
175− {'Once these keys have been set, your point is considered ' +
176− "'linked'. This operation cannot be undone."}
177− </Warning>
178− ) : (
179− <div />
180− )}
181−
182− <CheckboxButton
183− className="mt-8"
184− onToggle={this.toggleDiscontinuity}
185− state={state.discontinuity}
186− label={`I have lost important off-chain data relating to this point
187− and need to do a hard reset.
188− (For example, rebooting an Arvo ship.)`}
189− />
190−
191− <StatelessTransaction
192− // Upper scope
193− {...props}
194− // Tx
195− canGenerate={canGenerate}
196− createUnsignedTxn={this.createUnsignedTxn}
197− newNetworkSeed={state.newNetworkSeed}
198− newRevision={parseInt(pointDetails.keyRevisionNumber) + 1}
199− />
200− </View>
201− );
202− }
203−}
204−
205−export default compose(
206− withNetwork,
207− withWallet,
208− withPointCursor,
209− withPointCache
210−)(SetKeys);
src/views/SetProxy.jsdeleted−181
@@ -1,181 +0,0 @@
1−import React from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import * as need from '../lib/need';
4−import * as azimuth from 'azimuth-js';
5−import {
6− H1,
7− P,
8− InnerLabel,
9− ShowBlockie,
10− Anchor,
11− HorizontalSelector,
12−} from '../components/old/Base';
13−import { AddressInput } from '../components/old/Base';
14−import * as ob from 'urbit-ob';
15−
16−import { PROXY_TYPE, renderProxyType } from '../lib/proxy';
17−
18−import { NETWORK_TYPES } from '../lib/network';
19−
20−import StatelessTransaction from '../components/old/StatelessTransaction';
21−
22−import { isValidAddress } from '../lib/wallet';
23−import { withNetwork } from '../store/network';
24−import { compose } from '../lib/lib';
25−import { withPointCursor } from '../store/pointCursor';
26−import View from 'components/View';
27−
28−class _SetProxy extends React.Component {
29− constructor(props) {
30− super(props);
31−
32− const issuingPoint = need.point(props.pointCursor);
33−
34− this.state = {
35− proxyAddress: '',
36− issuingPoint: issuingPoint,
37− setProxy: true,
38− };
39−
40− this.statelessRef = React.createRef();
41− this.handleSetUnset = this.handleSetUnset.bind(this);
42− this.handleAddressInput = this.handleAddressInput.bind(this);
43− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
44− }
45−
46− handleSetUnset(selected) {
47− const set = selected === 'set';
48− this.setState({
49− setProxy: set,
50− proxyAddress: set ? '' : '0x0000000000000000000000000000000000000000',
51− });
52− }
53−
54− handleAddressInput(proxyAddress) {
55− this.setState({ proxyAddress });
56−
57− this.statelessRef.current.clearTxn();
58− }
59−
60− createUnsignedTxn(proxyAddress) {
61− const { state, props } = this;
62−
63− const validContracts = need.contracts(props.contracts);
64− const validPoint = need.point(props.pointCursor);
65−
66− const txArgs = [validContracts, validPoint, state.proxyAddress];
67−
68− if (props.proxyType === PROXY_TYPE.TRANSFER_PROXY) {
69− return Just(azimuth.ecliptic.setTransferProxy(...txArgs));
70− }
71−
72− if (props.proxyType === PROXY_TYPE.SPAWN_PROXY) {
73− return Just(azimuth.ecliptic.setSpawnProxy(...txArgs));
74− }
75−
76− if (props.proxyType === PROXY_TYPE.MANAGEMENT_PROXY) {
77− return Just(azimuth.ecliptic.setManagementProxy(...txArgs));
78− }
79−
80− return Nothing();
81− }
82−
83− render() {
84− const { props, state } = this;
85−
86− const renderedProxyType = renderProxyType(props.proxyType);
87− const validAddress = isValidAddress(state.proxyAddress);
88− const canGenerate = validAddress === true;
89−
90− const esvisible =
91− props.networkType === NETWORK_TYPES.ROPSTEN ||
92− props.networkType === NETWORK_TYPES.MAINNET;
93−
94− const esdomain =
95− props.networkType === NETWORK_TYPES.ROPSTEN
96− ? 'ropsten.etherscan.io'
97− : 'etherscan.io';
98−
99− const ucFirst = w => w.charAt(0).toUpperCase() + w.slice(1);
100−
101− const setUnset = [
102− { title: 'Set', value: 'set' },
103− { title: 'Unset', value: 'unset' },
104− ];
105−
106− const titleVerb = this.state.setProxy ? 'Set' : 'Unset';
107−
108− let addressInput = null;
109− if (this.state.setProxy) {
110− addressInput = (
111− <>
112− <P className="mt-8">
113− {'Please provide an Ethereum address to act as the ' +
114− `${renderedProxyType} proxy. You can also use Wallet ` +
115− 'Generator to generate a keypair.'}
116− </P>
117−
118− <AddressInput
119− className="mono mt-8"
120− prop-size="lg"
121− prop-format="innerLabel"
122− placeholder={`e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f`}
123− value={state.proxyAddress}
124− onChange={v => this.handleAddressInput(v)}>
125− <InnerLabel>{'Proxy Address'}</InnerLabel>
126− <ShowBlockie className={'mt-1'} address={state.proxyAddress} />
127− </AddressInput>
128−
129− <Anchor
130− className={'mt-1'}
131− prop-size={'sm'}
132− prop-disabled={!isValidAddress(state.proxyAddress) || !esvisible}
133− target={'_blank'}
134− href={`https://${esdomain}/address/${state.proxyAddress}`}>
135− {'View on Etherscan ↗'}
136− </Anchor>
137− </>
138− );
139− }
140−
141− return (
142− <View>
143− <H1>
144− {`${titleVerb} ${ucFirst(renderedProxyType)} Proxy For `}
145− <code>{`${ob.patp(state.issuingPoint)}`}</code>
146− </H1>
147−
148− <HorizontalSelector
149− options={setUnset}
150− onChange={this.handleSetUnset}
151− className="mt-8"
152− />
153−
154− {addressInput}
155−
156− <StatelessTransaction
157− ref={this.statelessRef}
158− createUnsignedTxn={this.createUnsignedTxn}
159− canGenerate={canGenerate}
160− />
161− </View>
162− );
163− }
164−}
165−
166−const SetProxy = compose(
167− withNetwork,
168− withPointCursor
169−)(_SetProxy);
170−
171−export const SetManagementProxy = props => (
172− <SetProxy {...props} proxyType={PROXY_TYPE.MANAGEMENT_PROXY} />
173−);
174−
175−export const SetSpawnProxy = props => (
176− <SetProxy {...props} proxyType={PROXY_TYPE.SPAWN_PROXY} />
177−);
178−
179−export const SetTransferProxy = props => (
180− <SetProxy {...props} proxyType={PROXY_TYPE.TRANSFER_PROXY} />
181−);
src/views/Transfer.jsdeleted−127
@@ -1,127 +0,0 @@
1−import React from 'react';
2−import { Just } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−
7−import { H1, InnerLabel, ShowBlockie, P, Anchor } from '../components/old/Base';
8−import { AddressInput } from '../components/old/Base';
9−import StatelessTransaction from '../components/old/StatelessTransaction';
10−
11−import { NETWORK_TYPES } from '../lib/network';
12−
13−import { isValidAddress } from '../lib/wallet';
14−import { withNetwork } from '../store/network';
15−import { compose } from '../lib/lib';
16−import { withPointCursor } from '../store/pointCursor';
17−import View from 'components/View';
18−
19−class Transfer extends React.Component {
20− constructor(props) {
21− super(props);
22−
23− const issuingPoint = need.point(props.pointCursor);
24−
25− this.state = {
26− receivingAddress: '',
27− issuingPoint: issuingPoint,
28− };
29−
30− this.handleAddressInput = this.handleAddressInput.bind(this);
31− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
32− this.statelessRef = React.createRef();
33− }
34−
35− handleAddressInput(receivingAddress) {
36− this.setState({ receivingAddress });
37− this.statelessRef.current.clearTxn();
38− }
39−
40− handleConfirmAvailability() {
41− this.confirmPointAvailability().then(r => {
42− this.setState({ isAvailable: r });
43− });
44− }
45−
46− createUnsignedTxn() {
47− const { state, props } = this;
48−
49− const validContracts = need.contracts(props.contracts);
50− const validPoint = need.point(props.pointCursor);
51−
52− const txn = azimuth.ecliptic.setTransferProxy(
53− validContracts,
54− validPoint,
55− state.receivingAddress
56− );
57−
58− return Just(txn);
59− }
60−
61− render() {
62− const { props, state } = this;
63−
64− const validAddress = isValidAddress(state.receivingAddress);
65− const canGenerate = validAddress === true;
66−
67− const esvisible =
68− props.networkType === NETWORK_TYPES.ROPSTEN ||
69− props.networkType === NETWORK_TYPES.MAINNET;
70−
71− const esdomain =
72− props.networkType === NETWORK_TYPES.ROPSTEN
73− ? 'ropsten.etherscan.io'
74− : 'etherscan.io';
75−
76− return (
77− <View>
78− <H1>
79− {'Transfer'} <code>{` ${ob.patp(state.issuingPoint)} `}</code>
80− {'To a New Owner'}
81− </H1>
82−
83− <P>
84− {`Please provide the Ethereum address of the new owner. You own these
85− points until the recipient accepts the incoming transfer.
86− You may cancel the transfer until the transfer is accepted.`}
87− </P>
88−
89− <AddressInput
90− className={'mono mt-8'}
91− prop-size="lg"
92− prop-format="innerLabel"
93− value={state.receivingAddress}
94− placeholder={`e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f`}
95− onChange={v => this.handleAddressInput(v)}>
96− <InnerLabel>{'New ownership address'}</InnerLabel>
97− <ShowBlockie className={'mt-1'} address={state.receivingAddress} />
98− </AddressInput>
99−
100− <Anchor
101− className={'mt-1'}
102− prop-size={'s'}
103− prop-disabled={!isValidAddress(state.receivingAddress) || !esvisible}
104− target={'_blank'}
105− href={`https://${esdomain}/address/${state.receivingAddress}`}>
106− {'View on Etherscan ↗'}
107− </Anchor>
108−
109− <StatelessTransaction
110− // Upper scope
111− {...props}
112− // Checks
113− userApproval={state.userApproval}
114− canGenerate={canGenerate}
115− // Methods
116− createUnsignedTxn={this.createUnsignedTxn}
117− ref={this.statelessRef}
118− />
119− </View>
120− );
121− }
122−}
123−
124−export default compose(
125− withNetwork,
126− withPointCursor
127−)(Transfer);