on master help sign in
release: support approving transfer of lockup
5ccd972fang
package.json+1−1
@@ -22,7 +22,7 @@
22 "@welldone-software/why-did-you-render": "^3.2.3",
22 "@welldone-software/why-did-you-render": "^3.2.3",
23 "async-retry": "^1.2.3",
23 "async-retry": "^1.2.3",
24 "azimuth-js": "^0.19.0",
24 "azimuth-js": "^0.19.0",
25− "azimuth-solidity": "^1.2.1",
25+ "azimuth-solidity": "^1.2.2",
26 "bip32": "^1.0.2",
26 "bip32": "^1.0.2",
27 "bip39": "^2.5.0",
27 "bip39": "^2.5.0",
28 "bn.js": "^4.11.8",
28 "bn.js": "^4.11.8",
src/lib/starRelease.js+24−2
@@ -64,6 +64,17 @@ function generateLinearWithdrawTxs(contracts, amount, to) {
64 return mapRange(amount, () => linearSR.withdrawTo(contracts, to));
64 return mapRange(amount, () => linearSR.withdrawTo(contracts, to));
65}
65}
66
66
67+export async function getLockupKind(contracts, address) {
68+ const conditional = await conditionalSR.getCommitment(contracts, address);
69+ const linear = await linearSR.getBatch(contracts, address);
70+ const haveConditional = conditional.total > 0;
71+ const haveLinear = linear.amount > 0;
72+ if (haveConditional && !haveLinear) return 'conditional';
73+ if (!haveConditional && haveLinear) return 'linear';
74+ if (haveConditional && haveLinear) return 'both';
75+ return 'none';
76+}
77+
67export async function getConditional(contracts, address) {
78export async function getConditional(contracts, address) {
68 const [commitment, batches, remaining] = await Promise.all([
79 const [commitment, batches, remaining] = await Promise.all([
69 conditionalSR.getCommitment(contracts, address),
80 conditionalSR.getCommitment(contracts, address),
@@ -110,7 +121,13 @@ export async function getConditional(contracts, address) {
110 const withdrawnTotal = withdrawn.reduce((acc, val) => acc + val, 0);
121 const withdrawnTotal = withdrawn.reduce((acc, val) => acc + val, 0);
111 const available = batchLimits.reduce((acc, val) => acc + val, 0);
122 const available = batchLimits.reduce((acc, val) => acc + val, 0);
112
123
124+ return {
125+ total,
126+ available,
113− return { total, available, withdrawn: withdrawnTotal, batchLimits };
127+ withdrawn: withdrawnTotal,
128+ batchLimits,
129+ approvedTransferTo: commitment.approvedTransferTo,
130+ };
114}
131}
115
132
116export async function getLinear(contracts, address) {
133export async function getLinear(contracts, address) {
@@ -128,5 +145,10 @@ export async function getLinear(contracts, address) {
128
145
129 const total = Math.min(amount, remaining);
146 const total = Math.min(amount, remaining);
130
147
148+ return {
131− return { available, withdrawn, total };
149+ available,
150+ withdrawn,
151+ total,
152+ approvedTransferTo: batch.approvedTransferTo,
153+ };
132}
154}
src/store/lib/useStarReleaseStore.js+8−1
@@ -7,6 +7,7 @@ import {
7 generateWithdrawTxs,
7 generateWithdrawTxs,
8 getLinear,
8 getLinear,
9 getConditional,
9 getConditional,
10+ getLockupKind,
10} from 'lib/starRelease';
11} from 'lib/starRelease';
11
12
12export default function useStarRelease() {
13export default function useStarRelease() {
@@ -28,9 +29,10 @@ export default function useStarRelease() {
28 const address = _wallet.address;
29 const address = _wallet.address;
29
30
30 try {
31 try {
31− const [linear, conditional] = await Promise.all([
32+ const [linear, conditional, kind] = await Promise.all([
32 getLinear(_contracts, address),
33 getLinear(_contracts, address),
33 getConditional(_contracts, address),
34 getConditional(_contracts, address),
35+ getLockupKind(_contracts, address),
34 ]);
36 ]);
35
37
36 const keys = Object.keys(linear);
38 const keys = Object.keys(linear);
@@ -38,6 +40,11 @@ export default function useStarRelease() {
38 (acc, key) => ({ ...acc, [key]: linear[key] + conditional[key] }),
40 (acc, key) => ({ ...acc, [key]: linear[key] + conditional[key] }),
39 {}
41 {}
40 );
42 );
43+ result.kind = kind;
44+ result.linear = { approvedTransferTo: linear.approvedTransferTo };
45+ result.conditional = {
46+ approvedTransferTo: conditional.approvedTransferTo,
47+ };
41 setBatchLimits(conditional.batchLimits);
48 setBatchLimits(conditional.batchLimits);
42
49
43 _setDetails(Just(result));
50 _setDetails(Just(result));
src/views/Release.js+4
@@ -15,20 +15,24 @@ import CopiableAddress from 'components/CopiableAddress';
15
15
16import Active from 'views/Release/Active';
16import Active from 'views/Release/Active';
17import Locked from 'views/Release/Locked';
17import Locked from 'views/Release/Locked';
18+import Transfer from 'views/Release/Transfer';
18import NavHeader from 'components/NavHeader';
19import NavHeader from 'components/NavHeader';
19
20
20const NAMES = {
21const NAMES = {
21 ACTIVE: 'ACTIVE',
22 ACTIVE: 'ACTIVE',
22 LOCKED: 'LOCKED',
23 LOCKED: 'LOCKED',
24+ TRANSFER: 'TRANSFER',
23};
25};
24
26
25const VIEWS = {
27const VIEWS = {
26 [NAMES.ACTIVE]: Active,
28 [NAMES.ACTIVE]: Active,
27 [NAMES.LOCKED]: Locked,
29 [NAMES.LOCKED]: Locked,
30+ [NAMES.TRANSFER]: Transfer,
28};
31};
29const OPTIONS = [
32const OPTIONS = [
30 { text: 'Active', value: NAMES.ACTIVE },
33 { text: 'Active', value: NAMES.ACTIVE },
31 { text: 'Locked', value: NAMES.LOCKED },
34 { text: 'Locked', value: NAMES.LOCKED },
35+ { text: 'Transfer', value: NAMES.TRANSFER },
32];
36];
33export default function ReleaseView() {
37export default function ReleaseView() {
34 const { pop, push, names } = useHistory();
38 const { pop, push, names } = useHistory();
src/views/Release/Locked.js−1
@@ -22,7 +22,6 @@ import { convertToNumber } from 'form/formatters';
22
22
23function MatchPluralizeStars({ quantity }) {
23function MatchPluralizeStars({ quantity }) {
24 const _quantity = quantity.getOrElse(0);
24 const _quantity = quantity.getOrElse(0);
25− console.log(quantity);
26 return (
25 return (
27 <>
26 <>
28 {' '}
27 {' '}
src/views/Release/Transfer.jsnew file+170
@@ -0,0 +1,170 @@
1+import React, { useCallback, useEffect, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
4+import { conditionalSR, linearSR } from 'azimuth-js';
5+
6+import * as need from 'lib/need';
7+import useEthereumTransaction from 'lib/useEthereumTransaction';
8+import { getLockupKind } from 'lib/starRelease';
9+import { eqAddr, isZeroAddress } from 'lib/wallet';
10+
11+import { useNetwork } from 'store/network';
12+import { useWallet } from 'store/wallet';
13+import { useHistory } from 'store/history';
14+import { useStarReleaseCache } from 'store/starRelease';
15+
16+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
17+import CopiableWithTooltip from 'components/CopiableWithTooltip';
18+import CopiableAddress from 'components/CopiableAddress';
19+import NoticeBox from 'components/NoticeBox';
20+
21+import { AddressInput } from 'form/Inputs';
22+import BridgeForm from 'form/BridgeForm';
23+import FormError from 'form/FormError';
24+import { composeValidator, buildAddressValidator } from 'form/validators';
25+import { isValidAddress } from 'ethereumjs-util';
26+
27+function useTransferLockup(kind) {
28+ const { contracts } = useNetwork();
29+
30+ return useEthereumTransaction(
31+ useCallback(
32+ to => {
33+ const _contracts = need.contracts(contracts);
34+ if (kind === 'conditional') {
35+ return conditionalSR.approveCommitmentTransfer(_contracts, to);
36+ } else {
37+ return linearSR.approveBatchTransfer(_contracts, to);
38+ }
39+ },
40+ [kind, contracts]
41+ ),
42+ () => {}
43+ );
44+}
45+
46+export default function Transfer({ className, goActive }) {
47+ const { wallet } = useWallet();
48+ const { contracts } = useNetwork();
49+ const { pop } = useHistory();
50+ const address = need.addressFromWallet(wallet);
51+
52+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
53+
54+ useEffect(() => {
55+ syncStarReleaseDetails();
56+ }, [syncStarReleaseDetails]);
57+
58+ const kind = useMemo(() => {
59+ return starReleaseDetails.map(a => a.kind).getOrElse('') === 'conditional'
60+ ? 'conditional'
61+ : 'linear';
62+ }, [starReleaseDetails]);
63+
64+ const approvedFor = useMemo(() => {
65+ let approved;
66+ if (kind === 'conditional') {
67+ approved = starReleaseDetails.map(a => a.conditional.approvedTransferTo);
68+ } else {
69+ approved = starReleaseDetails.map(a => a.linear.approvedTransferTo);
70+ }
71+ approved = approved.getOrElse('0x0');
72+ if (isZeroAddress(approved) || eqAddr(approved, address)) {
73+ return false;
74+ } else {
75+ return approved;
76+ }
77+ }, [starReleaseDetails, kind, address]);
78+
79+ const {
80+ bind,
81+ construct,
82+ unconstruct,
83+ inputsLocked,
84+ completed,
85+ } = useTransferLockup(kind);
86+
87+ const onValues = useCallback(
88+ async ({ valid, values }) => {
89+ if (valid) {
90+ construct(values.to);
91+ } else {
92+ unconstruct();
93+ }
94+ },
95+ [construct, unconstruct]
96+ );
97+
98+ const validate = useMemo(
99+ () =>
100+ composeValidator({
101+ to: async to => {
102+ if (!isValidAddress(to)) {
103+ return 'This is not a valid Ethereum address.';
104+ }
105+ const theirs = await getLockupKind(need.contracts(contracts), to);
106+ if (theirs === kind || theirs === 'both') {
107+ return `Target address already owns a ${kind} lockup.`;
108+ }
109+ },
110+ }),
111+ [kind, contracts]
112+ );
113+
114+ return (
115+ <Grid gap={completed ? 0 : 6} className={cn('mt4', className)} full>
116+ <BridgeForm onValues={onValues} validate={validate}>
117+ {({ values, form }) => (
118+ <>
119+ {!completed && (
120+ <>
121+ <Grid.Item full className="f5">
122+ Allow the specified address to take over your {kind} lockup.
123+ <br />
124+ After they accept the transfer, you will no longer be able to{' '}
125+ withdraw from this lockup.
126+ </Grid.Item>
127+ {approvedFor && (
128+ <Grid.Item full as={NoticeBox}>
129+ Transfer currently approved for{' '}
130+ <CopiableAddress>{approvedFor}</CopiableAddress>
131+ </Grid.Item>
132+ )}
133+ <Grid.Item full className="f5" cols={[1, 13]} as={Flex} col>
134+ <Flex.Item
135+ name="to"
136+ as={AddressInput}
137+ label="Approve transfer to"
138+ disabled={inputsLocked}
139+ />
140+ </Grid.Item>
141+ </>
142+ )}
143+
144+ <Grid.Item full as={FormError} />
145+ {completed && (
146+ <>
147+ <Grid.Item full className="pv4">
148+ <CopiableAddress>{values.to}</CopiableAddress> is now allowed{' '}
149+ to take ownership of this lockup. They can do so by visiting:
150+ </Grid.Item>
151+ <Grid.Item full as="code" className="f5 mb4 mono wrap">
152+ https://bridge.urbit.org/?kind=takeLockup&lock={kind}
153+ &from={address}
154+ </Grid.Item>
155+ <Grid.Divider />
156+ </>
157+ )}
158+ <Grid.Item
159+ full
160+ as={InlineEthereumTransaction}
161+ {...bind}
162+ label="Approve Transfer"
163+ onReturn={() => pop()}
164+ />
165+ </>
166+ )}
167+ </BridgeForm>
168+ </Grid>
169+ );
170+}