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