on master help sign in
feat: manual network seed + discontinuity
0f31fcbMatt Condon (shrugs)
src/components/InlineEthereumTransaction.js+15−6
@@ -1,4 +1,4 @@
1−import React from 'react';
1+import React, { useEffect } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import {
3import {
4 Grid,
4 Grid,
@@ -30,12 +30,14 @@ export default function InlineEthereumTransaction({
30 error,
30 error,
31 gasPrice,
31 gasPrice,
32 setGasPrice,
32 setGasPrice,
33+ resetGasPrice,
33 txHash,
34 txHash,
34 nonce,
35 nonce,
35 chainId,
36 chainId,
36 signedTransaction,
37 signedTransaction,
37
38
38 // additional from parent
39 // additional from parent
40+ label = 'Generate & Sign Transaction',
39 className,
41 className,
40 onReturn,
42 onReturn,
41}) {
43}) {
@@ -50,7 +52,7 @@ export default function InlineEthereumTransaction({
50 const [advancedInput, { data: advancedOpen }] = useCheckboxInput({
52 const [advancedInput, { data: advancedOpen }] = useCheckboxInput({
51 name: 'advanced',
53 name: 'advanced',
52 label: 'Advanced Configuration',
54 label: 'Advanced Configuration',
53− inverseLabel: 'Hide Advanced Configuration',
55+ inverseLabel: 'Cancel Advanced Configuration',
54 disabled: !showConfigureInput,
56 disabled: !showConfigureInput,
55 });
57 });
56
58
@@ -64,6 +66,13 @@ export default function InlineEthereumTransaction({
64 disabled: !showSignedTx,
66 disabled: !showSignedTx,
65 });
67 });
66
68
69+ // reset gas price when closing advanced configuration
70+ useEffect(() => {
71+ if (!advancedOpen) {
72+ resetGasPrice();
73+ }
74+ }, [advancedOpen, resetGasPrice]);
75+
67 const renderPrimaryButton = () => {
76 const renderPrimaryButton = () => {
68 if (error) {
77 if (error) {
69 return (
78 return (
@@ -101,7 +110,7 @@ export default function InlineEthereumTransaction({
101 onClick={generateAndSign}
110 onClick={generateAndSign}
102 disabled={!canSign}
111 disabled={!canSign}
103 loading={!canSign && initializing}>
112 loading={!canSign && initializing}>
104− Generate & Sign Transaction
113+ {label}
105 </Grid.Item>
114 </Grid.Item>
106 );
115 );
107 }
116 }
@@ -119,9 +128,8 @@ export default function InlineEthereumTransaction({
119
128
120 {showConfigureInput && (
129 {showConfigureInput && (
121 <>
130 <>
122− {!advancedOpen ? (
123− <Grid.Item full as={ToggleInput} {...advancedInput} />
131+ <Grid.Item full as={ToggleInput} {...advancedInput} />
124− ) : (
132+ {advancedOpen && (
125 <>
133 <>
126 <Grid.Item full as={Flex} row justify="between" className="mt2">
134 <Grid.Item full as={Flex} row justify="between" className="mt2">
127 <Flex.Item as={H5}>Gas Price</Flex.Item>
135 <Flex.Item as={H5}>Gas Price</Flex.Item>
@@ -152,6 +160,7 @@ export default function InlineEthereumTransaction({
152 <Grid.Item full as={HelpText} className="mt1">
160 <Grid.Item full as={HelpText} className="mt1">
153 Chain ID: {chainId}
161 Chain ID: {chainId}
154 </Grid.Item>
162 </Grid.Item>
163+ <Grid.Divider className="mt3" />
155 </>
164 </>
156 )}
165 )}
157 </>
166 </>
src/lib/useEthereumTransaction.js+14−3
@@ -47,6 +47,7 @@ export default function useEthereumTransaction(
47 const [chainId, setChainId] = useState();
47 const [chainId, setChainId] = useState();
48 const [nonce, setNonce] = useState();
48 const [nonce, setNonce] = useState();
49 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
49 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
50+ const [averageGasPrice, setAverageGasPrice] = useState(gasPrice); // gwei
50 const [gasLimit] = useState(initialGasLimit);
51 const [gasLimit] = useState(initialGasLimit);
51 const [unsignedTransaction, setUnsignedTransaction] = useState();
52 const [unsignedTransaction, setUnsignedTransaction] = useState();
52 const [signedTransaction, setSignedTransaction] = useState();
53 const [signedTransaction, setSignedTransaction] = useState();
@@ -54,6 +55,7 @@ export default function useEthereumTransaction(
54
55
55 const initializing = nonce === undefined || chainId === undefined;
56 const initializing = nonce === undefined || chainId === undefined;
56 const constructed = !!unsignedTransaction;
57 const constructed = !!unsignedTransaction;
58+ const isDefaultState = state === STATE.NONE;
57 const signed = state === STATE.SIGNED;
59 const signed = state === STATE.SIGNED;
58 const broadcasted = state === STATE.BROADCASTED;
60 const broadcasted = state === STATE.BROADCASTED;
59 const confirmed = state === STATE.CONFIRMED;
61 const confirmed = state === STATE.CONFIRMED;
@@ -126,10 +128,15 @@ export default function useEthereumTransaction(
126 setUnsignedTransaction(undefined);
128 setUnsignedTransaction(undefined);
127 setTxHash(undefined);
129 setTxHash(undefined);
128 setSignedTransaction(undefined);
130 setSignedTransaction(undefined);
129− setGasPrice(initialGasPrice);
131+ setGasPrice(averageGasPrice);
130 setState(STATE.NONE);
132 setState(STATE.NONE);
131 setError(undefined);
133 setError(undefined);
132− }, [initialGasPrice, setError]);
134+ }, [averageGasPrice, setError]);
135+
136+ const resetGasPrice = useCallback(() => setGasPrice(averageGasPrice), [
137+ setGasPrice,
138+ averageGasPrice,
139+ ]);
133
140
134 useLifecycle(() => {
141 useLifecycle(() => {
135 (async () => {
142 (async () => {
@@ -143,7 +150,9 @@ export default function useEthereumTransaction(
143
150
144 setNonce(nonce);
151 setNonce(nonce);
145 setChainId(chainId);
152 setChainId(chainId);
146− setGasPrice(parseInt(fromWei(estimatedGasPrice, 'gwei'), 10));
153+ const gasPrice = parseInt(fromWei(estimatedGasPrice, 'gwei'), 10);
154+ setAverageGasPrice(gasPrice);
155+ setGasPrice(gasPrice);
147 } catch (error) {
156 } catch (error) {
148 setError(error);
157 setError(error);
149 }
158 }
@@ -151,6 +160,7 @@ export default function useEthereumTransaction(
151 });
160 });
152
161
153 const values = {
162 const values = {
163+ isDefaultState,
154 initializing,
164 initializing,
155 construct,
165 construct,
156 constructed,
166 constructed,
@@ -168,6 +178,7 @@ export default function useEthereumTransaction(
168 signedTransaction,
178 signedTransaction,
169 gasPrice,
179 gasPrice,
170 setGasPrice,
180 setGasPrice,
181+ resetGasPrice,
171 nonce,
182 nonce,
172 chainId,
183 chainId,
173 };
184 };
src/lib/useInputs.js+1−1
@@ -71,7 +71,7 @@ export function useHexInput({ length, ...rest }) {
71 {
71 {
72 type: 'text', // or password
72 type: 'text', // or password
73 autoComplete: 'off',
73 autoComplete: 'off',
74− placeholder: EXAMPLE_PRIVATE_KEY,
74+ placeholder: `e.g. ${EXAMPLE_PRIVATE_KEY}`,
75 validators: useMemo(
75 validators: useMemo(
76 () => [
76 () => [
77 validateHexString,
77 validateHexString,
src/views/Admin/AdminNetworkingKeys.js+153−104
@@ -1,6 +1,14 @@
1import React, { useCallback, useEffect, useState } from 'react';
1import React, { useCallback, useEffect, useState } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3+import {
4+ Grid,
5+ Text,
6+ H5,
7+ Flex,
8+ ToggleInput,
9+ Input,
10+ CheckboxInput,
3−import { Grid, Text, H5, Flex } from 'indigo-react';
11+} from 'indigo-react';
4import * as azimuth from 'azimuth-js';
12import * as azimuth from 'azimuth-js';
5import { randomHex } from 'web3-utils';
13import { randomHex } from 'web3-utils';
6
14
@@ -23,11 +31,12 @@ import { addHexPrefix } from 'lib/wallet';
23
31
24import ViewHeader from 'components/ViewHeader';
32import ViewHeader from 'components/ViewHeader';
25import MiniBackButton from 'components/MiniBackButton';
33import MiniBackButton from 'components/MiniBackButton';
26−import { BootArvoButton, ForwardButton } from 'components/Buttons';
34+import { BootArvoButton } from 'components/Buttons';
27import FooterButton from 'components/FooterButton';
35import FooterButton from 'components/FooterButton';
28import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
36import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
29import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
37import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
30import NoticeBox from 'components/NoticeBox';
38import NoticeBox from 'components/NoticeBox';
39+import { useHexInput, useCheckboxInput } from 'lib/useInputs';
31
40
32const chainKeyProp = name => d =>
41const chainKeyProp = name => d =>
33 d[name] === ZERO_KEY ? Nothing() : Just(d[name]);
42 d[name] === ZERO_KEY ? Nothing() : Just(d[name]);
@@ -50,13 +59,15 @@ function useSetKeys() {
50 const _point = need.point(pointCursor);
59 const _point = need.point(pointCursor);
51 const _contracts = need.contracts(contracts);
60 const _contracts = need.contracts(contracts);
52
61
53− const [isDiscontinuity] = useState(false);
54− // TODO: set discontinuity with radio check?
55 const [ndNetworkSeed, setNdNetworkSeed] = useState();
62 const [ndNetworkSeed, setNdNetworkSeed] = useState();
56
63
57− const { construct, broadcasting, confirmed, bind } = useEthereumTransaction(
64+ const {
65+ isDefaultState,
66+ construct: _construct,
67+ broadcasting,
68+ confirmed,
69+ bind,
58− GAS_LIMITS.CONFIGURE_KEYS
70+ } = useEthereumTransaction(GAS_LIMITS.CONFIGURE_KEYS);
59− );
60
71
61 // sync point details after success
72 // sync point details after success
62 useEffect(() => {
73 useEffect(() => {
@@ -65,54 +76,56 @@ function useSetKeys() {
65 }
76 }
66 }, [_point, confirmed, syncOwnedPoint]);
77 }, [_point, confirmed, syncOwnedPoint]);
67
78
68− const setKeys = useCallback(async () => {
79+ const construct = useCallback(
80+ async (manualSeed, isDiscontinuity) => {
69− const details = need.details(getDetails(_point));
81+ const details = need.details(getDetails(_point));
70− const networkRevision = parseInt(details.keyRevisionNumber, 10);
82+ const networkRevision = parseInt(details.keyRevisionNumber, 10);
71−
83+
84+ let _seed = manualSeed;
85+ if (!_seed) {
86+ const networkSeed = await attemptNetworkSeedDerivation({
87+ urbitWallet,
88+ wallet,
89+ authMnemonic,
90+ details,
72− console.log('setting revision to ', networkRevision + 1);
91+ revision: networkRevision + 1,
92+ });
93+
94+ _seed = networkSeed.matchWith({
95+ Nothing: () => {
96+ const ndSeed = randomHex(64);
97+ setNdNetworkSeed(ndSeed);
98+ return ndSeed;
99+ },
100+ Just: p => p.value,
101+ });
102+ }
103+ const pair = deriveNetworkKeys(_seed);
104+
105+ const txn = azimuth.ecliptic.configureKeys(
106+ _contracts,
107+ _point,
108+ addHexPrefix(pair.crypt.public),
109+ addHexPrefix(pair.auth.public),
110+ CRYPTO_SUITE_VERSION,
111+ isDiscontinuity ? 1 : 0
112+ );
73
113
74− const seed = await attemptNetworkSeedDerivation({
114+ _construct(txn);
75− urbitWallet,
115+ },
76− wallet,
116+ [
77− authMnemonic,
117+ _construct,
78− details,
79− revision: networkRevision + 1,
80− });
81−
82− const _networkSeed = seed.matchWith({
83− Nothing: () => {
84− const ndSeed = randomHex(64);
85− setNdNetworkSeed(ndSeed);
86− return ndSeed;
87− },
88− Just: p => p.value,
89− });
90−
91− const pair = deriveNetworkKeys(_networkSeed);
92−
93− const txn = azimuth.ecliptic.configureKeys(
94 _contracts,
118 _contracts,
95 _point,
119 _point,
96− addHexPrefix(pair.crypt.public),
97− addHexPrefix(pair.auth.public),
98− CRYPTO_SUITE_VERSION,
99− isDiscontinuity
100− );
101−
102− construct(txn);
103− }, [
104− _contracts,
105− _point,
106− authMnemonic,
120+ authMnemonic,
107− construct,
108− getDetails,
121+ getDetails,
109− isDiscontinuity,
110− urbitWallet,
122+ urbitWallet,
111− wallet,
123+ wallet,
124+ ]
112− ]);
125+ );
113
126
114 return {
127 return {
115− setKeys,
128+ isDefaultState,
116 construct,
129 construct,
117 broadcasting,
130 broadcasting,
118 confirmed,
131 confirmed,
@@ -134,19 +147,62 @@ export default function AdminNetworkingKeys() {
134 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
147 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
135 });
148 });
136
149
137− const [didRequestSetKeys, _setDidRequestSetKeys] = useState(false);
150+ const [showNetworkSeedInput, { data: showNetworkSeed }] = useCheckboxInput({
151+ name: 'shownetworkseed',
152+ label: 'Use Custom Network Seed',
153+ inverseLabel: 'Back to Derived Network Seed',
154+ initialValue: false,
155+ });
156+ const [
157+ networkSeedInput,
158+ { pass: validNetworkSeed, data: networkSeed },
159+ { setValue: setNetworkSeed },
160+ ] = useHexInput({
161+ name: 'networkseed',
162+ label: 'Network Seed (64 bytes)',
163+ length: 32, // 64 bytes
164+ });
165+
166+ const [
167+ discontinuityInput,
168+ { pass: validDiscontinuity, data: isDiscontinuity },
169+ ] = useCheckboxInput({
170+ name: 'discontinuity',
171+ label: 'Trigger New Continuity Era',
172+ length: 64,
173+ initialValue: false,
174+ });
175+
138 const {
176 const {
139− setKeys,
177+ isDefaultState,
178+ construct,
140 broadcasting,
179 broadcasting,
141 confirmed,
180 confirmed,
142 ndNetworkSeed,
181 ndNetworkSeed,
143 bind,
182 bind,
144 } = useSetKeys();
183 } = useSetKeys();
145
184
146− const setDidRequestSetKeys = useCallback(() => {
185+ useEffect(() => {
147− setKeys();
186+ if (
148− _setDidRequestSetKeys(true);
187+ (!showNetworkSeed || (showNetworkSeed && validNetworkSeed)) &&
149− }, [setKeys, _setDidRequestSetKeys]);
188+ validDiscontinuity
189+ ) {
190+ construct(networkSeed, isDiscontinuity);
191+ }
192+ }, [
193+ construct,
194+ isDiscontinuity,
195+ networkSeed,
196+ showNetworkSeed,
197+ validDiscontinuity,
198+ validNetworkSeed,
199+ ]);
200+
201+ useEffect(() => {
202+ if (!showNetworkSeed) {
203+ setNetworkSeed('');
204+ }
205+ }, [setNetworkSeed, showNetworkSeed]);
150
206
151 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
207 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
152
208
@@ -166,33 +222,6 @@ export default function AdminNetworkingKeys() {
166 return 'Networking';
222 return 'Networking';
167 };
223 };
168
224
169− const renderButton = () => {
170− if (!didRequestSetKeys) {
171− return (
172− <Grid.Item
173− full
174− as={ForwardButton}
175− solid
176− onClick={() => setDidRequestSetKeys(true)}>
177− {hasKeys ? 'Reset' : 'Set'} Networking Keys
178− </Grid.Item>
179− );
180− }
181−
182− if (confirmed) {
183− return (
184− <Grid.Item
185− full
186− as={DownloadKeyfileButton}
187− solid
188− networkSeed={ndNetworkSeed}
189− />
190− );
191− }
192−
193− return null;
194− };
195−
196 const renderNetworkKeySection = (title, key) => (
225 const renderNetworkKeySection = (title, key) => (
197 <>
226 <>
198 <Grid.Item full as={H5} className="mt3 gray4">
227 <Grid.Item full as={H5} className="mt3 gray4">
@@ -279,32 +308,52 @@ export default function AdminNetworkingKeys() {
279 {renderTitle()}
308 {renderTitle()}
280 </Grid.Item>
309 </Grid.Item>
281
310
282− {!didRequestSetKeys && (
283− <Grid.Item full as={Text} className="mb3">
311+ <Grid.Item full as={Text} className="mb3">
284− {hasKeys
312+ {hasKeys
285− ? 'Here are your public keys that authenticate your Arvo.'
313+ ? 'Here are your public keys that authenticate your Arvo.'
286− : 'Set your networking keys to authenticate with Arvo.'}
314+ : 'Set your networking keys to authenticate with Arvo.'}
287− </Grid.Item>
315+ </Grid.Item>
288− )}
289
316
290 {confirmed && (
317 {confirmed && (
318+ <>
291− <Grid.Item full as={NoticeBox} className="mb3">
319+ <Grid.Item full as={NoticeBox} className="mb3">
292− You need this keyfile to authenticate with Arvo.
320+ You need this keyfile to authenticate with Arvo.
293− </Grid.Item>
321+ </Grid.Item>
322+ <Grid.Item
323+ full
324+ as={DownloadKeyfileButton}
325+ solid
326+ networkSeed={ndNetworkSeed}
327+ />
328+ </>
294 )}
329 )}
295
330
296− {renderButton()}
331+ {!confirmed && (
297−
332+ <>
298− {didRequestSetKeys && (
333+ <Grid.Item full as={ToggleInput} {...showNetworkSeedInput} />
299− <Grid.Item
334+ {showNetworkSeed && (
300− full
335+ <>
301− as={InlineEthereumTransaction}
336+ <Grid.Item full as={NoticeBox} className="mb2">
302− {...bind}
337+ When using a custom network seed, you'll need to download your
303− onReturn={() => pop()}
338+ Arvo keyfile immediately after this transaction is
304− />
339+ confirmed—Multipass does not store your seed.
340+ </Grid.Item>
341+ <Grid.Item full as={Input} {...networkSeedInput} />
342+ </>
343+ )}
344+ <Grid.Item full as={CheckboxInput} {...discontinuityInput} />
345+ </>
305 )}
346 )}
306
347
348+ <Grid.Item
349+ full
350+ as={InlineEthereumTransaction}
351+ {...bind}
352+ label={`${hasKeys ? 'Reset' : 'Set'} Networking Keys`}
353+ onReturn={() => pop()}
354+ />
355+
307− {!didRequestSetKeys && renderDetails()}
356+ {isDefaultState && renderDetails()}
308
357
309 {confirmed && (
358 {confirmed && (
310 <>
359 <>
@@ -314,7 +363,7 @@ export default function AdminNetworkingKeys() {
314 )}
363 )}
315 </Grid>
364 </Grid>
316
365
317− {!didRequestSetKeys && (
366+ {isDefaultState && (
318 <FooterButton onClick={goRelocate} disabled>
367 <FooterButton onClick={goRelocate} disabled>
319 Relocate
368 Relocate
320 </FooterButton>
369 </FooterButton>