on master help sign in
inviter: create deterministic tickets
673ee7dLiam Fitzgerald
src/lib/useInviter.js+8−6
@@ -35,10 +35,11 @@ const STATUS = {
35
35
36const useInviter = () => {
36const useInviter = () => {
37 const { contracts, web3, networkType } = useNetwork();
37 const { contracts, web3, networkType } = useNetwork();
38− const { wallet, walletType, walletHdPath } = useWallet();
38+ const { wallet, walletType, walletHdPath, authToken } = useWallet();
39 const { pointCursor } = usePointCursor();
39 const { pointCursor } = usePointCursor();
40 const { syncInvites } = usePointCache();
40 const { syncInvites } = usePointCache();
41 const { gasPrice } = useGasPrice();
41 const { gasPrice } = useGasPrice();
42+
42 const point = need.point(pointCursor);
43 const point = need.point(pointCursor);
43
44
44 const [needFunds, setNeedFunds] = useState(null);
45 const [needFunds, setNeedFunds] = useState(null);
@@ -65,7 +66,8 @@ const useInviter = () => {
65 const _contracts = contracts.getOrElse(null);
66 const _contracts = contracts.getOrElse(null);
66 const _web3 = web3.getOrElse(null);
67 const _web3 = web3.getOrElse(null);
67 const _wallet = wallet.getOrElse(null);
68 const _wallet = wallet.getOrElse(null);
69+ const _authToken = authToken.getOrElse(null);
68− if (!_contracts || !_web3 || !_wallet) {
70+ if (!_contracts || !_web3 || !_wallet || !_authToken) {
69 // not using need because we want a custom error
71 // not using need because we want a custom error
70 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
72 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
71 }
73 }
@@ -104,10 +106,10 @@ const useInviter = () => {
104 try {
106 try {
105 const planet = planets[i];
107 const planet = planets[i];
106
108
107− const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
109+ const {
108− MIN_PLANET
110+ ticket,
109− // we're always giving planets, so generate a ticket of the correct size
111+ owner,
110− );
112+ } = await wg.generateTemporaryDeterministicWallet(planet, _authToken);
111
113
112 const inviteTx = azimuth.delegatedSending.sendPoint(
114 const inviteTx = azimuth.delegatedSending.sendPoint(
113 _contracts,
115 _contracts,
src/lib/walletgen.js+8−1
@@ -56,7 +56,7 @@ export const makeDeterministicTicket = (point, seed) => {
56 Buffer.from(point.toString()),
56 Buffer.from(point.toString()),
57 Buffer.from('invites'),
57 Buffer.from('invites'),
58 ]);
58 ]);
59− const entropy = shas(seed, pointSalt);
59+ const entropy = shas(Buffer.from(seed, 'hex'), pointSalt);
60
60
61 const buf = entropy.slice(0, bytes);
61 const buf = entropy.slice(0, bytes);
62 const patq = ob.hex2patq(buf.toString('hex'));
62 const patq = ob.hex2patq(buf.toString('hex'));
@@ -97,3 +97,10 @@ export const generateTemporaryTicketAndWallet = async point => {
97
97
98 return { ticket, owner };
98 return { ticket, owner };
99};
99};
100+
101+export const generateTemporaryDeterministicWallet = async (point, seed) => {
102+ const ticket = makeDeterministicTicket(point, seed);
103+ const owner = await generateOwnershipWallet(ZOD, ticket);
104+
105+ return { ticket, owner };
106+};
src/store/lib/useInvitesStore.js+2−1
@@ -51,7 +51,8 @@ export default function useInvitesStore() {
51 active: !!active,
51 active: !!active,
52 }))
52 }))
53 .partition('active')
53 .partition('active')
54− .map(arr => arr.map(x => x.point));
54+ .map(arr => arr.map(x => x.point))
55+ .value();
55
56
56 addToInvitesCache({
57 addToInvitesCache({
57 [point]: {
58 [point]: {
src/store/wallet.js+1−1
@@ -73,7 +73,7 @@ function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
73 web3: _web3,
73 web3: _web3,
74 });
74 });
75
75
76− setAuthToken(token);
76+ setAuthToken(Just(token));
77 })();
77 })();
78 }, [wallet, walletType, walletHdPath, web3]);
78 }, [wallet, walletType, walletHdPath, web3]);
79
79
src/views/Invite/Cohort.js+168−17
@@ -1,11 +1,12 @@
1import React, { useMemo, useEffect, useCallback, useState } from 'react';
1import React, { useMemo, useEffect, useCallback, useState } from 'react';
2import { Grid, Flex, Button } from 'indigo-react';
2import { Grid, Flex, Button } from 'indigo-react';
3import * as ob from 'urbit-ob';
3import * as ob from 'urbit-ob';
4−import { Just } from 'folktale/maybe';
4+import { Just, Nothing } from 'folktale/maybe';
5import cn from 'classnames';
5import cn from 'classnames';
6
6
7import { usePointCursor } from 'store/pointCursor';
7import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
8import { usePointCache } from 'store/pointCache';
9+import { useWallet } from 'store/wallet';
9
10
10import View from 'components/View';
11import View from 'components/View';
11import Tabs from 'components/Tabs';
12import Tabs from 'components/Tabs';
@@ -19,10 +20,13 @@ import Crumbs from 'components/Crumbs';
19import { useLocalRouter } from 'lib/LocalRouter';
20import { useLocalRouter } from 'lib/LocalRouter';
20import useInvites from 'lib/useInvites';
21import useInvites from 'lib/useInvites';
21import * as need from 'lib/need';
22import * as need from 'lib/need';
23+import * as wg from 'lib/walletgen';
22
24
23import Inviter from 'views/Invite/Inviter';
25import Inviter from 'views/Invite/Inviter';
24
26
25import { ReactComponent as SearchIcon } from 'assets/search.svg';
27import { ReactComponent as SearchIcon } from 'assets/search.svg';
28+import CopyButton from 'components/CopyButton';
29+import { useSyncForeignPoints } from 'lib/useSyncPoints';
26
30
27function SearchInput({ className, value, onChange }) {
31function SearchInput({ className, value, onChange }) {
28 return (
32 return (
@@ -42,7 +46,7 @@ function SearchInput({ className, value, onChange }) {
42 );
46 );
43}
47}
44
48
45−function CohortMember({ point, pending = false, className }) {
49+function CohortMember({ point, pending = false, className, ...rest }) {
46 const { sentInvites } = useInvites(point);
50 const { sentInvites } = useInvites(point);
47 const patp = useMemo(() => ob.patp(point), [point]);
51 const patp = useMemo(() => ob.patp(point), [point]);
48 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
52 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
@@ -54,7 +58,7 @@ function CohortMember({ point, pending = false, className }) {
54 );
58 );
55
59
56 return (
60 return (
57− <Flex justify="between" className={cn('b1 b-gray2', className)}>
61+ <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
58 <Flex.Item className="w9 h9">
62 <Flex.Item className="w9 h9">
59 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
63 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
60 </Flex.Item>
64 </Flex.Item>
@@ -73,7 +77,13 @@ function CohortMember({ point, pending = false, className }) {
73 </Flex>
77 </Flex>
74 );
78 );
75}
79}
76−function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
80+function CohortList({
81+ acceptedPoints,
82+ pendingPoints,
83+ className,
84+ onlyPending,
85+ onSelectInvite,
86+}) {
77 const { syncInvites } = usePointCache();
87 const { syncInvites } = usePointCache();
78
88
79 const [query, setQuery] = useState('');
89 const [query, setQuery] = useState('');
@@ -98,7 +108,12 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
98 }),
108 }),
99 [query]
109 [query]
100 );
110 );
101−
111+ const selectInvite = useCallback(
112+ point => () => {
113+ onSelectInvite(point);
114+ },
115+ [onSelectInvite]
116+ );
102 const _pendingPoints = filterPoints(pendingPoints);
117 const _pendingPoints = filterPoints(pendingPoints);
103 const _acceptedPoints = filterPoints(acceptedPoints);
118 const _acceptedPoints = filterPoints(acceptedPoints);
104
119
@@ -115,6 +130,7 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
115 half={(idx % 2) + 1}
130 half={(idx % 2) + 1}
116 as={CohortMember}
131 as={CohortMember}
117 point={p}
132 point={p}
133+ onClick={selectInvite(p)}
118 />
134 />
119 ))}
135 ))}
120 {_pendingPoints.map((p, idx) => (
136 {_pendingPoints.map((p, idx) => (
@@ -124,6 +140,7 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
124 as={CohortMember}
140 as={CohortMember}
125 point={p}
141 point={p}
126 pending
142 pending
143+ onClick={selectInvite(p)}
127 />
144 />
128 ))}
145 ))}
129 </>
146 </>
@@ -139,12 +156,122 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
139 );
156 );
140}
157}
141
158
159+function hideIf(hider, hidden) {
160+ return hider ? '●●●●●●●' : hidden;
161+}
162+
163+function CohortMemberExpanded({ point, className, ...rest }) {
164+ const { sentInvites, acceptedInvites } = useInvites(point);
165+ const { getDetails } = usePointCache();
166+ const { authToken } = useWallet();
167+ const details = getDetails(point).getOrElse({});
168+ const { active } = details;
169+ const patp = useMemo(() => ob.patp(point), [point]);
170+ const colors = !active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
171+
172+ const [code, setCode] = useState(Nothing());
173+ const [codeVisible, setCodeVisible] = useState(false);
174+
175+ useEffect(() => {
176+ const fetchWallet = async () => {
177+ const _authToken = authToken.getOrElse(null);
178+ if (!_authToken || active) {
179+ return;
180+ }
181+ const ticket = await wg.makeDeterministicTicket(point, _authToken);
182+ setCode(Just(ticket));
183+ };
184+ fetchWallet();
185+ }, [authToken, point, active]);
186+
187+ const DetailText = useCallback(
188+ () =>
189+ !active ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
190+ [active, sentInvites]
191+ );
192+
193+ return (
194+ <Grid justify="between" className={cn('b1 b-gray2', className)} {...rest}>
195+ <Grid.Item rows={[1, 4]} cols={[1, 4]} className="h10 w10">
196+ <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
197+ </Grid.Item>
198+
199+ <Grid.Item cols={[4, 13]} className="mono mt-auto ">
200+ {patp}
201+ </Grid.Item>
202+
203+ {!active && (
204+ <>
205+ <Grid.Item
206+ cols={[4, 13]}
207+ className="mt-auto mb1 f6 gray4"
208+ as={Flex}
209+ col>
210+ Invite Code
211+ </Grid.Item>
212+ <Grid.Item
213+ cols={[4, 10]}
214+ className="mb-auto mt1 f6 gray4"
215+ as={Flex}
216+ col
217+ onClick={() => setCodeVisible(r => !r)}>
218+ {hideIf(!codeVisible, matchBlinky(code))}
219+ </Grid.Item>
220+ <Grid.Item
221+ className="mh-auto f6"
222+ cols={[10, 13]}
223+ as={CopyButton}
224+ text={code.getOrElse('')}
225+ />
226+ </>
227+ )}
228+
229+ {active && (
230+ <>
231+ <Grid.Item
232+ cols={[4, 13]}
233+ className="mt-auto mb1 f6 gray4"
234+ as={Flex}
235+ col>
236+ {matchBlinky(sentInvites)} invites sent
237+ </Grid.Item>
238+ <Grid.Item
239+ cols={[4, 13]}
240+ className="mb-auto mt1 f6 gray4"
241+ as={Flex}
242+ col>
243+ {matchBlinky(acceptedInvites)} invites accepted
244+ </Grid.Item>
245+ </>
246+ )}
247+
248+ {/* <Flex.Item>Invite Code</Flex.Item> */}
249+ {/* <Flex.Item>{matchBlinky(code)}</Flex.Item> */}
250+ {/* <span className="pointer underline">Get Invite code </span> */}
251+ {/* <br /> */}
252+ {/* {resend && <span>{matchBlinky(code)}</span>} */}
253+ {/* </Grid.Item> */}
254+ {/* {resend && ( */}
255+ {/* <Grid.Item cols={[5, 13]} rows={[2, 3]} className="mv-auto"></Grid.Item> */}
256+ {/* )} */}
257+ {/* <Grid.Item justify="evenly" as={Flex} col className="mono f6 ph4"> */}
258+ {/* <Flex.Item>{patp}</Flex.Item> */}
259+ {/* <Flex.Item className="gray4"> */}
260+ {/* <DetailText /> */}
261+ {/* </Flex.Item> */}
262+ {/* </Grid.Item> */}
263+ </Grid>
264+ );
265+}
266+
142export default function InviteCohort() {
267export default function InviteCohort() {
143 const { pop } = useLocalRouter();
268 const { pop } = useLocalRouter();
144
269
145 const { pointCursor } = usePointCursor();
270 const { pointCursor } = usePointCursor();
146 const point = need.point(pointCursor);
271 const point = need.point(pointCursor);
147
272
273+ const { syncDetails } = usePointCache();
274+
148 const {
275 const {
149 acceptedInvites,
276 acceptedInvites,
150 acceptedPoints,
277 acceptedPoints,
@@ -164,6 +291,17 @@ export default function InviteCohort() {
164 .map(a => sentInvites.getOrElse(0) + a)
291 .map(a => sentInvites.getOrElse(0) + a)
165 .getOrElse(0);
292 .getOrElse(0);
166
293
294+ const [selectedInvite, _setSelectedInvite] = useState();
295+
296+ useEffect(() => {
297+ Promise.all(_acceptedPoints.map(i => syncDetails(i)));
298+ }, [_acceptedPoints, syncDetails]);
299+
300+ const setSelectedInvite = useCallback(
301+ p => _setSelectedInvite(old => (old === p ? null : p)),
302+ []
303+ );
304+
167 return (
305 return (
168 <View pop={pop} inset>
306 <View pop={pop} inset>
169 <Grid gap={3}>
307 <Grid gap={3}>
@@ -209,18 +347,31 @@ export default function InviteCohort() {
209 )}
347 )}
210 {showInviteForm && <Inviter />}
348 {showInviteForm && <Inviter />}
211
349
350+ {true && (
212− <Grid.Item
351+ <Grid.Item
213− full
352+ full
214− center
353+ center
215− as={Tabs}
354+ as={Tabs}
216− views={VIEWS}
355+ views={VIEWS}
217− options={OPTIONS}
356+ options={OPTIONS}
218− currentTab={tab}
357+ currentTab={tab}
219− onTabChange={setTab}
358+ onTabChange={setTab}
220− acceptedPoints={_acceptedPoints}
359+ acceptedPoints={_acceptedPoints}
221− pendingPoints={_pendingPoints}
360+ pendingPoints={_pendingPoints}
222− onlyPending={tab === NAMES.PENDING}
361+ onlyPending={tab === NAMES.PENDING}
362+ onSelectInvite={setSelectedInvite}
363+ />
364+ )}
365+ {selectedInvite && (
366+ <Grid.Item
367+ full
368+ center
369+ as={CohortMemberExpanded}
370+ pending
371+ point={selectedInvite}
372+ onBack={() => setSelectedInvite(null)}
223− />
373+ />
374+ )}
224 </Grid>
375 </Grid>
225 </View>
376 </View>
226 );
377 );