on master help sign in
Add support for star release contracts
commitb77c5fad655e3ecce3b837ab4c2706e90a9cf958 authorLiam Fitzgerald <liam@urizen.co> dateThu 7 Nov 2019, 14:01 (7 years ago) parentfeb5b56
13 files changed+375−21
src/components/InlineEthereumTransaction.js+3−3 src/components/Passport.js+1−14 src/form/validators.js+4−2 src/indigo-react/indigo.css+10−1 src/lib/point.js+13 src/lib/routeNames.js+1 src/lib/router.js+2 src/lib/validators.js+2 src/store/lib/useStarReleaseStore.js+1−1 src/views/Points.js+26 src/views/Release.js+75 src/views/Release/Active.js+94 src/views/Release/Locked.js+143
src/components/InlineEthereumTransaction.js+3−3
@@ -10,7 +10,7 @@ import {
1010 Text,
1111} from 'indigo-react';
1212
13−import { useExploreTxUrl, useExploreTxUrls } from 'lib/explorer';
13+import { useExploreTxUrls } from 'lib/explorer';
1414import { hexify } from 'lib/txn';
1515import pluralize from 'lib/pluralize';
1616
@@ -289,13 +289,13 @@ function HashReceiptList({ txHashes }) {
289289
290290 {txHashes &&
291291 txHashes.map((txHash, i) => (
292− <Flex.Item as={Flex}>
292+ <Flex.Item className="mb3" as={Flex}>
293293 <>
294294 <Flex.Item
295295 key={i}
296296 flex
297297 as="code"
298− className="f6 mono gray4 wrap">
298+ className="f6 mono gray4 wrap mr3">
299299 {txHash}
300300 </Flex.Item>
301301 <Flex.Item as={LinkButton} href={txUrls[i]}>
src/components/Passport.js+1−14
@@ -8,6 +8,7 @@ import * as need from 'lib/need';
88import { chunkStr, Matrix, walk, rand } from 'lib/card';
99import usePermissionsForPoint from 'lib/usePermissionsForPoint';
1010import { useSyncOwnedPoints } from 'lib/useSyncPoints';
11+import { buildKeyType } from 'lib/point';
1112
1213import { useWallet } from 'store/wallet';
1314
@@ -50,20 +51,6 @@ function makeSigil(size, patp, colors) {
5051 }
5152}
5253
53−const buildKeyType = permissions => {
54− if (permissions.isOwner) {
55− return 'Ownership';
56− } else if (permissions.isManagementProxy) {
57− return 'Management';
58− } else if (permissions.isSpawnProxy) {
59− return 'Spawn';
60− } else if (permissions.isVotingProxy) {
61− return 'Voting';
62− } else if (permissions.isTransferProxy) {
63− return 'Transfer';
64− }
65−};
66−
6754const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
6855
6956/**
src/form/validators.js+4−2
@@ -11,6 +11,7 @@ import {
1111 validateHexLength,
1212 validateEthereumAddress,
1313 validateGreaterThan,
14+ validateLessThan,
1415 validateEmail,
1516 validateNotNullAddress,
1617 validateHexPrefix,
@@ -100,8 +101,9 @@ export const buildAddressValidator = () =>
100101 validateNotNullAddress,
101102 validateEthereumAddress,
102103 ]);
103−export const buildNumberValidator = (min = 0) =>
104− buildValidator([validateGreaterThan(min)]);
104+export const buildNumberValidator = (min = 0, max = NaN) =>
105+ buildValidator([validateGreaterThan(min), validateLessThan(max)]);
106+
105107export const buildEmailValidator = validate =>
106108 buildValidator([validateNotEmpty, validateEmail], validate);
107109
src/indigo-react/indigo.css+10−1
@@ -159,7 +159,16 @@ textarea {
159159 line-height: 1.15;
160160 /* 1 */
161161 margin: 0;
162− /* 2 */ }
162+/* 2 */ }
163+
164+/**
165+ *
166+ * Allow input to flex properly
167+ *
168+ */
169+input {
170+ min-width: 0;
171+}
163172
164173/**
165174 * Show the overflow in IE.
src/lib/point.jsnew file+13
@@ -0,0 +1,13 @@
1+export const buildKeyType = permissions => {
2+ if (permissions.isOwner) {
3+ return 'Ownership';
4+ } else if (permissions.isManagementProxy) {
5+ return 'Management';
6+ } else if (permissions.isSpawnProxy) {
7+ return 'Spawn';
8+ } else if (permissions.isVotingProxy) {
9+ return 'Voting';
10+ } else if (permissions.isTransferProxy) {
11+ return 'Transfer';
12+ }
13+};
src/lib/routeNames.js+1
@@ -20,4 +20,5 @@ export const ROUTE_NAMES = {
2020 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
2121 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
2222 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
23+ STAR_RELEASE: Symbol('STAR_RELEASE'),
2324};
src/lib/router.js+2
@@ -3,6 +3,7 @@ import Login from 'views/Login.js';
33import Admin from 'views/Admin';
44import CreateGalaxy from 'views/CreateGalaxy';
55import IssueChild from 'views/IssueChild';
6+import StarRelease from 'views/Release';
67
78import Point from 'views/Point';
89import Points from 'views/Points';
@@ -30,4 +31,5 @@ export const ROUTES = {
3031 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
3132 // TODO: replace this with deep link to AdminCancelTransfer
3233 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
34+ [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
3335};
src/lib/validators.js+2
@@ -93,6 +93,8 @@ export const validateMaximumLength = l => v =>
9393export const validateGreaterThan = l => v =>
9494 !(v > l) && `Must be at least ${l}.`;
9595
96+export const validateLessThan = l => v => !(v < l) && `Must be less than ${l}`;
97+
9698export const validateInSet = (set, error) => v => !set.has(v) && error;
9799
98100export const validateMaximumPatpByteLength = byteLength =>
src/store/lib/useStarReleaseStore.js+1−1
@@ -54,7 +54,7 @@ export default function useStarRelease() {
5454 if (!(_contracts && _wallet)) {
5555 return [];
5656 }
57− return generateWithdrawTxs(contracts, batchLimits, amount, to);
57+ return generateWithdrawTxs(_contracts, batchLimits, amount, to);
5858 },
5959 [batchLimits, contracts, wallet]
6060 );
src/views/Points.js+26
@@ -7,6 +7,7 @@ import { useHistory } from 'store/history';
77import { useWallet } from 'store/wallet';
88import { usePointCache } from 'store/pointCache';
99import { usePointCursor } from 'store/pointCursor';
10+import { useStarReleaseCache } from 'store/starRelease';
1011
1112import * as need from 'lib/need';
1213import { isZeroAddress, abbreviateAddress } from 'lib/wallet';
@@ -90,6 +91,7 @@ export default function Points() {
9091 rejectedPoints,
9192 addRejectedPoint,
9293 ] = useRejectedIncomingPointTransfers();
94+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
9395
9496 const maybeOutgoingPoints = useMemo(
9597 () =>
@@ -187,6 +189,13 @@ export default function Points() {
187189 const displayEmptyState =
188190 !loading && incomingPoints.length === 0 && allPoints.length === 0;
189191
192+ const starReleasing = starReleaseDetails
193+ .map(s => s.total > 0)
194+ .getOrElse(false);
195+
196+ useEffect(() => {
197+ syncStarReleaseDetails();
198+ }, [syncStarReleaseDetails]);
190199 // sync display details for known points
191200 useSyncKnownPoints([
192201 ...ownedPoints,
@@ -208,6 +217,11 @@ export default function Points() {
208217 push,
209218 ]);
210219
220+ const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
221+ names.STAR_RELEASE,
222+ push,
223+ ]);
224+
211225 if (
212226 loading ||
213227 (allPoints.length === 1 &&
@@ -334,6 +348,18 @@ export default function Points() {
334348 <Grid.Divider />
335349 </>
336350 )}
351+ {starReleasing && (
352+ <>
353+ <Grid.Divider />
354+ <Grid.Item
355+ full
356+ as={ForwardButton}
357+ detail="You have points being released"
358+ onClick={goStarRelease}>
359+ View Star Release
360+ </Grid.Item>
361+ </>
362+ )}
337363 <Grid.Item
338364 full
339365 as={ForwardButton}
src/views/Release.jsnew file+75
@@ -0,0 +1,75 @@
1+import React, { useState, useCallback } from 'react';
2+import { H4, Grid } from 'indigo-react';
3+import { Just } from 'folktale/maybe';
4+
5+import * as need from 'lib/need';
6+
7+import { useHistory } from 'store/history';
8+import { useWallet } from 'store/wallet';
9+import { usePointCursor } from 'store/pointCursor';
10+
11+import View from 'components/View';
12+import Tabs from 'components/Tabs';
13+import Crumbs from 'components/Crumbs';
14+
15+import Active from 'views/Release/Active';
16+import Locked from 'views/Release/Locked';
17+
18+const NAMES = {
19+ ACTIVE: 'ACTIVE',
20+ LOCKED: 'LOCKED',
21+};
22+
23+const VIEWS = {
24+ [NAMES.ACTIVE]: Active,
25+ [NAMES.LOCKED]: Locked,
26+};
27+const OPTIONS = [
28+ { text: 'Active', value: NAMES.ACTIVE },
29+ { text: 'Locked', value: NAMES.LOCKED },
30+];
31+export default function ReleaseView() {
32+ const { pop, push, names } = useHistory();
33+ const { wallet } = useWallet();
34+
35+ const { setPointCursor } = usePointCursor();
36+
37+ const goPoint = useCallback(
38+ point => {
39+ setPointCursor(Just(point));
40+
41+ push(names.POINT);
42+ },
43+ [push, names, setPointCursor]
44+ );
45+ // inputs
46+ const [currentTab, setCurrentTab] = useState(NAMES.ACTIVE);
47+
48+ const address = need.addressFromWallet(wallet);
49+
50+ return (
51+ <View pop={pop} inset>
52+ <Grid>
53+ <Grid.Item full as={Crumbs} />
54+ <Grid.Item full as={H4} className="mt4">
55+ {address}
56+ </Grid.Item>
57+
58+ <Grid.Item
59+ full
60+ as={Tabs}
61+ className="mt1"
62+ // Tabs
63+ views={VIEWS}
64+ options={OPTIONS}
65+ currentTab={currentTab}
66+ onTabChange={setCurrentTab}
67+ // Tab extra
68+ goPoint={goPoint}
69+ //
70+ //
71+ />
72+ </Grid>
73+ </View>
74+ );
75+}
src/views/Release/Active.jsnew file+94
@@ -0,0 +1,94 @@
1+import React, { useCallback } from 'react';
2+import { Grid } from 'indigo-react';
3+import cn from 'classnames';
4+import * as ob from 'urbit-ob';
5+
6+import Sigil from 'components/Sigil';
7+
8+import { usePointCache } from 'store/pointCache';
9+import { useWallet } from 'store/wallet';
10+
11+import * as need from 'lib/need';
12+import usePermissionsForPoint from 'lib/usePermissionsForPoint';
13+import { buildKeyType } from 'lib/point';
14+
15+function ActiveViewRow({ point, goPoint }) {
16+ const { wallet } = useWallet();
17+ const address = need.addressFromWallet(wallet);
18+ const permissions = usePermissionsForPoint(address, point);
19+
20+ const keyType = buildKeyType(permissions);
21+ const patp = ob.patp(point);
22+ const sigilSize = 50;
23+
24+ const onClick = useCallback(() => goPoint(point), [point, goPoint]);
25+ return (
26+ <>
27+ <Grid.Item className="flex-row align-center" cols={[1, 3]}>
28+ {patp}{' '}
29+ </Grid.Item>
30+ <Grid.Item className="flex-row align-center" cols={[3, 6]}>
31+ <div
32+ style={{
33+ display: 'inline-block',
34+ height: `${sigilSize}px`,
35+ width: `${sigilSize}px`,
36+ }}>
37+ <Sigil patp={patp} size={25} colors={['#FFFFFF', '#000000']} />
38+ </div>
39+ </Grid.Item>
40+ <Grid.Item className="flex-center" cols={[6, 10]}>
41+ {keyType}
42+ </Grid.Item>
43+ <Grid.Item
44+ onClick={onClick}
45+ cols={[10, 13]}
46+ className="flex-row-r align-center">
47+ ->
48+ </Grid.Item>
49+ <Grid.Divider />
50+ </>
51+ );
52+}
53+
54+export default function ActiveView({ className, goPoint }) {
55+ // const points = [2560, 2561, 2562];
56+ const { controlledPoints } = usePointCache();
57+ console.log(controlledPoints);
58+ const points = controlledPoints
59+ .matchWith({
60+ Nothing: () => [],
61+ Just: ps =>
62+ ps.value.matchWith({
63+ Error: () => null,
64+ Ok: ({ value }) => {
65+ return [
66+ ...value.ownedPoints,
67+ ...value.votingPoints,
68+ ...value.managingPoints,
69+ ...value.spawningPoints,
70+ ];
71+ },
72+ }),
73+ })
74+ .filter(Boolean)
75+ .filter(p => 255 < p && p < 65536);
76+
77+ return (
78+ <Grid full className={cn('f5', className)}>
79+ <Grid.Item cols={[1, 6]} third={1}>
80+ Point
81+ </Grid.Item>
82+ <Grid.Item className="flex-center" cols={[6, 10]}>
83+ Key Type
84+ </Grid.Item>
85+ <Grid.Divider />
86+ {points.map(point => (
87+ <ActiveViewRow
88+ key={point}
89+ goPoint={goPoint}
90+ point={point}></ActiveViewRow>
91+ ))}
92+ </Grid>
93+ );
94+}
src/views/Release/Locked.jsnew file+143
@@ -0,0 +1,143 @@
1+import React, { useCallback, useEffect, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
4+
5+import * as need from 'lib/need';
6+import useEthereumTransaction from 'lib/useEthereumTransaction';
7+import pluralize from 'lib/pluralize';
8+
9+import { useWallet } from 'store/wallet';
10+import { usePointCache } from 'store/pointCache';
11+import { useStarReleaseCache } from 'store/starRelease';
12+
13+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
14+
15+import { NumberInput, AddressInput } from 'form/Inputs';
16+import BridgeForm from 'form/BridgeForm';
17+import FormError from 'form/FormError';
18+import { composeValidator, buildNumberValidator } from 'form/validators';
19+import { convertToNumber } from 'form/formatters';
20+
21+function AddressIndicator({ isOwn, className }) {
22+ return isOwn ? (
23+ <div className={cn('green2', className)}>✓ Current Address</div>
24+ ) : (
25+ <div className={cn('red4', className)}>! Different address</div>
26+ );
27+}
28+
29+function useWithdrawStars() {
30+ const { syncControlledPoints } = usePointCache();
31+
32+ const { syncStarReleaseDetails, withdraw } = useStarReleaseCache();
33+ return useEthereumTransaction(
34+ useCallback((to, amount) => withdraw(amount, to)),
35+ useCallback(
36+ () => Promise.all([syncControlledPoints(), syncStarReleaseDetails()]),
37+ [syncStarReleaseDetails, syncControlledPoints]
38+ )
39+ );
40+}
41+
42+export default function LockedView({ className }) {
43+ const { wallet } = useWallet();
44+ const address = need.addressFromWallet(wallet);
45+ const { bind, construct, unconstruct, inputsLocked } = useWithdrawStars();
46+
47+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
48+
49+ useEffect(() => {
50+ syncStarReleaseDetails();
51+ }, [syncStarReleaseDetails]);
52+
53+ const onValues = useCallback(
54+ ({ valid, values }) => {
55+ if (valid) {
56+ construct(values.address, convertToNumber(values.numStars));
57+ } else {
58+ unconstruct();
59+ }
60+ },
61+ [construct, unconstruct]
62+ );
63+
64+ const available = starReleaseDetails
65+ .map(b => b.available - b.withdrawn)
66+ .getOrElse(0);
67+ const total = starReleaseDetails.map(b => b.total).getOrElse(0);
68+ const withdrawn = starReleaseDetails.map(b => b.withdrawn).getOrElse(0);
69+
70+ const validate = useMemo(
71+ () =>
72+ composeValidator({
73+ numStars: buildNumberValidator(0, available + 1),
74+ }),
75+ [available]
76+ );
77+
78+ const initialValues = { address };
79+
80+ return (
81+ <Grid gap={6} className={cn('mt4', className)} full>
82+ <BridgeForm
83+ initialValues={initialValues}
84+ onValues={onValues}
85+ validate={validate}>
86+ {({ values, reset }) => (
87+ <>
88+ <Grid.Item full className="f5">
89+ You have{' '}
90+ <span
91+ className={cn({
92+ green2: available > 0,
93+ red4: available === 0,
94+ })}>
95+ {available}
96+ </span>
97+ {pluralize(available, ' star ', ' stars ')}
98+ available to withdraw
99+ <br />
100+ You have <span className="green2">{total - withdrawn}</span>
101+ {pluralize(available, ' star ', ' stars ')}
102+ total
103+ </Grid.Item>
104+ <Grid.Item full className="f5" cols={[1, 10]} as={Flex} col>
105+ <Flex.Item
106+ name="address"
107+ as={AddressInput}
108+ label="Withdraw To"
109+ disabled={inputsLocked}
110+ />
111+ <Flex.Item
112+ as={AddressIndicator}
113+ isOwn={address === values.address}
114+ />
115+ </Grid.Item>
116+
117+ <Grid.Item
118+ cols={[1, 5]}
119+ label="Stars"
120+ style={{ minWidth: '0' }}
121+ as={NumberInput}
122+ placeholder="#"
123+ name="numStars"
124+ disabled={inputsLocked}
125+ />
126+ <Grid.Item as={Flex} col cols={[5, 8]}>
127+ <Flex.Item className="f6 lh-tall">&nbsp;</Flex.Item>
128+ <Flex.Item className="f3">/ {available}</Flex.Item>
129+ </Grid.Item>
130+ <Grid.Item full as={FormError} />
131+ <Grid.Item
132+ full
133+ as={InlineEthereumTransaction}
134+ {...bind}
135+ onReturn={() => reset()}
136+ label="Withdraw Stars"
137+ />
138+ </>
139+ )}
140+ </BridgeForm>
141+ </Grid>
142+ );
143+}