on master help sign in
feat: update admin views, preparing for ethereum hook/components
commitaf192664f86ed821b01e7dfabe608b8fa3975943 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateMon 15 Jul 2019, 16:52 (7 years ago) parentdee2cd5
24 files changed+518−187
src/components/Buttons.js+6 src/components/DownloadKeyfileButton.js+31 src/indigo-react/components/LinkButton.js+35 src/indigo-react/index.js+1 src/lib/dateFormat.js+5 src/lib/keys.js+11−2 src/lib/useInputs.js+6−1 src/lib/useKeyfileGenerator.js+6−10 src/lib/validators.js+13−1 src/lib/wallet.js+1−1 src/store/lib/useOptimisticDetailsStore.js−38 src/store/lib/usePointStore.js−3 src/store/lib/useRekeyDateStore.js+15−9 src/views/Activate/ActivatePassport.js+4−4 src/views/Admin.js+11−5 src/views/Admin/AdminEditPermissions.js+41−56 src/views/Admin/AdminHome.js+18−28 src/views/Admin/AdminNetworkingKeys.js+220 src/views/Admin/AdminRedownload.js+5−3 src/views/Admin/AdminSetProxy.js+34−12 src/views/Admin/AdminTransfer.js+47 src/views/GenKeyfile.js+1−1 src/views/Invite.js+4−4 src/views/Point.js+3−9
src/components/Buttons.js+6
@@ -17,3 +17,9 @@ export const OfflineButton = props => (
1717 Offline
1818 </OutButton>
1919);
20+
21+export const BootArvoButton = props => (
22+ <ForwardButton detail="Boot your computer" {...props}>
23+ Boot Arvo
24+ </ForwardButton>
25+);
src/components/DownloadKeyfileButton.jsnew file+31
@@ -0,0 +1,31 @@
1+import React from 'react';
2+
3+import { DownloadButton } from 'components/Buttons';
4+import Blinky from 'components/Blinky';
5+
6+import { usePointCursor } from 'store/pointCursor';
7+
8+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
9+import * as need from 'lib/need';
10+
11+export default function DownloadKeyfileButton({ className, ...rest }) {
12+ const { pointCursor } = usePointCursor();
13+ const point = need.point(pointCursor);
14+ const {
15+ generating,
16+ available,
17+ downloaded,
18+ generateAndDownload,
19+ } = useKeyfileGenerator(point);
20+
21+ return (
22+ <DownloadButton
23+ className={className}
24+ disabled={downloaded || !available}
25+ accessory={generating ? <Blinky /> : undefined}
26+ onClick={generateAndDownload}
27+ {...rest}>
28+ Download Arvo Keyfile
29+ </DownloadButton>
30+ );
31+}
src/indigo-react/components/LinkButton.jsnew file+35
@@ -0,0 +1,35 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+export default function LinkButton({
5+ as: As = 'a',
6+ solid = false,
7+ disabled = false,
8+ className,
9+ onClick,
10+ children,
11+ ...rest
12+}) {
13+ return (
14+ <span
15+ onClick={!disabled && onClick ? onClick : undefined}
16+ style={{
17+ ...(disabled && {
18+ pointerEvents: 'none',
19+ cursor: 'not-allowed',
20+ }),
21+ }}
22+ className={cn(
23+ 'pointer ph2 underline',
24+ {
25+ white: solid,
26+ black: !solid && !disabled,
27+ gray4: !solid && disabled,
28+ },
29+ className
30+ )}
31+ {...rest}>
32+ {children}
33+ </span>
34+ );
35+}
src/indigo-react/index.js+1
@@ -7,6 +7,7 @@ export { default as CheckboxInput } from './components/CheckboxInput';
77export { default as AccessoryIcon } from './components/AccessoryIcon';
88export { default as Button } from './components/Button';
99export { default as IconButton } from './components/IconButton';
10+export { default as LinkButton } from './components/LinkButton';
1011
1112export * from './components/Typography';
1213
src/lib/dateFormat.js+5
@@ -6,3 +6,8 @@ export const formatDots = date => {
66
77 return [year, month, day].join('.');
88};
9+
10+export const formatTime = date => date.toLocaleTimeString();
11+
12+export const formatDotsWithTime = date =>
13+ `${formatDots(date)} ${formatTime(date)}`;
src/lib/keys.js+11−2
@@ -1,12 +1,11 @@
11import BN from 'bn.js';
22import { Just, Nothing } from 'folktale/maybe';
3−import * as need from './need';
43
54import * as noun from '../nockjs/noun';
65import * as serial from '../nockjs/serial';
76import * as kg from 'urbit-key-generation/dist';
87
9−import { eqAddr, addHexPrefix } from './wallet';
8+import { eqAddr, addHexPrefix, CURVE_ZERO_ADDR } from './wallet';
109
1110const NETWORK_KEY_CURVE_PARAMETER = '42';
1211
@@ -163,3 +162,13 @@ export const keysMatchChain = (pair, details) => {
163162 authenticationKey === addHexPrefix(auth.public)
164163 );
165164};
165+
166+export const segmentNetworkKey = hex => {
167+ const sl = i => hex.slice(i, i + 4);
168+ const rowFrom = i => `${sl(i)}.${sl(i + 4)}.${sl(i + 8)}.${sl(i + 12)}`;
169+ if (hex === CURVE_ZERO_ADDR) {
170+ return null;
171+ }
172+
173+ return [rowFrom(2), rowFrom(18), rowFrom(34), rowFrom(50)];
174+};
src/lib/useInputs.js+6−1
@@ -15,6 +15,7 @@ import {
1515 validateOneOf,
1616 validateMaximumPatpByteLength,
1717 validateEthereumAddress,
18+ validateNotNullAddress,
1819} from 'lib/validators';
1920import { prependSig } from 'lib/transformers';
2021
@@ -212,7 +213,11 @@ export function useSelectInput({ initialValue, options, ...rest }) {
212213 );
213214}
214215
215−const kAddressValidators = [validateEthereumAddress, validateNotEmpty];
216+const kAddressValidators = [
217+ validateEthereumAddress,
218+ validateNotNullAddress,
219+ validateNotEmpty,
220+];
216221export function useAddressInput({ ...rest }) {
217222 return useFirstOf(
218223 useForm([
src/lib/useKeyfileGenerator.js+6−10
@@ -16,21 +16,15 @@ import {
1616import usePermissionsForPoint from './usePermissionsForPoint';
1717
1818export default function useKeyfileGenerator(point) {
19− const { getDetails, getOptimisticDetails } = usePointCache();
19+ const { getDetails } = usePointCache();
2020 const { urbitWallet, wallet, authMnemonic } = useWallet();
2121
22+ const [downloaded, setDownloaded] = useState(false);
2223 const [generating, setGenerating] = useState(false);
2324 const [keyfile, setKeyfile] = useState();
2425
2526 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− });
27+ const networkRevision = parseInt(details.keyRevisionNumber, 10);
3428
3529 const { isOwner, isManagementProxy } = usePermissionsForPoint(point);
3630 const hasNetworkingKeys = networkRevision > 0;
@@ -82,7 +76,8 @@ export default function useKeyfileGenerator(point) {
8276 `${ob.patp(point).slice(1)}-${networkRevision}.key`
8377 // TODO: ^ unifiy "remove tilde" calls
8478 );
85− }, [point, keyfile, networkRevision]);
79+ setDownloaded(true);
80+ }, [point, keyfile, networkRevision, setDownloaded]);
8681
8782 const generateAndDownload = useCallback(async () => {
8883 await generate();
@@ -92,6 +87,7 @@ export default function useKeyfileGenerator(point) {
9287 return {
9388 generating,
9489 available,
90+ downloaded,
9591 generateAndDownload,
9692 };
9793}
src/lib/validators.js+13−1
@@ -2,7 +2,7 @@ import * as bip39 from 'bip39';
22import * as ob from 'urbit-ob';
33import { identity, includes } from 'lodash';
44
5−import { isValidAddress } from './wallet';
5+import { isValidAddress, ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT } from './wallet';
66import patp2dec from './patp2dec';
77import { patpStringLength } from './lib';
88
@@ -204,6 +204,13 @@ export const validateExactly = (value, error) => m =>
204204 error,
205205 });
206206
207+export const validateNotEqual = (values = []) => m =>
208+ simpleValidatorWrapper({
209+ prevMessage: m,
210+ validator: d => !values.includes(d),
211+ error: `Cannot be ${m.data}`,
212+ });
213+
207214export const validateLength = l => m =>
208215 simpleValidatorWrapper({
209216 prevMessage: m,
@@ -237,3 +244,8 @@ export const validatePatpByteLength = byteLength => {
237244export const validateMaximumPatpByteLength = byteLength => {
238245 return validateMaximumLength(patpStringLength(byteLength));
239246};
247+
248+export const validateNotNullAddress = validateNotEqual(
249+ [ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT],
250+ 'Cannot be 0x0'
251+);
src/lib/wallet.js+1−1
@@ -4,12 +4,12 @@ import keccak from 'keccak';
44import { reduce } from 'lodash';
55import { Just, Nothing } from 'folktale/maybe';
66import * as secp256k1 from 'secp256k1';
7−import * as ob from 'urbit-ob';
87import * as kg from 'urbit-key-generation/dist';
98import { isAddress } from 'web3-utils';
109
1110export const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
1211export const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
12+export const ETH_ZERO_ADDR_SHORT = '0x0';
1313export const CURVE_ZERO_ADDR =
1414 '0x0000000000000000000000000000000000000000000000000000000000000000';
1515
src/store/lib/useOptimisticDetailsStore.jsdeleted−38
@@ -1,38 +0,0 @@
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−3
@@ -6,7 +6,6 @@ import useRekeyDateStore from './useRekeyDateStore';
66import useInvitesStore from './useInvitesStore';
77import useControlledPointsStore from './useControlledPointsStore';
88import useEclipticOwnerStore from './useEclipticOwnerStore';
9−import useOptimisticPointDetailsStore from './useOptimisticDetailsStore';
109
1110export default function usePointStore() {
1211 const { syncDetails, ...details } = useDetailsStore();
@@ -18,7 +17,6 @@ export default function usePointStore() {
1817 ...controlledPoints
1918 } = useControlledPointsStore();
2019 const ecliptic = useEclipticOwnerStore();
21− const optimistic = useOptimisticPointDetailsStore();
2220
2321 // sync all of the on-chain info required to display a known point
2422 const syncKnownPoint = useCallback(
@@ -51,7 +49,6 @@ export default function usePointStore() {
5149 ...invites,
5250 ...controlledPoints,
5351 ...ecliptic,
54− ...optimistic,
5552 syncInvites,
5653 syncControlledPoints,
5754 syncKnownPoint,
src/store/lib/useRekeyDateStore.js+15−9
@@ -1,5 +1,6 @@
11import { useCallback, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3+import Result from 'folktale/result';
34
45import { useNetwork } from '../network';
56
@@ -16,6 +17,10 @@ export default function useRekeyDateStore() {
1617 [_setRekeyDateCache]
1718 );
1819
20+ /**
21+ * @param {number} point
22+ * @return {Maybe<Result<Date, String>>}
23+ */
1924 const getRekeyDate = useCallback(
2025 point => rekeyDateCache[point] || Nothing(),
2126 [rekeyDateCache]
@@ -24,11 +29,8 @@ export default function useRekeyDateStore() {
2429 const syncRekeyDate = useCallback(
2530 async point => {
2631 const _web3 = web3.getOrElse(null);
27− if (!_web3) {
28− return;
29− }
3032 const _contracts = contracts.getOrElse(null);
31− if (!_contracts) {
33+ if (!_web3 || !_contracts) {
3234 return;
3335 }
3436
@@ -37,26 +39,30 @@ export default function useRekeyDateStore() {
3739 const logs = await _contracts.azimuth.getPastEvents('ChangedKeys', {
3840 fromBlock: 0,
3941 toBlock: 'latest',
40− filter: { point: [point] },
42+ filter: {
43+ point: [point],
44+ },
4145 });
4246
4347 if (logs.length === 0) {
4448 //TODO: better encoding for "no rekeyDate" state?
4549 addToRekeyDateCache({
46− [point]: Just('unknown'),
50+ [point]: Just(Result.Error('No rekey date available.')),
4751 });
4852 } else {
4953 // last log is most recent
5054 const blockNumber = logs[logs.length - 1].blockNumber;
5155 const block = await _web3.eth.getBlock(blockNumber);
5256 addToRekeyDateCache({
53− //TODO date format
54− [point]: Just(new Date(block.timestamp * 1000).toString()),
57+ [point]: Just(Result.Ok(new Date(block.timestamp * 1000))),
5558 });
5659 }
5760 },
5861 [web3, contracts, addToRekeyDateCache]
5962 );
6063
61− return { getRekeyDate, syncRekeyDate };
64+ return {
65+ getRekeyDate,
66+ syncRekeyDate,
67+ };
6268}
src/views/Activate/ActivatePassport.js+4−4
@@ -64,8 +64,8 @@ export default function ActivatePassport() {
6464 );
6565
6666 return (
67− <LocalRouterProvider value={router}>
68− <View full={fullView}>
67+ <View full={fullView}>
68+ <LocalRouterProvider value={router}>
6969 <Grid gap={gap} className="mt8 mb10">
7070 <Grid.Item half={half && 1} full={full}>
7171 <Passport
@@ -82,7 +82,7 @@ export default function ActivatePassport() {
8282 resetActivateRouter={resetActivateRouter}
8383 />
8484 </Grid>
85− </View>
86− </LocalRouterProvider>
85+ </LocalRouterProvider>
86+ </View>
8787 );
8888}
src/views/Admin.js+11−5
@@ -15,6 +15,8 @@ import AdminEditPermissions from './Admin/AdminEditPermissions';
1515import AdminRedownload from './Admin/AdminRedownload';
1616import AdminReticket from './Admin/AdminReticket';
1717import AdminSetProxy from './Admin/AdminSetProxy';
18+import AdminTransfer from './Admin/AdminTransfer';
19+import AdminNetworkingKeys from './Admin/AdminNetworkingKeys';
1820
1921const NAMES = {
2022 HOME: 'HOME',
@@ -22,6 +24,8 @@ const NAMES = {
2224 REDOWNLOAD: 'REDOWNLOAD',
2325 RETICKET: 'RETICKET',
2426 SET_PROXY: 'SET_PROXY',
27+ TRANSFER: 'TRANSFER',
28+ NETWORKING_KEYS: 'NETWORKING_KEYS',
2529};
2630
2731const VIEWS = {
@@ -30,6 +34,8 @@ const VIEWS = {
3034 [NAMES.REDOWNLOAD]: AdminRedownload,
3135 [NAMES.RETICKET]: AdminReticket,
3236 [NAMES.SET_PROXY]: AdminSetProxy,
37+ [NAMES.TRANSFER]: AdminTransfer,
38+ [NAMES.NETWORKING_KEYS]: AdminNetworkingKeys,
3339};
3440
3541export default function Admin() {
@@ -43,10 +49,11 @@ export default function Admin() {
4349 });
4450
4551 return (
46− <LocalRouterProvider value={router}>
47− <View inset>
52+ <View inset>
53+ <LocalRouterProvider value={router}>
4854 <Grid className="mb4">
4955 <Grid.Item
56+ full
5057 as={Crumbs}
5158 routes={[
5259 {
@@ -57,11 +64,10 @@ export default function Admin() {
5764 text: 'Admin',
5865 },
5966 ]}
60− full
6167 />
6268 </Grid>
6369 <Route />
64− </View>
65− </LocalRouterProvider>
70+ </LocalRouterProvider>
71+ </View>
6672 );
6773}
src/views/Admin/AdminEditPermissions.js+41−56
@@ -5,7 +5,7 @@ import { Grid } from 'indigo-react';
55import { azimuth } from 'azimuth-js';
66
77import { ForwardButton } from 'components/Buttons';
8−import { matchBlinky, matchBlinkyDate } from 'components/Blinky';
8+import { matchBlinkyDate } from 'components/Blinky';
99
1010import { ETH_ZERO_ADDR } from 'lib/wallet';
1111
@@ -18,38 +18,20 @@ import ViewHeader from 'components/ViewHeader';
1818import usePermissionsForPoint from 'lib/usePermissionsForPoint';
1919import { PROXY_TYPE, proxyTypeToHuman } from 'lib/proxy';
2020import { useLocalRouter } from 'lib/LocalRouter';
21−import { formatDots } from 'lib/dateFormat';
2221import capitalize from 'lib/capitalize';
2322import { eqAddr } from 'lib/wallet';
24−
25−//TODO consolidate with azimuth-js' getActivationBlock
26−const getRekeyDate = async (web3, contracts, point) => {
27− const logs = await contracts.azimuth.getPastEvents('ChangedKeys', {
28− fromBlock: 0,
29− toBlock: 'latest',
30− filter: { point: [point] },
31− });
32− if (logs.length === 0) {
33− return null;
34− } else {
35− // last log is most recent
36− const block = await web3.eth.getBlock(logs[logs.length - 1].blockNumber);
37− return new Date(block.timestamp * 1000);
38− }
39−};
23+import MiniBackButton from 'components/MiniBackButton';
24+import FooterButton from 'components/FooterButton';
4025
4126export default function AdminEditPermissions() {
42− const { push, names } = useLocalRouter();
43− const { web3, contracts } = useNetwork();
27+ const { pop, push, names } = useLocalRouter();
28+ const { contracts } = useNetwork();
4429 const { wallet } = useWallet();
4530 const { pointCursor } = usePointCursor();
46− const { getDetails } = usePointCache();
31+ const { getDetails, getRekeyDate } = usePointCache();
4732
48− const _web3 = need.web3(web3);
4933 const _contracts = need.contracts(contracts);
5034
51− const [rekeyDate, setRekeyDate] = useState(Nothing());
52−
5335 const point = need.point(pointCursor);
5436 const pointSize = azimuth.getPointSize(point);
5537 const isParent = pointSize !== azimuth.PointSize.Planet;
@@ -57,42 +39,33 @@ export default function AdminEditPermissions() {
5739 const userAddress = need.wallet(wallet).address;
5840
5941 const details = need.details(getDetails(point));
42+ const networkRevision = parseInt(details.keyRevisionNumber, 10);
6043 const { canManage, isOwner } = usePermissionsForPoint(userAddress, point);
6144
62− useLifecycle(() => {
63− getRekeyDate(_web3, _contracts, point).then(res => {
64− if (res === null) setRekeyDate(Just(new Date()));
65− else setRekeyDate(Just(res));
66− });
67− });
68−
69− const goToProxy = useCallback(
45+ const goSetProxy = useCallback(
7046 proxyType => push(names.SET_PROXY, { proxyType }),
7147 [push, names]
7248 );
7349
74− const goSetKeys = useCallback(() => push(names.SET_KEYS), [push, names]);
50+ const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
51+ push,
52+ names,
53+ ]);
7554
76− const proxyAction = (proxyType, address, enabled) => {
55+ const renderProxyAction = (proxyType, address) => {
7756 if (eqAddr(address, ETH_ZERO_ADDR)) {
7857 address = 'Not set';
7958 } else if (eqAddr(address, _contracts.delegatedSending.address)) {
80− address = address + ' (invites contract)';
59+ address = `${address} (invites contract)`;
8160 }
8261
83− const detail = enabled
84− ? address
85− : `You do not have permission to change the ${proxyTypeToHuman(
86− proxyType
87− )} proxy.`;
88−
8962 return (
9063 <>
9164 <Grid.Item full>
9265 <ForwardButton
93− disabled={!enabled}
94− onClick={() => goToProxy(proxyType)}
95− detail={detail}>
66+ disabled={!isOwner}
67+ onClick={() => goSetProxy(proxyType)}
68+ detail={address}>
9669 {capitalize(proxyTypeToHuman(proxyType))} Proxy Address
9770 </ForwardButton>
9871 </Grid.Item>
@@ -101,18 +74,30 @@ export default function AdminEditPermissions() {
10174 );
10275 };
10376
104− const networkKeysDetail =
105− details.keyRevisionNumber === 0 ? (
106− 'Not yet configured'
107− ) : (
77+ const renderNetworkKeysDetail = () => {
78+ if (networkRevision === 0) {
79+ return 'Not yet configured.';
80+ }
81+
82+ const renderRevision = maybeDate => (
10883 <>
109− Revision #{details.keyRevisionNumber}, since{' '}
110− {matchBlinkyDate(rekeyDate)}
84+ Revision #{networkRevision}, since {matchBlinkyDate(maybeDate)}
11185 </>
11286 );
11387
88+ return getRekeyDate(point).matchWith({
89+ Nothing: () => renderRevision(Nothing()),
90+ Just: p =>
91+ p.value.matchWith({
92+ Ok: r => renderRevision(Just(r.value)),
93+ Error: 'Unknown',
94+ }),
95+ });
96+ };
97+
11498 return (
11599 <Grid>
100+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
116101 <Grid.Item full as={ViewHeader}>
117102 Permissions
118103 </Grid.Item>
@@ -122,19 +107,19 @@ export default function AdminEditPermissions() {
122107 </Grid.Item>
123108 <Grid.Divider />
124109
125− {proxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy, isOwner)}
110+ {renderProxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy)}
126111
127− {isParent && proxyAction(PROXY_TYPE.SPAWN, details.spawnProxy, isOwner)}
112+ {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
128113
129− {isSenate && proxyAction(PROXY_TYPE.VOTING, details.votingProxy, isOwner)}
114+ {isSenate && renderProxyAction(PROXY_TYPE.VOTING, details.votingProxy)}
130115
131116 <Grid.Item
132117 full
133118 as={ForwardButton}
134119 disabled={!canManage}
135− onClick={goSetKeys}
136− detail={networkKeysDetail}>
137− Network Keys
120+ onClick={goNetworkingKeys}
121+ detail={renderNetworkKeysDetail()}>
122+ Networking Keys
138123 </Grid.Item>
139124 <Grid.Divider />
140125 </Grid>
src/views/Admin/AdminHome.js+18−28
@@ -3,23 +3,21 @@ import { Just } from 'folktale/maybe';
33import { Grid } from 'indigo-react';
44import * as need from 'lib/need';
55
6−import { ForwardButton, DownloadButton } from 'components/Buttons';
6+import { ForwardButton } from 'components/Buttons';
77
8−import { useHistory } from 'store/history';
98import { useWallet } from 'store/wallet';
109import { usePointCursor } from 'store/pointCursor';
1110
12−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
1311import usePermissionsForPoint from 'lib/usePermissionsForPoint';
1412
1513import FooterButton from 'components/FooterButton';
1614import ViewHeader from 'components/ViewHeader';
17−import Blinky from 'components/Blinky';
1815import { useLocalRouter } from 'lib/LocalRouter';
16+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
17+import MiniBackButton from 'components/MiniBackButton';
1918
2019export default function AdminHome() {
21− const history = useHistory();
22− const { push, names } = useLocalRouter();
20+ const { push, names, pop } = useLocalRouter();
2321 const { urbitWallet, wallet } = useWallet();
2422 const { pointCursor } = usePointCursor();
2523
@@ -27,27 +25,20 @@ export default function AdminHome() {
2725 const address = need.addressFromWallet(wallet);
2826 const { isOwner, canTransfer } = usePermissionsForPoint(address, point);
2927
30− const {
31− generating: generatingKeyfile,
32− available: keyfileAvailable,
33− generateAndDownload: generateAndDownloadKeyfile,
34− } = useKeyfileGenerator(point);
35−
3628 const goRedownload = useCallback(() => push(names.REDOWNLOAD), [push, names]);
3729 const goReticket = useCallback(() => push(names.RETICKET), [push, names]);
3830 const goEditPermissions = useCallback(() => push(names.EDIT_PERMISSIONS), [
3931 push,
4032 names,
4133 ]);
42− const goTransfer = useCallback(() => history.push(history.names.TRANSFER), [
43− history,
44− ]);
34+ const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
4535
4636 const canDownloadPassport = Just.hasInstance(urbitWallet);
4737
4838 return (
4939 <>
5040 <Grid>
41+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
5142 <Grid.Item full as={ViewHeader}>
5243 Admin
5344 </Grid.Item>
@@ -63,13 +54,9 @@ export default function AdminHome() {
6354 <Grid.Divider />
6455 <Grid.Item
6556 full
66− as={DownloadButton}
67− disabled={!keyfileAvailable}
68− accessory={generatingKeyfile ? <Blinky /> : undefined}
69− onClick={generateAndDownloadKeyfile}
70− detail="Download your Arvo Keyfile">
71− Download Arvo Keyfile
72− </Grid.Item>
57+ as={DownloadKeyfileButton}
58+ detail="Download your Arvo Keyfile"
59+ />
7360 <Grid.Divider />
7461 <Grid.Item
7562 full
@@ -88,13 +75,16 @@ export default function AdminHome() {
8875 Edit Permissions
8976 </Grid.Item>
9077 <Grid.Divider />
78+ <Grid.Item
79+ full
80+ as={ForwardButton}
81+ onClick={goTransfer}
82+ detail="Transfer ownership of this point"
83+ disabled={!canTransfer}>
84+ Transfer
85+ </Grid.Item>
86+ <Grid.Divider />
9187 </Grid>
92− <FooterButton
93− detail="Transfer this identity to a new owner"
94− disabled={!canTransfer}
95− onClick={goTransfer}>
96− Transfer
97− </FooterButton>
9888 </>
9989 );
10090}
src/views/Admin/AdminNetworkingKeys.jsnew file+220
@@ -0,0 +1,220 @@
1+import React, { useCallback, useState } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+import { Grid, Text, H5, Flex } from 'indigo-react';
4+
5+import { usePointCursor } from 'store/pointCursor';
6+import { usePointCache } from 'store/pointCache';
7+
8+import { useLocalRouter } from 'lib/LocalRouter';
9+import * as need from 'lib/need';
10+import { segmentNetworkKey } from 'lib/keys';
11+import { formatDotsWithTime } from 'lib/dateFormat';
12+
13+import ViewHeader from 'components/ViewHeader';
14+import MiniBackButton from 'components/MiniBackButton';
15+import {
16+ GenerateButton,
17+ BootArvoButton,
18+ ForwardButton,
19+} from 'components/Buttons';
20+import FooterButton from 'components/FooterButton';
21+import WarningBox from 'components/WarningBox';
22+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
23+
24+const renderNetworkKey = key => {
25+ const segments = segmentNetworkKey(key);
26+
27+ if (!segments) {
28+ return 'Unset';
29+ }
30+
31+ return segments.join('\n');
32+};
33+
34+function useSetKeys() {
35+ const setting = false;
36+ const didSet = false;
37+ const setKeys = useCallback(async () => {}, []);
38+
39+ return {
40+ setting,
41+ didSet,
42+ setKeys,
43+ };
44+}
45+
46+export default function AdminNetworkingKeys() {
47+ const { push, names, pop } = useLocalRouter();
48+ const { pointCursor } = usePointCursor();
49+ const { getDetails, getRekeyDate } = usePointCache();
50+
51+ const point = need.point(pointCursor);
52+ const details = getDetails(point);
53+
54+ const hasKeys = details.matchWith({
55+ Nothing: () => false,
56+ Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
57+ });
58+ const [configuring, setConfiguring] = useState(false);
59+
60+ const { setKeys, setting, didSet } = useSetKeys();
61+
62+ const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
63+ const configureTransaction = useCallback(() => setConfiguring(true), [
64+ setConfiguring,
65+ ]);
66+
67+ const inDefaultState = !configuring && !setting && !didSet;
68+
69+ const renderTitle = () => {
70+ if (didSet) {
71+ return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
72+ }
73+
74+ if (setting) {
75+ return 'Setting Network Keys...';
76+ }
77+
78+ if (!hasKeys) {
79+ return 'Networking keys have not yet been set.';
80+ }
81+
82+ return 'Networking';
83+ };
84+
85+ const renderButton = () => {
86+ if (didSet) {
87+ return <Grid.Item full as={DownloadKeyfileButton} solid />;
88+ }
89+
90+ if (configuring || setting) {
91+ return <Grid.Item full as={GenerateButton} solid onClick={setKeys} />;
92+ }
93+
94+ return (
95+ <Grid.Item full as={ForwardButton} solid onClick={configureTransaction}>
96+ {hasKeys ? 'Reset' : 'Set'} Networking Keys
97+ </Grid.Item>
98+ );
99+ };
100+
101+ const renderNetworkKeySection = (title, key) => (
102+ <>
103+ <Grid.Item full as={H5} className="mt3 gray4">
104+ {title}
105+ </Grid.Item>
106+ {key.matchWith({
107+ Nothing: () => (
108+ <Grid.Item full as={Text} className="f5 gray4">
109+ Unset
110+ </Grid.Item>
111+ ),
112+ Just: ({ value: key }) => (
113+ <>
114+ <Grid.Item full as="code" className="f5 wrap">
115+ 0x
116+ </Grid.Item>
117+ <Grid.Item full as="code" className="f5 wrap">
118+ {renderNetworkKey(key)}
119+ </Grid.Item>
120+ </>
121+ ),
122+ })}
123+ </>
124+ );
125+
126+ const renderDetail = (title, value) => (
127+ <Flex.Item as={Flex} col>
128+ <Flex.Item as={H5} className="gray4">
129+ {title}
130+ </Flex.Item>
131+ {value.matchWith({
132+ Nothing: () => (
133+ <Flex.Item as={Text} className="f5 gray4">
134+ Unset
135+ </Flex.Item>
136+ ),
137+ Just: ({ value }) => <Flex.Item as={Text}>{value}</Flex.Item>,
138+ })}
139+ </Flex.Item>
140+ );
141+
142+ const renderDetails = () => {
143+ return (
144+ <>
145+ {renderNetworkKeySection(
146+ 'Authentication',
147+ details.map(d => d.authenticationKey)
148+ )}
149+ {renderNetworkKeySection(
150+ 'Encryption',
151+ details.map(d => d.encryptionKey)
152+ )}
153+ <Grid.Item full as={Flex} row justify="between" className="mt3">
154+ {renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
155+ {renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
156+ {renderDetail(
157+ 'Crypto Suite Ver.',
158+ details.map(d => d.cryptoSuiteVersion)
159+ )}
160+ </Grid.Item>
161+ {hasKeys && (
162+ <Grid.Item full as={Flex} row justify="between" className="mt3">
163+ {renderDetail(
164+ 'Last Set',
165+ getRekeyDate(point).map(date =>
166+ date.matchWith({
167+ Ok: r => formatDotsWithTime(r.value),
168+ Error: () => Nothing(),
169+ })
170+ )
171+ )}
172+ </Grid.Item>
173+ )}
174+ </>
175+ );
176+ };
177+
178+ return (
179+ <>
180+ <Grid>
181+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
182+
183+ <Grid.Item full as={ViewHeader}>
184+ {renderTitle()}
185+ </Grid.Item>
186+
187+ {inDefaultState && (
188+ <Grid.Item full as={Text} className="mb3">
189+ {hasKeys
190+ ? 'Here are your public keys that authenticate your Arvo.'
191+ : 'Set your networking keys to authenticate with Arvo.'}
192+ </Grid.Item>
193+ )}
194+
195+ {didSet && (
196+ <Grid.Item full as={WarningBox} className="mb3">
197+ You need this keyfile to authenticate with Arvo.
198+ </Grid.Item>
199+ )}
200+
201+ {renderButton()}
202+
203+ {inDefaultState && renderDetails()}
204+
205+ {didSet && (
206+ <>
207+ <Grid.Item full as={BootArvoButton} disabled />
208+ <Grid.Divider />
209+ </>
210+ )}
211+ </Grid>
212+
213+ {inDefaultState && (
214+ <FooterButton onClick={goRelocate} disabled>
215+ Relocate
216+ </FooterButton>
217+ )}
218+ </>
219+ );
220+}
src/views/Admin/AdminRedownload.js+5−3
@@ -10,9 +10,10 @@ import PaperRenderer from 'components/PaperRenderer';
1010import { downloadWallet } from 'lib/invite';
1111
1212import { useWallet } from 'store/wallet';
13−import { Grid, Text } from 'indigo-react';
13+import { Grid, P } from 'indigo-react';
1414import ViewHeader from 'components/ViewHeader';
1515import { useLocalRouter } from 'lib/LocalRouter';
16+import MiniBackButton from 'components/MiniBackButton';
1617
1718export default function AdminRedownload() {
1819 const { urbitWallet } = useWallet();
@@ -34,11 +35,12 @@ export default function AdminRedownload() {
3435
3536 return (
3637 <>
37− <Grid gap={4}>
38+ <Grid>
39+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
3840 <Grid.Item full as={ViewHeader}>
3941 Download Passport
4042 </Grid.Item>
41− <Grid.Item full as={Text}>
43+ <Grid.Item full as={P}>
4244 After you’ve downloaded your passport, back up the ticket manually or
4345 store on a trusted device.
4446 </Grid.Item>
src/views/Admin/AdminSetProxy.js+34−12
@@ -1,12 +1,12 @@
11import React, { useCallback } from 'react';
2−import { Grid, P, Text, Input } from 'indigo-react';
2+import { Grid, P, Text, Input, Flex, LinkButton } from 'indigo-react';
33import * as azimuth from 'azimuth-js';
44
55import { useNetwork } from 'store/network';
66
77import * as need from 'lib/need';
88import { useLocalRouter } from 'lib/LocalRouter';
9−import { ETH_ZERO_ADDR } from 'lib/wallet';
9+import { ETH_ZERO_ADDR, eqAddr } from 'lib/wallet';
1010
1111import ViewHeader from 'components/ViewHeader';
1212import { usePointCursor } from 'store/pointCursor';
@@ -64,12 +64,22 @@ function useSetProxy(proxyType) {
6464 [proxyType, contracts, pointCursor]
6565 );
6666
67− const generate = useCallback(async () => {
68− //
69− }, []);
67+ const generate = useCallback(
68+ async address => {
69+ const utx = buildUnsignedTx(address);
70+ console.log(utx);
71+ },
72+ [buildUnsignedTx]
73+ );
74+
75+ const unset = useCallback(() => generate(ETH_ZERO_ADDR), [generate]);
76+
77+ const locked = false;
7078
7179 return {
80+ locked,
7281 generate,
82+ unset,
7383 };
7484}
7585
@@ -82,12 +92,17 @@ export default function AdminSetProxy() {
8292
8393 const properProxyType = capitalize(proxyTypeToHuman(data.proxyType));
8494
85− const [addressInput, { pass }] = useAddressInput({
95+ const { generate, unset, locked } = useSetProxy(data.proxyType);
96+ const [addressInput, { pass, data: address }] = useAddressInput({
8697 name: 'address',
8798 label: `New ${properProxyType} Address`,
99+ disabled: locked,
88100 });
89101
90− const { generate } = useSetProxy(data.proxyType);
102+ const _generate = useCallback(() => generate(address), [address, generate]);
103+
104+ const proxyAddress = proxyFromDetails(details, data.proxyType);
105+ const isProxySet = !eqAddr(ETH_ZERO_ADDR, proxyAddress);
91106
92107 return (
93108 <Grid>
@@ -95,19 +110,26 @@ export default function AdminSetProxy() {
95110 <Grid.Item full as={ViewHeader}>
96111 {properProxyType} Address
97112 </Grid.Item>
98− <Grid.Item full as={P}>
113+ <Grid.Item full as={Text} className="mb4">
99114 {proxyTypeToHumanDescription(data.proxyType)}
100115 </Grid.Item>
101− <Grid.Item full as={Text} className="f6">
116+ <Grid.Item full as={Text} className="f6 mb1">
102117 Current {properProxyType} Address
103118 </Grid.Item>
104− <Grid.Item full as={Text} className="mono">
105− {proxyFromDetails(details, data.proxyType)}
119+ <Grid.Item full as={Flex} row justify="between" align="center">
120+ <Flex.Item flex as={Text} className="mono">
121+ {isProxySet ? proxyAddress : 'Unset'}
122+ </Flex.Item>
123+ {isProxySet && (
124+ <Flex.Item as={LinkButton} onClick={unset}>
125+ Unset
126+ </Flex.Item>
127+ )}
106128 </Grid.Item>
107129
108130 <Grid.Item full as={Input} {...addressInput} className="mv4" />
109131
110− <Grid.Item full as={GenerateButton} onClick={generate}>
132+ <Grid.Item full as={GenerateButton} onClick={_generate} disabled={!pass}>
111133 Generate & Sign Transaction
112134 </Grid.Item>
113135 </Grid>
src/views/Admin/AdminTransfer.jsnew file+47
@@ -0,0 +1,47 @@
1+import React, { useCallback } from 'react';
2+import { Grid, P, Text, Input } from 'indigo-react';
3+import { useLocalRouter } from 'lib/LocalRouter';
4+
5+import ViewHeader from 'components/ViewHeader';
6+import MiniBackButton from 'components/MiniBackButton';
7+import { GenerateButton } from 'components/Buttons';
8+import { useAddressInput } from 'lib/useInputs';
9+import useCurrentPointName from 'lib/useCurrentPointName';
10+
11+function useTransfer() {
12+ const transfer = useCallback(async () => {}, []);
13+
14+ return {
15+ transfer,
16+ };
17+}
18+
19+export default function AdminTransfer() {
20+ const { pop } = useLocalRouter();
21+ const name = useCurrentPointName();
22+
23+ const [addressInput, { pass }] = useAddressInput({
24+ name: 'address',
25+ label: `Ethereum Address`,
26+ });
27+
28+ const { transfer } = useTransfer();
29+
30+ return (
31+ <Grid>
32+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
33+ <Grid.Item full as={ViewHeader}>
34+ Transfer Point
35+ </Grid.Item>
36+ <Grid.Item full as={P}>
37+ Transfer {name} to a new owner.
38+ </Grid.Item>
39+
40+ <Grid.Item full as={Input} {...addressInput} className="mv4" />
41+
42+ <Grid.Item full as={GenerateButton} onClick={transfer} disabled={!pass}>
43+ Generate & Sign Transaction
44+ </Grid.Item>
45+ </Grid>
46+ );
47+}
src/views/GenKeyfile.js+1−1
@@ -33,7 +33,7 @@ class GenKeyfile extends React.Component {
3333 // in case we did SetKeys earlier this session, make sure to generate the
3434 // newer keyfile, rather than the one that will expire soon
3535 const revision = this.props.networkRevision.matchWith({
36− Nothing: () => parseInt(pointDetails.keyRevisionNumber),
36+ Nothing: () => parseInt(pointDetails.keyRevisionNumber, 10),
3737 Just: p => p.value,
3838 });
3939
src/views/Invite.js+4−4
@@ -53,8 +53,8 @@ export default function Invite() {
5353 const acceptedInvitesText = matchBlinky(acceptedInvites);
5454
5555 return (
56− <LocalRouterProvider value={router}>
57− <View inset>
56+ <View inset>
57+ <LocalRouterProvider value={router}>
5858 <Grid className="mb4">
5959 <Grid.Item
6060 as={Crumbs}
@@ -82,7 +82,7 @@ export default function Invite() {
8282 </Grid.Item>
8383 </Grid>
8484 <Route />
85− </View>
86− </LocalRouterProvider>
85+ </LocalRouterProvider>
86+ </View>
8787 );
8888}
src/views/Point.js+3−9
@@ -1,4 +1,4 @@
1−import React, { useCallback, useMemo } from 'react';
1+import React, { useCallback } from 'react';
22import { Just } from 'folktale/maybe';
33import * as need from 'lib/need';
44import { Grid } from 'indigo-react';
@@ -9,21 +9,17 @@ import { useHistory } from 'store/history';
99
1010import View from 'components/View';
1111import Passport from 'components/Passport';
12−import { ForwardButton } from 'components/Buttons';
12+import { ForwardButton, BootArvoButton } from 'components/Buttons';
1313import FooterButton from 'components/FooterButton';
1414import { matchBlinky } from 'components/Blinky';
1515
1616import useInvites from 'lib/useInvites';
1717import { useSyncOwnedPoints } from 'lib/useSyncPoints';
18−import { ROUTE_NAMES } from 'lib/routeNames';
1918
2019import Actions from './Point/Actions';
2120import { useWallet } from 'store/wallet';
2221import usePermissionsForPoint from 'lib/usePermissionsForPoint';
2322
24−const isPlanet = point =>
25− azimuth.getPointSize(point) === azimuth.PointSize.Planet;
26−
2723export default function Point() {
2824 const { push, names } = useHistory();
2925 const { pointCursor } = usePointCursor();
@@ -91,9 +87,7 @@ export default function Point() {
9187 Admin
9288 </Grid.Item>
9389 <Grid.Divider />
94− <Grid.Item full as={ForwardButton} detail="Boot your computer" disabled>
95− Boot Arvo
96− </Grid.Item>
90+ <Grid.Item full as={BootArvoButton} disabled />
9791 {showActions && (
9892 <Grid.Item full>
9993 <Actions />