on 261ea40 help sign in
bridge/src/views/Admin/AdminNetworkingKeys.js
382 lines 11 kB history raw
1import React, { useCallback, useEffect, useRef, useState } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
3import {
4 Grid,
5 Text,
6 H5,
7 Flex,
8 ToggleInput,
9 Input,
10 CheckboxInput,
11} from 'indigo-react';
12import * as azimuth from 'azimuth-js';
13import { randomHex } from 'web3-utils';
15import { usePointCursor } from 'store/pointCursor';
16import { usePointCache } from 'store/pointCache';
17import { useNetwork } from 'store/network';
18import { useWallet } from 'store/wallet';
20import { useLocalRouter } from 'lib/LocalRouter';
21import * as need from 'lib/need';
22import {
23 segmentNetworkKey,
24 attemptNetworkSeedDerivation,
25 deriveNetworkKeys,
26 CRYPTO_SUITE_VERSION,
27 CURVE_ZERO_ADDR,
28} from 'lib/keys';
29import { formatDotsWithTime } from 'lib/dateFormat';
30import useEthereumTransaction from 'lib/useEthereumTransaction';
31import { GAS_LIMITS } from 'lib/constants';
32import { addHexPrefix } from 'lib/wallet';
33import { useHexInput, useCheckboxInput } from 'lib/useInputs';
34import useKeyfileGenerator from 'lib/useKeyfileGenerator';
36import ViewHeader from 'components/ViewHeader';
37import MiniBackButton from 'components/MiniBackButton';
38import { BootArvoButton } from 'components/Buttons';
39import FooterButton from 'components/FooterButton';
40import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
41import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
42import NoticeBox from 'components/NoticeBox';
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 [showNetworkSeedInput, { data: showNetworkSeed }] = useCheckboxInput({
168 name: 'shownetworkseed',
169 label: 'Use Custom Network Seed',
170 inverseLabel: 'Back to Derived Network Seed',
171 initialValue: false,
172 disabled: inputsLocked,
173 });
175 const [
176 networkSeedInput,
177 { pass: validNetworkSeed, data: networkSeed },
178 { reset: resetNetworkSeed },
179 ] = useHexInput({
180 name: 'networkseed',
181 label: 'Network Seed (64 bytes)',
182 length: 32, // 64 bytes
183 disabled: inputsLocked,
184 });
186 const [
187 discontinuityInput,
188 { pass: validDiscontinuity, data: isDiscontinuity },
189 ] = useCheckboxInput({
190 name: 'discontinuity',
191 label: 'Trigger New Continuity Era',
192 initialValue: false,
193 disabled: inputsLocked,
194 });
196 useEffect(() => {
197 const nothingOrValidSeed =
198 !showNetworkSeed || (showNetworkSeed && validNetworkSeed);
199 if (nothingOrValidSeed && validDiscontinuity) {
200 construct(networkSeed, isDiscontinuity);
201 } else {
202 unconstruct();
203 }
204 }, [
205 construct,
206 unconstruct,
207 isDiscontinuity,
208 networkSeed,
209 showNetworkSeed,
210 validDiscontinuity,
211 validNetworkSeed,
212 ]);
214 useEffect(() => {
215 if (!showNetworkSeed) {
216 resetNetworkSeed();
217 }
218 }, [resetNetworkSeed, showNetworkSeed]);
220 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
222 const renderTitle = () => {
223 if (completed) {
224 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
225 }
227 if (broadcasting) {
228 return 'Setting Network Keys...';
229 }
231 if (!hasKeys) {
232 return 'Networking keys have not yet been set.';
233 }
235 return 'Networking';
236 };
238 const renderNetworkKeySection = (title, key) => (
239 <>
240 <Grid.Item full as={H5} className="mt3 gray4">
241 {title}
242 </Grid.Item>
243 {key.matchWith({
244 Nothing: () => (
245 <Grid.Item full as="code" className="f5 gray4">
246 Unset
247 </Grid.Item>
248 ),
249 Just: ({ value: key }) => (
250 <>
251 <Grid.Item full as="code" className="f5 wrap">
252 0x
253 </Grid.Item>
254 <Grid.Item full as="code" className="f5 wrap">
255 {renderNetworkKey(key)}
256 </Grid.Item>
257 </>
258 ),
259 })}
260 </>
261 );
263 const renderDetail = (title, value) => (
264 <Flex.Item as={Flex} col>
265 <Flex.Item as={H5} className="gray4">
266 {title}
267 </Flex.Item>
268 {value.matchWith({
269 Nothing: () => (
270 <Flex.Item as={Text} className="f5 gray4">
271 Unset
272 </Flex.Item>
273 ),
274 Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
275 })}
276 </Flex.Item>
277 );
279 const renderDetails = () => {
280 return (
281 <>
282 {renderNetworkKeySection(
283 'Authentication',
284 details.chain(chainKeyProp('authenticationKey'))
285 )}
286 {renderNetworkKeySection(
287 'Encryption',
288 details.chain(chainKeyProp('encryptionKey'))
289 )}
290 <Grid.Item full as={Flex} row justify="between" className="mt3">
291 {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
292 {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
293 {renderDetail(
294 'Crypto Suite Ver.',
295 details.map(d => d.cryptoSuiteVersion)
296 )}
297 </Grid.Item>
298 {hasKeys && (
299 <Grid.Item full as={Flex} row justify="between" className="mt3">
300 {renderDetail(
301 'Last Set',
302 getRekeyDate(point).map(date =>
303 date.matchWith({
304 Ok: r => formatDotsWithTime(r.value),
305 Error: () => Nothing(),
306 })
307 )
308 )}
309 </Grid.Item>
310 )}
311 </>
312 );
313 };
315 return (
316 <>
317 <Grid>
318 <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
320 <Grid.Item full as={ViewHeader}>
321 {renderTitle()}
322 </Grid.Item>
324 <Grid.Item full as={Text} className="mb3">
325 {hasKeys
326 ? 'Here are your public keys that authenticate your Arvo.'
327 : 'Set your networking keys to authenticate with Arvo.'}
328 </Grid.Item>
330 {completed && (
331 <>
332 <Grid.Item full as={NoticeBox} className="mb3">
333 You need this keyfile to authenticate with Arvo. If you have
334 already booted Arvo, run <code>|rekey '0wkeyfile.contents'</code>
335 </Grid.Item>
336 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
337 </>
338 )}
340 {!completed && (
341 <>
342 <Grid.Item full as={ToggleInput} {...showNetworkSeedInput} />
343 {showNetworkSeed && (
344 <>
345 <Grid.Item full as={NoticeBox} className="mb2">
346 When using a custom network seed, you'll need to download your
347 Arvo keyfile immediately after this transaction is
348 completed—Multipass does not store your seed.
349 </Grid.Item>
350 <Grid.Item full as={Input} {...networkSeedInput} />
351 </>
352 )}
353 <Grid.Item full as={CheckboxInput} {...discontinuityInput} />
354 </>
355 )}
357 <Grid.Item
358 full
359 as={InlineEthereumTransaction}
360 {...bind}
361 label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
362 onReturn={() => pop()}
363 />
365 {isDefaultState && renderDetails()}
367 {completed && (
368 <>
369 <Grid.Item full as={BootArvoButton} disabled />
370 <Grid.Divider />
371 </>
372 )}
373 </Grid>
375 {isDefaultState && (
376 <FooterButton onClick={goRelocate} disabled>
377 Relocate
378 </FooterButton>
379 )}
380 </>
381 );