on master help sign in
checkpoint: initial implementation of set proxy view
c9deecbMatt Condon (shrugs)
src/components/Blinky.js+13
@@ -1,18 +1,31 @@
1import React, { useState, useRef } from 'react';
1import React, { useState, useRef } from 'react';
2+import { times } from 'lodash';
2
3
3import useInterval from 'lib/useInterval';
4import useInterval from 'lib/useInterval';
5+import { formatDots } from 'lib/dateFormat';
4
6
5// TODO: make these characters display as the same width
7// TODO: make these characters display as the same width
6export const LOADING_CHARACTER = '▓';
8export const LOADING_CHARACTER = '▓';
7export const INTERSTITIAL_CHARACTER = '░';
9export const INTERSTITIAL_CHARACTER = '░';
8const BLINK_AFTER_MS = 2500; // ms
10const BLINK_AFTER_MS = 2500; // ms
9
11
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+
10export const matchBlinky = obj =>
17export const matchBlinky = obj =>
11 obj.matchWith({
18 obj.matchWith({
12 Nothing: () => <Blinky delayed />,
19 Nothing: () => <Blinky delayed />,
13 Just: p => p.value,
20 Just: p => p.value,
14 });
21 });
15
22
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+
16export default function Blinky({
29export default function Blinky({
17 a = LOADING_CHARACTER,
30 a = LOADING_CHARACTER,
18 b = INTERSTITIAL_CHARACTER,
31 b = INTERSTITIAL_CHARACTER,
src/components/Buttons.js+3
@@ -5,6 +5,9 @@ import { Button } from 'indigo-react';
5export const ForwardButton = props => <Button accessory="->" {...props} />;
5export const ForwardButton = props => <Button accessory="->" {...props} />;
6export const DownloadButton = props => <Button accessory="↓" {...props} />;
6export const DownloadButton = props => <Button accessory="↓" {...props} />;
7export const RestartButton = props => <Button accessory="↺" solid {...props} />;
7export const RestartButton = props => <Button accessory="↺" solid {...props} />;
8+export const GenerateButton = props => (
9+ <Button accessory="○" solid {...props} />
10+);
8export const OutButton = props => (
11export const OutButton = props => (
9 <Button as="a" target="_blank" accessory="↗" {...props} />
12 <Button as="a" target="_blank" accessory="↗" {...props} />
10);
13);
src/components/Passport.js+6−11
@@ -6,16 +6,14 @@ import { Grid, Flex, Text } from 'indigo-react';
6import { times } from 'lodash';
6import { times } from 'lodash';
7
7
8import usePointBirthday from 'lib/usePointBirthday';
8import usePointBirthday from 'lib/usePointBirthday';
9−import { formatDots } from 'lib/dateFormat';
10
9
10+import {
11+ LOADING_CHARACTER,
11−import Blinky, { LOADING_CHARACTER, INTERSTITIAL_CHARACTER } from './Blinky';
12+ INTERSTITIAL_CHARACTER,
13+ matchBlinkyDate,
14+} from './Blinky';
12import MaybeSigil from './MaybeSigil';
15import MaybeSigil from './MaybeSigil';
13
16
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−
19const TICKET_CHAR_LENGTH = 30;
17const TICKET_CHAR_LENGTH = 30;
20const TICKET = times(TICKET_CHAR_LENGTH, i =>
18const TICKET = times(TICKET_CHAR_LENGTH, i =>
21 i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
19 i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
@@ -40,10 +38,7 @@ export default function Passport({
40 mini = false,
38 mini = false,
41}) {
39}) {
42 const birthday = usePointBirthday(point.getOrElse(null));
40 const birthday = usePointBirthday(point.getOrElse(null));
43− const visualBirthday = birthday.matchWith({
41+ const visualBirthday = matchBlinkyDate(birthday);
44− Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
45− Just: p => formatDots(p.value),
46− });
47
42
48 const name = point.matchWith({
43 const name = point.matchWith({
49 Nothing: () => Nothing(),
44 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 = {
1+export const PROXY_TYPE = {
2− MANAGEMENT_PROXY: Symbol('MANAGEMENT_PROXY'),
2+ MANAGEMENT: 'MANAGEMENT',
3− SPAWN_PROXY: Symbol('SPAWN_PROXY'),
3+ SPAWN: 'SPAWN',
4− TRANSFER_PROXY: Symbol('TRANSFER_PROXY'),
4+ TRANSFER: 'TRANSFER',
5+ VOTING: 'VOTING',
5};
6};
6
7
7−const renderProxyType = proxyType =>
8+export const proxyTypeToHuman = proxyType => {
8− proxyType === PROXY_TYPE.MANAGEMENT_PROXY
9+ switch (proxyType) {
10+ case PROXY_TYPE.MANAGEMENT:
9− ? 'management'
11+ return 'management';
10− : proxyType === PROXY_TYPE.SPAWN_PROXY
12+ case PROXY_TYPE.SPAWN:
11− ? 'spawn'
13+ return 'spawn';
12− : proxyType === PROXY_TYPE.TRANSFER_PROXY
14+ case PROXY_TYPE.TRANSFER:
13− ? 'transfer'
15+ return 'transfer';
14− : 'proxy';
16+ case PROXY_TYPE.VOTING:
17+ return 'voting';
18+ default:
19+ throw new Error(`Unknown proxyType: ${proxyType}`);
20+ }
21+};
15
22
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 {
14 validateHexString,
14 validateHexString,
15 validateOneOf,
15 validateOneOf,
16 validateMaximumPatpByteLength,
16 validateMaximumPatpByteLength,
17+ validateEthereumAddress,
17} from 'lib/validators';
18} from 'lib/validators';
18import { prependSig } from 'lib/transformers';
19import { prependSig } from 'lib/transformers';
19
20
@@ -211,6 +212,23 @@ export function useSelectInput({ initialValue, options, ...rest }) {
211 );
212 );
212}
213}
213
214
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+
214const kEmailValidators = [validateEmail, validateNotEmpty];
232const kEmailValidators = [validateEmail, validateNotEmpty];
215export const buildEmailInputConfig = extra => ({
233export const buildEmailInputConfig = extra => ({
216 type: 'email',
234 type: 'email',
src/views/Admin.js+3
@@ -14,12 +14,14 @@ import AdminHome from './Admin/AdminHome';
14import AdminEditPermissions from './Admin/AdminEditPermissions';
14import AdminEditPermissions from './Admin/AdminEditPermissions';
15import AdminRedownload from './Admin/AdminRedownload';
15import AdminRedownload from './Admin/AdminRedownload';
16import AdminReticket from './Admin/AdminReticket';
16import AdminReticket from './Admin/AdminReticket';
17+import AdminSetProxy from './Admin/AdminSetProxy';
17
18
18const NAMES = {
19const NAMES = {
19 HOME: 'HOME',
20 HOME: 'HOME',
20 EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
21 EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
21 REDOWNLOAD: 'REDOWNLOAD',
22 REDOWNLOAD: 'REDOWNLOAD',
22 RETICKET: 'RETICKET',
23 RETICKET: 'RETICKET',
24+ SET_PROXY: 'SET_PROXY',
23};
25};
24
26
25const VIEWS = {
27const VIEWS = {
@@ -27,6 +29,7 @@ const VIEWS = {
27 [NAMES.EDIT_PERMISSIONS]: AdminEditPermissions,
29 [NAMES.EDIT_PERMISSIONS]: AdminEditPermissions,
28 [NAMES.REDOWNLOAD]: AdminRedownload,
30 [NAMES.REDOWNLOAD]: AdminRedownload,
29 [NAMES.RETICKET]: AdminReticket,
31 [NAMES.RETICKET]: AdminReticket,
32+ [NAMES.SET_PROXY]: AdminSetProxy,
30};
33};
31
34
32export default function Admin() {
35export default function Admin() {
src/views/Admin/AdminEditPermissions.js+43−46
@@ -4,19 +4,21 @@ import * as need from 'lib/need';
4import { Grid } from 'indigo-react';
4import { Grid } from 'indigo-react';
5
5
6import { ForwardButton } from 'components/Buttons';
6import { ForwardButton } from 'components/Buttons';
7−import { matchBlinky } from 'components/Blinky';
7+import { matchBlinky, matchBlinkyDate } from 'components/Blinky';
8
8
9−import { ROUTE_NAMES } from 'lib/routeNames';
10import { ETH_ZERO_ADDR } from 'lib/wallet';
9import { ETH_ZERO_ADDR } from 'lib/wallet';
11
10
12import useLifecycle from 'lib/useLifecycle';
11import useLifecycle from 'lib/useLifecycle';
13import { useNetwork } from 'store/network';
12import { useNetwork } from 'store/network';
14−import { useHistory } from 'store/history';
15import { useWallet } from 'store/wallet';
13import { useWallet } from 'store/wallet';
16import { usePointCursor } from 'store/pointCursor';
14import { usePointCursor } from 'store/pointCursor';
17import { usePointCache } from 'store/pointCache';
15import { usePointCache } from 'store/pointCache';
18import ViewHeader from 'components/ViewHeader';
16import ViewHeader from 'components/ViewHeader';
19import usePermissionsForPoint from 'lib/usePermissionsForPoint';
17import 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';
20
22
21//TODO consolidate with azimuth-js' getActivationBlock
23//TODO consolidate with azimuth-js' getActivationBlock
22const getRekeyDate = async (web3, contracts, point) => {
24const getRekeyDate = async (web3, contracts, point) => {
@@ -35,7 +37,7 @@ const getRekeyDate = async (web3, contracts, point) => {
35};
37};
36
38
37export default function AdminEditPermissions() {
39export default function AdminEditPermissions() {
38− const history = useHistory();
40+ const { push, names } = useLocalRouter();
39 const { web3, contracts } = useNetwork();
41 const { web3, contracts } = useNetwork();
40 const { wallet } = useWallet();
42 const { wallet } = useWallet();
41 const { pointCursor } = usePointCursor();
43 const { pointCursor } = usePointCursor();
@@ -46,8 +48,8 @@ export default function AdminEditPermissions() {
46 const point = need.point(pointCursor);
48 const point = need.point(pointCursor);
47 const userAddress = need.wallet(wallet).address;
49 const userAddress = need.wallet(wallet).address;
48
50
49− const pointDetails = need.details(getDetails(point));
51+ const details = need.details(getDetails(point));
50− const { isOwner, canManage, canSpawn, canVote } = usePermissionsForPoint(
52+ const { canManage, canTransfer, canSpawn, canVote } = usePermissionsForPoint(
51 userAddress,
53 userAddress,
52 point
54 point
53 );
55 );
@@ -55,52 +57,49 @@ export default function AdminEditPermissions() {
55 useLifecycle(() => {
57 useLifecycle(() => {
56 getRekeyDate(need.web3(web3), need.contracts(contracts), point).then(
58 getRekeyDate(need.web3(web3), need.contracts(contracts), point).then(
57 res => {
59 res => {
58− if (res === null) setRekeyDate(Just('unknown'));
60+ if (res === null) setRekeyDate(Just(new Date()));
59− else setRekeyDate(Just(res.toString()));
61+ else setRekeyDate(Just(res));
60 }
62 }
61 );
63 );
62 });
64 });
63
65
64− const goSetManagement = useCallback(
66+ const goToProxy = useCallback(
65− () => history.push(ROUTE_NAMES.SET_MANAGEMENT_PROXY),
67+ proxyType => push(names.SET_PROXY, { proxyType }),
66− [history]
68+ [push, names]
67 );
69 );
68
70
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), [
71+ const goSetKeys = useCallback(() => push(names.SET_KEYS), [push, names]);
80− history,
81− ]);
82
72
83− const proxyAction = (name, address, onClick) => {
73+ const proxyAction = (proxyType, address, enabled) => {
84 if (address === ETH_ZERO_ADDR) {
74 if (address === ETH_ZERO_ADDR) {
85 address = 'Not yet set';
75 address = 'Not yet set';
86 }
76 }
87
77
78+ const detail = enabled
79+ ? address
80+ : `You do not have permission to change the ${proxyTypeToHuman(
81+ proxyType
82+ )} proxy.`;
83+
88 return (
84 return (
89 <Grid.Item full>
85 <Grid.Item full>
90− <ForwardButton disabled={!isOwner} onClick={onClick} detail={address}>
86+ <ForwardButton
91− {`${name} Proxy Address`}
87+ disabled={!enabled}
88+ onClick={() => goToProxy(proxyType)}
89+ detail={detail}>
90+ {capitalize(proxyTypeToHuman(proxyType))} Proxy Address
92 </ForwardButton>
91 </ForwardButton>
93 </Grid.Item>
92 </Grid.Item>
94 );
93 );
95 };
94 };
96
95
97 const networkKeysDetail =
96 const networkKeysDetail =
98− pointDetails.keyRevisionNumber === 0 ? (
97+ details.keyRevisionNumber === 0 ? (
99 'Not yet configured'
98 'Not yet configured'
100 ) : (
99 ) : (
101 <>
100 <>
102− {`Revision #${pointDetails.keyRevisionNumber}, since `}
101+ Revision #{details.keyRevisionNumber}, since{' '}
103− {matchBlinky(rekeyDate)}
102+ {matchBlinkyDate(rekeyDate)}
104 </>
103 </>
105 );
104 );
106
105
@@ -109,33 +108,31 @@ export default function AdminEditPermissions() {
109 <Grid.Item full as={ViewHeader}>
108 <Grid.Item full as={ViewHeader}>
110 Permissions
109 Permissions
111 </Grid.Item>
110 </Grid.Item>
111+
112− <Grid.Item full as={ForwardButton} detail={pointDetails.owner} disabled>
112+ <Grid.Item full as={ForwardButton} detail={details.owner} disabled>
113 Ownership Address
113 Ownership Address
114 </Grid.Item>
114 </Grid.Item>
115 <Grid.Divider />
115 <Grid.Divider />
116+
116− {proxyAction('Management', pointDetails.managementProxy, goSetManagement)}
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)}
117 <Grid.Divider />
127 <Grid.Divider />
118
128
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− )}
132 <Grid.Item
129 <Grid.Item
133 full
130 full
134 as={ForwardButton}
131 as={ForwardButton}
135 disabled={!canManage}
132 disabled={!canManage}
136 onClick={goSetKeys}
133 onClick={goSetKeys}
137 detail={networkKeysDetail}>
134 detail={networkKeysDetail}>
138− Network keys
135+ Network Keys
139 </Grid.Item>
136 </Grid.Item>
140 <Grid.Divider />
137 <Grid.Divider />
141 </Grid>
138 </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 {
13import { AddressInput } from '../components/old/Base';
13import { AddressInput } from '../components/old/Base';
14import * as ob from 'urbit-ob';
14import * as ob from 'urbit-ob';
15
15
16−import { PROXY_TYPE, renderProxyType } from '../lib/proxy';
16+import { PROXY_TYPE } from '../lib/proxy';
17
17
18import { NETWORK_TYPES } from '../lib/network';
18import { NETWORK_TYPES } from '../lib/network';
19
19
@@ -83,7 +83,7 @@ class _SetProxy extends React.Component {
83 render() {
83 render() {
84 const { props, state } = this;
84 const { props, state } = this;
85
85
86− const renderedProxyType = renderProxyType(props.proxyType);
86+ const renderedProxyType = props.proxyType.toString();
87 const validAddress = isValidAddress(state.proxyAddress);
87 const validAddress = isValidAddress(state.proxyAddress);
88 const canGenerate = validAddress === true;
88 const canGenerate = validAddress === true;
89
89