on master help sign in
Merge pull request #433 from urbit/lf/deterministic-tickets
b2d092fFang
src/indigo-react/indigo.css+22
@@ -3624,6 +3624,28 @@ caption,
3624/*******************************************************************************
3624/*******************************************************************************
3625 Margin
3625 Margin
3626*******************************************************************************/
3626*******************************************************************************/
3627+.m-auto {
3628+ margin: auto; }
3629+
3630+.mt-auto {
3631+ margin-top: auto; }
3632+
3633+.mb-auto {
3634+ margin-bottom: auto; }
3635+
3636+.ml-auto {
3637+ margin-left: auto; }
3638+
3639+.mr-auto {
3640+ margin-right: auto; }
3641+
3642+.mv-auto {
3643+ margin-top: auto;
3644+ margin-bottom: auto; }
3645+
3646+.mh-auto {
3647+ margin-left: auto;
3648+ margin-right: auto; }
3627.m0 {
3649.m0 {
3628 margin: 0px; }
3650 margin: 0px; }
3629
3651
src/lib/networkCode.js+1−1
@@ -18,7 +18,7 @@ function buf2hex(buf) {
18 .toString('hex');
18 .toString('hex');
19}
19}
20
20
21−function shas(buf, salt) {
21+export function shas(buf, salt) {
22 return shax(xor(salt, shax(buf)));
22 return shax(xor(salt, shax(buf)));
23}
23}
24
24
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+32−6
@@ -2,6 +2,7 @@ import ob from 'urbit-ob';
2import kg from 'urbit-key-generation';
2import kg from 'urbit-key-generation';
3import * as more from 'more-entropy';
3import * as more from 'more-entropy';
4import { chunk, flatMap, zipWith } from 'lodash';
4import { chunk, flatMap, zipWith } from 'lodash';
5+import { shax, shas } from 'lib/networkCode';
5
6
6import {
7import {
7 MIN_STAR,
8 MIN_STAR,
@@ -15,14 +16,16 @@ import {
15
16
16const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
17const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
17
18
19+const getTicketBitSize = point =>
20+ point < MIN_STAR
21+ ? GALAXY_ENTROPY_BITS
22+ : point < MIN_PLANET
23+ ? STAR_ENTROPY_BITS
24+ : PLANET_ENTROPY_BITS;
25+
18// returns a promise for a ticket string
26// returns a promise for a ticket string
19export const makeTicket = point => {
27export const makeTicket = point => {
20− const bits =
28+ const bits = getTicketBitSize(point);
21− point < MIN_STAR
22− ? GALAXY_ENTROPY_BITS
23− : point < MIN_PLANET
24− ? STAR_ENTROPY_BITS
25− : PLANET_ENTROPY_BITS;
26
29
27 const bytes = bits / 8;
30 const bytes = bits / 8;
28 const some = new Uint8Array(bytes);
31 const some = new Uint8Array(bytes);
@@ -44,6 +47,22 @@ export const makeTicket = point => {
44 });
47 });
45};
48};
46
49
50+export const makeDeterministicTicket = (point, seed) => {
51+ const bits = getTicketBitSize(point);
52+
53+ const bytes = bits / 8;
54+
55+ const pointSalt = Buffer.concat([
56+ Buffer.from(point.toString()),
57+ Buffer.from('invites'),
58+ ]);
59+ const entropy = shas(Buffer.from(seed, 'hex'), pointSalt);
60+
61+ const buf = entropy.slice(0, bytes);
62+ const patq = ob.hex2patq(buf.toString('hex'));
63+ return patq;
64+};
65+
47// return a wallet object
66// return a wallet object
48export const generateWallet = async (point, ticket, boot) => {
67export const generateWallet = async (point, ticket, boot) => {
49 const config = {
68 const config = {
@@ -78,3 +97,10 @@ export const generateTemporaryTicketAndWallet = async point => {
78
97
79 return { ticket, owner };
98 return { ticket, owner };
80};
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/lib/usePointStore.js+6−1
@@ -25,7 +25,12 @@ export default function usePointStore() {
25 );
25 );
26
26
27 // sync all of the on-chain info required to display a foreign point
27 // sync all of the on-chain info required to display a foreign point
28− const syncForeignPoint = useCallback(async point => {}, []);
28+ const syncForeignPoint = useCallback(
29+ async point => {
30+ syncDetails(point);
31+ },
32+ [syncDetails]
33+ );
29
34
30 // sync all of the on-chain info required for a point that the user owns
35 // sync all of the on-chain info required for a point that the user owns
31 const syncOwnedPoint = useCallback(
36 const syncOwnedPoint = useCallback(
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+178−17
@@ -1,11 +1,13 @@
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+import { delegatedSending } from 'azimuth-js';
6
7
7import { usePointCursor } from 'store/pointCursor';
8import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
9import { usePointCache } from 'store/pointCache';
10+import { useWallet } from 'store/wallet';
9
11
10import View from 'components/View';
12import View from 'components/View';
11import Tabs from 'components/Tabs';
13import Tabs from 'components/Tabs';
@@ -19,10 +21,14 @@ import Crumbs from 'components/Crumbs';
19import { useLocalRouter } from 'lib/LocalRouter';
21import { useLocalRouter } from 'lib/LocalRouter';
20import useInvites from 'lib/useInvites';
22import useInvites from 'lib/useInvites';
21import * as need from 'lib/need';
23import * as need from 'lib/need';
24+import * as wg from 'lib/walletgen';
22
25
23import Inviter from 'views/Invite/Inviter';
26import Inviter from 'views/Invite/Inviter';
24
27
25import { ReactComponent as SearchIcon } from 'assets/search.svg';
28import { ReactComponent as SearchIcon } from 'assets/search.svg';
29+import CopyButton from 'components/CopyButton';
30+import { useSyncForeignPoints } from 'lib/useSyncPoints';
31+import { useNetwork } from 'store/network';
26
32
27function SearchInput({ className, value, onChange }) {
33function SearchInput({ className, value, onChange }) {
28 return (
34 return (
@@ -42,7 +48,7 @@ function SearchInput({ className, value, onChange }) {
42 );
48 );
43}
49}
44
50
45−function CohortMember({ point, pending = false, className }) {
51+function CohortMember({ point, pending = false, className, ...rest }) {
46 const { sentInvites } = useInvites(point);
52 const { sentInvites } = useInvites(point);
47 const patp = useMemo(() => ob.patp(point), [point]);
53 const patp = useMemo(() => ob.patp(point), [point]);
48 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
54 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
@@ -54,7 +60,7 @@ function CohortMember({ point, pending = false, className }) {
54 );
60 );
55
61
56 return (
62 return (
57− <Flex justify="between" className={cn('b1 b-gray2', className)}>
63+ <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
58 <Flex.Item className="w9 h9">
64 <Flex.Item className="w9 h9">
59 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
65 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
60 </Flex.Item>
66 </Flex.Item>
@@ -73,7 +79,13 @@ function CohortMember({ point, pending = false, className }) {
73 </Flex>
79 </Flex>
74 );
80 );
75}
81}
76−function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
82+function CohortList({
83+ acceptedPoints,
84+ pendingPoints,
85+ className,
86+ onlyPending,
87+ onSelectInvite,
88+}) {
77 const { syncInvites } = usePointCache();
89 const { syncInvites } = usePointCache();
78
90
79 const [query, setQuery] = useState('');
91 const [query, setQuery] = useState('');
@@ -98,7 +110,12 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
98 }),
110 }),
99 [query]
111 [query]
100 );
112 );
101−
113+ const selectInvite = useCallback(
114+ point => () => {
115+ onSelectInvite(point);
116+ },
117+ [onSelectInvite]
118+ );
102 const _pendingPoints = filterPoints(pendingPoints);
119 const _pendingPoints = filterPoints(pendingPoints);
103 const _acceptedPoints = filterPoints(acceptedPoints);
120 const _acceptedPoints = filterPoints(acceptedPoints);
104
121
@@ -115,6 +132,7 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
115 half={(idx % 2) + 1}
132 half={(idx % 2) + 1}
116 as={CohortMember}
133 as={CohortMember}
117 point={p}
134 point={p}
135+ onClick={selectInvite(p)}
118 />
136 />
119 ))}
137 ))}
120 {_pendingPoints.map((p, idx) => (
138 {_pendingPoints.map((p, idx) => (
@@ -124,6 +142,7 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
124 as={CohortMember}
142 as={CohortMember}
125 point={p}
143 point={p}
126 pending
144 pending
145+ onClick={selectInvite(p)}
127 />
146 />
128 ))}
147 ))}
129 </>
148 </>
@@ -139,12 +158,132 @@ function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
139 );
158 );
140}
159}
141
160
161+function hideIf(hider, hidden) {
162+ return hider ? '●●●●●●●' : hidden;
163+}
164+
165+function CohortMemberExpanded({ point, className, ...rest }) {
166+ const { sentInvites, acceptedInvites } = useInvites(point);
167+ const { getDetails } = usePointCache();
168+ const { authToken } = useWallet();
169+ const { contracts } = useNetwork();
170+ const details = getDetails(point);
171+ const { active } = details.getOrElse({});
172+ const patp = useMemo(() => ob.patp(point), [point]);
173+ const colors = useMemo(
174+ () => (!active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']),
175+ [active]
176+ );
177+
178+ const [code, setCode] = useState(Nothing());
179+ const [codeVisible, setCodeVisible] = useState(false);
180+
181+ useEffect(() => {
182+ const fetchWallet = async () => {
183+ const _authToken = authToken.getOrElse(null);
184+ const _details = details.getOrElse(null);
185+ const _contracts = contracts.getOrElse(null);
186+ if (
187+ !_authToken ||
188+ !_details ||
189+ _details.active ||
190+ !_contracts ||
191+ !codeVisible
192+ ) {
193+ setCode(Nothing());
194+ return;
195+ }
196+ const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
197+ point,
198+ _authToken
199+ );
200+
201+ if (owner.keys.address !== _details.transferProxy) {
202+ setCode(Just(null));
203+ } else {
204+ setCode(Just(ticket));
205+ }
206+ };
207+ fetchWallet();
208+ }, [codeVisible]);
209+
210+ useEffect(() => {
211+ setCodeVisible(false);
212+ }, [point]);
213+
214+ return (
215+ <Grid justify="between" className={cn('b1 b-gray2', className)} {...rest}>
216+ <Grid.Item rows={[1, 4]} cols={[1, 4]} className="h10 w10">
217+ <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
218+ </Grid.Item>
219+
220+ <Grid.Item cols={[4, 13]} className="mono mt-auto ">
221+ {patp}
222+ </Grid.Item>
223+
224+ {!active && (
225+ <>
226+ <Grid.Item
227+ cols={[4, 13]}
228+ className="mt-auto mb1 f6 gray4"
229+ as={Flex}
230+ col>
231+ Invite Code
232+ </Grid.Item>
233+ <Grid.Item cols={[4, 10]} className="mb-auto mt1 f6 gray4">
234+ {(code.getOrElse(true) === null &&
235+ 'This invite code cannot be recovered') ||
236+ hideIf(!codeVisible, matchBlinky(code))}
237+ </Grid.Item>
238+ {!codeVisible && (
239+ <Grid.Item
240+ cols={[10, 13]}
241+ className="mh-auto f6 underline pointer"
242+ onClick={() => setCodeVisible(true)}>
243+ Show
244+ </Grid.Item>
245+ )}
246+ {codeVisible && code.getOrElse('') !== null && (
247+ <Grid.Item
248+ className="mh-auto f6 t-right"
249+ cols={[10, 13]}
250+ as={CopyButton}
251+ text={code.getOrElse('')}
252+ />
253+ )}
254+ </>
255+ )}
256+
257+ {active && (
258+ <>
259+ <Grid.Item
260+ cols={[4, 13]}
261+ className="mt-auto mb1 f6 gray4"
262+ as={Flex}
263+ col>
264+ {matchBlinky(sentInvites)} invites sent
265+ </Grid.Item>
266+ <Grid.Item
267+ cols={[4, 13]}
268+ className="mb-auto mt1 f6 gray4"
269+ as={Flex}
270+ col>
271+ {matchBlinky(acceptedInvites)} invites accepted
272+ </Grid.Item>
273+ </>
274+ )}
275+ </Grid>
276+ );
277+}
278+
142export default function InviteCohort() {
279export default function InviteCohort() {
143 const { pop } = useLocalRouter();
280 const { pop } = useLocalRouter();
144
281
145 const { pointCursor } = usePointCursor();
282 const { pointCursor } = usePointCursor();
146 const point = need.point(pointCursor);
283 const point = need.point(pointCursor);
147
284
285+ const { syncDetails } = usePointCache();
286+
148 const {
287 const {
149 acceptedInvites,
288 acceptedInvites,
150 acceptedPoints,
289 acceptedPoints,
@@ -164,6 +303,15 @@ export default function InviteCohort() {
164 .map(a => sentInvites.getOrElse(0) + a)
303 .map(a => sentInvites.getOrElse(0) + a)
165 .getOrElse(0);
304 .getOrElse(0);
166
305
306+ const [selectedInvite, _setSelectedInvite] = useState();
307+
308+ useSyncForeignPoints([..._acceptedPoints, ..._pendingPoints]);
309+
310+ const setSelectedInvite = useCallback(
311+ p => _setSelectedInvite(old => (old === p ? null : p)),
312+ []
313+ );
314+
167 return (
315 return (
168 <View pop={pop} inset>
316 <View pop={pop} inset>
169 <Grid gap={3}>
317 <Grid gap={3}>
@@ -209,18 +357,31 @@ export default function InviteCohort() {
209 )}
357 )}
210 {showInviteForm && <Inviter />}
358 {showInviteForm && <Inviter />}
211
359
360+ {true && (
212− <Grid.Item
361+ <Grid.Item
213− full
362+ full
214− center
363+ center
215− as={Tabs}
364+ as={Tabs}
216− views={VIEWS}
365+ views={VIEWS}
217− options={OPTIONS}
366+ options={OPTIONS}
218− currentTab={tab}
367+ currentTab={tab}
219− onTabChange={setTab}
368+ onTabChange={setTab}
220− acceptedPoints={_acceptedPoints}
369+ acceptedPoints={_acceptedPoints}
221− pendingPoints={_pendingPoints}
370+ pendingPoints={_pendingPoints}
222− onlyPending={tab === NAMES.PENDING}
371+ onlyPending={tab === NAMES.PENDING}
372+ onSelectInvite={setSelectedInvite}
373+ />
374+ )}
375+ {selectedInvite && (
376+ <Grid.Item
377+ full
378+ center
379+ as={CohortMemberExpanded}
380+ pending
381+ point={selectedInvite}
382+ onBack={() => setSelectedInvite(null)}
223− />
383+ />
384+ )}
224 </Grid>
385 </Grid>
225 </View>
386 </View>
226 );
387 );
src/views/Login/Ledger.js+1−1
@@ -78,7 +78,7 @@ export default function Ledger({ className, goHome }) {
78 walletHdPath: addHdPrefix(values.hdpath),
78 walletHdPath: addHdPrefix(values.hdpath),
79 });
79 });
80
80
81− setAuthToken(authToken);
81+ setAuthToken(Just(authToken));
82 setWallet(Just(hd));
82 setWallet(Just(hd));
83 setWalletHdPath(addHdPrefix(values.hdpath));
83 setWalletHdPath(addHdPrefix(values.hdpath));
84 } catch (error) {
84 } catch (error) {
src/views/Login/Metamask.js+1−1
@@ -33,7 +33,7 @@ export default function Metamask({ className, goHome }) {
33 web3: _web3,
33 web3: _web3,
34 });
34 });
35
35
36− setAuthToken(authToken);
36+ setAuthToken(Just(authToken));
37 setWallet(Just(wallet));
37 setWallet(Just(wallet));
38 setWalletType(WALLET_TYPES.METAMASK);
38 setWalletType(WALLET_TYPES.METAMASK);
39 } catch (e) {
39 } catch (e) {
src/views/Login/Trezor.js+1−1
@@ -74,7 +74,7 @@ export default function Trezor({ className, goHome }) {
74 walletHdPath: values.hdPath,
74 walletHdPath: values.hdPath,
75 });
75 });
76
76
77− setAuthToken(authToken);
77+ setAuthToken(Just(authToken));
78 setWallet(Just(hd));
78 setWallet(Just(hd));
79 setWalletHdPath(values.hdPath);
79 setWalletHdPath(values.hdPath);
80 },
80 },
src/views/Point.js+2−1
@@ -41,6 +41,7 @@ export default function Point() {
41 canManage,
41 canManage,
42 canSpawn,
42 canSpawn,
43 canVote,
43 canVote,
44+ isOwner,
44 } = useCurrentPermissions();
45 } = useCurrentPermissions();
45
46
46 // fetch the invites for the current cursor
47 // fetch the invites for the current cursor
@@ -194,7 +195,7 @@ export default function Point() {
194 </Grid.Item>
195 </Grid.Item>
195 </>
196 </>
196 )}
197 )}
197− {!showInviteForm && availableInvites.getOrElse(0) > 0 && (
198+ {!showInviteForm && availableInvites.getOrElse(0) > 0 && isOwner && (
198 <Grid.Item
199 <Grid.Item
199 full
200 full
200 solid
201 solid