on f6537ea help sign in
bridge/src/views/Admin/AdminNetworkingKeys.js
433 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 filename,
78 bind: keyfileBind,
79 } = useKeyfileGenerator(ndNetworkSeed);
81 const buildNetworkSeed = useCallback(
82 async manualSeed => {
83 if (manualSeed !== undefined) {
84 setNdNetworkSeed(manualSeed);
85 return manualSeed;
86 } else {
87 const newNetworkRevision = networkRevision + 1;
88 console.log(`deriving seed with revision ${newNetworkRevision}`);
90 const networkSeed = await attemptNetworkSeedDerivation({
91 urbitWallet,
92 wallet,
93 authMnemonic,
94 details: _details,
95 revision: newNetworkRevision,
96 });
98 if (Just.hasInstance(networkSeed)) {
99 return networkSeed.value;
100 }
102 randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
103 setNdNetworkSeed(randomSeed.current);
105 return randomSeed.current;
106 }
107 },
108 [_details, authMnemonic, networkRevision, urbitWallet, wallet]
109 );
111 const { completed: _completed, ...rest } = useEthereumTransaction(
112 useCallback(
113 async (manualSeed, isDiscontinuity) => {
114 const seed = await buildNetworkSeed(manualSeed);
115 const pair = deriveNetworkKeys(seed);
117 return azimuth.ecliptic.configureKeys(
118 _contracts,
119 _point,
120 addHexPrefix(pair.crypt.public),
121 addHexPrefix(pair.auth.public),
122 CRYPTO_SUITE_VERSION,
123 isDiscontinuity
124 );
125 },
126 [_contracts, _point, buildNetworkSeed]
127 ),
128 useCallback(
129 () => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
130 [_point, syncDetails, syncRekeyDate]
131 ),
132 GAS_LIMITS.CONFIGURE_KEYS
133 );
135 // only treat the transaction as completed once we also have keys to download
136 const completed = _completed && keyfileAvailable;
138 return {
139 completed,
140 ndNetworkSeed,
141 filename,
142 keyfileBind,
143 ...rest,
144 };
147export default function AdminNetworkingKeys() {
148 const { push, names, pop } = useLocalRouter();
149 const { pointCursor } = usePointCursor();
150 const { getDetails, getRekeyDate } = usePointCache();
152 const point = need.point(pointCursor);
153 const details = getDetails(point);
155 const hasKeys = details.matchWith({
156 Nothing: () => false,
157 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
158 // we actually don't mind the default NaN behavior of parseInt here,
159 // since NaN > 0 === false and that's a reasonable result
160 });
162 const [usedDiscontinuity, setUsedDiscontinuity] = useState(false);
164 const {
165 isDefaultState,
166 construct,
167 unconstruct,
168 broadcasting,
169 completed,
170 inputsLocked,
171 filename,
172 bind,
173 keyfileBind,
174 } = useSetKeys();
176 const validateForm = useCallback((values, errors) => {
177 if (values.useNetworkSeed && errors.networkSeed) {
178 return errors;
179 }
180 }, []);
182 const validate = useMemo(
183 () =>
184 composeValidator(
185 {
186 useNetworkSeed: buildCheckboxValidator(),
187 networkSeed: buildHexValidator(64), // 64 chars
188 useDiscontinuity: buildCheckboxValidator(),
189 },
190 validateForm
191 ),
192 [validateForm]
193 );
195 const onValues = useCallback(
196 ({ valid, values, form }) => {
197 if (valid) {
198 construct(
199 values.useNetworkSeed ? values.networkSeed : undefined,
200 values.useDiscontinuity
201 );
202 setUsedDiscontinuity(values.useDiscontinuity);
203 } else {
204 unconstruct();
205 }
207 if (!values.useNetworkSeed && values.networkSeed) {
208 form.change('networkSeed', '');
209 }
210 },
211 [construct, unconstruct, setUsedDiscontinuity]
212 );
214 const initialValues = useMemo(
215 () => ({
216 useNetworkSeed: false,
217 useDiscontinuity: false,
218 }),
219 []
220 );
222 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
224 const usageMessage = useMemo(() => {
225 const need = 'You need this keyfile to authenticate with Arvo.';
226 const boot = (
227 <>
228 boot fresh with{' '}
229 <code>
230 urbit -w {ob.patp(point).slice(1)} -k {filename}
231 </code>
232 </>
233 );
234 if (usedDiscontinuity) {
235 return (
236 <>
237 {need} Since you broke continuity, (re)move your old files, and {boot}
238 </>
239 );
240 } else {
241 return (
242 <>
243 {need} If you have already booted Arvo, run{' '}
244 <code>|rekey '0wkeyfile.contents'</code>. Otherwise, {boot}
245 </>
246 );
247 }
248 }, [usedDiscontinuity, filename, point]);
250 const renderTitle = () => {
251 if (completed) {
252 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
253 }
255 if (broadcasting) {
256 return 'Setting Network Keys...';
257 }
259 if (!hasKeys) {
260 return 'Networking keys have not yet been set.';
261 }
263 return 'Networking';
264 };
266 const renderNetworkKeySection = (title, key) => (
267 <>
268 <Grid.Item full as={H5} className="mt3 gray4">
269 {title}
270 </Grid.Item>
271 {key.matchWith({
272 Nothing: () => (
273 <Grid.Item full as="code" className="f5 mono gray4">
274 Unset
275 </Grid.Item>
276 ),
277 Just: ({ value: key }) => (
278 <>
279 <Grid.Item full as="code" className="f5 mono wrap">
280 0x
281 </Grid.Item>
282 <Grid.Item full as="code" className="f5 mono wrap">
283 {renderNetworkKey(key)}
284 </Grid.Item>
285 </>
286 ),
287 })}
288 </>
289 );
291 const renderDetail = (title, value) => (
292 <Flex.Item as={Flex} col>
293 <Flex.Item as={H5} className="gray4">
294 {title}
295 </Flex.Item>
296 {value.matchWith({
297 Nothing: () => (
298 <Flex.Item as={Text} className="f5 gray4">
299 Unset
300 </Flex.Item>
301 ),
302 Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
303 })}
304 </Flex.Item>
305 );
307 const renderDetails = () => {
308 return (
309 <>
310 {renderNetworkKeySection(
311 'Authentication',
312 details.chain(chainKeyProp('authenticationKey'))
313 )}
314 {renderNetworkKeySection(
315 'Encryption',
316 details.chain(chainKeyProp('encryptionKey'))
317 )}
318 <Grid.Item full as={Flex} row justify="between" className="mt3">
319 {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
320 {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
321 {renderDetail(
322 'Crypto Suite Ver.',
323 details.map(d => d.cryptoSuiteVersion)
324 )}
325 </Grid.Item>
326 {hasKeys && (
327 <Grid.Item full as={Flex} row justify="between" className="mt3">
328 {renderDetail(
329 'Last Set',
330 getRekeyDate(point).map(date =>
331 date.matchWith({
332 Ok: r => formatDotsWithTime(r.value),
333 Error: () => Nothing(),
334 })
335 )
336 )}
337 </Grid.Item>
338 )}
339 </>
340 );
341 };
343 return (
344 <>
345 <Grid>
346 <Grid.Item full as={ViewHeader}>
347 {renderTitle()}
348 </Grid.Item>
350 <Grid.Item full as={Text} className="mb3">
351 {hasKeys
352 ? 'Here are your public keys that authenticate your Arvo.'
353 : 'Set your networking keys to authenticate with Arvo.'}
354 </Grid.Item>
356 {completed && (
357 <>
358 <Grid.Item full as={NoticeBox} className="mb3">
359 {usageMessage}
360 </Grid.Item>
361 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
362 </>
363 )}
365 {!completed && (
366 <BridgeForm
367 validate={validate}
368 onValues={onValues}
369 initialValues={initialValues}>
370 {() => (
371 <>
372 <Grid.Item
373 full
374 as={ToggleInput}
375 name="useNetworkSeed"
376 label="Use Custom Network Seed"
377 inverseLabel="Back to Derived Network Seed"
378 disabled={inputsLocked}
379 />
380 <Condition when="useNetworkSeed" is={true}>
381 <Grid.Item full as={NoticeBox} className="mb2">
382 When using a custom network seed, you'll need to download
383 your Arvo keyfile immediately after this transaction is
384 Bridge does not store your seed.
385 </Grid.Item>
386 <Grid.Item
387 full
388 as={HexInput}
389 name="networkSeed"
390 label="Network Seed (32 bytes)"
391 disabled={inputsLocked}
392 />
393 </Condition>
394 <Grid.Item
395 full
396 as={CheckboxInput}
397 name="useDiscontinuity"
398 label="Breach Continuity"
399 disabled={inputsLocked}
400 />
402 <Grid.Item full as={FormError} />
404 <Grid.Item
405 full
406 as={InlineEthereumTransaction}
407 {...bind}
408 label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
409 onReturn={() => pop()}
410 />
411 </>
412 )}
413 </BridgeForm>
414 )}
416 {isDefaultState && renderDetails()}
418 {completed && (
419 <>
420 <Grid.Item full as={BootUrbitOSButton} />
421 <Grid.Divider />
422 </>
423 )}
424 </Grid>
426 {isDefaultState && (
427 <FooterButton onClick={goRelocate} disabled>
428 Relocate
429 </FooterButton>
430 )}
431 </>
432 );