on e82c628 help sign in
bridge/src/views/Admin/AdminNetworkingKeys.js
400 lines 11 kB history raw
1import React, { useCallback, useMemo, useRef, useState } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
3import { Grid, Text, H5, Flex, ToggleInput, CheckboxInput } from 'indigo-react';
4import * as azimuth from 'azimuth-js';
5import { randomHex } from 'web3-utils';
7import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
9import { useNetwork } from 'store/network';
10import { useWallet } from 'store/wallet';
12import { useLocalRouter } from 'lib/LocalRouter';
13import * as need from 'lib/need';
14import {
15 segmentNetworkKey,
16 attemptNetworkSeedDerivation,
17 deriveNetworkKeys,
18 CRYPTO_SUITE_VERSION,
19 CURVE_ZERO_ADDR,
20} from 'lib/keys';
21import { formatDotsWithTime } from 'lib/dateFormat';
22import useEthereumTransaction from 'lib/useEthereumTransaction';
23import { GAS_LIMITS } from 'lib/constants';
24import { addHexPrefix } from 'lib/wallet';
25import useKeyfileGenerator from 'lib/useKeyfileGenerator';
27import ViewHeader from 'components/ViewHeader';
28import { BootArvoButton } from 'components/Buttons';
29import FooterButton from 'components/FooterButton';
30import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
31import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
32import NoticeBox from 'components/NoticeBox';
34import { HexInput } from 'form/Inputs';
35import {
36 composeValidator,
37 buildCheckboxValidator,
38 buildHexValidator,
39} from 'form/validators';
40import BridgeForm from 'form/BridgeForm';
41import Condition from 'form/Condition';
42import FormError from 'form/FormError';
44const chainKeyProp = name => d =>
45 d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
47const renderNetworkKey = key => {
48 const segments = segmentNetworkKey(key);
50 if (!segments) {
51 return 'Unset';
52 }
54 return segments.join('\n');
55};
57function useSetKeys() {
58 const { urbitWallet, wallet, authMnemonic } = useWallet();
59 const { pointCursor } = usePointCursor();
60 const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
61 const { contracts } = useNetwork();
63 const _point = need.point(pointCursor);
64 const _contracts = need.contracts(contracts);
65 const _details = need.details(getDetails(_point));
67 const networkRevision = parseInt(_details.keyRevisionNumber, 10);
68 const randomSeed = useRef();
70 // NOTE: nd = 'nondeterministic' (can also be a 'manual' seed)
71 const [ndNetworkSeed, setNdNetworkSeed] = useState();
73 const {
74 available: keyfileAvailable,
75 bind: keyfileBind,
76 } = useKeyfileGenerator(ndNetworkSeed);
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}`);
87 const networkSeed = await attemptNetworkSeedDerivation({
88 urbitWallet,
89 wallet,
90 authMnemonic,
91 details: _details,
92 revision: newNetworkRevision,
93 });
95 if (Just.hasInstance(networkSeed)) {
96 return networkSeed.value;
97 }
99 randomSeed.current = randomSeed.current || randomHex(64);
100 setNdNetworkSeed(randomSeed.current);
102 return randomSeed.current;
103 }
104 },
105 [_details, authMnemonic, networkRevision, urbitWallet, wallet]
106 );
108 const { completed: _completed, ...rest } = useEthereumTransaction(
109 useCallback(
110 async (manualSeed, isDiscontinuity) => {
111 const seed = await buildNetworkSeed(manualSeed);
112 const pair = deriveNetworkKeys(seed);
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 );
132 // only treat the transaction as completed once we also have keys to download
133 const completed = _completed && keyfileAvailable;
135 return {
136 completed,
137 ndNetworkSeed,
138 keyfileBind,
139 ...rest,
140 };
143export default function AdminNetworkingKeys() {
144 const { push, names, pop } = useLocalRouter();
145 const { pointCursor } = usePointCursor();
146 const { getDetails, getRekeyDate } = usePointCache();
148 const point = need.point(pointCursor);
149 const details = getDetails(point);
151 const hasKeys = details.matchWith({
152 Nothing: () => false,
153 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
154 });
156 const {
157 isDefaultState,
158 construct,
159 unconstruct,
160 broadcasting,
161 completed,
162 inputsLocked,
163 bind,
164 keyfileBind,
165 } = useSetKeys();
167 const validateForm = useCallback((values, errors) => {
168 if (values.useNetworkSeed && errors.networkSeed) {
169 return errors;
170 }
172 return {};
173 }, []);
175 const validate = useMemo(
176 () =>
177 composeValidator(
178 {
179 useNetworkSeed: buildCheckboxValidator(),
180 networkSeed: buildHexValidator(32),
181 useDiscontinuity: buildCheckboxValidator(),
182 },
183 validateForm
184 ),
185 [validateForm]
186 );
188 const onValues = useCallback(
189 ({ valid, values, form }) => {
190 if (valid) {
191 construct(
192 values.useNetworkSeed ? values.networkSeed : undefined,
193 values.useDiscontinuity
194 );
195 } else {
196 unconstruct();
197 }
199 if (!values.useNetworkSeed && values.networkSeed) {
200 form.change('networkSeed', '');
201 }
202 },
203 [construct, unconstruct]
204 );
206 const initialValues = useMemo(
207 () => ({
208 useNetworkSeed: false,
209 useDiscontinuity: false,
210 }),
211 []
212 );
214 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
216 const renderTitle = () => {
217 if (completed) {
218 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
219 }
221 if (broadcasting) {
222 return 'Setting Network Keys...';
223 }
225 if (!hasKeys) {
226 return 'Networking keys have not yet been set.';
227 }
229 return 'Networking';
230 };
232 const renderNetworkKeySection = (title, key) => (
233 <>
234 <Grid.Item full as={H5} className="mt3 gray4">
235 {title}
236 </Grid.Item>
237 {key.matchWith({
238 Nothing: () => (
239 <Grid.Item full as="code" className="f5 gray4">
240 Unset
241 </Grid.Item>
242 ),
243 Just: ({ value: key }) => (
244 <>
245 <Grid.Item full as="code" className="f5 wrap">
246 0x
247 </Grid.Item>
248 <Grid.Item full as="code" className="f5 wrap">
249 {renderNetworkKey(key)}
250 </Grid.Item>
251 </>
252 ),
253 })}
254 </>
255 );
257 const renderDetail = (title, value) => (
258 <Flex.Item as={Flex} col>
259 <Flex.Item as={H5} className="gray4">
260 {title}
261 </Flex.Item>
262 {value.matchWith({
263 Nothing: () => (
264 <Flex.Item as={Text} className="f5 gray4">
265 Unset
266 </Flex.Item>
267 ),
268 Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
269 })}
270 </Flex.Item>
271 );
273 const renderDetails = () => {
274 return (
275 <>
276 {renderNetworkKeySection(
277 'Authentication',
278 details.chain(chainKeyProp('authenticationKey'))
279 )}
280 {renderNetworkKeySection(
281 'Encryption',
282 details.chain(chainKeyProp('encryptionKey'))
283 )}
284 <Grid.Item full as={Flex} row justify="between" className="mt3">
285 {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
286 {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
287 {renderDetail(
288 'Crypto Suite Ver.',
289 details.map(d => d.cryptoSuiteVersion)
290 )}
291 </Grid.Item>
292 {hasKeys && (
293 <Grid.Item full as={Flex} row justify="between" className="mt3">
294 {renderDetail(
295 'Last Set',
296 getRekeyDate(point).map(date =>
297 date.matchWith({
298 Ok: r => formatDotsWithTime(r.value),
299 Error: () => Nothing(),
300 })
301 )
302 )}
303 </Grid.Item>
304 )}
305 </>
306 );
307 };
309 return (
310 <>
311 <Grid>
312 <Grid.Item full as={ViewHeader}>
313 {renderTitle()}
314 </Grid.Item>
316 <Grid.Item full as={Text} className="mb3">
317 {hasKeys
318 ? 'Here are your public keys that authenticate your Arvo.'
319 : 'Set your networking keys to authenticate with Arvo.'}
320 </Grid.Item>
322 {completed && (
323 <>
324 <Grid.Item full as={NoticeBox} className="mb3">
325 You need this keyfile to authenticate with Arvo. If you have
326 already booted Arvo, run <code>|rekey '0wkeyfile.contents'</code>
327 </Grid.Item>
328 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
329 </>
330 )}
332 {!completed && (
333 <BridgeForm
334 validate={validate}
335 onValues={onValues}
336 initialValues={initialValues}>
337 {({ handleSubmit }) => (
338 <>
339 <Grid.Item
340 full
341 as={ToggleInput}
342 name="useNetworkSeed"
343 label="Use Custom Network Seed"
344 inverseLabel="Back to Derived Network Seed"
345 disabled={inputsLocked}
346 />
347 <Condition when="useNetworkSeed" is={true}>
348 <Grid.Item full as={NoticeBox} className="mb2">
349 When using a custom network seed, you'll need to download
350 your Arvo keyfile immediately after this transaction is
351 completed—Multipass does not store your seed.
352 </Grid.Item>
353 <Grid.Item
354 full
355 as={HexInput}
356 name="networkSeed"
357 label="Network Seed (64 bytes)"
358 disabled={inputsLocked}
359 />
360 </Condition>
361 <Grid.Item
362 full
363 as={CheckboxInput}
364 name="useDiscontinuity"
365 label="Trigger New Continuity Era"
366 disabled={inputsLocked}
367 />
369 <Grid.Item full as={FormError} />
371 <Grid.Item
372 full
373 as={InlineEthereumTransaction}
374 {...bind}
375 label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
376 onReturn={() => pop()}
377 />
378 </>
379 )}
380 </BridgeForm>
381 )}
383 {isDefaultState && renderDetails()}
385 {completed && (
386 <>
387 <Grid.Item full as={BootArvoButton} disabled />
388 <Grid.Divider />
389 </>
390 )}
391 </Grid>
393 {isDefaultState && (
394 <FooterButton onClick={goRelocate} disabled>
395 Relocate
396 </FooterButton>
397 )}
398 </>
399 );