on bfbafde help sign in
bridge/src/views/Admin/AdminNetworkingKeys.js
434 lines 12 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 ob from 'urbit-ob';
6import { randomHex } from 'web3-utils';
8import { usePointCursor } from 'store/pointCursor';
9import { usePointCache } from 'store/pointCache';
10import { useNetwork } from 'store/network';
11import { useWallet } from 'store/wallet';
13import { useLocalRouter } from 'lib/LocalRouter';
14import * as need from 'lib/need';
15import {
16 segmentNetworkKey,
17 attemptNetworkSeedDerivation,
18 deriveNetworkKeys,
19 CRYPTO_SUITE_VERSION,
20 CURVE_ZERO_ADDR,
21} from 'lib/keys';
22import { formatDotsWithTime } from 'lib/dateFormat';
23import useEthereumTransaction from 'lib/useEthereumTransaction';
24import { GAS_LIMITS } from 'lib/constants';
25import { addHexPrefix } from 'lib/wallet';
26import useKeyfileGenerator from 'lib/useKeyfileGenerator';
28import ViewHeader from 'components/ViewHeader';
29import { BootUrbitOSButton } from 'components/Buttons';
30import FooterButton from 'components/FooterButton';
31import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
32import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
33import NoticeBox from 'components/NoticeBox';
35import { HexInput } from 'form/Inputs';
36import {
37 composeValidator,
38 buildCheckboxValidator,
39 buildHexValidator,
40} from 'form/validators';
41import BridgeForm from 'form/BridgeForm';
42import Condition from 'form/Condition';
43import FormError from 'form/FormError';
44import convertToInt from 'lib/convertToInt';
46const chainKeyProp = name => d =>
47 d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
49const renderNetworkKey = key => {
50 const segments = segmentNetworkKey(key);
52 if (!segments) {
53 return 'Unset';
54 }
56 return segments.join('\n');
57};
59function useSetKeys() {
60 const { urbitWallet, wallet, authMnemonic } = useWallet();
61 const { pointCursor } = usePointCursor();
62 const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
63 const { contracts } = useNetwork();
65 const _point = need.point(pointCursor);
66 const _contracts = need.contracts(contracts);
67 const _details = need.details(getDetails(_point));
69 const networkRevision = convertToInt(_details.keyRevisionNumber, 10);
70 const randomSeed = useRef();
72 // NOTE: nd = 'nondeterministic' (can also be a 'manual' seed)
73 const [ndNetworkSeed, setNdNetworkSeed] = useState();
75 const {
76 available: keyfileAvailable,
77 generating: keyfileGenerating,
78 filename,
79 bind: keyfileBind,
80 } = useKeyfileGenerator(ndNetworkSeed);
82 const buildNetworkSeed = useCallback(
83 async manualSeed => {
84 if (manualSeed !== undefined) {
85 setNdNetworkSeed(manualSeed);
86 return manualSeed;
87 } else {
88 const newNetworkRevision = networkRevision + 1;
89 console.log(`deriving seed with revision ${newNetworkRevision}`);
91 const networkSeed = await attemptNetworkSeedDerivation({
92 urbitWallet,
93 wallet,
94 authMnemonic,
95 details: _details,
96 revision: newNetworkRevision,
97 });
99 if (Just.hasInstance(networkSeed)) {
100 return networkSeed.value;
101 }
103 randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
104 setNdNetworkSeed(randomSeed.current);
106 return randomSeed.current;
107 }
108 },
109 [_details, authMnemonic, networkRevision, urbitWallet, wallet]
110 );
112 const { completed: _completed, ...rest } = useEthereumTransaction(
113 useCallback(
114 async (manualSeed, isDiscontinuity) => {
115 const seed = await buildNetworkSeed(manualSeed);
116 const pair = deriveNetworkKeys(seed);
118 return azimuth.ecliptic.configureKeys(
119 _contracts,
120 _point,
121 addHexPrefix(pair.crypt.public),
122 addHexPrefix(pair.auth.public),
123 CRYPTO_SUITE_VERSION,
124 isDiscontinuity
125 );
126 },
127 [_contracts, _point, buildNetworkSeed]
128 ),
129 useCallback(
130 () => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
131 [_point, syncDetails, syncRekeyDate]
132 ),
133 GAS_LIMITS.CONFIGURE_KEYS
134 );
136 // only treat the transaction as completed once we also have keys to download
137 const completed = _completed && keyfileAvailable && !keyfileGenerating;
139 return {
140 completed,
141 ndNetworkSeed,
142 filename,
143 keyfileBind,
144 ...rest,
145 };
148export default function AdminNetworkingKeys() {
149 const { push, names, pop } = useLocalRouter();
150 const { pointCursor } = usePointCursor();
151 const { getDetails, getRekeyDate } = usePointCache();
153 const point = need.point(pointCursor);
154 const details = getDetails(point);
156 const hasKeys = details.matchWith({
157 Nothing: () => false,
158 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
159 // we actually don't mind the default NaN behavior of parseInt here,
160 // since NaN > 0 === false and that's a reasonable result
161 });
163 const [usedDiscontinuity, setUsedDiscontinuity] = useState(false);
165 const {
166 isDefaultState,
167 construct,
168 unconstruct,
169 broadcasting,
170 completed,
171 inputsLocked,
172 filename,
173 bind,
174 keyfileBind,
175 } = useSetKeys();
177 const validateForm = useCallback((values, errors) => {
178 if (values.useNetworkSeed && errors.networkSeed) {
179 return errors;
180 }
181 }, []);
183 const validate = useMemo(
184 () =>
185 composeValidator(
186 {
187 useNetworkSeed: buildCheckboxValidator(),
188 networkSeed: buildHexValidator(64), // 64 chars
189 useDiscontinuity: buildCheckboxValidator(),
190 },
191 validateForm
192 ),
193 [validateForm]
194 );
196 const onValues = useCallback(
197 ({ valid, values, form }) => {
198 if (valid) {
199 construct(
200 values.useNetworkSeed ? values.networkSeed : undefined,
201 values.useDiscontinuity
202 );
203 setUsedDiscontinuity(values.useDiscontinuity);
204 } else {
205 unconstruct();
206 }
208 if (!values.useNetworkSeed && values.networkSeed) {
209 form.change('networkSeed', '');
210 }
211 },
212 [construct, unconstruct, setUsedDiscontinuity]
213 );
215 const initialValues = useMemo(
216 () => ({
217 useNetworkSeed: false,
218 useDiscontinuity: false,
219 }),
220 []
221 );
223 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
225 const usageMessage = useMemo(() => {
226 const need = 'You need this keyfile to authenticate with Arvo.';
227 const boot = (
228 <>
229 boot fresh with{' '}
230 <code>
231 urbit -w {ob.patp(point).slice(1)} -k {filename}
232 </code>
233 </>
234 );
235 if (usedDiscontinuity) {
236 return (
237 <>
238 {need} Since you broke continuity, (re)move your old files, and {boot}
239 </>
240 );
241 } else {
242 return (
243 <>
244 {need} If you have already booted Arvo, run{' '}
245 <code>|rekey '0wkeyfile.contents'</code>. Otherwise, {boot}
246 </>
247 );
248 }
249 }, [usedDiscontinuity, filename, point]);
251 const renderTitle = () => {
252 if (completed) {
253 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
254 }
256 if (broadcasting) {
257 return 'Setting Network Keys...';
258 }
260 if (!hasKeys) {
261 return 'Networking keys have not yet been set.';
262 }
264 return 'Networking';
265 };
267 const renderNetworkKeySection = (title, key) => (
268 <>
269 <Grid.Item full as={H5} className="mt3 gray4">
270 {title}
271 </Grid.Item>
272 {key.matchWith({
273 Nothing: () => (
274 <Grid.Item full as="code" className="f5 mono gray4">
275 Unset
276 </Grid.Item>
277 ),
278 Just: ({ value: key }) => (
279 <>
280 <Grid.Item full as="code" className="f5 mono wrap">
281 0x
282 </Grid.Item>
283 <Grid.Item full as="code" className="f5 mono wrap">
284 {renderNetworkKey(key)}
285 </Grid.Item>
286 </>
287 ),
288 })}
289 </>
290 );
292 const renderDetail = (title, value) => (
293 <Flex.Item as={Flex} col>
294 <Flex.Item as={H5} className="gray4">
295 {title}
296 </Flex.Item>
297 {value.matchWith({
298 Nothing: () => (
299 <Flex.Item as={Text} className="f5 gray4">
300 Unset
301 </Flex.Item>
302 ),
303 Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
304 })}
305 </Flex.Item>
306 );
308 const renderDetails = () => {
309 return (
310 <>
311 {renderNetworkKeySection(
312 'Authentication',
313 details.chain(chainKeyProp('authenticationKey'))
314 )}
315 {renderNetworkKeySection(
316 'Encryption',
317 details.chain(chainKeyProp('encryptionKey'))
318 )}
319 <Grid.Item full as={Flex} row justify="between" className="mt3">
320 {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
321 {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
322 {renderDetail(
323 'Crypto Suite Ver.',
324 details.map(d => d.cryptoSuiteVersion)
325 )}
326 </Grid.Item>
327 {hasKeys && (
328 <Grid.Item full as={Flex} row justify="between" className="mt3">
329 {renderDetail(
330 'Last Set',
331 getRekeyDate(point).map(date =>
332 date.matchWith({
333 Ok: r => formatDotsWithTime(r.value),
334 Error: () => Nothing(),
335 })
336 )
337 )}
338 </Grid.Item>
339 )}
340 </>
341 );
342 };
344 return (
345 <>
346 <Grid>
347 <Grid.Item full as={ViewHeader}>
348 {renderTitle()}
349 </Grid.Item>
351 <Grid.Item full as={Text} className="mb3">
352 {hasKeys
353 ? 'Here are your public keys that authenticate your Arvo.'
354 : 'Set your networking keys to authenticate with Arvo.'}
355 </Grid.Item>
357 {completed && (
358 <>
359 <Grid.Item full as={NoticeBox} className="mb3">
360 {usageMessage}
361 </Grid.Item>
362 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
363 </>
364 )}
366 {!completed && (
367 <BridgeForm
368 validate={validate}
369 onValues={onValues}
370 initialValues={initialValues}>
371 {() => (
372 <>
373 <Grid.Item
374 full
375 as={ToggleInput}
376 name="useNetworkSeed"
377 label="Use Custom Network Seed"
378 inverseLabel="Back to Derived Network Seed"
379 disabled={inputsLocked}
380 />
381 <Condition when="useNetworkSeed" is={true}>
382 <Grid.Item full as={NoticeBox} className="mb2">
383 When using a custom network seed, you'll need to download
384 your Arvo keyfile immediately after this transaction is
385 Bridge does not store your seed.
386 </Grid.Item>
387 <Grid.Item
388 full
389 as={HexInput}
390 name="networkSeed"
391 label="Network Seed (32 bytes)"
392 disabled={inputsLocked}
393 />
394 </Condition>
395 <Grid.Item
396 full
397 as={CheckboxInput}
398 name="useDiscontinuity"
399 label="Breach Continuity"
400 disabled={inputsLocked}
401 />
403 <Grid.Item full as={FormError} />
405 <Grid.Item
406 full
407 as={InlineEthereumTransaction}
408 {...bind}
409 label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
410 onReturn={() => pop()}
411 />
412 </>
413 )}
414 </BridgeForm>
415 )}
417 {isDefaultState && renderDetails()}
419 {completed && (
420 <>
421 <Grid.Item full as={BootUrbitOSButton} />
422 <Grid.Divider />
423 </>
424 )}
425 </Grid>
427 {isDefaultState && (
428 <FooterButton onClick={goRelocate} disabled>
429 Relocate
430 </FooterButton>
431 )}
432 </>
433 );