@@ -0,0 +1,294 @@
1+import React, { useCallback, useMemo, useRef, useState } from 'react';
2+import { Just } from 'folktale/maybe';
3+import { Grid, ToggleInput, CheckboxInput } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
5+import { randomHex } from 'web3-utils';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
9+import { useNetwork } from 'store/network';
10+import { useWallet } from 'store/wallet';
11+
12+import { useLocalRouter } from 'lib/LocalRouter';
13+import * as need from 'lib/need';
14+import {
15+ attemptNetworkSeedDerivation,
16+ deriveNetworkKeys,
17+ CRYPTO_SUITE_VERSION,
18+} from 'lib/keys';
19+import useEthereumTransaction from 'lib/useEthereumTransaction';
20+import { GAS_LIMITS } from 'lib/constants';
21+import { addHexPrefix } from 'lib/wallet';
22+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
23+
24+import ViewHeader from 'components/ViewHeader';
25+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
26+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
27+import NoticeBox from 'components/NoticeBox';
28+
29+import { HexInput } from 'form/Inputs';
30+import {
31+ composeValidator,
32+ buildCheckboxValidator,
33+ buildHexValidator,
34+} from 'form/validators';
35+import BridgeForm from 'form/BridgeForm';
36+import Condition from 'form/Condition';
37+import FormError from 'form/FormError';
38+import convertToInt from 'lib/convertToInt';
39+
40+function useSetKeys() {
41+ const { urbitWallet, wallet, authMnemonic } = useWallet();
42+ const { pointCursor } = usePointCursor();
43+ const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
44+ const { contracts } = useNetwork();
45+
46+ const _point = need.point(pointCursor);
47+ const _contracts = need.contracts(contracts);
48+ const _details = need.details(getDetails(_point));
49+
50+ const networkRevision = convertToInt(_details.keyRevisionNumber, 10);
51+ const randomSeed = useRef();
52+
53+ // NOTE: nd = 'nondeterministic' (can also be a 'manual' seed)
54+ const [ndNetworkSeed, setNdNetworkSeed] = useState();
55+
56+ const {
57+ available: keyfileAvailable,
58+ filename,
59+ bind: keyfileBind,
60+ } = useKeyfileGenerator(ndNetworkSeed);
61+
62+ const buildNetworkSeed = useCallback(
63+ async manualSeed => {
64+ if (manualSeed !== undefined) {
65+ setNdNetworkSeed(manualSeed);
66+ return manualSeed;
67+ } else {
68+ const newNetworkRevision = networkRevision + 1;
69+ console.log(`deriving seed with revision ${newNetworkRevision}`);
70+
71+ const networkSeed = await attemptNetworkSeedDerivation({
72+ urbitWallet,
73+ wallet,
74+ authMnemonic,
75+ details: _details,
76+ revision: newNetworkRevision,
77+ });
78+
79+ if (Just.hasInstance(networkSeed)) {
80+ return networkSeed.value;
81+ }
82+
83+ randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
84+ setNdNetworkSeed(randomSeed.current);
85+
86+ return randomSeed.current;
87+ }
88+ },
89+ [_details, authMnemonic, networkRevision, urbitWallet, wallet]
90+ );
91+
92+ const { completed: _completed, ...rest } = useEthereumTransaction(
93+ useCallback(
94+ async (manualSeed, isDiscontinuity) => {
95+ const seed = await buildNetworkSeed(manualSeed);
96+ const pair = deriveNetworkKeys(seed);
97+
98+ return azimuth.ecliptic.configureKeys(
99+ _contracts,
100+ _point,
101+ addHexPrefix(pair.crypt.public),
102+ addHexPrefix(pair.auth.public),
103+ CRYPTO_SUITE_VERSION,
104+ isDiscontinuity
105+ );
106+ },
107+ [_contracts, _point, buildNetworkSeed]
108+ ),
109+ useCallback(
110+ () => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
111+ [_point, syncDetails, syncRekeyDate]
112+ ),
113+ GAS_LIMITS.CONFIGURE_KEYS
114+ );
115+
116+ // only treat the transaction as completed once we also have keys to download
117+ const completed = _completed && keyfileAvailable;
118+
119+ return {
120+ completed,
121+ ndNetworkSeed,
122+ filename,
123+ keyfileBind,
124+ ...rest,
125+ };
126+}
127+
128+export default function UrbitOSNetworkingKeys() {
129+ const { push, names, pop } = useLocalRouter();
130+ const { pointCursor } = usePointCursor();
131+ const { getDetails } = usePointCache();
132+
133+ const point = need.point(pointCursor);
134+ const details = getDetails(point);
135+
136+ const hasKeys = details.matchWith({
137+ Nothing: () => false,
138+ Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
139+ // we actually don't mind the default NaN behavior of parseInt here,
140+ // since NaN > 0 === false and that's a reasonable result
141+ });
142+
143+ const {
144+ isDefaultState,
145+ construct,
146+ unconstruct,
147+ broadcasting,
148+ completed,
149+ inputsLocked,
150+ bind,
151+ keyfileBind,
152+ } = useSetKeys();
153+
154+ const validateForm = useCallback((values, errors) => {
155+ if (values.useNetworkSeed && errors.networkSeed) {
156+ return errors;
157+ }
158+ }, []);
159+
160+ const validate = useMemo(
161+ () =>
162+ composeValidator(
163+ {
164+ useNetworkSeed: buildCheckboxValidator(),
165+ networkSeed: buildHexValidator(64), // 64 chars
166+ useDiscontinuity: buildCheckboxValidator(),
167+ },
168+ validateForm
169+ ),
170+ [validateForm]
171+ );
172+
173+ const onValues = useCallback(
174+ ({ valid, values, form }) => {
175+ if (valid) {
176+ construct(
177+ values.useNetworkSeed ? values.networkSeed : undefined,
178+ values.useDiscontinuity
179+ );
180+ } else {
181+ unconstruct();
182+ }
183+
184+ if (!values.useNetworkSeed && values.networkSeed) {
185+ form.change('networkSeed', '');
186+ }
187+ },
188+ [construct, unconstruct]
189+ );
190+
191+ const initialValues = useMemo(
192+ () => ({
193+ useNetworkSeed: false,
194+ useDiscontinuity: false,
195+ }),
196+ []
197+ );
198+
199+ const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
200+
201+ const usageMessage = 'You need this to authenticate with arvo';
202+
203+ const renderTitle = () => {
204+ if (completed) {
205+ return 'Networking keys are now set.';
206+ }
207+
208+ if (broadcasting) {
209+ return 'Setting Network Keys...';
210+ }
211+
212+ if (!hasKeys) {
213+ return 'Networking keys have not yet been set.';
214+ }
215+
216+ return 'Networking';
217+ };
218+
219+ return (
220+ <>
221+ <Grid>
222+ <Grid.Item full as={ViewHeader}>
223+ {renderTitle()}
224+ </Grid.Item>
225+ {completed && (
226+ <>
227+ <Grid.Item
228+ full
229+ className="mb3"
230+ as={DownloadKeyfileButton}
231+ solid
232+ {...keyfileBind}
233+ />
234+ <Grid.Item full className="mb3 bg-red3 white t-center p3">
235+ {usageMessage}
236+ </Grid.Item>
237+ </>
238+ )}
239+ <BridgeForm
240+ validate={validate}
241+ onValues={onValues}
242+ initialValues={initialValues}>
243+ {() => (
244+ <>
245+ {!completed && (
246+ <>
247+ <Grid.Item
248+ full
249+ as={ToggleInput}
250+ name="useNetworkSeed"
251+ label="Use Custom Network Seed"
252+ inverseLabel="Back to Derived Network Seed"
253+ disabled={inputsLocked}
254+ />
255+ <Condition when="useNetworkSeed" is={true}>
256+ <Grid.Item full as={NoticeBox} className="mb2">
257+ When using a custom network seed, you'll need to download
258+ your Arvo keyfile immediately after this transaction is
259+ Bridge does not store your seed.
260+ </Grid.Item>
261+ <Grid.Item
262+ full
263+ as={HexInput}
264+ name="networkSeed"
265+ label="Network Seed (32 bytes)"
266+ disabled={inputsLocked}
267+ />
268+ </Condition>
269+ <Grid.Item
270+ full
271+ as={CheckboxInput}
272+ name="useDiscontinuity"
273+ label="Breach Continuity"
274+ disabled={inputsLocked}
275+ />
276+
277+ <Grid.Item full as={FormError} />
278+ </>
279+ )}
280+
281+ <Grid.Item
282+ full
283+ as={InlineEthereumTransaction}
284+ {...bind}
285+ label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
286+ onReturn={() => pop()}
287+ />
288+ </>
289+ )}
290+ </BridgeForm>
291+ </Grid>
292+ </>
293+ );
294+}