on master help sign in
feat: implement network key cycling
1a6981cMatt Condon (shrugs)
src/components/Buttons.js+3−1
@@ -6,7 +6,9 @@ import Blinky from './Blinky';
6export const ForwardButton = ({ loading, ...props }) => (
6export const ForwardButton = ({ loading, ...props }) => (
7 <Button accessory={loading ? <Blinky /> : '->'} {...props} />
7 <Button accessory={loading ? <Blinky /> : '->'} {...props} />
8);
8);
9−export const DownloadButton = props => <Button accessory="↓" {...props} />;
9+export const DownloadButton = ({ loading, ...props }) => (
10+ <Button accessory={loading ? <Blinky /> : '↓'} {...props} />
11+);
10export const RestartButton = props => <Button accessory="↺" {...props} />;
12export const RestartButton = props => <Button accessory="↺" {...props} />;
11export const GenerateButton = props => (
13export const GenerateButton = props => (
12 <Button accessory="○" solid {...props} />
14 <Button accessory="○" solid {...props} />
src/components/DownloadKeyfileButton.js+13−11
@@ -1,29 +1,31 @@
1import React from 'react';
1import React from 'react';
2
2
3import { DownloadButton } from 'components/Buttons';
3import { DownloadButton } from 'components/Buttons';
4−import Blinky from 'components/Blinky';
5
4
6import { usePointCursor } from 'store/pointCursor';
5import { usePointCursor } from 'store/pointCursor';
7
6
8import useKeyfileGenerator from 'lib/useKeyfileGenerator';
7import useKeyfileGenerator from 'lib/useKeyfileGenerator';
9import * as need from 'lib/need';
8import * as need from 'lib/need';
10
9
11−export default function DownloadKeyfileButton({ className, ...rest }) {
10+export default function DownloadKeyfileButton({
11+ networkSeed,
12+ className,
13+ ...rest
14+}) {
12 const { pointCursor } = usePointCursor();
15 const { pointCursor } = usePointCursor();
13− const point = need.point(pointCursor);
16+ const _point = need.point(pointCursor);
14− const {
17+
15− generating,
16− available,
17− downloaded,
18− generateAndDownload,
19− } = useKeyfileGenerator(point);
18+ const { generating, available, downloaded, download } = useKeyfileGenerator(
19+ _point,
20+ networkSeed
21+ );
20
22
21 return (
23 return (
22 <DownloadButton
24 <DownloadButton
23 className={className}
25 className={className}
24 disabled={downloaded || !available}
26 disabled={downloaded || !available}
25− accessory={generating ? <Blinky /> : undefined}
27+ loading={generating}
26− onClick={generateAndDownload}
28+ onClick={download}
27 {...rest}>
29 {...rest}>
28 Download Arvo Keyfile
30 Download Arvo Keyfile
29 </DownloadButton>
31 </DownloadButton>
src/components/InlineEthereumTransaction.js+18−4
@@ -1,4 +1,5 @@
1import React from 'react';
1import React from 'react';
2+import cn from 'classnames';
2import {
3import {
3 Grid,
4 Grid,
4 ToggleInput,
5 ToggleInput,
@@ -10,6 +11,8 @@ import {
10import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
11import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
11import { useCheckboxInput } from 'lib/useInputs';
12import { useCheckboxInput } from 'lib/useInputs';
12import Blinky from './Blinky';
13import Blinky from './Blinky';
14+import { hexify } from 'lib/txn';
15+import { useExploreTxUrl } from 'lib/explorer';
13
16
14export default function InlineEthereumTransaction({
17export default function InlineEthereumTransaction({
15 // from useEthereumTransaction.bind
18 // from useEthereumTransaction.bind
@@ -24,14 +27,17 @@ export default function InlineEthereumTransaction({
24 error,
27 error,
25 setGasPrice,
28 setGasPrice,
26 txHash,
29 txHash,
30+ signedTransaction,
27
31
28− // additional
32+ // additional from parent
29 className,
33 className,
30 onReturn,
34 onReturn,
31}) {
35}) {
36+ const exploreHashUrl = useExploreTxUrl(txHash);
32 const [configureInput, { data: configurationOpen }] = useCheckboxInput({
37 const [configureInput, { data: configurationOpen }] = useCheckboxInput({
33 name: 'configure',
38 name: 'configure',
34 label: 'Configure Transaction',
39 label: 'Configure Transaction',
40+ inverseLabel: 'Cancel Configure Transaction',
35 disabled: signed || broadcasted || confirmed,
41 disabled: signed || broadcasted || confirmed,
36 });
42 });
37
43
@@ -41,6 +47,7 @@ export default function InlineEthereumTransaction({
41 ] = useCheckboxInput({
47 ] = useCheckboxInput({
42 name: 'viewsigned',
48 name: 'viewsigned',
43 label: 'View Signed Transaction',
49 label: 'View Signed Transaction',
50+ inverseLabel: 'Hide Signed Transaction',
44 disabled: !signed,
51 disabled: !signed,
45 });
52 });
46
53
@@ -105,7 +112,7 @@ export default function InlineEthereumTransaction({
105 };
112 };
106
113
107 return (
114 return (
108− <Grid className={className}>
115+ <Grid className={cn(className, 'mt1')}>
109 {renderButton()}
116 {renderButton()}
110
117
111 {error && (
118 {error && (
@@ -119,7 +126,14 @@ export default function InlineEthereumTransaction({
119 )}
126 )}
120
127
121 {showSignedTx && (
128 {showSignedTx && (
129+ <>
122− <Grid.Item full as={ToggleInput} {...viewSignedTransaction} />
130+ <Grid.Item full as={ToggleInput} {...viewSignedTransaction} />
131+ {signedTransactionOpen && (
132+ <Grid.Item full as="code" className="f6 mono gray4 wrap">
133+ {hexify(signedTransaction.serialize())}
134+ </Grid.Item>
135+ )}
136+ </>
123 )}
137 )}
124
138
125 {showReceipt && (
139 {showReceipt && (
@@ -128,7 +142,7 @@ export default function InlineEthereumTransaction({
128 <Grid.Item full as={Flex} col className="pv4">
142 <Grid.Item full as={Flex} col className="pv4">
129 <Flex.Item as={Flex} row justify="between">
143 <Flex.Item as={Flex} row justify="between">
130 <Flex.Item as={H5}>Transaction Hash</Flex.Item>
144 <Flex.Item as={H5}>Transaction Hash</Flex.Item>
131− <Flex.Item as={LinkButton} href="https://google.com">
145+ <Flex.Item as={LinkButton} href={exploreHashUrl}>
132 Etherscan↗
146 Etherscan↗
133 </Flex.Item>
147 </Flex.Item>
134 </Flex.Item>
148 </Flex.Item>
src/indigo-react/components/Button.js+9−6
@@ -17,11 +17,6 @@ export default function Button({
17 children,
17 children,
18 ...rest
18 ...rest
19}) {
19}) {
20− const textColor = {
21− white: solid,
22− black: !solid && !disabled,
23− gray4: !solid && disabled,
24− };
25 return (
20 return (
26 <Grid
21 <Grid
27 as={As}
22 as={As}
@@ -48,7 +43,15 @@ export default function Button({
48 }}
43 }}
49 onClick={!disabled && onClick ? onClick : undefined}
44 onClick={!disabled && onClick ? onClick : undefined}
50 {...rest}>
45 {...rest}>
46+ <Grid.Item
47+ full
48+ as={Flex}
51− <Grid.Item full as={Flex} justify="between" className={cn(textColor)}>
49+ justify="between"
50+ className={cn({
51+ white: solid,
52+ black: !solid && !disabled,
53+ gray4: !solid && disabled,
54+ })}>
52 <span>{children}</span>
55 <span>{children}</span>
53 <div className={cn('pl4')}>{accessory}</div>
56 <div className={cn('pl4')}>{accessory}</div>
54 </Grid.Item>
57 </Grid.Item>
src/indigo-react/components/ToggleInput.js+3−2
@@ -8,6 +8,7 @@ export default function ToggleInput({
8 // visuals
8 // visuals
9 name,
9 name,
10 label,
10 label,
11+ inverseLabel,
11 className,
12 className,
12
13
13 // callbacks
14 // callbacks
@@ -55,12 +56,12 @@ export default function ToggleInput({
55 <Flex.Item
56 <Flex.Item
56 flex
57 flex
57 as="label"
58 as="label"
58− className={cn('f6 pv1 lh-tall us-none flex-row align-center', {
59+ className={cn('f6 pv2 lh-tall us-none flex-row align-center', {
59 clickable: !disabled,
60 clickable: !disabled,
60 })}
61 })}
61 htmlFor={name}>
62 htmlFor={name}>
62 <LinkButton disabled={disabled} className="f5">
63 <LinkButton disabled={disabled} className="f5">
63− {bind.checked ? `Hide ${label}` : label}
64+ {bind.checked ? inverseLabel : label}
64 </LinkButton>
65 </LinkButton>
65 </Flex.Item>
66 </Flex.Item>
66 </Flex>
67 </Flex>
src/lib/constants.js+4
@@ -17,6 +17,9 @@ const GALAXY_ENTROPY_BITS = 384;
17
17
18const SEED_ENTROPY_BITS = 128;
18const SEED_ENTROPY_BITS = 128;
19
19
20+const ZERO_KEY =
21+ '0x0000000000000000000000000000000000000000000000000000000000000000';
22+
20//TODO move into azimuth-js
23//TODO move into azimuth-js
21const GAS_LIMITS = {
24const GAS_LIMITS = {
22 SPAWN: 250000,
25 SPAWN: 250000,
@@ -83,4 +86,5 @@ export {
83 GALAXY_ENTROPY_BITS,
86 GALAXY_ENTROPY_BITS,
84 SEED_ENTROPY_BITS,
87 SEED_ENTROPY_BITS,
85 ZOD,
88 ZOD,
89+ ZERO_KEY,
86};
90};
src/lib/explorer.jsnew file+28
@@ -0,0 +1,28 @@
1+import { useNetwork } from 'store/network';
2+import { NETWORK_TYPES } from './network';
3+
4+function useEtherscanDomain() {
5+ const { networkType } = useNetwork();
6+
7+ switch (networkType) {
8+ case NETWORK_TYPES.ROPSTEN:
9+ return 'https://ropsten.etherscan.io';
10+ case NETWORK_TYPES.OFFLINE:
11+ case NETWORK_TYPES.MAINNET:
12+ case NETWORK_TYPES.LOCAL:
13+ default:
14+ return 'https://etherscan.io';
15+ }
16+}
17+
18+export function useExploreTxUrl(txHash) {
19+ const domain = useEtherscanDomain();
20+
21+ return `${domain}/tx/${txHash}`;
22+}
23+
24+export function useExploreAddressUrl(address) {
25+ const domain = useEtherscanDomain();
26+
27+ return `${domain}/address/${address}`;
28+}
src/lib/invite.js+2−3
@@ -1,6 +1,5 @@
1import Tx from 'ethereumjs-tx';
1import Tx from 'ethereumjs-tx';
2import * as azimuth from 'azimuth-js';
2import * as azimuth from 'azimuth-js';
3−import * as kg from 'urbit-key-generation/dist';
4import * as wg from './walletgen';
3import * as wg from './walletgen';
5import * as tank from './tank';
4import * as tank from './tank';
6
5
@@ -8,7 +7,7 @@ import JSZip from 'jszip';
8import saveAs from 'file-saver';
7import saveAs from 'file-saver';
9
8
10import { hexify } from './txn';
9import { hexify } from './txn';
11−import { deriveNetworkSeedFromUrbitWallet } from './keys';
10+import { deriveNetworkSeedFromUrbitWallet, deriveNetworkKeys } from './keys';
12import { addHexPrefix } from './wallet';
11import { addHexPrefix } from './wallet';
13import { sendAndAwaitAll } from './txn';
12import { sendAndAwaitAll } from './txn';
14import { GAS_LIMITS } from './constants';
13import { GAS_LIMITS } from './constants';
@@ -132,7 +131,7 @@ export async function reticketPointBetweenWallets({
132 Just: p => p.value,
131 Just: p => p.value,
133 });
132 });
134
133
135− const networkKeys = kg.deriveNetworkKeys(networkSeed);
134+ const networkKeys = deriveNetworkKeys(networkSeed);
136
135
137 const keysTx = azimuth.ecliptic.configureKeys(
136 const keysTx = azimuth.ecliptic.configureKeys(
138 contracts,
137 contracts,
src/lib/keys.js+7−7
@@ -84,12 +84,12 @@ export const deriveNetworkSeedFromUrbitWallet = async (
84 urbitWallet,
84 urbitWallet,
85 revision = 1
85 revision = 1
86) => {
86) => {
87− return deriveNetworkSeedFromSeed(urbitWallet.management.seed, revision);
87+ return await deriveNetworkSeedFromSeed(urbitWallet.management.seed, revision);
88};
88};
89
89
90/**
90/**
91 * @param {Maybe<any>} wallet
91 * @param {Maybe<any>} wallet
92− * @param {Maybe<string>} authMnemonic
92+ * @param {string} authMnemonic
93 * @param {object} details
93 * @param {object} details
94 * @param {number} revision
94 * @param {number} revision
95 * @return {Promise<Maybe<string>>}
95 * @return {Promise<Maybe<string>>}
@@ -100,9 +100,12 @@ export const deriveNetworkSeedFromMnemonic = async (
100 details,
100 details,
101 revision = 1
101 revision = 1
102) => {
102) => {
103+ const isOwner = eqAddr(wallet.address, details.owner);
103 const isManagementProxy = eqAddr(wallet.address, details.managementProxy);
104 const isManagementProxy = eqAddr(wallet.address, details.managementProxy);
104
105
106+ // the network seed is only derivable from mnemonic if the derived
107+ // management seed or owner equals the record we have on chain
105− if (isManagementProxy) {
108+ if (isOwner || isManagementProxy) {
106 return await deriveNetworkSeedFromSeed(authMnemonic, revision);
109 return await deriveNetworkSeedFromSeed(authMnemonic, revision);
107 }
110 }
108
111
@@ -115,10 +118,7 @@ export const deriveNetworkSeedFromMnemonic = async (
115 * @return {Promise<Maybe<string>>}
118 * @return {Promise<Maybe<string>>}
116 */
119 */
117const deriveNetworkSeedFromSeed = async (seed, revision) => {
120const deriveNetworkSeedFromSeed = async (seed, revision) => {
118− const networkSeed = await kg.deriveNetworkSeed(seed, '', revision);
121+ return Just(await kg.deriveNetworkSeed(seed, '', revision));
119− // apparently deriveNetworkSeed can return empty string
120− // and perhaps even malformed
121− return networkSeed === '' ? Nothing() : Just(networkSeed);
122};
122};
123
123
124/**
124/**
src/lib/useEthereumTransaction.js+3−6
@@ -1,5 +1,4 @@
1import { useCallback, useEffect, useState } from 'react';
1import { useCallback, useEffect, useState } from 'react';
2−import { Just } from 'folktale/maybe';
3
2
4import { useNetwork } from 'store/network';
3import { useNetwork } from 'store/network';
5import { useWallet } from 'store/wallet';
4import { useWallet } from 'store/wallet';
@@ -8,10 +7,9 @@ import {
8 signTransaction,
7 signTransaction,
9 sendSignedTransaction,
8 sendSignedTransaction,
10 waitForTransactionConfirm,
9 waitForTransactionConfirm,
11− hexify,
12} from './txn';
10} from './txn';
13import * as need from './need';
11import * as need from './need';
14−import { toHex, toWei, fromWei } from 'web3-utils';
12+import { fromWei } from 'web3-utils';
15
13
16const STATE = {
14const STATE = {
17 NONE: 'NONE',
15 NONE: 'NONE',
@@ -66,9 +64,6 @@ export default function useEthereumTransaction(initialGasLimit = 0) {
66 const generateAndSign = useCallback(async () => {
64 const generateAndSign = useCallback(async () => {
67 try {
65 try {
68 setError(undefined);
66 setError(undefined);
69− // TODO: set varaibles
70− // txn.gas = toHex(gasLimit);
71− // txn.gasPrice = toHex(toWei(gasPrice, 'gwei'));
72
67
73 const txn = await signTransaction({
68 const txn = await signTransaction({
74 wallet: _wallet,
69 wallet: _wallet,
@@ -160,6 +155,7 @@ export default function useEthereumTransaction(initialGasLimit = 0) {
160 error,
155 error,
161 inputsLocked,
156 inputsLocked,
162 txHash,
157 txHash,
158+ signedTransaction,
163 bind: {
159 bind: {
164 constructed,
160 constructed,
165 canSign,
161 canSign,
@@ -172,6 +168,7 @@ export default function useEthereumTransaction(initialGasLimit = 0) {
172 error,
168 error,
173 setGasPrice,
169 setGasPrice,
174 txHash,
170 txHash,
171+ signedTransaction,
175 },
172 },
176 };
173 };
177}
174}
src/lib/useInputs.js+1−1
@@ -1,4 +1,4 @@
1−import React, { useCallback, useState, useMemo } from 'react';
1+import React, { useCallback, useMemo } from 'react';
2import { AccessoryIcon, useForm } from 'indigo-react';
2import { AccessoryIcon, useForm } from 'indigo-react';
3import { identity } from 'lodash';
3import { identity } from 'lodash';
4import * as bip39 from 'bip39';
4import * as bip39 from 'bip39';
src/lib/useKeyfileGenerator.js+42−30
@@ -1,4 +1,5 @@
1−import { useCallback, useState } from 'react';
1+import { useCallback, useEffect, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
2import saveAs from 'file-saver';
3import saveAs from 'file-saver';
3import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
4
5
@@ -15,55 +16,67 @@ import {
15} from './keys';
16} from './keys';
16import usePermissionsForPoint from './usePermissionsForPoint';
17import usePermissionsForPoint from './usePermissionsForPoint';
17
18
18−export default function useKeyfileGenerator(point) {
19+export default function useKeyfileGenerator(point, manualNetworkSeed) {
19 const { getDetails } = usePointCache();
20 const { getDetails } = usePointCache();
20 const { urbitWallet, wallet, authMnemonic } = useWallet();
21 const { urbitWallet, wallet, authMnemonic } = useWallet();
21
22
22 const [downloaded, setDownloaded] = useState(false);
23 const [downloaded, setDownloaded] = useState(false);
23 const [generating, setGenerating] = useState(false);
24 const [generating, setGenerating] = useState(false);
24− const [keyfile, setKeyfile] = useState();
25+ const [keyfile, setKeyfile] = useState(false);
25
26
27+ const _address = need.wallet(wallet).address;
26− const details = need.details(getDetails(point));
28+ const _details = need.details(getDetails(point));
29+
27− const networkRevision = parseInt(details.keyRevisionNumber, 10);
30+ const networkRevision = parseInt(_details.keyRevisionNumber, 10);
31+ const { isOwner, isManagementProxy } = usePermissionsForPoint(
32+ _address,
33+ point
34+ );
28
35
29− const { isOwner, isManagementProxy } = usePermissionsForPoint(point);
30 const hasNetworkingKeys = networkRevision > 0;
36 const hasNetworkingKeys = networkRevision > 0;
37+ const available =
31− const available = (isOwner || isManagementProxy) && hasNetworkingKeys;
38+ (isOwner || isManagementProxy) && hasNetworkingKeys && keyfile;
32
39
33 const generate = useCallback(async () => {
40 const generate = useCallback(async () => {
34 setGenerating(true);
41 setGenerating(true);
35
42
43+ console.log('generating for network revision', networkRevision);
44+
45+ const networkSeed = manualNetworkSeed
46+ ? Just(manualNetworkSeed)
36− const networkSeed = await attemptNetworkSeedDerivation({
47+ : await attemptNetworkSeedDerivation({
37− urbitWallet,
48+ urbitWallet,
38− wallet,
49+ wallet,
39− authMnemonic,
50+ authMnemonic,
40− details,
51+ details: _details,
41− revision: networkRevision,
52+ revision: networkRevision,
42− });
53+ });
43
54
44− const _networkSeed = need.value(networkSeed, () => {
55+ if (Nothing.hasInstance(networkSeed)) {
45 setGenerating(false);
56 setGenerating(false);
46− throw new Error('Could not derive networking seed.');
57+ console.log('nondeterministic seed!');
58+ // TODO: tell user their seed is nondeterministic
59+ return;
47− });
60+ }
61+
62+ const _networkSeed = networkSeed.value;
48
63
49 const pair = deriveNetworkKeys(_networkSeed);
64 const pair = deriveNetworkKeys(_networkSeed);
50
65
51− if (!keysMatchChain(pair, details)) {
66+ if (!keysMatchChain(pair, _details)) {
52 setGenerating(false);
67 setGenerating(false);
53− throw new Error(
54− 'Derived networking keys do not match public keys on chain.'
68+ console.log('Derived networking keys do not match public keys on chain.');
55− );
69+ // TODO: tell usrs their seed is nondeterministic
56 }
70 }
57
71
58 setKeyfile(compileNetworkingKey(pair, point, networkRevision));
72 setKeyfile(compileNetworkingKey(pair, point, networkRevision));
59 setGenerating(false);
73 setGenerating(false);
60 }, [
74 }, [
61− setGenerating,
75+ manualNetworkSeed,
62− setKeyfile,
63 urbitWallet,
76 urbitWallet,
64 wallet,
77 wallet,
65 authMnemonic,
78 authMnemonic,
66− details,
79+ _details,
67 networkRevision,
80 networkRevision,
68 point,
81 point,
69 ]);
82 ]);
@@ -77,17 +90,16 @@ export default function useKeyfileGenerator(point) {
77 // TODO: ^ unifiy "remove tilde" calls
90 // TODO: ^ unifiy "remove tilde" calls
78 );
91 );
79 setDownloaded(true);
92 setDownloaded(true);
80− }, [point, keyfile, networkRevision, setDownloaded]);
93+ }, [keyfile, point, networkRevision]);
81
94
82− const generateAndDownload = useCallback(async () => {
95+ useEffect(() => {
83− await generate();
96+ generate();
84− download();
85− }, [generate, download]);
97+ }, [generate]);
86
98
87 return {
99 return {
88 generating,
100 generating,
89 available,
101 available,
90 downloaded,
102 downloaded,
91− generateAndDownload,
103+ download,
92 };
104 };
93}
105}
src/lib/usePermissionsForPoint.js+5−1
@@ -38,15 +38,19 @@ export default function usePermissionsForPoint(address, point) {
38
38
39 const canManage = isOwner || isManagementProxy;
39 const canManage = isOwner || isManagementProxy;
40 const canTransfer = isOwner || isTransferProxy;
40 const canTransfer = isOwner || isTransferProxy;
41−
42 const canSpawn = (isStar || isGalaxy) && (isOwner || isSpawnProxy);
41 const canSpawn = (isStar || isGalaxy) && (isOwner || isSpawnProxy);
43 const canVote = isGalaxy && (isOwner || isVotingProxy);
42 const canVote = isGalaxy && (isOwner || isVotingProxy);
44
43
45 return {
44 return {
45+ isPlanet,
46+ isStar,
47+ isGalaxy,
48+ //
46 isOwner,
49 isOwner,
47 isActiveOwner,
50 isActiveOwner,
48 isTransferProxy,
51 isTransferProxy,
49 isManagementProxy,
52 isManagementProxy,
53+ //
50 canManage,
54 canManage,
51 canTransfer,
55 canTransfer,
52 canSpawn,
56 canSpawn,
src/lib/walletgen.js+2−2
@@ -55,12 +55,12 @@ export const generateWallet = async (point, ticket, boot) => {
55 boot: boot,
55 boot: boot,
56 };
56 };
57
57
58− const wallet = await kg.generateWallet(config);
59−
60 // This is here to notify anyone who opens console because the thread
58 // This is here to notify anyone who opens console because the thread
61 // hangs, blocking UI updates so this cannot be done in the UI
59 // hangs, blocking UI updates so this cannot be done in the UI
62 console.log('Generating Wallet for point address: ', point);
60 console.log('Generating Wallet for point address: ', point);
63
61
62+ const wallet = await kg.generateWallet(config);
63+
64 return wallet;
64 return wallet;
65};
65};
66
66
src/views/Admin/AdminEditPermissions.js+3−5
@@ -1,4 +1,4 @@
1−import React, { useState, useCallback } from 'react';
1+import React, { useCallback } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3import * as need from 'lib/need';
3import * as need from 'lib/need';
4import { Grid } from 'indigo-react';
4import { Grid } from 'indigo-react';
@@ -9,7 +9,6 @@ import { matchBlinkyDate } from 'components/Blinky';
9
9
10import { ETH_ZERO_ADDR } from 'lib/wallet';
10import { ETH_ZERO_ADDR } from 'lib/wallet';
11
11
12−import useLifecycle from 'lib/useLifecycle';
13import { useNetwork } from 'store/network';
12import { useNetwork } from 'store/network';
14import { useWallet } from 'store/wallet';
13import { useWallet } from 'store/wallet';
15import { usePointCursor } from 'store/pointCursor';
14import { usePointCursor } from 'store/pointCursor';
@@ -21,7 +20,6 @@ import { useLocalRouter } from 'lib/LocalRouter';
21import capitalize from 'lib/capitalize';
20import capitalize from 'lib/capitalize';
22import { eqAddr } from 'lib/wallet';
21import { eqAddr } from 'lib/wallet';
23import MiniBackButton from 'components/MiniBackButton';
22import MiniBackButton from 'components/MiniBackButton';
24−import FooterButton from 'components/FooterButton';
25
23
26export default function AdminEditPermissions() {
24export default function AdminEditPermissions() {
27 const { pop, push, names } = useLocalRouter();
25 const { pop, push, names } = useLocalRouter();
@@ -36,11 +34,11 @@ export default function AdminEditPermissions() {
36 const pointSize = azimuth.getPointSize(point);
34 const pointSize = azimuth.getPointSize(point);
37 const isParent = pointSize !== azimuth.PointSize.Planet;
35 const isParent = pointSize !== azimuth.PointSize.Planet;
38 const isSenate = pointSize === azimuth.PointSize.Galaxy;
36 const isSenate = pointSize === azimuth.PointSize.Galaxy;
39− const userAddress = need.wallet(wallet).address;
37+ const address = need.wallet(wallet).address;
40
38
41 const details = need.details(getDetails(point));
39 const details = need.details(getDetails(point));
42 const networkRevision = parseInt(details.keyRevisionNumber, 10);
40 const networkRevision = parseInt(details.keyRevisionNumber, 10);
43− const { canManage, isOwner } = usePermissionsForPoint(userAddress, point);
41+ const { canManage, isOwner } = usePermissionsForPoint(address, point);
44
42
45 const goSetProxy = useCallback(
43 const goSetProxy = useCallback(
46 proxyType => push(names.SET_PROXY, { proxyType }),
44 proxyType => push(names.SET_PROXY, { proxyType }),
src/views/Admin/AdminHome.js−1
@@ -10,7 +10,6 @@ import { usePointCursor } from 'store/pointCursor';
10
10
11import usePermissionsForPoint from 'lib/usePermissionsForPoint';
11import usePermissionsForPoint from 'lib/usePermissionsForPoint';
12
12
13−import FooterButton from 'components/FooterButton';
14import ViewHeader from 'components/ViewHeader';
13import ViewHeader from 'components/ViewHeader';
15import { useLocalRouter } from 'lib/LocalRouter';
14import { useLocalRouter } from 'lib/LocalRouter';
16import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
15import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
src/views/Admin/AdminNetworkingKeys.js+145−39
@@ -1,25 +1,38 @@
1−import React, { useCallback, useState } from 'react';
1+import React, { useCallback, useEffect, useState } from 'react';
2−import { Nothing } from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import { Grid, Text, H5, Flex } from 'indigo-react';
3import { Grid, Text, H5, Flex } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
5+import { randomHex } from 'web3-utils';
4
6
5import { usePointCursor } from 'store/pointCursor';
7import { usePointCursor } from 'store/pointCursor';
6import { usePointCache } from 'store/pointCache';
8import { usePointCache } from 'store/pointCache';
7
9
8import { useLocalRouter } from 'lib/LocalRouter';
10import { useLocalRouter } from 'lib/LocalRouter';
9import * as need from 'lib/need';
11import * as need from 'lib/need';
12+import {
10−import { segmentNetworkKey } from 'lib/keys';
13+ segmentNetworkKey,
14+ attemptNetworkSeedDerivation,
15+ deriveNetworkKeys,
16+} from 'lib/keys';
11import { formatDotsWithTime } from 'lib/dateFormat';
17import { formatDotsWithTime } from 'lib/dateFormat';
12
18
13import ViewHeader from 'components/ViewHeader';
19import ViewHeader from 'components/ViewHeader';
14import MiniBackButton from 'components/MiniBackButton';
20import MiniBackButton from 'components/MiniBackButton';
15−import {
16− GenerateButton,
17− BootArvoButton,
18− ForwardButton,
19−} from 'components/Buttons';
21+import { BootArvoButton, ForwardButton } from 'components/Buttons';
20import FooterButton from 'components/FooterButton';
22import FooterButton from 'components/FooterButton';
21import WarningBox from 'components/WarningBox';
23import WarningBox from 'components/WarningBox';
22import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
24import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
25+import useEthereumTransaction from 'lib/useEthereumTransaction';
26+import { GAS_LIMITS, ZERO_KEY } from 'lib/constants';
27+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
28+import { addHexPrefix } from 'lib/wallet';
29+import { useNetwork } from 'store/network';
30+import { useWallet } from 'store/wallet';
31+
32+const CRYPTO_SUITE_VERSION = 1;
33+
34+const chainKeyProp = name => d =>
35+ d[name] === ZERO_KEY ? Nothing() : Just(d[name]);
23
36
24const renderNetworkKey = key => {
37const renderNetworkKey = key => {
25 const segments = segmentNetworkKey(key);
38 const segments = segmentNetworkKey(key);
@@ -32,14 +45,81 @@ const renderNetworkKey = key => {
32};
45};
33
46
34function useSetKeys() {
47function useSetKeys() {
35− const setting = false;
48+ const { urbitWallet, wallet, authMnemonic } = useWallet();
36− const didSet = false;
49+ const { pointCursor } = usePointCursor();
50+ const { syncOwnedPoint, getDetails } = usePointCache();
51+ const { contracts } = useNetwork();
52+ const _point = need.point(pointCursor);
53+ const _contracts = need.contracts(contracts);
54+
55+ const [isDiscontinuity] = useState(false);
56+ // TODO: set discontinuity with radio check?
57+ const [ndNetworkSeed, setNdNetworkSeed] = useState();
58+
59+ const { construct, broadcasting, confirmed, bind } = useEthereumTransaction(
60+ GAS_LIMITS.CONFIGURE_KEYS
61+ );
62+
63+ // sync point details after success
64+ useEffect(() => {
65+ if (confirmed) {
66+ syncOwnedPoint(_point);
67+ }
68+ }, [_point, confirmed, syncOwnedPoint]);
69+
37− const setKeys = useCallback(async () => {}, []);
70+ const setKeys = useCallback(async () => {
71+ const details = need.details(getDetails(_point));
72+ const networkRevision = parseInt(details.keyRevisionNumber, 10);
73+
74+ console.log('setting revision to ', networkRevision + 1);
75+
76+ const seed = await attemptNetworkSeedDerivation({
77+ urbitWallet,
78+ wallet,
79+ authMnemonic,
80+ details,
81+ revision: networkRevision + 1,
82+ });
83+
84+ const _networkSeed = seed.matchWith({
85+ Nothing: () => {
86+ const ndSeed = randomHex(64);
87+ setNdNetworkSeed(ndSeed);
88+ return ndSeed;
89+ },
90+ Just: p => p.value,
91+ });
92+
93+ const pair = deriveNetworkKeys(_networkSeed);
94+
95+ const txn = azimuth.ecliptic.configureKeys(
96+ _contracts,
97+ _point,
98+ addHexPrefix(pair.crypt.public),
99+ addHexPrefix(pair.auth.public),
100+ CRYPTO_SUITE_VERSION,
101+ isDiscontinuity
102+ );
103+
104+ construct(txn);
105+ }, [
106+ _contracts,
107+ _point,
108+ authMnemonic,
109+ construct,
110+ getDetails,
111+ isDiscontinuity,
112+ urbitWallet,
113+ wallet,
114+ ]);
38
115
39 return {
116 return {
40− setting,
41− didSet,
42 setKeys,
117 setKeys,
118+ construct,
119+ broadcasting,
120+ confirmed,
121+ ndNetworkSeed,
122+ bind,
43 };
123 };
44}
124}
45
125
@@ -55,23 +135,29 @@ export default function AdminNetworkingKeys() {
55 Nothing: () => false,
135 Nothing: () => false,
56 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
136 Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
57 });
137 });
58− const [configuring, setConfiguring] = useState(false);
59
138
139+ const [didRequestSetKeys, _setDidRequestSetKeys] = useState(false);
140+ const {
141+ setKeys,
142+ broadcasting,
143+ confirmed,
144+ ndNetworkSeed,
145+ bind,
60− const { setKeys, setting, didSet } = useSetKeys();
146+ } = useSetKeys();
61
147
62− const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
148+ const setDidRequestSetKeys = useCallback(() => {
63− const configureTransaction = useCallback(() => setConfiguring(true), [
149+ setKeys();
64− setConfiguring,
150+ _setDidRequestSetKeys(true);
65− ]);
151+ }, [setKeys, _setDidRequestSetKeys]);
66
152
67− const inDefaultState = !configuring && !setting && !didSet;
153+ const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
68
154
69 const renderTitle = () => {
155 const renderTitle = () => {
70− if (didSet) {
156+ if (confirmed) {
71 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
157 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
72 }
158 }
73
159
74− if (setting) {
160+ if (broadcasting) {
75 return 'Setting Network Keys...';
161 return 'Setting Network Keys...';
76 }
162 }
77
163
@@ -83,19 +169,30 @@ export default function AdminNetworkingKeys() {
83 };
169 };
84
170
85 const renderButton = () => {
171 const renderButton = () => {
86− if (didSet) {
172+ if (!didRequestSetKeys) {
87− return <Grid.Item full as={DownloadKeyfileButton} solid />;
173+ return (
174+ <Grid.Item
175+ full
176+ as={ForwardButton}
177+ solid
178+ onClick={() => setDidRequestSetKeys(true)}>
179+ {hasKeys ? 'Reset' : 'Set'} Networking Keys
180+ </Grid.Item>
181+ );
88 }
182 }
89
183
90− if (configuring || setting) {
184+ if (confirmed) {
91− return <Grid.Item full as={GenerateButton} solid onClick={setKeys} />;
185+ return (
186+ <Grid.Item
187+ full
188+ as={DownloadKeyfileButton}
189+ solid
190+ networkSeed={ndNetworkSeed}
191+ />
192+ );
92 }
193 }
93
194
94− return (
195+ return null;
95− <Grid.Item full as={ForwardButton} solid onClick={configureTransaction}>
96− {hasKeys ? 'Reset' : 'Set'} Networking Keys
97− </Grid.Item>
98− );
99 };
196 };
100
197
101 const renderNetworkKeySection = (title, key) => (
198 const renderNetworkKeySection = (title, key) => (
@@ -105,7 +202,7 @@ export default function AdminNetworkingKeys() {
105 </Grid.Item>
202 </Grid.Item>
106 {key.matchWith({
203 {key.matchWith({
107 Nothing: () => (
204 Nothing: () => (
108− <Grid.Item full as={Text} className="f5 gray4">
205+ <Grid.Item full as="code" className="f5 gray4">
109 Unset
206 Unset
110 </Grid.Item>
207 </Grid.Item>
111 ),
208 ),
@@ -144,11 +241,11 @@ export default function AdminNetworkingKeys() {
144 <>
241 <>
145 {renderNetworkKeySection(
242 {renderNetworkKeySection(
146 'Authentication',
243 'Authentication',
147− details.map(d => d.authenticationKey)
244+ details.chain(chainKeyProp('authenticationKey'))
148 )}
245 )}
149 {renderNetworkKeySection(
246 {renderNetworkKeySection(
150 'Encryption',
247 'Encryption',
151− details.map(d => d.encryptionKey)
248+ details.chain(chainKeyProp('encryptionKey'))
152 )}
249 )}
153 <Grid.Item full as={Flex} row justify="between" className="mt3">
250 <Grid.Item full as={Flex} row justify="between" className="mt3">
154 {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
251 {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
@@ -184,7 +281,7 @@ export default function AdminNetworkingKeys() {
184 {renderTitle()}
281 {renderTitle()}
185 </Grid.Item>
282 </Grid.Item>
186
283
187− {inDefaultState && (
284+ {!didRequestSetKeys && (
188 <Grid.Item full as={Text} className="mb3">
285 <Grid.Item full as={Text} className="mb3">
189 {hasKeys
286 {hasKeys
190 ? 'Here are your public keys that authenticate your Arvo.'
287 ? 'Here are your public keys that authenticate your Arvo.'
@@ -192,7 +289,7 @@ export default function AdminNetworkingKeys() {
192 </Grid.Item>
289 </Grid.Item>
193 )}
290 )}
194
291
195− {didSet && (
292+ {confirmed && (
196 <Grid.Item full as={WarningBox} className="mb3">
293 <Grid.Item full as={WarningBox} className="mb3">
197 You need this keyfile to authenticate with Arvo.
294 You need this keyfile to authenticate with Arvo.
198 </Grid.Item>
295 </Grid.Item>
@@ -200,9 +297,18 @@ export default function AdminNetworkingKeys() {
200
297
201 {renderButton()}
298 {renderButton()}
202
299
300+ {didRequestSetKeys && (
301+ <Grid.Item
302+ full
303+ as={InlineEthereumTransaction}
304+ {...bind}
305+ onReturn={() => pop()}
306+ />
307+ )}
308+
203− {inDefaultState && renderDetails()}
309+ {!didRequestSetKeys && renderDetails()}
204
310
205− {didSet && (
311+ {confirmed && (
206 <>
312 <>
207 <Grid.Item full as={BootArvoButton} disabled />
313 <Grid.Item full as={BootArvoButton} disabled />
208 <Grid.Divider />
314 <Grid.Divider />
@@ -210,7 +316,7 @@ export default function AdminNetworkingKeys() {
210 )}
316 )}
211 </Grid>
317 </Grid>
212
318
213− {inDefaultState && (
319+ {!didRequestSetKeys && (
214 <FooterButton onClick={goRelocate} disabled>
320 <FooterButton onClick={goRelocate} disabled>
215 Relocate
321 Relocate
216 </FooterButton>
322 </FooterButton>
src/views/Admin/AdminSetProxy.js+22−7
@@ -24,7 +24,7 @@ import MiniBackButton from 'components/MiniBackButton';
24import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
24import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
25import { GAS_LIMITS } from 'lib/constants';
25import { GAS_LIMITS } from 'lib/constants';
26
26
27−const proxyFromDetails = (details, proxyType) => {
27+const proxyFromDetails = (details, contracts, proxyType) => {
28 switch (proxyType) {
28 switch (proxyType) {
29 case PROXY_TYPE.MANAGEMENT:
29 case PROXY_TYPE.MANAGEMENT:
30 return details.managementProxy;
30 return details.managementProxy;
@@ -33,13 +33,17 @@ const proxyFromDetails = (details, proxyType) => {
33 case PROXY_TYPE.TRANSFER:
33 case PROXY_TYPE.TRANSFER:
34 return details.transferProxy;
34 return details.transferProxy;
35 case PROXY_TYPE.VOTING:
35 case PROXY_TYPE.VOTING:
36+ if (eqAddr(details.votingProxy, contracts.delegatedSending.address)) {
37+ return `${details.votingProxy} (invites contract)`;
38+ } else {
36− return details.votingProxy;
39+ return details.votingProxy;
40+ }
37 default:
41 default:
38 throw new Error(`Unknown proxyType: ${proxyType}`);
42 throw new Error(`Unknown proxyType: ${proxyType}`);
39 }
43 }
40};
44};
41
45
42−function useSetProxy(proxyType, address) {
46+function useSetProxy(proxyType) {
43 const { contracts } = useNetwork();
47 const { contracts } = useNetwork();
44 const { pointCursor } = usePointCursor();
48 const { pointCursor } = usePointCursor();
45 const { syncOwnedPoint } = usePointCache();
49 const { syncOwnedPoint } = usePointCache();
@@ -109,8 +113,11 @@ export default function AdminSetProxy() {
109 const { data, pop } = useLocalRouter();
113 const { data, pop } = useLocalRouter();
110 const { getDetails } = usePointCache();
114 const { getDetails } = usePointCache();
111 const { pointCursor } = usePointCursor();
115 const { pointCursor } = usePointCursor();
116+ const { contracts } = useNetwork();
117+
112− const point = need.point(pointCursor);
118+ const _point = need.point(pointCursor);
113− const details = need.details(getDetails(point));
119+ const _details = need.details(getDetails(_point));
120+ const _contracts = need.contracts(contracts);
114
121
115 const properProxyType = capitalize(proxyTypeToHuman(data.proxyType));
122 const properProxyType = capitalize(proxyTypeToHuman(data.proxyType));
116
123
@@ -129,26 +136,34 @@ export default function AdminSetProxy() {
129 }
136 }
130 }, [pass, address, construct]);
137 }, [pass, address, construct]);
131
138
132− const proxyAddress = proxyFromDetails(details, data.proxyType);
139+ const proxyAddress = proxyFromDetails(_details, _contracts, data.proxyType);
133 const isProxySet = !eqAddr(ETH_ZERO_ADDR, proxyAddress);
140 const isProxySet = !eqAddr(ETH_ZERO_ADDR, proxyAddress);
134
141
142+ const proxyAddressLabel = `${
143+ confirmed ? 'New' : 'Current'
144+ } ${properProxyType} Address`;
145+
135 return (
146 return (
136 <Grid>
147 <Grid>
137 <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
148 <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
149+
138 <Grid.Item full as={ViewHeader}>
150 <Grid.Item full as={ViewHeader}>
139 {properProxyType} Address
151 {properProxyType} Address
140 </Grid.Item>
152 </Grid.Item>
153+
141 <Grid.Item full as={Text} className="mb4 f5">
154 <Grid.Item full as={Text} className="mb4 f5">
142 {proxyTypeToHumanDescription(data.proxyType)}
155 {proxyTypeToHumanDescription(data.proxyType)}
143 </Grid.Item>
156 </Grid.Item>
157+
144 <Grid.Item
158 <Grid.Item
145 full
159 full
146 as={Text}
160 as={Text}
147 className={cn('f6 mb1', {
161 className={cn('f6 mb1', {
148 green3: confirmed,
162 green3: confirmed,
149 })}>
163 })}>
150− {confirmed ? 'New' : 'Current'} {properProxyType} Address
164+ {proxyAddressLabel}
151 </Grid.Item>
165 </Grid.Item>
166+
152 <Grid.Item full as={Flex} row justify="between" align="center">
167 <Grid.Item full as={Flex} row justify="between" align="center">
153 <Flex.Item
168 <Flex.Item
154 flex
169 flex
src/views/Admin/Reticket/ReticketConfirm.js+7
@@ -28,6 +28,7 @@ export default function ReticketConfirm({ newWallet, setNewWallet }) {
28 // to read the disclaimer text
28 // to read the disclaimer text
29 //TODO also check we can pay for the transactions?
29 //TODO also check we can pay for the transactions?
30 useLifecycle(() => {
30 useLifecycle(() => {
31+ let mounted = true;
31 (async () => {
32 (async () => {
32 if (Just.hasInstance(newWallet)) {
33 if (Just.hasInstance(newWallet)) {
33 // skip if already computed (hitting the back button for example)
34 // skip if already computed (hitting the back button for example)
@@ -38,8 +39,14 @@ export default function ReticketConfirm({ newWallet, setNewWallet }) {
38 if (isDevelopment) {
39 if (isDevelopment) {
39 console.log(`The new ticket for ${name} is ${wal.ticket}`);
40 console.log(`The new ticket for ${name} is ${wal.ticket}`);
40 }
41 }
42+ if (!mounted) {
43+ return;
44+ }
45+
41 setGeneratedWallet(Just(wal));
46 setGeneratedWallet(Just(wal));
42 })();
47 })();
48+
49+ return () => (mounted = false);
43 });
50 });
44
51
45 const goDownload = useCallback(() => push(names.DOWNLOAD), [push, names]);
52 const goDownload = useCallback(() => push(names.DOWNLOAD), [push, names]);
src/views/Invite/InviteEmail.js+1−1
@@ -121,7 +121,7 @@ export default function InviteEmail() {
121 const [hovered, setHovered] = useSetState();
121 const [hovered, setHovered] = useSetState();
122 const [invites, addInvite, clearInvites] = useSetState();
122 const [invites, addInvite, clearInvites] = useSetState();
123 const [receipts, addReceipt, clearReceipts] = useSetState();
123 const [receipts, addReceipt, clearReceipts] = useSetState();
124− const [errors, addError, clearErrors] = useSetState();
124+ const [errors, addError] = useSetState();
125
125
126 // manage general state that affects the whole form
126 // manage general state that affects the whole form
127 const [status, setStatus] = useState(STATUS.INPUT);
127 const [status, setStatus] = useState(STATUS.INPUT);
src/views/Point.js−1
@@ -19,7 +19,6 @@ import { useSyncOwnedPoints } from 'lib/useSyncPoints';
19import Actions from './Point/Actions';
19import Actions from './Point/Actions';
20import { useWallet } from 'store/wallet';
20import { useWallet } from 'store/wallet';
21import usePermissionsForPoint from 'lib/usePermissionsForPoint';
21import usePermissionsForPoint from 'lib/usePermissionsForPoint';
22−import MiniBackButton from 'components/MiniBackButton';
23
22
24export default function Point() {
23export default function Point() {
25 const { push, names } = useHistory();
24 const { push, names } = useHistory();