on master help sign in
checkpoint: initial implementation of set proxy view
commitc9deecb1efb5062cf4e6bb9c62590d7ef3ecb993 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 9 Jul 2019, 23:46 (7 years ago) parentf660f7b
10 files changed+238−72
src/components/Blinky.js+13 src/components/Buttons.js+3 src/components/Passport.js+6−11 src/lib/capitalize.js+1 src/lib/proxy.js+34−13 src/lib/useInputs.js+18 src/views/Admin.js+3 src/views/Admin/AdminEditPermissions.js+43−46 src/views/Admin/AdminSetProxy.js+115 src/views/SetProxy.js+2−2
src/components/Blinky.js+13
@@ -1,18 +1,31 @@
11import React, { useState, useRef } from 'react';
2+import { times } from 'lodash';
23
34import useInterval from 'lib/useInterval';
5+import { formatDots } from 'lib/dateFormat';
46
57// TODO: make these characters display as the same width
68export const LOADING_CHARACTER = '▓';
79export const INTERSTITIAL_CHARACTER = '░';
810const BLINK_AFTER_MS = 2500; // ms
911
12+const buildDate = char =>
13+ [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
14+const DATE_A = buildDate(LOADING_CHARACTER);
15+const DATE_B = buildDate(INTERSTITIAL_CHARACTER);
16+
1017export const matchBlinky = obj =>
1118 obj.matchWith({
1219 Nothing: () => <Blinky delayed />,
1320 Just: p => p.value,
1421 });
1522
23+export const matchBlinkyDate = obj =>
24+ obj.matchWith({
25+ Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
26+ Just: p => formatDots(p.value),
27+ });
28+
1629export default function Blinky({
1730 a = LOADING_CHARACTER,
1831 b = INTERSTITIAL_CHARACTER,
src/components/Buttons.js+3
@@ -5,6 +5,9 @@ import { Button } from 'indigo-react';
55export const ForwardButton = props => <Button accessory="->" {...props} />;
66export const DownloadButton = props => <Button accessory="↓" {...props} />;
77export const RestartButton = props => <Button accessory="↺" solid {...props} />;
8+export const GenerateButton = props => (
9+ <Button accessory="○" solid {...props} />
10+);
811export const OutButton = props => (
912 <Button as="a" target="_blank" accessory="↗" {...props} />
1013);
src/components/Passport.js+6−11
@@ -6,16 +6,14 @@ import { Grid, Flex, Text } from 'indigo-react';
66import { times } from 'lodash';
77
88import usePointBirthday from 'lib/usePointBirthday';
9−import { formatDots } from 'lib/dateFormat';
109
11−import Blinky, { LOADING_CHARACTER, INTERSTITIAL_CHARACTER } from './Blinky';
10+import {
11+ LOADING_CHARACTER,
12+ INTERSTITIAL_CHARACTER,
13+ matchBlinkyDate,
14+} from './Blinky';
1215import MaybeSigil from './MaybeSigil';
1316
14−const buildDate = char =>
15− [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
16−const DATE_A = buildDate(LOADING_CHARACTER);
17−const DATE_B = buildDate(INTERSTITIAL_CHARACTER);
18−
1917const TICKET_CHAR_LENGTH = 30;
2018const TICKET = times(TICKET_CHAR_LENGTH, i =>
2119 i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
@@ -40,10 +38,7 @@ export default function Passport({
4038 mini = false,
4139}) {
4240 const birthday = usePointBirthday(point.getOrElse(null));
43− const visualBirthday = birthday.matchWith({
44− Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
45− Just: p => formatDots(p.value),
46− });
41+ const visualBirthday = matchBlinkyDate(birthday);
4742
4843 const name = point.matchWith({
4944 Nothing: () => Nothing(),
src/lib/capitalize.jsnew file+1
@@ -0,0 +1 @@
1+export default str => str.charAt(0).toUpperCase() + str.slice(1);
src/lib/proxy.js+34−13
@@ -1,16 +1,37 @@
1−const PROXY_TYPE = {
2− MANAGEMENT_PROXY: Symbol('MANAGEMENT_PROXY'),
3− SPAWN_PROXY: Symbol('SPAWN_PROXY'),
4− TRANSFER_PROXY: Symbol('TRANSFER_PROXY'),
1+export const PROXY_TYPE = {
2+ MANAGEMENT: 'MANAGEMENT',
3+ SPAWN: 'SPAWN',
4+ TRANSFER: 'TRANSFER',
5+ VOTING: 'VOTING',
56};
67
7−const renderProxyType = proxyType =>
8− proxyType === PROXY_TYPE.MANAGEMENT_PROXY
9− ? 'management'
10− : proxyType === PROXY_TYPE.SPAWN_PROXY
11− ? 'spawn'
12− : proxyType === PROXY_TYPE.TRANSFER_PROXY
13− ? 'transfer'
14− : 'proxy';
8+export const proxyTypeToHuman = proxyType => {
9+ switch (proxyType) {
10+ case PROXY_TYPE.MANAGEMENT:
11+ return 'management';
12+ case PROXY_TYPE.SPAWN:
13+ return 'spawn';
14+ case PROXY_TYPE.TRANSFER:
15+ return 'transfer';
16+ case PROXY_TYPE.VOTING:
17+ return 'voting';
18+ default:
19+ throw new Error(`Unknown proxyType: ${proxyType}`);
20+ }
21+};
1522
16−export { PROXY_TYPE, renderProxyType };
23+export const proxyTypeToHumanDescription = proxyType => {
24+ switch (proxyType) {
25+ case PROXY_TYPE.MANAGEMENT:
26+ // TODO: is the below copy actually true?
27+ return "The management address proves your point's identity to Arvo.";
28+ case PROXY_TYPE.SPAWN:
29+ return 'The spawn proxy is allowed to spawn points from this point.';
30+ case PROXY_TYPE.TRANSFER:
31+ return 'The transfer proxy is allowed to transfer this point.';
32+ case PROXY_TYPE.VOTING:
33+ return "The voting proxy is allowed to vote on this point's behalf.";
34+ default:
35+ throw new Error(`Unknown proxyType: ${proxyType}`);
36+ }
37+};
src/lib/useInputs.js+18
@@ -14,6 +14,7 @@ import {
1414 validateHexString,
1515 validateOneOf,
1616 validateMaximumPatpByteLength,
17+ validateEthereumAddress,
1718} from 'lib/validators';
1819import { prependSig } from 'lib/transformers';
1920
@@ -211,6 +212,23 @@ export function useSelectInput({ initialValue, options, ...rest }) {
211212 );
212213}
213214
215+const kAddressValidators = [validateEthereumAddress, validateNotEmpty];
216+export function useAddressInput({ ...rest }) {
217+ return useFirstOf(
218+ useForm([
219+ {
220+ type: 'string',
221+ label: 'Ethereum Address',
222+ placeholder: 'e.g. 0x6DEfFb0caFDB11D175F123F6891AA64F01c24F7d',
223+ autoComplete: 'off',
224+ validators: kAddressValidators,
225+ mono: true,
226+ ...rest,
227+ },
228+ ])
229+ );
230+}
231+
214232const kEmailValidators = [validateEmail, validateNotEmpty];
215233export const buildEmailInputConfig = extra => ({
216234 type: 'email',
src/views/Admin.js+3
@@ -14,12 +14,14 @@ import AdminHome from './Admin/AdminHome';
1414import AdminEditPermissions from './Admin/AdminEditPermissions';
1515import AdminRedownload from './Admin/AdminRedownload';
1616import AdminReticket from './Admin/AdminReticket';
17+import AdminSetProxy from './Admin/AdminSetProxy';
1718
1819const NAMES = {
1920 HOME: 'HOME',
2021 EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
2122 REDOWNLOAD: 'REDOWNLOAD',
2223 RETICKET: 'RETICKET',
24+ SET_PROXY: 'SET_PROXY',
2325};
2426
2527const VIEWS = {
@@ -27,6 +29,7 @@ const VIEWS = {
2729 [NAMES.EDIT_PERMISSIONS]: AdminEditPermissions,
2830 [NAMES.REDOWNLOAD]: AdminRedownload,
2931 [NAMES.RETICKET]: AdminReticket,
32+ [NAMES.SET_PROXY]: AdminSetProxy,
3033};
3134
3235export default function Admin() {
src/views/Admin/AdminEditPermissions.js+43−46
@@ -4,19 +4,21 @@ import * as need from 'lib/need';
44import { Grid } from 'indigo-react';
55
66import { ForwardButton } from 'components/Buttons';
7−import { matchBlinky } from 'components/Blinky';
7+import { matchBlinky, matchBlinkyDate } from 'components/Blinky';
88
9−import { ROUTE_NAMES } from 'lib/routeNames';
109import { ETH_ZERO_ADDR } from 'lib/wallet';
1110
1211import useLifecycle from 'lib/useLifecycle';
1312import { useNetwork } from 'store/network';
14−import { useHistory } from 'store/history';
1513import { useWallet } from 'store/wallet';
1614import { usePointCursor } from 'store/pointCursor';
1715import { usePointCache } from 'store/pointCache';
1816import ViewHeader from 'components/ViewHeader';
1917import usePermissionsForPoint from 'lib/usePermissionsForPoint';
18+import { PROXY_TYPE, proxyTypeToHuman } from 'lib/proxy';
19+import { useLocalRouter } from 'lib/LocalRouter';
20+import { formatDots } from 'lib/dateFormat';
21+import capitalize from 'lib/capitalize';
2022
2123//TODO consolidate with azimuth-js' getActivationBlock
2224const getRekeyDate = async (web3, contracts, point) => {
@@ -35,7 +37,7 @@ const getRekeyDate = async (web3, contracts, point) => {
3537};
3638
3739export default function AdminEditPermissions() {
38− const history = useHistory();
40+ const { push, names } = useLocalRouter();
3941 const { web3, contracts } = useNetwork();
4042 const { wallet } = useWallet();
4143 const { pointCursor } = usePointCursor();
@@ -46,8 +48,8 @@ export default function AdminEditPermissions() {
4648 const point = need.point(pointCursor);
4749 const userAddress = need.wallet(wallet).address;
4850
49− const pointDetails = need.details(getDetails(point));
50− const { isOwner, canManage, canSpawn, canVote } = usePermissionsForPoint(
51+ const details = need.details(getDetails(point));
52+ const { canManage, canTransfer, canSpawn, canVote } = usePermissionsForPoint(
5153 userAddress,
5254 point
5355 );
@@ -55,52 +57,49 @@ export default function AdminEditPermissions() {
5557 useLifecycle(() => {
5658 getRekeyDate(need.web3(web3), need.contracts(contracts), point).then(
5759 res => {
58− if (res === null) setRekeyDate(Just('unknown'));
59− else setRekeyDate(Just(res.toString()));
60+ if (res === null) setRekeyDate(Just(new Date()));
61+ else setRekeyDate(Just(res));
6062 }
6163 );
6264 });
6365
64− const goSetManagement = useCallback(
65− () => history.push(ROUTE_NAMES.SET_MANAGEMENT_PROXY),
66− [history]
66+ const goToProxy = useCallback(
67+ proxyType => push(names.SET_PROXY, { proxyType }),
68+ [push, names]
6769 );
6870
69− const goSetSpawn = useCallback(
70− () => history.push(ROUTE_NAMES.SET_SPAWN_PROXY),
71− [history]
72− );
73−
74− const goSetVoting = useCallback(
75− () => history.push(ROUTE_NAMES.SET_VOTING_PROXY),
76− [history]
77− );
78−
79− const goSetKeys = useCallback(() => history.push(ROUTE_NAMES.SET_KEYS), [
80− history,
81− ]);
71+ const goSetKeys = useCallback(() => push(names.SET_KEYS), [push, names]);
8272
83− const proxyAction = (name, address, onClick) => {
73+ const proxyAction = (proxyType, address, enabled) => {
8474 if (address === ETH_ZERO_ADDR) {
8575 address = 'Not yet set';
8676 }
8777
78+ const detail = enabled
79+ ? address
80+ : `You do not have permission to change the ${proxyTypeToHuman(
81+ proxyType
82+ )} proxy.`;
83+
8884 return (
8985 <Grid.Item full>
90− <ForwardButton disabled={!isOwner} onClick={onClick} detail={address}>
91− {`${name} Proxy Address`}
86+ <ForwardButton
87+ disabled={!enabled}
88+ onClick={() => goToProxy(proxyType)}
89+ detail={detail}>
90+ {capitalize(proxyTypeToHuman(proxyType))} Proxy Address
9291 </ForwardButton>
9392 </Grid.Item>
9493 );
9594 };
9695
9796 const networkKeysDetail =
98− pointDetails.keyRevisionNumber === 0 ? (
97+ details.keyRevisionNumber === 0 ? (
9998 'Not yet configured'
10099 ) : (
101100 <>
102− {`Revision #${pointDetails.keyRevisionNumber}, since `}
103− {matchBlinky(rekeyDate)}
101+ Revision #{details.keyRevisionNumber}, since{' '}
102+ {matchBlinkyDate(rekeyDate)}
104103 </>
105104 );
106105
@@ -109,33 +108,31 @@ export default function AdminEditPermissions() {
109108 <Grid.Item full as={ViewHeader}>
110109 Permissions
111110 </Grid.Item>
112− <Grid.Item full as={ForwardButton} detail={pointDetails.owner} disabled>
111+
112+ <Grid.Item full as={ForwardButton} detail={details.owner} disabled>
113113 Ownership Address
114114 </Grid.Item>
115115 <Grid.Divider />
116− {proxyAction('Management', pointDetails.managementProxy, goSetManagement)}
116+
117+ {proxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy, canManage)}
118+ <Grid.Divider />
119+
120+ {proxyAction(PROXY_TYPE.SPAWN, details.spawnProxy, canSpawn)}
121+ <Grid.Divider />
122+
123+ {proxyAction(PROXY_TYPE.TRANSFER, details.transferProxy, canTransfer)}
124+ <Grid.Divider />
125+
126+ {proxyAction(PROXY_TYPE.VOTING, details.votingProxy, canVote)}
117127 <Grid.Divider />
118128
119− {canSpawn && (
120− <>
121− {proxyAction('Spawn', pointDetails.spawnProxy, goSetSpawn)}
122− <Grid.Divider />
123− </>
124− )}
125−
126− {canVote && (
127− <>
128− {proxyAction('Voting', pointDetails.votingProxy, goSetVoting)}
129− <Grid.Divider />
130− </>
131− )}
132129 <Grid.Item
133130 full
134131 as={ForwardButton}
135132 disabled={!canManage}
136133 onClick={goSetKeys}
137134 detail={networkKeysDetail}>
138− Network keys
135+ Network Keys
139136 </Grid.Item>
140137 <Grid.Divider />
141138 </Grid>
src/views/Admin/AdminSetProxy.jsnew file+115
@@ -0,0 +1,115 @@
1+import React, { useCallback } from 'react';
2+import { Grid, P, Text, Input } from 'indigo-react';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from 'store/network';
6+
7+import * as need from 'lib/need';
8+import { useLocalRouter } from 'lib/LocalRouter';
9+import { ETH_ZERO_ADDR } from 'lib/wallet';
10+
11+import ViewHeader from 'components/ViewHeader';
12+import { usePointCursor } from 'store/pointCursor';
13+import {
14+ PROXY_TYPE,
15+ proxyTypeToHuman,
16+ proxyTypeToHumanDescription,
17+} from 'lib/proxy';
18+import MiniBackButton from 'components/MiniBackButton';
19+import capitalize from 'lib/capitalize';
20+import { usePointCache } from 'store/pointCache';
21+import { GenerateButton } from 'components/Buttons';
22+import { useAddressInput } from 'lib/useInputs';
23+
24+const proxyFromDetails = (details, proxyType) => {
25+ switch (proxyType) {
26+ case PROXY_TYPE.MANAGEMENT:
27+ return details.managementProxy;
28+ case PROXY_TYPE.SPAWN:
29+ return details.spawnProxy;
30+ case PROXY_TYPE.TRANSFER:
31+ return details.transferProxy;
32+ case PROXY_TYPE.VOTING:
33+ return details.votingProxy;
34+ default:
35+ throw new Error(`Unknown proxyType: ${proxyType}`);
36+ }
37+};
38+
39+function useSetProxy(proxyType) {
40+ // TODO: abstract eth login into higher level useTransaction()
41+ const { contracts } = useNetwork();
42+ const { pointCursor } = usePointCursor();
43+
44+ const buildUnsignedTx = useCallback(
45+ address => {
46+ const _contracts = need.contracts(contracts);
47+ const _point = need.point(pointCursor);
48+
49+ const txArgs = [_contracts, _point, address];
50+
51+ switch (proxyType) {
52+ case PROXY_TYPE.MANAGEMENT:
53+ return azimuth.ecliptic.setManagementProxy(...txArgs);
54+ case PROXY_TYPE.SPAWN:
55+ return azimuth.ecliptic.setSpawnProxy(...txArgs);
56+ case PROXY_TYPE.TRANSFER:
57+ return azimuth.ecliptic.setTransferProxy(...txArgs);
58+ case PROXY_TYPE.VOTING:
59+ return azimuth.ecliptic.setTransferProxy(...txArgs);
60+ default:
61+ throw new Error(`Unknown proxyType ${proxyType}`);
62+ }
63+ },
64+ [proxyType, contracts, pointCursor]
65+ );
66+
67+ const generate = useCallback(async () => {
68+ //
69+ }, []);
70+
71+ return {
72+ generate,
73+ };
74+}
75+
76+export default function AdminSetProxy() {
77+ const { data, pop } = useLocalRouter();
78+ const { getDetails } = usePointCache();
79+ const { pointCursor } = usePointCursor();
80+ const point = need.point(pointCursor);
81+ const details = need.details(getDetails(point));
82+
83+ const properProxyType = capitalize(proxyTypeToHuman(data.proxyType));
84+
85+ const [addressInput, { pass }] = useAddressInput({
86+ name: 'address',
87+ label: `New ${properProxyType} Address`,
88+ });
89+
90+ const { generate } = useSetProxy(data.proxyType);
91+
92+ return (
93+ <Grid>
94+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
95+ <Grid.Item full as={ViewHeader}>
96+ {properProxyType} Address
97+ </Grid.Item>
98+ <Grid.Item full as={P}>
99+ {proxyTypeToHumanDescription(data.proxyType)}
100+ </Grid.Item>
101+ <Grid.Item full as={Text} className="f6">
102+ Current {properProxyType} Address
103+ </Grid.Item>
104+ <Grid.Item full as={Text} className="mono">
105+ {proxyFromDetails(details, data.proxyType)}
106+ </Grid.Item>
107+
108+ <Grid.Item full as={Input} {...addressInput} className="mv4" />
109+
110+ <Grid.Item full as={GenerateButton} onClick={generate}>
111+ Generate & Sign Transaction
112+ </Grid.Item>
113+ </Grid>
114+ );
115+}
src/views/SetProxy.js+2−2
@@ -13,7 +13,7 @@ import {
1313import { AddressInput } from '../components/old/Base';
1414import * as ob from 'urbit-ob';
1515
16−import { PROXY_TYPE, renderProxyType } from '../lib/proxy';
16+import { PROXY_TYPE } from '../lib/proxy';
1717
1818import { NETWORK_TYPES } from '../lib/network';
1919
@@ -83,7 +83,7 @@ class _SetProxy extends React.Component {
8383 render() {
8484 const { props, state } = this;
8585
86− const renderedProxyType = renderProxyType(props.proxyType);
86+ const renderedProxyType = props.proxyType.toString();
8787 const validAddress = isValidAddress(state.proxyAddress);
8888 const canGenerate = validAddress === true;
8989