on master help sign in
checkpoint: initial refactor of Admin.js
1a0a4ddMatt Condon (shrugs)
src/components/View.js+6−2
@@ -3,9 +3,13 @@ import cn from 'classnames';
3import { Flex } from 'indigo-react';
3import { Flex } from 'indigo-react';
4
4
5import Footer from './Footer';
5import Footer from './Footer';
6+import useBreakpoints from 'lib/useBreakpoints';
6
7
7// View is a top-level component that all Views must render to inherit styling
8// View is a top-level component that all Views must render to inherit styling
8function View({ className, children, inset = false, full = false, ...rest }) {
9function View({ className, children, inset = false, full = false, ...rest }) {
10+ const isMobile = useBreakpoints([true, false, false]);
11+ const shouldInset = inset && !isMobile;
12+
9 return (
13 return (
10 <Flex
14 <Flex
11 col
15 col
@@ -21,8 +25,8 @@ function View({ className, children, inset = false, full = false, ...rest }) {
21 {...rest}>
25 {...rest}>
22 <Flex.Item
26 <Flex.Item
23 className={cn('pb5', {
27 className={cn('pb5', {
24− pt10: inset,
28+ pt10: shouldInset,
25− pt5: !inset,
29+ pt5: !shouldInset,
26 })}>
30 })}>
27 {children}
31 {children}
28 </Flex.Item>
32 </Flex.Item>
src/components/ViewHeader.jsnew file+7
@@ -0,0 +1,7 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { H5 } from 'indigo-react';
4+
5+export default function ViewHeader({ children, className }) {
6+ return <H5 className={cn(className, 'mv4')}>{children}</H5>;
7+}
src/components/old/Base.js−2
@@ -63,7 +63,6 @@ import Warning from './Warning';
63import ValidatedSigil from './ValidatedSigil';
63import ValidatedSigil from './ValidatedSigil';
64import ValidatedBlockie from './ValidatedBlockie';
64import ValidatedBlockie from './ValidatedBlockie';
65import ShowBlockie from './ShowBlockie';
65import ShowBlockie from './ShowBlockie';
66−import Passport from './Passport';
67
66
68// HOC validated form components
67// HOC validated form components
69
68
@@ -178,5 +177,4 @@ export {
178 P,
177 P,
179 Code,
178 Code,
180 Warning,
179 Warning,
181− Passport,
182};
180};
src/components/old/Passport.jsdeleted−235
@@ -1,235 +0,0 @@
1−import React from 'react';
2−import { pour } from 'sigil-js';
3−import * as ob from 'urbit-ob';
4−import ReactSVGComponents from '../ReactSVGComponents';
5−import PropTypes from 'prop-types';
6−import { Button } from './Base';
7−import PaperRenderer from 'components/PaperRenderer';
8−
9−import { WALLET_STATES } from '../../lib/invite';
10−import { downloadWallet } from '../../lib/invite';
11−import { compose } from '../../lib/lib';
12−import { withWallet } from '../../store/wallet';
13−
14−class Passport extends React.Component {
15− constructor(props) {
16− super(props);
17−
18− this.state = {
19− paper: null,
20− };
21−
22− this.masterCensor = this.generateCensor();
23− this.managementCensor = this.generateCensor();
24−
25− this.download = this.download.bind(this);
26− }
27−
28− download() {
29− downloadWallet(this.state.paper);
30− this.props.pushWalletState(WALLET_STATES.DOWNLOADED);
31− }
32−
33− generateCensor() {
34− let blocks = [];
35−
36− new Uint32Array(86).forEach((_, i) => {
37− let rand = Math.round(Math.random());
38− let filled = rand || i === 0 || i === 43;
39−
40− let className = filled ? 'passport-block bg-gray-30' : 'passport-block';
41−
42− blocks.push(<div key={i} data-key={i} className={className} />);
43− });
44−
45− return <div className="flex flex-wrap passport-censor">{blocks}</div>;
46− }
47−
48− getCurrentDate() {
49− let date = new Date();
50− const fil = n => (n >= 10 ? n : `0${n}`);
51− let fmtDate =
52− `${date.getUTCFullYear()}.` +
53− `${date.getUTCMonth() + 1}.` +
54− `${fil(date.getUTCDate())}`;
55−
56− return fmtDate;
57− }
58−
59− getWalletProgress() {
60− let { walletStates } = this.props;
61−
62− if (!walletStates.includes(WALLET_STATES.UNLOCKING)) return null;
63− if (walletStates.includes(WALLET_STATES.TRANSACTIONS)) return null;
64−
65− let progressTimes = {
66− [WALLET_STATES.UNLOCKING]: { ms: '2813', pct: '44%' },
67− [WALLET_STATES.GENERATING]: { ms: '2433', pct: '38%' },
68− [WALLET_STATES.RENDERING]: { ms: '986', pct: '15%' },
69− };
70−
71− const progressBars = Object.keys(progressTimes).map(state => {
72− let progressIndex = walletStates.indexOf(state);
73− let progressDone =
74− progressIndex !== -1 && progressIndex < walletStates.length - 1;
75−
76− let progressStyle = {
77− width: progressTimes[state].pct,
78− };
79−
80− return (
81− <div className="passport-wallet-progress" style={progressStyle}>
82− {progressDone && <div className="passport-wallet-progress-fill" />}
83− </div>
84− );
85− });
86−
87− return <div className="flex space-between mt-3">{progressBars}</div>;
88− }
89−
90− getWalletStates() {
91− let { walletStates } = this.props;
92−
93− if (walletStates.includes(WALLET_STATES.TRANSACTIONS)) return null;
94−
95− let stateElems = walletStates.map(state => {
96− if (state === WALLET_STATES.DOWNLOADED) return null;
97−
98− const lastIndex = walletStates.indexOf(state) === walletStates.length - 1;
99− const checkmark = lastIndex ? null : (
100− <div className="text-700 text-sm lh-6 green-dark">✓</div>
101− );
102−
103− return (
104− <div className="flex justify-between">
105− <div className="text-mono text-sm lh-6 green-dark">{state}</div>
106− {checkmark}
107− </div>
108− );
109− });
110−
111− return <div className="mt-3">{stateElems}</div>;
112− }
113−
114− render() {
115− let pointVal = this.props.point.matchWith({
116− Just: p => p.value,
117− Nothing: () => '',
118− });
119−
120− let patp = pointVal === '' ? '' : ob.patp(pointVal);
121−
122− let wallet = this.props.wallet.matchWith({
123− Just: w => w.value,
124− Nothing: () => null,
125− });
126−
127− let ownershipAddress = this.props.wallet.matchWith({
128− Just: w => w.value.ownership.keys.address,
129− Nothing: () => null,
130− });
131−
132− const sigil =
133− patp !== '' ? (
134− pour({
135− patp: patp,
136− renderer: ReactSVGComponents,
137− size: 128,
138− })
139− ) : (
140− <div className="passport-sigil-blank" />
141− );
142−
143− let currentDate = this.getCurrentDate();
144− let walletStates = this.getWalletStates();
145− let walletProgress = this.getWalletProgress();
146− let paperReady =
147− this.state.paper &&
148− this.props.walletStates.includes(WALLET_STATES.PAPER_READY);
149− let downloadColor = paperReady ? 'green' : 'black';
150−
151− let downloadBtn = (
152− <Button
153− disabled={!paperReady}
154− prop-size={'lg wide'}
155− prop-color={downloadColor}
156− onClick={this.download}>
157− {'Download'}
158− </Button>
159− );
160−
161− let paperCollateral = wallet ? (
162− <PaperRenderer
163− point={pointVal}
164− wallet={wallet}
165− callback={data => {
166− this.setState({ paper: data });
167− this.props.pushWalletState(WALLET_STATES.PAPER_READY);
168− }}
169− />
170− ) : (
171− <div />
172− );
173−
174− let walletAddresses = (
175− <div className="mt-8">
176− <div className="passport-label text-mono">Ethereum Address</div>
177− <div className="passport-value">
178− {ownershipAddress || 'Ungenerated'}
179− </div>
180−
181− <div className="passport-label gray-50 mt-3">Master Ticket</div>
182− <div className="passport-value passport-censor overflow-hidden">
183− {this.masterCensor}
184− </div>
185−
186− <div className="passport-label gray-50 mt-5">Management Seed</div>
187− <div className="passport-value passport-censor overflow-hidden">
188− {this.managementCensor}
189− </div>
190− </div>
191− );
192−
193− let walletMetadata = (
194− <div className="flex">
195− <div className="passport-sigil">{sigil}</div>
196− <div className="passport-metadata ml-4">
197− <div className="passport-label text-mono">Point</div>
198− <div className="text-mono">{patp || '-'}</div>
199−
200− <div className="passport-label gray-50 mt-6">Created on</div>
201− <div className="passport-value">{patp ? currentDate : '-'}</div>
202−
203− <div className="passport-label gray-50 mt-2">Invited by</div>
204− <div className="passport-value">{this.props.invitedBy || '-'}</div>
205− </div>
206− </div>
207− );
208−
209− return (
210− <div>
211− <div className="passport-border">
212− <div className={`passport ${paperReady && 'passport-filled'}`}>
213− {walletMetadata}
214− {walletAddresses}
215− {paperCollateral}
216− </div>
217− {downloadBtn}
218− </div>
219−
220− {walletProgress}
221− {walletStates}
222− </div>
223− );
224− }
225−}
226−
227−Passport.propTypes = {
228− wallet: PropTypes.object,
229−};
230−
231−Passport.defaultProps = {
232− wallet: null,
233−};
234−
235−export default compose(withWallet)(Passport);
src/indigo-react/components/Button.js+1−1
@@ -27,7 +27,7 @@ export default function Button({
27 as={As}
27 as={As}
28 gap={1}
28 gap={1}
29 className={cn(
29 className={cn(
30− 'pointer pv4 truncate flex-row justify-between',
30+ 'pointer pv4 truncate flex-row justify-between us-none',
31 {
31 {
32 p4: solid,
32 p4: solid,
33 },
33 },
src/lib/invite.js+12−48
@@ -9,26 +9,13 @@ import * as tank from './tank';
9import JSZip from 'jszip';
9import JSZip from 'jszip';
10import saveAs from 'file-saver';
10import saveAs from 'file-saver';
11
11
12−import { sendSignedTransaction, hexify } from './txn';
12+import { hexify, sendAndAwaitConfirm } from './txn';
13−import { attemptNetworkSeedDerivation } from './keys';
13+import { deriveNetworkSeedFromUrbitWallet } from './keys';
14−import { addHexPrefix, WALLET_TYPES } from './wallet';
14+import { addHexPrefix } from './wallet';
15import { GAS_LIMITS } from './constants';
15import { GAS_LIMITS } from './constants';
16
16
17−const INVITE_STAGES = {
17+// the initial network key revision is always 1
18− INVITE_LOGIN: 'invite login',
18+const INITIAL_NETWORK_KEY_REVISION = 1;
19− INVITE_WALLET: 'invite wallet',
20− INVITE_VERIFY: 'invite verify',
21− INVITE_TRANSACTIONS: 'invite transactions',
22−};
23−
24−const WALLET_STATES = {
25− UNLOCKING: 'Unlocking invite wallet',
26− GENERATING: 'Generating your wallet',
27− RENDERING: 'Creating paper collateral',
28− PAPER_READY: 'Download your wallet',
29− DOWNLOADED: 'Wallet downloaded',
30− TRANSACTIONS: 'Sending transactions',
31−};
32
19
33const TRANSACTION_STATES = {
20const TRANSACTION_STATES = {
34 GENERATING: {
21 GENERATING: {
@@ -57,14 +44,14 @@ const TRANSACTION_STATES = {
57 },
44 },
58};
45};
59
46
60−async function generateWallet(point) {
47+export async function generateWallet(point) {
61 const ticket = await wg.makeTicket(point);
48 const ticket = await wg.makeTicket(point);
62 const wallet = await wg.generateWallet(point, ticket);
49 const wallet = await wg.generateWallet(point, ticket);
63 return wallet;
50 return wallet;
64}
51}
65
52
66//TODO should be moved to lib/walletgen
53//TODO should be moved to lib/walletgen
67−async function downloadWallet(paper) {
54+export async function downloadWallet(paper) {
68 return new Promise((resolve, reject) => {
55 return new Promise((resolve, reject) => {
69 const zip = new JSZip();
56 const zip = new JSZip();
70 //TODO the categories here aren't explained in bridge at all...
57 //TODO the categories here aren't explained in bridge at all...
@@ -94,7 +81,7 @@ async function downloadWallet(paper) {
94 });
81 });
95}
82}
96
83
97−async function claimPointFromInvite({
84+export async function claimPointFromInvite({
98 inviteWallet,
85 inviteWallet,
99 wallet,
86 wallet,
100 point,
87 point,
@@ -136,14 +123,10 @@ async function claimPointFromInvite({
136 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
123 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
137
124
138 // configure networking public keys
125 // configure networking public keys
139−
126+ const seed = await deriveNetworkSeedFromUrbitWallet(
140− //TODO feels like more of this logic should live in a lib?
141− const seed = await attemptNetworkSeedDerivation(true, {
142− walletType: WALLET_TYPES.TICKET,
143− urbitWallet: Just(wallet),
127+ Just(wallet),
144− pointCursor: Just(point),
128+ INITIAL_NETWORK_KEY_REVISION
145− pointCache: { [point]: { keyRevisionNumber: 0 } },
146− });
129+ );
147
130
148 const networkSeed = seed.matchWith({
131 const networkSeed = seed.matchWith({
149 Nothing: () => {
132 Nothing: () => {
@@ -290,22 +273,3 @@ async function claimPointFromInvite({
290
273
291 progress(TRANSACTION_STATES.DONE);
274 progress(TRANSACTION_STATES.DONE);
292}
275}
293−
294−async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
295− await Promise.all(
296− signedTxs.map(tx => {
297− return new Promise((resolve, reject) => {
298− sendSignedTransaction(web3, tx, usedTank, resolve);
299− });
300− })
301− );
302−}
303−
304−export {
305− generateWallet,
306− downloadWallet,
307− claimPointFromInvite,
308− INVITE_STAGES,
309− WALLET_STATES,
310− TRANSACTION_STATES,
311−};
src/lib/keys.js+96−59
@@ -6,8 +6,9 @@ import * as noun from '../nockjs/noun';
6import * as serial from '../nockjs/serial';
6import * as serial from '../nockjs/serial';
7import * as kg from 'urbit-key-generation/dist';
7import * as kg from 'urbit-key-generation/dist';
8
8
9−import { BRIDGE_ERROR } from './error';
10−import { WALLET_TYPES, eqAddr } from './wallet';
9+import { eqAddr, addHexPrefix } from './wallet';
10+
11+const NETWORK_KEY_CURVE_PARAMETER = '42';
11
12
12// ctsy joemfb
13// ctsy joemfb
13const b64 = buf => {
14const b64 = buf => {
@@ -50,9 +51,20 @@ const jam = seed => {
50 return Buffer.from(pad, 'hex').reverse();
51 return Buffer.from(pad, 'hex').reverse();
51};
52};
52
53
53−const genKey = (networkSeed, point, revision) => {
54+// simple alias to avoid importing kg into component scope
54− const pair = kg.deriveNetworkKeys(networkSeed);
55+export const deriveNetworkKeys = seed => kg.deriveNetworkKeys(seed);
56+
57+/**
58+ * @param {object} pair
59+ * @param {number} point
60+ * @param {number} revision
61+ * @return {string}
62+ */
63+export const compileNetworkingKey = (pair, point, revision) => {
64+ const bnsec = new BN(
55− const bnsec = new BN(pair.crypt.private + pair.auth.private + '42', 'hex'); // '42' is curve parameter
65+ pair.crypt.private + pair.auth.private + NETWORK_KEY_CURVE_PARAMETER,
66+ 'hex'
67+ );
56
68
57 const sed = noun.dwim(
69 const sed = noun.dwim(
58 noun.Atom.fromInt(point),
70 noun.Atom.fromInt(point),
@@ -64,68 +76,93 @@ const genKey = (networkSeed, point, revision) => {
64 return b64(jam(sed));
76 return b64(jam(sed));
65};
77};
66
78
67−// 'next' refers to setting the next set of keys
79+/**
68−// if false, we use revision - 1
80+ * @param {Maybe<any>} urbitWallet
69−const attemptNetworkSeedDerivation = async (next, args) => {
81+ * @param {number} revision
82+ * @return {Promise<Maybe<string>>}
83+ */
84+export const deriveNetworkSeedFromUrbitWallet = async (
70− const { walletType, urbitWallet, authMnemonic } = args;
85+ urbitWallet,
71−
72− // NB (jtobin):
73− //
74− // following code is intentionally verbose for sake of clarity
75−
76− const point = need.point(args.pointCursor);
77− const pointDetails = need.fromPointCache(args.pointCache, point);
78−
79− const revision =
86+ revision = 1
80− next === true
87+) => {
81− ? parseInt(pointDetails.keyRevisionNumber)
88+ const _urbitWallet = need.wallet(urbitWallet);
82− : parseInt(pointDetails.keyRevisionNumber) - 1;
89+ return deriveNetworkSeedFromSeed(_urbitWallet.management.seed, revision);
83−
90+};
84− let managementSeed = '';
85−
86− const ticketLike = [WALLET_TYPES.TICKET, WALLET_TYPES.SHARDS];
87−
88− if (ticketLike.includes(walletType)) {
89− const uwal = urbitWallet.matchWith({
90− Just: uw => uw.value,
91− Nothing: _ => {
92− throw BRIDGE_ERROR.MISSING_URBIT_WALLET;
93− },
94− });
95−
96− managementSeed = uwal.management.seed;
97− } else if (walletType === WALLET_TYPES.MNEMONIC) {
98− const walProxy = need.addressFromWallet(args.wallet);
99
91
100− const mnemonic = authMnemonic.matchWith({
92+/**
101− Just: mnem => mnem.value,
93+ * @param {Maybe<any>} wallet
102− Nothing: _ => {
94+ * @param {Maybe<string>} authMnemonic
103− throw BRIDGE_ERROR.MISSING_MNEMONIC;
95+ * @param {object} details
104− },
96+ * @param {number} revision
105− });
97+ * @return {Promise<Maybe<string>>}
98+ */
99+export const deriveNetworkSeedFromMnemonic = async (
100+ wallet,
101+ authMnemonic,
102+ details,
103+ revision = 1
104+) => {
105+ const _wallet = need.wallet(wallet);
106+ const _mnemonic = need.authMnemonic(authMnemonic);
107+ const isManagementProxy = eqAddr(_wallet.address, details.managementProxy);
108+
109+ if (isManagementProxy) {
110+ return await deriveNetworkSeedFromSeed(_mnemonic, revision);
111+ }
106
112
107− const chainProxy = pointDetails.managementProxy;
113+ return Nothing();
114+};
108
115
109− // the network seed is only derivable from mnemonic if the derived
116+/**
110− // management seed equals the record we have on chain
117+ * @param {string} seed
111− const networkSeedDerivable = eqAddr(walProxy, chainProxy);
118+ * @param {number} revision
119+ * @return {Promise<Maybe<string>>}
120+ */
121+const deriveNetworkSeedFromSeed = async (seed, revision) => {
122+ const networkSeed = await kg.deriveNetworkSeed(seed, '', revision);
123+ // apparently deriveNetworkSeed can return empty string
124+ // and perhaps even malformed
125+ return networkSeed === '' ? Nothing() : Just(networkSeed);
126+};
112
127
113− if (networkSeedDerivable === true) {
128+/**
114− managementSeed = mnemonic;
129+ * @return {Promise<Maybe<string>>}
115− }
130+ */
131+export const attemptNetworkSeedDerivation = async ({
132+ urbitWallet,
133+ wallet,
134+ authMnemonic,
135+ details,
136+ revision,
137+}) => {
138+ if (Just.hasInstance(urbitWallet)) {
139+ return await deriveNetworkSeedFromUrbitWallet(urbitWallet, revision);
116 }
140 }
117
141
118− if (managementSeed !== '') {
142+ if (Just.hasInstance(wallet) && Just.hasInstance(authMnemonic)) {
119− const seed = await kg.deriveNetworkSeed(managementSeed, '', revision);
143+ return await deriveNetworkSeedFromMnemonic(
120−
144+ wallet,
121− if (seed === '') {
145+ authMnemonic,
122− return Nothing();
146+ details,
123− }
147+ revision
124−
148+ );
125− return Just(seed);
126 }
149 }
127
150
128 return Nothing();
151 return Nothing();
129};
152};
130
153
131−export { genKey, attemptNetworkSeedDerivation };
154+/**
155+ *
156+ * @param {object} pair
157+ * @param {object} details
158+ * @return {boolean}
159+ */
160+export const keysMatchChain = (pair, details) => {
161+ const { crypt, auth } = pair;
162+ const { encryptionKey, authenticationKey } = details;
163+
164+ return (
165+ encryptionKey === addHexPrefix(crypt.public) &&
166+ authenticationKey === addHexPrefix(auth.public)
167+ );
168+};
src/lib/need.js+9−4
@@ -8,12 +8,17 @@ const needBuilder = fn => obj => {
8 fn();
8 fn();
9 }
9 }
10
10
11− return obj.matchWith({
11+ return obj.orElse(fn);
12− Nothing: fn,
13− Just: p => p.value,
14− });
15};
12};
16
13
14+export const details = needBuilder(() => {
15+ throw new Error('need details of point');
16+});
17+
18+export const authMnemonic = needBuilder(() => {
19+ throw new Error('need auth mnemonic');
20+});
21+
17export const web3 = needBuilder(() => {
22export const web3 = needBuilder(() => {
18 throw new Error(BRIDGE_ERROR.MISSING_WEB3.message);
23 throw new Error(BRIDGE_ERROR.MISSING_WEB3.message);
19});
24});
src/lib/router.js−2
@@ -8,7 +8,6 @@ import Permissions from '../views/Permissions';
8import AcceptTransfer from '../views/AcceptTransfer';
8import AcceptTransfer from '../views/AcceptTransfer';
9import CancelTransfer from '../views/CancelTransfer';
9import CancelTransfer from '../views/CancelTransfer';
10import CreateGalaxy from '../views/CreateGalaxy';
10import CreateGalaxy from '../views/CreateGalaxy';
11−import GenKeyfile from '../views/GenKeyfile';
12import IssueChild from '../views/IssueChild';
11import IssueChild from '../views/IssueChild';
13import ViewPoint from '../views/ViewPoint';
12import ViewPoint from '../views/ViewPoint';
14import SentTransaction from '../views/SentTransaction';
13import SentTransaction from '../views/SentTransaction';
@@ -49,5 +48,4 @@ export const ROUTES = {
49 [ROUTE_NAMES.SET_KEYS]: SetKeys,
48 [ROUTE_NAMES.SET_KEYS]: SetKeys,
50 [ROUTE_NAMES.TRANSFER]: Transfer,
49 [ROUTE_NAMES.TRANSFER]: Transfer,
51 [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
50 [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
52− [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
53};
51};
src/lib/txn.js+12−32
@@ -12,36 +12,6 @@ import { WALLET_TYPES, addHexPrefix } from './wallet';
12const CHECK_BLOCK_EVERY_MS =
12const CHECK_BLOCK_EVERY_MS =
13 process.env.NODE_ENV === 'development' ? 1000 : 5000;
13 process.env.NODE_ENV === 'development' ? 1000 : 5000;
14
14
15−const TXN_PURPOSE = {
16− SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
17− SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
18− SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
19− CREATE_GALAXY: Symbol('CREATE_GALAXY'),
20− ISSUE_CHILD: Symbol('ISSUE_CHILD'),
21− SET_KEYS: Symbol('SET_KEYS'),
22− TRANSFER: Symbol('TRANSFER'),
23− CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
24−};
25−
26−const renderTxnPurpose = purpose =>
27− purpose === TXN_PURPOSE.SET_MANAGEMENT_PROXY
28− ? 'set this management proxy'
29− : purpose === TXN_PURPOSE.SET_SPAWN_PROXY
30− ? 'set this spawn proxy'
31− : purpose === TXN_PURPOSE.SET_TRANSFER_PROXY
32− ? 'set this transfer proxy'
33− : purpose === TXN_PURPOSE.CREATE_GALAXY
34− ? 'create this galaxy'
35− : purpose === TXN_PURPOSE.ISSUE_CHILD
36− ? 'issue this point'
37− : purpose === TXN_PURPOSE.SET_KEYS
38− ? 'set these network keys'
39− : purpose === TXN_PURPOSE.TRANSFER
40− ? 'transfer this point'
41− : purpose === TXN_PURPOSE.CANCEL_TRANSFER
42− ? 'cancel this transfer'
43− : 'perform this transaction';
44−
45const signTransaction = async config => {
15const signTransaction = async config => {
46 let {
16 let {
47 wallet,
17 wallet,
@@ -209,6 +179,16 @@ const isTransactionConfirmed = async (web3, txHash) => {
209 return receipt !== null;
179 return receipt !== null;
210};
180};
211
181
182+async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
183+ await Promise.all(
184+ signedTxs.map(tx => {
185+ return new Promise((resolve, reject) => {
186+ sendSignedTransaction(web3, tx, usedTank, resolve);
187+ });
188+ })
189+ );
190+}
191+
212const hexify = val => addHexPrefix(val.toString('hex'));
192const hexify = val => addHexPrefix(val.toString('hex'));
213
193
214const renderSignedTx = stx => ({
194const renderSignedTx = stx => ({
@@ -230,6 +210,7 @@ const getTxnInfo = async (web3, addr) => {
230 };
210 };
231};
211};
232
212
213+// TODO(shrugs): deprecate, unifiy with other callsites
233const canDecodePatp = p => {
214const canDecodePatp = p => {
234 try {
215 try {
235 ob.patp2dec(p);
216 ob.patp2dec(p);
@@ -243,10 +224,9 @@ export {
243 signTransaction,
224 signTransaction,
244 sendSignedTransaction,
225 sendSignedTransaction,
245 waitForTransactionConfirm,
226 waitForTransactionConfirm,
227+ sendAndAwaitConfirm,
246 isTransactionConfirmed,
228 isTransactionConfirmed,
247− TXN_PURPOSE,
248 getTxnInfo,
229 getTxnInfo,
249− renderTxnPurpose,
250 hexify,
230 hexify,
251 renderSignedTx,
231 renderSignedTx,
252 toHex,
232 toHex,
src/lib/useKeyfileGenerator.jsnew file+97
@@ -0,0 +1,97 @@
1+import { useCallback, useState } from 'react';
2+import saveAs from 'file-saver';
3+import * as ob from 'urbit-ob';
4+
5+import { usePointCache } from 'store/pointCache';
6+import { useWallet } from 'store/wallet';
7+
8+import * as need from 'lib/need';
9+
10+import {
11+ attemptNetworkSeedDerivation,
12+ keysMatchChain,
13+ deriveNetworkKeys,
14+ compileNetworkingKey,
15+} from './keys';
16+import usePermissionsForPoint from './usePermissionsForPoint';
17+
18+export default function useKeyfileGenerator(point) {
19+ const { getDetails, getOptimisticDetails } = usePointCache();
20+ const { urbitWallet, wallet, authMnemonic } = useWallet();
21+
22+ const [generating, setGenerating] = useState(false);
23+ const [keyfile, setKeyfile] = useState();
24+
25+ const details = need.details(getDetails(point));
26+ const optimisticDetails = getOptimisticDetails(point);
27+
28+ // use the optimistic key revision number if available
29+ // (because we may have set networking keys earlier)
30+ const networkRevision = optimisticDetails.keyRevisionNumber.matchWith({
31+ Nothing: () => parseInt(details.keyRevisionNumber, 10),
32+ Just: p => p.value,
33+ });
34+
35+ const { isOwner, isManagementProxy } = usePermissionsForPoint(point);
36+ const hasNetworkingKeys = networkRevision > 0;
37+ const available = (isOwner || isManagementProxy) && hasNetworkingKeys;
38+
39+ const generate = useCallback(async () => {
40+ setGenerating(true);
41+
42+ const networkSeed = await attemptNetworkSeedDerivation({
43+ urbitWallet,
44+ wallet,
45+ authMnemonic,
46+ details,
47+ revision: networkRevision,
48+ });
49+
50+ const _networkSeed = networkSeed.orElse(() => {
51+ setGenerating(false);
52+ throw new Error('Could not derive networking seed.');
53+ });
54+
55+ const pair = deriveNetworkKeys(_networkSeed);
56+
57+ if (!keysMatchChain(pair, details)) {
58+ setGenerating(false);
59+ throw new Error(
60+ 'Derived networking keys do not match public keys on chain.'
61+ );
62+ }
63+
64+ setKeyfile(compileNetworkingKey(pair, point, networkRevision));
65+ setGenerating(false);
66+ }, [
67+ setGenerating,
68+ setKeyfile,
69+ urbitWallet,
70+ wallet,
71+ authMnemonic,
72+ details,
73+ networkRevision,
74+ point,
75+ ]);
76+
77+ const download = useCallback(() => {
78+ saveAs(
79+ new Blob([keyfile], {
80+ type: 'text/plain;charset=utf-8',
81+ }),
82+ `${ob.patp(point).slice(1)}-${networkRevision}.key`
83+ // TODO: ^ unifiy "remove tilde" calls
84+ );
85+ }, [point, keyfile, networkRevision]);
86+
87+ const generateAndDownload = useCallback(async () => {
88+ await generate();
89+ download();
90+ }, [generate, download]);
91+
92+ return {
93+ generating,
94+ available,
95+ generateAndDownload,
96+ };
97+}
src/lib/usePermissionsForPoint.jsnew file+29
@@ -0,0 +1,29 @@
1+import { eqAddr } from './wallet';
2+import { usePointCache } from 'store/pointCache';
3+
4+const NULL_PERMISSIONS = {
5+ isOwner: false,
6+ isTransferProxy: false,
7+ isManagementProxy: false,
8+ canTransfer: false,
9+};
10+
11+/**
12+ * @param {string} address
13+ * @param {number} point
14+ */
15+export default function usePermissionsForPoint(address, point) {
16+ const { getDetails } = usePointCache();
17+
18+ return getDetails(point).matchWith({
19+ Nothing: () => NULL_PERMISSIONS,
20+ Just: ({ value: details }) => {
21+ const isOwner = eqAddr(address, details.owner);
22+ const isTransferProxy = eqAddr(address, details.transferProxy);
23+ const isManagementProxy = eqAddr(address, details.managementProxy);
24+ const canTransfer = isOwner || isTransferProxy;
25+
26+ return { isOwner, isTransferProxy, isManagementProxy, canTransfer };
27+ },
28+ });
29+}
src/lib/wallet.js+1
@@ -42,6 +42,7 @@ export const addressFromSecp256k1Public = pub => {
42 return toChecksumAddress(addr);
42 return toChecksumAddress(addr);
43};
43};
44
44
45+// TODO: move all of these generic crypto helpers to another file
45export const addHexPrefix = hex =>
46export const addHexPrefix = hex =>
46 hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
47 hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
47
48
src/store/lib/useInvitesStore.js+3−11
@@ -1,8 +1,9 @@
1−import { useCallback, useState } from 'react';
1+import { useCallback } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
6+import useSetState from 'lib/useSetState';
6
7
7// the default value of a point's invites
8// the default value of a point's invites
8const EMPTY_INVITES = {
9const EMPTY_INVITES = {
@@ -13,16 +14,7 @@ const EMPTY_INVITES = {
13
14
14export default function useInvitesStore() {
15export default function useInvitesStore() {
15 const { contracts } = useNetwork();
16 const { contracts } = useNetwork();
16− const [invitesCache, _setInvitesCache] = useState({});
17−
18− const addToInvitesCache = useCallback(
17+ const [invitesCache, addToInvitesCache] = useSetState({});
19− entry =>
20− _setInvitesCache(cache => ({
21− ...cache,
22− ...entry,
23− })),
24− [_setInvitesCache]
25− );
26
18
27 const getInvites = useCallback(
19 const getInvites = useCallback(
28 point => invitesCache[point] || EMPTY_INVITES,
20 point => invitesCache[point] || EMPTY_INVITES,
src/store/lib/useOptimisticDetailsStore.jsnew file+38
@@ -0,0 +1,38 @@
1+import { useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+
4+import useSetState from 'lib/useSetState';
5+
6+const DEFAULT_OPTIMISTIC_DETAILS = {
7+ keyRevisionNumber: Nothing(),
8+};
9+
10+export default function useOptimisticDetailsStore() {
11+ const [optimisticDetails, addToOptimisticDetails] = useSetState({});
12+
13+ const getOptimisticDetails = useCallback(
14+ point => optimisticDetails[point] || DEFAULT_OPTIMISTIC_DETAILS,
15+ [optimisticDetails]
16+ );
17+
18+ const _buildSetter = key =>
19+ useCallback(
20+ (point, value) =>
21+ addToOptimisticDetails({
22+ [point]: {
23+ ...DEFAULT_OPTIMISTIC_DETAILS,
24+ ...optimisticDetails[point],
25+ ...optimisticDetails({ [key]: Just(value) }),
26+ },
27+ }),
28+ // NOTE: this usage of useCallback is ok because it is called a constant
29+ // number of times. the linter also doesn't understand that it's within
30+ // a closure, so we disable the rule here.
31+ // eslint-disable-next-line react-hooks/exhaustive-deps
32+ [addToOptimisticDetails, optimisticDetails]
33+ );
34+
35+ const setOptimisticKeyRevision = _buildSetter('keyRevisionNumber');
36+
37+ return { getOptimisticDetails, setOptimisticKeyRevision };
38+}
src/store/lib/usePointStore.js+17−10
@@ -6,36 +6,42 @@ import useRekeyDateStore from './useRekeyDateStore';
6import useInvitesStore from './useInvitesStore';
6import useInvitesStore from './useInvitesStore';
7import useControlledPointsStore from './useControlledPointsStore';
7import useControlledPointsStore from './useControlledPointsStore';
8import useEclipticOwnerStore from './useEclipticOwnerStore';
8import useEclipticOwnerStore from './useEclipticOwnerStore';
9+import useOptimisticPointDetailsStore from './useOptimisticDetailsStore';
9
10
10export default function usePointStore() {
11export default function usePointStore() {
11 const { syncDetails, ...details } = useDetailsStore();
12 const { syncDetails, ...details } = useDetailsStore();
12 const { syncBirthday, ...birthdays } = useBirthdaysStore();
13 const { syncBirthday, ...birthdays } = useBirthdaysStore();
13 const { syncRekeyDate, ...rekeyDates } = useRekeyDateStore();
14 const { syncRekeyDate, ...rekeyDates } = useRekeyDateStore();
14 const { syncInvites, ...invites } = useInvitesStore();
15 const { syncInvites, ...invites } = useInvitesStore();
15− const ecliptic = useEclipticOwnerStore();
16 const {
16 const {
17 syncControlledPoints,
17 syncControlledPoints,
18 ...controlledPoints
18 ...controlledPoints
19 } = useControlledPointsStore();
19 } = useControlledPointsStore();
20+ const ecliptic = useEclipticOwnerStore();
21+ const optimistic = useOptimisticPointDetailsStore();
20
22
21 // sync all of the on-chain info required to display a known point
23 // sync all of the on-chain info required to display a known point
22 const syncKnownPoint = useCallback(
24 const syncKnownPoint = useCallback(
25+ point => {
26+ syncBirthday(point);
23− async point => Promise.all([syncBirthday(point), syncRekeyDate(point)]),
27+ syncRekeyDate(point);
28+ },
24 [syncBirthday, syncRekeyDate]
29 [syncBirthday, syncRekeyDate]
25 );
30 );
26
31
27 // sync all of the on-chain info required to display a foreign point
32 // sync all of the on-chain info required to display a foreign point
28− const syncForeignPoint = useCallback(async point => Promise.all([]), []);
33+ const syncForeignPoint = useCallback(point => {}, []);
29
34
30 // sync all of the on-chain info required for a point that the user owns
35 // sync all of the on-chain info required for a point that the user owns
31 const syncOwnedPoint = useCallback(
36 const syncOwnedPoint = useCallback(
32− async point =>
37+ point => {
33− Promise.all([
38+ syncForeignPoint(point);
34− syncKnownPoint(point),
39+ syncKnownPoint(point);
40+ //
35− syncDetails(point),
41+ syncDetails(point);
36− syncInvites(point),
42+ syncInvites(point);
37− ]),
43+ },
38− [syncKnownPoint, syncDetails, syncInvites]
44+ [syncForeignPoint, syncKnownPoint, syncDetails, syncInvites]
39 );
45 );
40
46
41 return {
47 return {
@@ -45,6 +51,7 @@ export default function usePointStore() {
45 ...invites,
51 ...invites,
46 ...controlledPoints,
52 ...controlledPoints,
47 ...ecliptic,
53 ...ecliptic,
54+ ...optimistic,
48 syncInvites,
55 syncInvites,
49 syncControlledPoints,
56 syncControlledPoints,
50 syncKnownPoint,
57 syncKnownPoint,
src/store/wallet.js+1
@@ -55,6 +55,7 @@ function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
55 const setWallet = useCallback(
55 const setWallet = useCallback(
56 wallet => {
56 wallet => {
57 // force that public addresses are derived for each wallet
57 // force that public addresses are derived for each wallet
58+ // NOTE wallet is Maybe<> and .map is Maybe#map
58 wallet.map(wal => {
59 wallet.map(wal => {
59 wal.address = wal.address || addressFromSecp256k1Public(wal.publicKey);
60 wal.address = wal.address || addressFromSecp256k1Public(wal.publicKey);
60 return wal;
61 return wal;
src/views/Admin.js+40−71
@@ -1,84 +1,53 @@
1−import React, { useCallback } from 'react';
1+import React from 'react';
2−import { Just } from 'folktale/maybe';
3import { Grid } from 'indigo-react';
2import { Grid } from 'indigo-react';
4−import * as need from 'lib/need';
5
3
6import View from 'components/View';
4import View from 'components/View';
7−import { ForwardButton } from 'components/Buttons';
8−
9−import { ROUTE_NAMES } from 'lib/routeNames';
10−import { eqAddr } from 'lib/wallet';
11
5
12import { useHistory } from 'store/history';
6import { useHistory } from 'store/history';
13−import { useWallet } from 'store/wallet';
7+import Crumbs from 'components/Crumbs';
14−import { usePointCursor } from 'store/pointCursor';
8+import useCurrentPointName from 'lib/useCurrentPointName';
15−import { usePointCache } from 'store/pointCache';
9+import useRouter from 'lib/useRouter';
16−import FooterButton from 'components/FooterButton';
10+import AdminHome from './Admin/AdminHome';
17−
11+import { LocalRouterProvider } from 'lib/LocalRouter';
18−export default function Admin() {
19− const history = useHistory();
20− const { urbitWallet, wallet } = useWallet();
21− const { pointCursor } = usePointCursor();
22− const { pointCache } = usePointCache();
23−
24− const point = need.point(pointCursor);
25− const pointDetails = need.fromPointCache(pointCache, point);
26− const address = need.addressFromWallet(wallet);
27
12
28− const goRedownload = useCallback(() => history.push(ROUTE_NAMES.REDOWNLOAD), [
13+const NAMES = {
29− history,
14+ HOME: 'HOME',
30− ]);
15+};
31
16
32− const goReticket = useCallback(() => history.push(ROUTE_NAMES.RETICKET), [
17+const VIEWS = {
33− history,
18+ [NAMES.HOME]: AdminHome,
34− ]);
19+};
35
20
36− const goEditPerms = useCallback(() => history.push(ROUTE_NAMES.PERMISSIONS), [
21+export default function Admin() {
37− history,
22+ const history = useHistory();
38− ]);
23+ const name = useCurrentPointName();
39−
40− const goTransfer = useCallback(() => history.push(ROUTE_NAMES.TRANSFER), [
41− history,
42− ]);
43
24
44− const canDownloadPassport = Just.hasInstance(urbitWallet);
25+ const { Route, ...router } = useRouter({
45− const isOwner = eqAddr(address, pointDetails.owner);
26+ names: NAMES,
27+ views: VIEWS,
28+ initialRoutes: [{ key: NAMES.HOME }],
29+ });
46
30
47 return (
31 return (
48− <View>
32+ <LocalRouterProvider value={router}>
33+ <View inset>
49− <Grid className="pt2">
34+ <Grid className="mb4">
50− <Grid.Item
35+ <Grid.Item
51− full
36+ as={Crumbs}
52− as={ForwardButton}
37+ routes={[
53− disabled={!canDownloadPassport}
38+ {
54− onClick={goRedownload}
39+ text: name,
55− detail="Re-download your paper wallet">
40+ action: () => history.pop(),
56− Download Passport
41+ },
57− </Grid.Item>
42+ {
58− <Grid.Divider />
43+ text: 'Admin',
59− <Grid.Item
44+ },
60− full
45+ ]}
61− as={ForwardButton}
46+ full
62− disabled={!isOwner}
47+ />
63− onClick={goReticket}
64− detail="Move to brand new wallet, re-setting all permissions">
65− Reticket
66− </Grid.Item>
67− <Grid.Item
68− full
69− as={ForwardButton}
70− onClick={goEditPerms}
71− detail="Management, networking keys, etc.">
72− Edit permissions
73− </Grid.Item>
48+ </Grid>
74− </Grid>
49+ <Route />
75−
50+ </View>
76− <FooterButton
51+ </LocalRouterProvider>
77− detail="Transfer this identity to a new owner"
78− disabled={!isOwner}
79− onClick={goTransfer}>
80− Transfer
81− </FooterButton>
82− </View>
83 );
52 );
84}
53}
src/views/Admin/AdminHome.jsnew file+105
@@ -0,0 +1,105 @@
1+import React, { useCallback } from 'react';
2+import { Just } from 'folktale/maybe';
3+import { Grid } from 'indigo-react';
4+import * as need from 'lib/need';
5+
6+import { ForwardButton, DownloadButton } from 'components/Buttons';
7+
8+import { useHistory } from 'store/history';
9+import { useWallet } from 'store/wallet';
10+import { usePointCursor } from 'store/pointCursor';
11+
12+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
13+import usePermissionsForPoint from 'lib/usePermissionsForPoint';
14+
15+import FooterButton from 'components/FooterButton';
16+import ViewHeader from 'components/ViewHeader';
17+import Blinky from 'components/Blinky';
18+
19+export default function AdminHome() {
20+ const history = useHistory();
21+ const { urbitWallet, wallet } = useWallet();
22+ const { pointCursor } = usePointCursor();
23+
24+ const point = need.point(pointCursor);
25+ const address = need.addressFromWallet(wallet);
26+ const { isOwner, canTransfer } = usePermissionsForPoint(address, point);
27+
28+ const {
29+ generating: generatingKeyfile,
30+ available: keyfileAvailable,
31+ generateAndDownload: generateAndDownloadKeyfile,
32+ } = useKeyfileGenerator(point);
33+
34+ const goRedownload = useCallback(
35+ () => history.push(history.names.REDOWNLOAD),
36+ [history]
37+ );
38+
39+ const goReticket = useCallback(() => history.push(history.names.RETICKET), [
40+ history,
41+ ]);
42+
43+ const goEditPermissions = useCallback(
44+ () => history.push(history.names.PERMISSIONS),
45+ [history]
46+ );
47+
48+ const goTransfer = useCallback(() => history.push(history.names.TRANSFER), [
49+ history,
50+ ]);
51+
52+ const canDownloadPassport = Just.hasInstance(urbitWallet);
53+
54+ return (
55+ <Grid>
56+ <Grid.Item full as={ViewHeader}>
57+ Admin
58+ </Grid.Item>
59+
60+ <Grid.Item
61+ full
62+ as={ForwardButton}
63+ disabled={!canDownloadPassport}
64+ onClick={goRedownload}
65+ detail="Re-download your paper wallet">
66+ Download Passport
67+ </Grid.Item>
68+ <Grid.Divider />
69+ <Grid.Item
70+ full
71+ as={DownloadButton}
72+ disabled={!keyfileAvailable}
73+ accessory={generatingKeyfile ? <Blinky /> : undefined}
74+ onClick={generateAndDownloadKeyfile}
75+ detail="Download your Arvo Keyfile">
76+ Download Arvo Keyfile
77+ </Grid.Item>
78+ <Grid.Divider />
79+ <Grid.Item
80+ full
81+ as={ForwardButton}
82+ disabled={!isOwner}
83+ onClick={goReticket}
84+ detail="Move to brand new wallet, resetting all permissions">
85+ Reticket
86+ </Grid.Item>
87+ <Grid.Divider />
88+ <Grid.Item
89+ full
90+ as={ForwardButton}
91+ onClick={goEditPermissions}
92+ detail="Proxies, networking keys, etc.">
93+ Edit permissions
94+ </Grid.Item>
95+ <Grid.Divider />
96+
97+ <FooterButton
98+ detail="Transfer this identity to a new owner"
99+ disabled={!canTransfer}
100+ onClick={goTransfer}>
101+ Transfer
102+ </FooterButton>
103+ </Grid>
104+ );
105+}
src/views/Invite.js+4−3
@@ -1,6 +1,6 @@
1import React from 'react';
1import React from 'react';
2import * as need from 'lib/need';
2import * as need from 'lib/need';
3−import { Grid, H5, Text } from 'indigo-react';
3+import { Grid, Text } from 'indigo-react';
4
4
5import { usePointCursor } from 'store/pointCursor';
5import { usePointCursor } from 'store/pointCursor';
6import { useHistory } from 'store/history';
6import { useHistory } from 'store/history';
@@ -18,6 +18,7 @@ import { LocalRouterProvider } from 'lib/LocalRouter';
18
18
19import InviteUrl from './Invite/InviteUrl';
19import InviteUrl from './Invite/InviteUrl';
20import InviteEmail from './Invite/InviteEmail';
20import InviteEmail from './Invite/InviteEmail';
21+import ViewHeader from 'components/ViewHeader';
21
22
22const NAMES = {
23const NAMES = {
23 URL: 'URL',
24 URL: 'URL',
@@ -53,7 +54,7 @@ export default function Invite() {
53
54
54 return (
55 return (
55 <LocalRouterProvider value={router}>
56 <LocalRouterProvider value={router}>
56− <View>
57+ <View inset>
57 <Grid className="mb4">
58 <Grid className="mb4">
58 <Grid.Item
59 <Grid.Item
59 as={Crumbs}
60 as={Crumbs}
@@ -68,7 +69,7 @@ export default function Invite() {
68 ]}
69 ]}
69 full
70 full
70 />
71 />
71− <Grid.Item as={H5} className="mv4" full>
72+ <Grid.Item full as={ViewHeader}>
72 Invite
73 Invite
73 </Grid.Item>
74 </Grid.Item>
74 <Grid.Item as={Text} className="f5" full>
75 <Grid.Item as={Text} className="f5" full>
src/views/Point/Actions.js−23
@@ -67,19 +67,6 @@ function Actions() {
67 eqAddr(deets.value.owner, addr),
67 eqAddr(deets.value.owner, addr),
68 });
68 });
69
69
70− const canGenKeyfile = pointDetails.matchWith({
71− Nothing: () => false,
72− Just: deets => {
73− const hasPermission =
74− eqAddr(deets.value.owner, addr) ||
75− eqAddr(deets.value.managementProxy, addr);
76−
77− const isBooted = deets.value.keyRevisionNumber > 0;
78−
79− return hasPermission && isBooted;
80− },
81− });
82−
83 const canAcceptTransfer = pointDetails.matchWith({
70 const canAcceptTransfer = pointDetails.matchWith({
84 Nothing: () => false,
71 Nothing: () => false,
85 Just: deets => eqAddr(deets.value.transferProxy, addr),
72 Just: deets => eqAddr(deets.value.transferProxy, addr),
@@ -142,16 +129,6 @@ function Actions() {
142 }}>
129 }}>
143 {'Cancel outgoing transfer'}
130 {'Cancel outgoing transfer'}
144 </Button>
131 </Button>
145−
146− <Button
147− prop-size={'sm'}
148− prop-type={'link'}
149− disabled={!canGenKeyfile}
150− onClick={() => {
151− history.push(ROUTE_NAMES.GEN_KEYFILE);
152− }}>
153− {'Generate Arvo keyfile'}
154− </Button>
155 </Grid.Item>
132 </Grid.Item>
156 <Grid.Item third={2}>
133 <Grid.Item third={2}>
157 <Button
134 <Button