on master help sign in
Merge pull request #335 from urbit/linear-release

Linear and conditional star release contract support

commita2f3a5d8309f7dca8b860881ea55752a5b5bc646 authorFang <git@fang.io> dateThu 19 Dec 2019, 18:02 (7 years ago) parent7a1be541074b35
18 files changed+659−36
package-lock.json+21 src/components/InlineEthereumTransaction.js+1−1 src/components/Passport.js+1−14 src/form/validators.js+4−2 src/indigo-react/indigo.css+13−1 src/lib/contracts.js+6 src/lib/point.js+13 src/lib/routeNames.js+1 src/lib/router.js+2 src/lib/starRelease.js+97 src/lib/validators.js+2 src/store/Provider.js+2 src/store/lib/useStarReleaseStore.js+63 src/store/starRelease.js+20 src/views/Points.js+52−18 src/views/Release.js+81 src/views/Release/Active.js+101 src/views/Release/Locked.js+179
package-lock.json+21
@@ -2592,10 +2592,31 @@
25922592 },
25932593 "azimuth-js": {
25942594 "version": "0.17.0",
2595+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.17.0.tgz",
2596+ "integrity": "sha512-g1/4gfhMHrj6NjCp96c5FBS202nIBnGiXcGTl9i0kh3rB6AE85rd0Un+z+qOhrVJbUdJF0axTvw3xsB5o5u+7Q==",
25952597 "requires": {
25962598 "azimuth-solidity": "1.2.1",
25972599 "ethereumjs-util": "^5.2.0",
25982600 "urbit-ob": "4.1.4"
2601+ },
2602+ "dependencies": {
2603+ "azimuth-solidity": {
2604+ "version": "1.2.1",
2605+ "resolved": "https://registry.npmjs.org/azimuth-solidity/-/azimuth-solidity-1.2.1.tgz",
2606+ "integrity": "sha512-T2tnvxKQ2tbmnCt0qSH2tjCiTee+A1XdHCFimU48CqdUimuYR8Mmj2mRe9GHqY0x6C8TX0ifwJL5pnR996sJLw==",
2607+ "requires": {
2608+ "babel-polyfill": "^6.26.0",
2609+ "babel-register": "^6.26.0",
2610+ "openzeppelin-solidity": "1.12.0",
2611+ "truffle": "4.1.11"
2612+ },
2613+ "dependencies": {
2614+ "openzeppelin-solidity": {
2615+ "version": "1.12.0",
2616+ "bundled": true
2617+ }
2618+ }
2619+ }
25992620 }
26002621 },
26012622 "azimuth-solidity": {
src/components/InlineEthereumTransaction.js+1−1
@@ -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
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+13−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.
@@ -3388,6 +3397,9 @@ caption,
33883397.h-auto {
33893398 height: auto; }
33903399
3400+.h-full {
3401+ height: 100%; }
3402+
33913403@media only screen and (min-width: 550px) {
33923404 .h-auto-ns {
33933405 height: auto; } }
src/lib/contracts.js+6
@@ -4,18 +4,24 @@ const CONTRACT_ADDRESSES = {
44 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
55 polls: '0x935452c45eda2958976a429c9733c40302995efd',
66 delegatedSending: '0xb71c0b6cee1bcae56dfe95cd9d3e41ddd7eafc43',
7+ linearSR: '0xa32c3019b256880A8c5458379188eea6F3934e2F',
8+ conditionalSR: '0x35EB3B102d9C1B69Ac1469C1B1FE1799850CD3EB',
79 },
810 ROPSTEN: {
911 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
1012 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
1113 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971',
1214 delegatedSending: '0x3e8ca510354bc2fdbbd6150252d93105c9c27bbe',
15+ linearSR: '0x1f8edd031ee414740aedb39b84fb8f2f66ca422f',
16+ conditionalSR: '0x1f8edd031ee414740aedb39b84fb8f2f66ca422f',
1317 },
1418 MAINNET: {
1519 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
1620 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
1721 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4',
1822 delegatedSending: '0xF7908Ab1F1e352F83c5ebc75051c0565AEaea5FB',
23+ linearSR: '0x86cd9cd0992f04231751e3761de45cecea5d1801',
24+ conditionalSR: '0x8c241098c3d3498fe1261421633fd57986d74aea',
1925 },
2026};
2127
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
@@ -4,6 +4,7 @@ import Admin from 'views/Admin';
44import Senate from 'views/Senate';
55import CreateGalaxy from 'views/CreateGalaxy';
66import IssueChild from 'views/IssueChild';
7+import StarRelease from 'views/Release';
78
89import Point from 'views/Point';
910import Points from 'views/Points';
@@ -32,4 +33,5 @@ export const ROUTES = {
3233 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
3334 // TODO: replace this with deep link to AdminCancelTransfer
3435 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
36+ [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
3537};
src/lib/starRelease.jsnew file+97
@@ -0,0 +1,97 @@
1+import { linearSR, conditionalSR } from 'azimuth-js';
2+
3+function mapRange(amount, cb) {
4+ return [...Array(amount).keys()].map(cb);
5+}
6+
7+// Preferentially withdraw from conditional contracts and use linear
8+// to mop up the rest
9+export function generateWithdrawTxs(contracts, batchLimits, amount, to) {
10+ const [left, ...csrTxs] = generateConditionalWithdrawTxs(
11+ contracts,
12+ batchLimits,
13+ amount,
14+ to
15+ );
16+ const lsrTxs = generateLinearWithdrawTxs(contracts, left, to);
17+ return [...csrTxs, ...lsrTxs];
18+}
19+
20+/**
21+ * Recurse over batchLimits to generate a valid number of withdraw txs.
22+ * amount may exceed the number of withdraw txs available in the consolidated SR contract.
23+ *
24+ * @param {object} contracts - The azimuth contracts object
25+ * @param {array} batchLimits - An array of the maximum withdraw limit for each batch
26+ * @param {amount} amount - The number of txs to be generated
27+ * @param {string} to - The addresss to withdraw to
28+ * @return {array} An array with the number of transactions missing as the head
29+ * (i.e. requested amount exceeded available), and the generated transactions
30+ * as the rest of the list
31+ */
32+function generateConditionalWithdrawTxs(
33+ contracts,
34+ batchLimits,
35+ amount,
36+ to,
37+ offset = 0
38+) {
39+ // Successfully withdrawn all requested, none missing
40+ if (amount <= 0) {
41+ return [0];
42+ }
43+ // Return missing amount in head of list
44+ if (batchLimits.length === 0) {
45+ return [amount];
46+ }
47+ const [next, ...rest] = batchLimits;
48+ const currentAmount = Math.min(amount, next);
49+ return [
50+ ...generateConditionalWithdrawTxs(
51+ contracts,
52+ rest,
53+ amount - currentAmount,
54+ to,
55+ offset + 1
56+ ),
57+ ...mapRange(currentAmount, () =>
58+ conditionalSR.withdrawTo(contracts, offset, to)
59+ ),
60+ ];
61+}
62+
63+function generateLinearWithdrawTxs(contracts, amount, to) {
64+ return mapRange(amount, () => linearSR.withdrawTo(contracts, to));
65+}
66+
67+export async function getConditional(contracts, address) {
68+ const [commitment, batches] = await Promise.all([
69+ conditionalSR.getCommitment(contracts, address),
70+ conditionalSR.getBatches(contracts, address),
71+ ]);
72+
73+ const { total } = commitment;
74+ const withdrawLimits = await Promise.all(
75+ batches.map((_, idx) =>
76+ conditionalSR.getWithdrawLimit(contracts, address, idx)
77+ )
78+ );
79+ const available = withdrawLimits.reduce((acc, val) => acc + val, 0);
80+
81+ const withdrawnAmounts = await conditionalSR.getWithdrawn(contracts, address);
82+
83+ const withdrawn = withdrawnAmounts.reduce((acc, val) => acc + val, 0);
84+ const batchLimits = withdrawLimits.map(
85+ (limit, idx) => limit - withdrawnAmounts[idx]
86+ );
87+
88+ return { total, available, withdrawn, batchLimits };
89+}
90+
91+export async function getLinear(contracts, address) {
92+ const { amount, withdrawn } = await linearSR.getBatch(contracts, address);
93+
94+ const available = // Contract errors on withdrawing from a user that does not have any
95+ amount > 0 ? await linearSR.getWithdrawLimit(contracts, address) : 0;
96+ return { available, withdrawn, total: amount };
97+}
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/Provider.js+2
@@ -5,6 +5,7 @@ import { OnlineProvider } from './online';
55import { NetworkProvider } from './network';
66import { WalletProvider } from './wallet';
77import { PointCursorProvider } from './pointCursor';
8+import { StarReleaseCacheProvider } from './starRelease';
89import { PointCacheProvider } from './pointCache';
910import { TxnCursorProvider } from './txnCursor';
1011import { MediaQueryProvider } from 'lib/MediaQuery';
@@ -19,4 +20,5 @@ export default nest([
1920 PointCacheProvider,
2021 TxnCursorProvider,
2122 MediaQueryProvider,
23+ StarReleaseCacheProvider,
2224]);
src/store/lib/useStarReleaseStore.jsnew file+63
@@ -0,0 +1,63 @@
1+import { useState, useCallback } from 'react';
2+import { Nothing, Just } from 'folktale/maybe';
3+
4+import { useNetwork } from 'store/network';
5+import { useWallet } from 'store/wallet';
6+import {
7+ generateWithdrawTxs,
8+ getLinear,
9+ getConditional,
10+} from 'lib/starRelease';
11+
12+export default function useStarRelease() {
13+ const { contracts } = useNetwork();
14+ const { wallet } = useWallet();
15+
16+ const [starReleaseDetails, _setDetails] = useState(Nothing());
17+
18+ const [batchLimits, setBatchLimits] = useState([]);
19+
20+ const syncStarReleaseDetails = useCallback(async () => {
21+ const _wallet = wallet.getOrElse(null);
22+ const _contracts = contracts.getOrElse(null);
23+ if (!(_contracts && _wallet)) {
24+ return;
25+ }
26+ _setDetails(Nothing());
27+
28+ const address = _wallet.address;
29+
30+ try {
31+ const [linear, conditional] = await Promise.all([
32+ getLinear(_contracts, address),
33+ getConditional(_contracts, address),
34+ ]);
35+
36+ const keys = Object.keys(linear);
37+ const result = keys.reduce(
38+ (acc, key) => ({ ...acc, [key]: linear[key] + conditional[key] }),
39+ {}
40+ );
41+ setBatchLimits(conditional.batchLimits);
42+
43+ _setDetails(Just(result));
44+ } catch (e) {
45+ _setDetails(Nothing());
46+ console.error(e);
47+ }
48+ }, [contracts, wallet]);
49+
50+ const withdraw = useCallback(
51+ (amount, to) => {
52+ const _wallet = wallet.getOrElse(null);
53+ const _contracts = contracts.getOrElse(null);
54+ if (!(_contracts && _wallet)) {
55+ return [];
56+ }
57+ return generateWithdrawTxs(_contracts, batchLimits, amount, to);
58+ },
59+ [batchLimits, contracts, wallet]
60+ );
61+
62+ return { starReleaseDetails, syncStarReleaseDetails, withdraw };
63+}
src/store/starRelease.jsnew file+20
@@ -0,0 +1,20 @@
1+import React, { createContext, useContext } from 'react';
2+
3+import useStarReleaseStore from './lib/useStarReleaseStore';
4+
5+export const StarReleaseCacheContext = createContext(null);
6+
7+export function StarReleaseCacheProvider({ children }) {
8+ const cache = useStarReleaseStore();
9+
10+ return (
11+ <StarReleaseCacheContext.Provider value={cache}>
12+ {children}
13+ </StarReleaseCacheContext.Provider>
14+ );
15+}
16+
17+// Hook version
18+export function useStarReleaseCache() {
19+ return useContext(StarReleaseCacheContext);
20+}
src/views/Points.js+52−18
@@ -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';
@@ -91,6 +92,7 @@ export default function Points() {
9192 rejectedPoints,
9293 addRejectedPoint,
9394 ] = useRejectedIncomingPointTransfers();
95+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
9496
9597 const maybeOutgoingPoints = useMemo(
9698 () =>
@@ -124,7 +126,10 @@ export default function Points() {
124126 // if there are any pending transfers, incoming or outgoing, stay on this
125127 // page, because those can only be completed/cancelled here.
126128 useEffect(() => {
127− if (Nothing.hasInstance(maybeOutgoingPoints)) {
129+ if (
130+ Nothing.hasInstance(maybeOutgoingPoints) ||
131+ Nothing.hasInstance(starReleaseDetails)
132+ ) {
128133 return;
129134 }
130135 controlledPoints.matchWith({
@@ -145,7 +150,8 @@ export default function Points() {
145150 if (
146151 all.length === 1 &&
147152 incoming.length === 0 &&
148− maybeOutgoingPoints.value.length === 0
153+ maybeOutgoingPoints.value.length === 0 &&
154+ starReleaseDetails.value.total === 0
149155 ) {
150156 setPointCursor(Just(all[0]));
151157 popAndPush(names.POINT);
@@ -161,6 +167,7 @@ export default function Points() {
161167 setPointCursor,
162168 popAndPush,
163169 names,
170+ starReleaseDetails,
164171 ]);
165172
166173 const address = need.addressFromWallet(wallet);
@@ -188,6 +195,13 @@ export default function Points() {
188195 const displayEmptyState =
189196 !loading && incomingPoints.length === 0 && allPoints.length === 0;
190197
198+ const starReleasing = starReleaseDetails
199+ .map(s => s.total > 0)
200+ .getOrElse(false);
201+
202+ useEffect(() => {
203+ syncStarReleaseDetails();
204+ }, [syncStarReleaseDetails]);
191205 // sync display details for known points
192206 useSyncKnownPoints([
193207 ...ownedPoints,
@@ -204,11 +218,17 @@ export default function Points() {
204218 push,
205219 ]);
206220
221+ const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
222+ names.STAR_RELEASE,
223+ push,
224+ ]);
225+
207226 if (
208227 loading ||
209228 (allPoints.length === 1 &&
210229 incomingPoints.length === 0 &&
211− outgoingPoints.length === 0)
230+ outgoingPoints.length === 0 &&
231+ !starReleasing)
212232 ) {
213233 return (
214234 <View inset pop={pop}>
@@ -316,21 +336,35 @@ export default function Points() {
316336 </Grid.Item>
317337 )}
318338
319− {isEclipticOwner && (
320− <Footer>
321− <Grid>
322− <Grid.Divider />
323− <Grid.Item
324− full
325− as={ForwardButton}
326− detail="You have the authority to create a new Galaxy."
327− onClick={goCreateGalaxy}>
328− Create a galaxy
329− </Grid.Item>
330− <Grid.Divider />
331− </Grid>
332− </Footer>
333− )}
339+ <Footer>
340+ <Grid>
341+ <Grid.Divider />
342+ {isEclipticOwner && (
343+ <>
344+ <Grid.Item
345+ full
346+ as={ForwardButton}
347+ detail="You have the authority to create a new Galaxy."
348+ onClick={goCreateGalaxy}>
349+ Create a galaxy
350+ </Grid.Item>
351+ <Grid.Divider />
352+ </>
353+ )}
354+ {starReleasing && (
355+ <>
356+ <Grid.Item
357+ full
358+ as={ForwardButton}
359+ detail="You have points being released"
360+ onClick={goStarRelease}>
361+ View Star Release
362+ </Grid.Item>
363+ <Grid.Divider />
364+ </>
365+ )}
366+ </Grid>
367+ </Footer>
334368 </Grid>
335369 </View>
336370 );
src/views/Release.jsnew file+81
@@ -0,0 +1,81 @@
1+import React, { useState, useCallback } from 'react';
2+import { H1, 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+import CopiableAddress from 'components/CopiableAddress';
15+
16+import Active from 'views/Release/Active';
17+import Locked from 'views/Release/Locked';
18+
19+const NAMES = {
20+ ACTIVE: 'ACTIVE',
21+ LOCKED: 'LOCKED',
22+};
23+
24+const VIEWS = {
25+ [NAMES.ACTIVE]: Active,
26+ [NAMES.LOCKED]: Locked,
27+};
28+const OPTIONS = [
29+ { text: 'Active', value: NAMES.ACTIVE },
30+ { text: 'Locked', value: NAMES.LOCKED },
31+];
32+export default function ReleaseView() {
33+ const { pop, push, names } = useHistory();
34+ const { wallet } = useWallet();
35+
36+ const { setPointCursor } = usePointCursor();
37+
38+ const goPoint = useCallback(
39+ point => {
40+ setPointCursor(Just(point));
41+
42+ push(names.POINT);
43+ },
44+ [push, names, setPointCursor]
45+ );
46+ // inputs
47+ const [currentTab, setCurrentTab] = useState(NAMES.ACTIVE);
48+
49+ const goActive = useCallback(() => {
50+ setCurrentTab(NAMES.ACTIVE);
51+ }, [setCurrentTab]);
52+
53+ const address = need.addressFromWallet(wallet);
54+
55+ return (
56+ <View pop={pop} inset>
57+ <Grid>
58+ <Grid.Item full as={Crumbs} />
59+ <Grid.Item full as={H1} className="f6 mono gray4 us-none pointer">
60+ <CopiableAddress text={address}>{address}</CopiableAddress>
61+ </Grid.Item>
62+
63+ <Grid.Item
64+ full
65+ as={Tabs}
66+ className="mt1"
67+ // Tabs
68+ views={VIEWS}
69+ options={OPTIONS}
70+ currentTab={currentTab}
71+ onTabChange={setCurrentTab}
72+ // Tab extra
73+ goPoint={goPoint}
74+ goActive={goActive}
75+ //
76+ //
77+ />
78+ </Grid>
79+ </View>
80+ );
81+}
src/views/Release/Active.jsnew file+101
@@ -0,0 +1,101 @@
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 { useSyncOwnedPoints } from 'lib/useSyncPoints';
14+import { buildKeyType } from 'lib/point';
15+
16+function ActiveViewRow({ point, goPoint }) {
17+ const { wallet } = useWallet();
18+ const address = need.addressFromWallet(wallet);
19+ useSyncOwnedPoints([point]);
20+ const permissions = usePermissionsForPoint(address, point);
21+
22+ const keyType = buildKeyType(permissions);
23+ const patp = ob.patp(point);
24+ const sigilSize = 50;
25+
26+ const onClick = useCallback(() => goPoint(point), [point, goPoint]);
27+ return (
28+ <>
29+ <Grid.Item className="flex-row align-center" cols={[1, 3]}>
30+ {patp}{' '}
31+ </Grid.Item>
32+ <Grid.Item className="flex-row align-center" cols={[3, 6]}>
33+ <div
34+ style={{
35+ display: 'inline-block',
36+ height: `${sigilSize}px`,
37+ width: `${sigilSize}px`,
38+ }}>
39+ <Sigil patp={patp} size={25} colors={['#FFFFFF', '#000000']} />
40+ </div>
41+ </Grid.Item>
42+ <Grid.Item className="flex-row align-center" cols={[6, 10]}>
43+ {keyType}
44+ </Grid.Item>
45+ <Grid.Item
46+ onClick={onClick}
47+ cols={[10, 13]}
48+ className="flex-row-r align-center mr2">
49+ ->
50+ </Grid.Item>
51+ <Grid.Divider />
52+ </>
53+ );
54+}
55+
56+export default function ActiveView({ className, goPoint }) {
57+ // const points = [2560, 2561, 2562];
58+ const { controlledPoints } = usePointCache();
59+ console.log(controlledPoints);
60+ const points = controlledPoints
61+ .matchWith({
62+ Nothing: () => [],
63+ Just: ps =>
64+ ps.value.matchWith({
65+ Error: () => null,
66+ Ok: ({ value }) => {
67+ return [
68+ ...value.ownedPoints,
69+ ...value.votingPoints,
70+ ...value.managingPoints,
71+ ...value.spawningPoints,
72+ ];
73+ },
74+ }),
75+ })
76+ .filter(Boolean)
77+ .filter(p => 255 < p && p < 65536);
78+
79+ return (
80+ <Grid full className={cn('f5', className)}>
81+ <Grid.Item className="pv3 " cols={[1, 6]} third={1}>
82+ Point
83+ </Grid.Item>
84+ <Grid.Item className="pv3 " cols={[6, 10]}>
85+ Key Type
86+ </Grid.Item>
87+ <Grid.Divider />
88+ {points.length === 0 && (
89+ <Grid.Item full className="flex-center pv8 f5 gray4">
90+ You have no active stars
91+ </Grid.Item>
92+ )}
93+ {points.map(point => (
94+ <ActiveViewRow
95+ key={point}
96+ goPoint={goPoint}
97+ point={point}></ActiveViewRow>
98+ ))}
99+ </Grid>
100+ );
101+}
src/views/Release/Locked.jsnew file+179
@@ -0,0 +1,179 @@
1+import React, { useCallback, useEffect, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
4+import { Just } from 'folktale/maybe';
5+
6+import * as need from 'lib/need';
7+import useEthereumTransaction from 'lib/useEthereumTransaction';
8+import pluralize from 'lib/pluralize';
9+
10+import { useWallet } from 'store/wallet';
11+import { usePointCache } from 'store/pointCache';
12+import { useStarReleaseCache } from 'store/starRelease';
13+
14+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
15+import { matchBlinky } from 'components/Blinky';
16+
17+import { NumberInput, AddressInput } from 'form/Inputs';
18+import BridgeForm from 'form/BridgeForm';
19+import FormError from 'form/FormError';
20+import { composeValidator, buildNumberValidator } from 'form/validators';
21+import { convertToNumber } from 'form/formatters';
22+
23+function MatchPluralizeStars({ quantity }) {
24+ const _quantity = quantity.getOrElse(0);
25+ console.log(quantity);
26+ return (
27+ <>
28+ {' '}
29+ <span
30+ className={cn({
31+ green2: _quantity > 0,
32+ red4: Just.hasInstance(quantity) && _quantity === 0,
33+ })}>
34+ {matchBlinky(quantity)}
35+ </span>
36+ {pluralize(_quantity, ' star ', ' stars ')}
37+ </>
38+ );
39+}
40+
41+function AddressIndicator({ isOwn, className }) {
42+ return isOwn ? (
43+ <div className={cn('green2', className)}>✓ Current Address</div>
44+ ) : (
45+ <div className={cn('red4', className)}>! Different address</div>
46+ );
47+}
48+
49+function useWithdrawStars() {
50+ const { syncControlledPoints } = usePointCache();
51+
52+ const { syncStarReleaseDetails, withdraw } = useStarReleaseCache();
53+ return useEthereumTransaction(
54+ useCallback((to, amount) => withdraw(amount, to), [withdraw]),
55+ useCallback(
56+ () => Promise.all([syncControlledPoints(), syncStarReleaseDetails()]),
57+ [syncStarReleaseDetails, syncControlledPoints]
58+ )
59+ );
60+}
61+
62+export default function Locked({ className, goActive }) {
63+ const { wallet } = useWallet();
64+ const address = need.addressFromWallet(wallet);
65+ const {
66+ bind,
67+ construct,
68+ unconstruct,
69+ inputsLocked,
70+ completed,
71+ reset,
72+ } = useWithdrawStars();
73+
74+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
75+
76+ useEffect(() => {
77+ syncStarReleaseDetails();
78+ }, [syncStarReleaseDetails]);
79+
80+ const onValues = useCallback(
81+ ({ valid, values }) => {
82+ if (valid) {
83+ construct(values.address, convertToNumber(values.numStars));
84+ } else {
85+ unconstruct();
86+ }
87+ },
88+ [construct, unconstruct]
89+ );
90+
91+ const canWithdraw = starReleaseDetails.map(b => b.available - b.withdrawn);
92+ const total = starReleaseDetails.map(b => b.total);
93+
94+ const validate = useMemo(
95+ () =>
96+ composeValidator(
97+ {
98+ numStars: buildNumberValidator(0, canWithdraw + 1),
99+ },
100+ (values, errors) => (inputsLocked ? false : errors)
101+ ),
102+ [canWithdraw, inputsLocked]
103+ );
104+
105+ const initialValues = { address };
106+
107+ return (
108+ <Grid gap={completed ? 0 : 6} className={cn('mt4', className)} full>
109+ <BridgeForm
110+ initialValues={initialValues}
111+ onValues={onValues}
112+ validate={validate}>
113+ {({ values, form }) => (
114+ <>
115+ {!completed && (
116+ <>
117+ <Grid.Item full className="f5">
118+ You have <MatchPluralizeStars quantity={canWithdraw} />
119+ available to withdraw
120+ <br />
121+ You have
122+ <MatchPluralizeStars quantity={total} />
123+ total
124+ </Grid.Item>
125+ <Grid.Item
126+ cols={[1, 4]}
127+ label="Stars"
128+ as={NumberInput}
129+ placeholder="#"
130+ name="numStars"
131+ disabled={inputsLocked}
132+ accessory={
133+ <div className="flex-center h-full">
134+ / &nbsp;{matchBlinky(canWithdraw)}
135+ </div>
136+ }
137+ />
138+ <Grid.Item full className="f5" cols={[1, 13]} as={Flex} col>
139+ <Flex.Item
140+ name="address"
141+ as={AddressInput}
142+ label="Withdraw To"
143+ disabled={inputsLocked}
144+ />
145+ <Flex.Item
146+ as={AddressIndicator}
147+ isOwn={address === values.address}
148+ />
149+ </Grid.Item>
150+ </>
151+ )}
152+
153+ <Grid.Item full as={FormError} />
154+ {completed && (
155+ <>
156+ <Grid.Item full className="pv4">
157+ {values.numStars}{' '}
158+ {pluralize(values.numStars, 'star has', 'stars have')} been
159+ withdrawn
160+ </Grid.Item>
161+ <Grid.Divider />
162+ </>
163+ )}
164+ <Grid.Item
165+ full
166+ as={InlineEthereumTransaction}
167+ {...bind}
168+ label="Withdraw Stars"
169+ onReturn={() => {
170+ reset();
171+ form.reset();
172+ }}
173+ />
174+ </>
175+ )}
176+ </BridgeForm>
177+ </Grid>
178+ );
179+}