on master help sign in
invite-converter: first pass
2992104Hunter Miller
src/components/L2/Headers/L2PointHeader.tsx+12−6
@@ -11,17 +11,20 @@ import AccountsDropdown from '../Dropdowns/AccountsDropdown';
11import './L2PointHeader.scss';
11import './L2PointHeader.scss';
12import LayerIndicator from '../LayerIndicator';
12import LayerIndicator from '../LayerIndicator';
13import { useInviteStore } from 'views/Invite/useInvites';
13import { useInviteStore } from 'views/Invite/useInvites';
14−
14+import { InviteConverter } from '../InviteConverter';
15+import Point from 'lib/types/Point';
15export interface L2PointHeaderProps {
16export interface L2PointHeaderProps {
16 hideHome?: boolean;
17 hideHome?: boolean;
17 hideInvites?: boolean;
18 hideInvites?: boolean;
18 showMigrate?: boolean;
19 showMigrate?: boolean;
20+ points?: Point[];
19}
21}
20
22
21const L2PointHeader = ({
23const L2PointHeader = ({
22 hideHome = false,
24 hideHome = false,
23 hideInvites = false,
25 hideInvites = false,
24 showMigrate = false,
26 showMigrate = false,
27+ points,
25}: L2PointHeaderProps) => {
28}: L2PointHeaderProps) => {
26 const { point, pointList } = useRollerStore();
29 const { point, pointList } = useRollerStore();
27 const { invites, inviteJobs } = useInviteStore();
30 const { invites, inviteJobs } = useInviteStore();
@@ -86,11 +89,14 @@ const L2PointHeader = ({
86 {/* uncomment for L2 spawn badge */}
89 {/* uncomment for L2 spawn badge */}
87 {/* {point.isL1 && point.isL2Spawn && <Box className="l2-spawn">L2 Spawn</Box>} */}
90 {/* {point.isL1 && point.isL2Spawn && <Box className="l2-spawn">L2 Spawn</Box>} */}
88 </Row>
91 </Row>
92+ <Row>
89− <Row className="info">
93+ <Row className="info" mr={3}>
90− {renderInviteButton()}
94+ {renderInviteButton()}
91− <Box className="history" onClick={goToHistory}>
95+ <Box className="history" onClick={goToHistory}>
92− History
96+ History
93− </Box>
97+ </Box>
98+ </Row>
99+ {points && <InviteConverter points={points} />}
94 </Row>
100 </Row>
95 </Row>
101 </Row>
96 );
102 );
src/components/L2/InviteConverter.tsxnew file+266
@@ -0,0 +1,266 @@
1+import {
2+ Checkbox,
3+ Col,
4+ H3,
5+ Row,
6+ Button as IndigoButton,
7+ Text,
8+} from '@tlon/indigo-react';
9+import { Button } from 'indigo-react';
10+import Modal from './Modal';
11+import { useCallback, useEffect, useState } from 'react';
12+import Point from 'lib/types/Point';
13+import useRoller from 'lib/useRoller';
14+import BodyPane from './Window/BodyPane';
15+import {
16+ InviteGeneratingStatus,
17+ useInvites,
18+ useInviteStore,
19+} from 'views/Invite/useInvites';
20+import { GeneratingModal } from 'views/Invite/GeneratingModal';
21+import { usePointCache } from 'store/pointCache';
22+
23+interface StarMap {
24+ [key: string]: {
25+ point: Point;
26+ children: Point[];
27+ };
28+}
29+
30+interface InviteConverterProps {
31+ points: Point[];
32+}
33+
34+function selectText(selected: string[], children: Point[]) {
35+ if (
36+ allChildrenSelected(
37+ selected,
38+ children.map(p => p.patp)
39+ )
40+ ) {
41+ return 'Deselect All';
42+ }
43+
44+ return 'Select All';
45+}
46+
47+function allChildrenSelected(selected: string[], childrenPatps: string[]) {
48+ const filtered = childrenPatps.filter(p => selected.includes(p));
49+ return filtered.length === childrenPatps.length;
50+}
51+
52+export const InviteConverter = ({ points }: InviteConverterProps) => {
53+ const { syncControlledPoints }: any = usePointCache();
54+ const { api, getPendingTransactions, getAndUpdatePoint } = useRoller();
55+ const { inviteJobs } = useInviteStore();
56+ const { generateInviteCodes } = useInvites();
57+ const [show, setShow] = useState(false);
58+ const [selected, setSelected] = useState<string[]>([]);
59+ const [starMap, setStarMap] = useState<StarMap>({});
60+ const [generatingStatus, setGeneratingStatus] = useState<
61+ InviteGeneratingStatus
62+ >('initial');
63+ const [currentPoint, setCurrentPoint] = useState<Point>();
64+ const currentJob = inviteJobs[currentPoint?.value || 0];
65+
66+ useEffect(() => {
67+ if (!points) {
68+ return;
69+ }
70+
71+ async function collect() {
72+ const pointMap: StarMap = {};
73+ const stars = points.filter(point => point.isStar);
74+
75+ for (const star of stars) {
76+ const spawns = await api.getSpawned(star.value);
77+ const filteredPoints = points.filter(p => spawns.includes(p.value));
78+
79+ if (filteredPoints.length !== 0) {
80+ pointMap[star.patp] = {
81+ point: star,
82+ children: filteredPoints,
83+ };
84+ }
85+ }
86+
87+ setStarMap(pointMap);
88+ }
89+
90+ collect();
91+ }, [points]);
92+
93+ const selectChildren = useCallback(
94+ (children: Point[]) => {
95+ setSelected(state => {
96+ const childrenPatps = children.map(p => p.patp);
97+ const withoutChildren = state.filter(p => !childrenPatps.includes(p));
98+
99+ if (allChildrenSelected(state, childrenPatps)) {
100+ return withoutChildren;
101+ } else {
102+ return withoutChildren.concat(childrenPatps);
103+ }
104+ });
105+ },
106+ [setSelected]
107+ );
108+
109+ const togglePoint = useCallback(
110+ (point: Point) => {
111+ setSelected(state => {
112+ if (state.includes(point.patp)) {
113+ return state.filter(p => p !== point.patp);
114+ } else {
115+ return state.concat([point.patp]);
116+ }
117+ });
118+ },
119+ [setSelected]
120+ );
121+
122+ const convert = useCallback(async () => {
123+ if (generatingStatus !== 'initial') {
124+ return;
125+ }
126+
127+ setGeneratingStatus('generating');
128+
129+ for (const { point, children } of Object.values(starMap)) {
130+ debugger;
131+ const filtered = children
132+ .filter(p => selected.includes(p.patp))
133+ .map(p => p.value);
134+
135+ if (filtered.length <= 0) {
136+ continue;
137+ }
138+
139+ setCurrentPoint(point);
140+ await generateInviteCodes(point, filtered, false);
141+ await getAndUpdatePoint(point.value);
142+ }
143+
144+ getPendingTransactions();
145+ syncControlledPoints();
146+ setGeneratingStatus('finished');
147+ }, [
148+ generatingStatus,
149+ selected,
150+ starMap,
151+ generateInviteCodes,
152+ getAndUpdatePoint,
153+ getPendingTransactions,
154+ syncControlledPoints,
155+ ]);
156+
157+ if (generatingStatus !== 'initial') {
158+ return (
159+ <GeneratingModal
160+ status={generatingStatus}
161+ current={currentJob?.generatingNum}
162+ total={
163+ generatingStatus === 'finished'
164+ ? selected.length
165+ : selected.filter(p => {
166+ const children = starMap[currentPoint?.patp || '']?.children;
167+ return children.map(c => c.patp).includes(p);
168+ }).length
169+ }
170+ fromStar={currentPoint?.patp}
171+ spawn={false}
172+ hide={() => {
173+ setGeneratingStatus('initial');
174+ setSelected([]);
175+ setShow(false);
176+ if (generatingStatus !== 'errored') {
177+ //pop();
178+ }
179+ }}
180+ />
181+ );
182+ }
183+
184+ return (
185+ <>
186+ {/*
187+ @ts-ignore */}
188+ <Button solid center onClick={() => setShow(true)}>
189+ Convert to Invites
190+ </Button>
191+ <Modal show={show} hide={() => setShow(false)}>
192+ <BodyPane p={0}>
193+ <Col width="420px">
194+ <H3 mb={2}>Convert Planet Spawns to Invites</H3>
195+ <Text>
196+ This allows you to convert a spawned planets into invites. After
197+ this operation, invites will show up under the point they were
198+ spawned from.
199+ </Text>
200+ <Col
201+ bg="washedGray"
202+ height="300px"
203+ mt={3}
204+ p={2}
205+ borderRadius={2}
206+ overflowY="auto">
207+ {Object.entries(starMap).map(([patp, { children }]) => (
208+ <Col mb={4}>
209+ <Row>
210+ <Text mb={2} fontSize={2}>
211+ Spawned by{' '}
212+ <Text bold fontSize={2}>
213+ {patp}
214+ </Text>
215+ </Text>
216+ <IndigoButton
217+ height="18px"
218+ px={2}
219+ fontSize={0}
220+ ml="auto"
221+ onClick={() => selectChildren(children)}>
222+ {selectText(selected, children)}
223+ </IndigoButton>
224+ </Row>
225+ {children.map(point => (
226+ <Row
227+ alignItems="center"
228+ justifyContent="space-between"
229+ ml={3}
230+ mb={2}
231+ py={1}
232+ borderBottomWidth="1px"
233+ borderBottomStyle="solid"
234+ borderBottomColor="washedGray">
235+ <Text>{point.patp}</Text>
236+ <Checkbox
237+ selected={selected.includes(point.patp)}
238+ onClick={() => togglePoint(point)}
239+ />
240+ </Row>
241+ ))}
242+ </Col>
243+ ))}
244+ </Col>
245+ <Row mt={4} justifyContent="end">
246+ <IndigoButton
247+ className="secondary"
248+ onClick={() => setShow(false)}>
249+ Close
250+ </IndigoButton>
251+ {/*
252+ @ts-ignore */}
253+ <IndigoButton
254+ className={selected.length === 0 ? '' : 'primary'}
255+ ml={2}
256+ onClick={convert}
257+ disabled={selected.length === 0}>
258+ Convert
259+ </IndigoButton>
260+ </Row>
261+ </Col>
262+ </BodyPane>
263+ </Modal>
264+ </>
265+ );
266+};
src/components/L2/Window/BodyPane.scss−1
@@ -1,7 +1,6 @@
1@import 'style/shared/variables';
1@import 'style/shared/variables';
2
2
3.body-pane {
3.body-pane {
4− padding: 16px;
5 display: flex;
4 display: flex;
6 flex-direction: column;
5 flex-direction: column;
7 align-items: center;
6 align-items: center;
src/components/L2/Window/BodyPane.tsx+17−4
@@ -1,15 +1,28 @@
1import React from 'react';
1import React from 'react';
2−import { Box } from '@tlon/indigo-react';
2+import { Box, BoxProps } from '@tlon/indigo-react';
3
3
4import './BodyPane.scss';
4import './BodyPane.scss';
5
5
6−export interface BodyPaneProps {
6+export interface BodyPaneProps extends BoxProps {
7 children: React.ReactNode;
7 children: React.ReactNode;
8 className?: string;
8 className?: string;
9}
9}
10
10
11−const BodyPane = ({ children, className = '' }: BodyPaneProps) => {
11+const BodyPane = ({
12+ children,
13+ className = '',
14+ color,
15+ ...props
16+}: BodyPaneProps) => {
17+ return (
18+ <Box
12− return <Box className={`body-pane ${className}`}>{children}</Box>;
19+ className={`body-pane ${className}`}
20+ color={color as string & BoxProps['color']}
21+ p={3}
22+ {...props}>
23+ {children}
24+ </Box>
25+ );
13};
26};
14
27
15export default BodyPane;
28export default BodyPane;
src/views/Invite/GenerateInvites.tsx+14−4
@@ -20,12 +20,14 @@ import {
20import { DEFAULT_NUM_INVITES } from 'lib/constants';
20import { DEFAULT_NUM_INVITES } from 'lib/constants';
21import { useTimerStore } from 'store/timerStore';
21import { useTimerStore } from 'store/timerStore';
22import { useWallet } from 'store/wallet';
22import { useWallet } from 'store/wallet';
23+import { UnspawnedPoints } from '@urbit/roller-api';
24+import { getPendingSpawns } from 'lib/utils/roller';
23
25
24export const GenerateInvites = () => {
26export const GenerateInvites = () => {
25 const { pop } = useLocalRouter();
27 const { pop } = useLocalRouter();
26 const { walletType }: any = useWallet();
28 const { walletType }: any = useWallet();
27 const { point, nextQuotaTime } = useRollerStore();
29 const { point, nextQuotaTime } = useRollerStore();
28− const { getPendingTransactions, getAndUpdatePoint } = useRoller();
30+ const { api, getPendingTransactions, getAndUpdatePoint } = useRoller();
29 const { nextRoll } = useTimerStore();
31 const { nextRoll } = useTimerStore();
30 const { inviteJobs } = useInviteStore();
32 const { inviteJobs } = useInviteStore();
31 const { generateInviteCodes } = useInvites();
33 const { generateInviteCodes } = useInvites();
@@ -38,7 +40,14 @@ export const GenerateInvites = () => {
38 const createInvites = useCallback(async () => {
40 const createInvites = useCallback(async () => {
39 setGeneratingStatus('generating');
41 setGeneratingStatus('generating');
40 try {
42 try {
43+ let planets: UnspawnedPoints = await api.getUnspawned(point.value);
44+ const pendingTxs = await api.getPendingByShip(point.value);
45+ const pendingSpawns = getPendingSpawns(pendingTxs);
46+ planets = planets
47+ .filter((point: number) => !pendingSpawns.has(point))
48+ .slice(0, numInvites);
49+
41− await generateInviteCodes(numInvites);
50+ await generateInviteCodes(point, planets);
42 getPendingTransactions();
51 getPendingTransactions();
43 getAndUpdatePoint(point.value);
52 getAndUpdatePoint(point.value);
44 setGeneratingStatus('finished');
53 setGeneratingStatus('finished');
@@ -53,11 +62,12 @@ export const GenerateInvites = () => {
53 setGeneratingStatus('errored');
62 setGeneratingStatus('errored');
54 }
63 }
55 }, [
64 }, [
56− generateInviteCodes,
65+ api,
66+ point,
57 numInvites,
67 numInvites,
68+ generateInviteCodes,
58 getPendingTransactions,
69 getPendingTransactions,
59 getAndUpdatePoint,
70 getAndUpdatePoint,
60− point,
61 setGeneratingStatus,
71 setGeneratingStatus,
62 ]);
72 ]);
63
73
src/views/Invite/GeneratingModal.tsx+15−8
@@ -7,7 +7,9 @@ interface GeneratingModalProps {
7 status: InviteGeneratingStatus;
7 status: InviteGeneratingStatus;
8 current: number;
8 current: number;
9 total: number;
9 total: number;
10+ fromStar?: string;
10 error?: string;
11 error?: string;
12+ spawn?: boolean;
11 hide: () => void;
13 hide: () => void;
12}
14}
13
15
@@ -17,6 +19,8 @@ export const GeneratingModal = ({
17 total,
19 total,
18 hide,
20 hide,
19 error,
21 error,
22+ fromStar,
23+ spawn = true,
20}: GeneratingModalProps) => {
24}: GeneratingModalProps) => {
21 const successfulInvites = current > 1 ? current - 1 : 0;
25 const successfulInvites = current > 1 ? current - 1 : 0;
22 return (
26 return (
@@ -27,15 +31,18 @@ export const GeneratingModal = ({
27 <Col width="350px">
31 <Col width="350px">
28 {status === 'generating' && (
32 {status === 'generating' && (
29 <>
33 <>
34+ <Col mb={3}>
30− <Row alignItems="center" marginBottom={2}>
35+ <Row alignItems="center">
31− <LoadingSpinner dark />
36+ <LoadingSpinner dark />
32− <H3 marginLeft={2}>
37+ <H3 marginLeft={2}>
33− Generating {current} of {total} Invites
38+ Generating {current} of {total} Invites
34− </H3>
39+ </H3>
35− </Row>
40+ </Row>
41+ {fromStar && <Text ml={4}>from {fromStar}</Text>}
42+ </Col>
36 <Text>
43 <Text>
37− Each invite will require four transactions to be signed, unless
44+ Each invite will require {spawn ? 'four' : 'three'} transactions
38− your point is owned by a master ticket.
45+ to be signed, unless your point is owned by a master ticket.
39 </Text>
46 </Text>
40 </>
47 </>
41 )}
48 )}
src/views/Invite/useInvites.ts+78−71
@@ -1,4 +1,4 @@
1−import { UnspawnedPoints } from '@urbit/roller-api';
1+import { SpawnedPoints, UnspawnedPoints } from '@urbit/roller-api';
2import { POINT_DOMINIONS } from 'lib/constants';
2import { POINT_DOMINIONS } from 'lib/constants';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
4import * as need from 'lib/need';
4import * as need from 'lib/need';
@@ -26,6 +26,7 @@ import { useWallet } from 'store/wallet';
26import { usePointCache } from 'store/pointCache';
26import { usePointCache } from 'store/pointCache';
27import create from 'zustand';
27import create from 'zustand';
28import { generateCsvLine } from 'lib/utils/invite';
28import { generateCsvLine } from 'lib/utils/invite';
29+import Point from 'lib/types/Point';
29
30
30const inviteTemplate = (
31const inviteTemplate = (
31 planet: number,
32 planet: number,
@@ -97,7 +98,7 @@ export function useInvites() {
97 const { web3, contracts }: any = useNetwork();
98 const { web3, contracts }: any = useNetwork();
98 const allPoints: any = usePointCache();
99 const allPoints: any = usePointCache();
99 const getDetails = allPoints?.getDetails;
100 const getDetails = allPoints?.getDetails;
100− const { api, ls } = useRoller();
101+ const { api, ls, performL2Reticket } = useRoller();
101 const { point, pointList, setPendingTransactions } = useRollerStore();
102 const { point, pointList, setPendingTransactions } = useRollerStore();
102 const {
103 const {
103 invites,
104 invites,
@@ -209,15 +210,17 @@ export function useInvites() {
209 ]);
210 ]);
210
211
211 const generateInviteCodes = useCallback(
212 const generateInviteCodes = useCallback(
212− async (numInvites: number) => {
213+ async (
213− const _point = need.point(pointCursor);
214+ point: Point,
215+ planets: SpawnedPoints | UnspawnedPoints,
216+ spawn: boolean = true
217+ ) => {
214 const _contracts = contracts.getOrElse(null);
218 const _contracts = contracts.getOrElse(null);
215 const _web3 = web3.getOrElse(null);
219 const _web3 = web3.getOrElse(null);
216 const _wallet = wallet.getOrElse(null);
220 const _wallet = wallet.getOrElse(null);
217 const _authToken = authToken.getOrElse(null);
221 const _authToken = authToken.getOrElse(null);
218− const _details = getDetails(_point);
219
222
220− if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
223+ if (!_contracts || !_web3 || !_wallet || !_authToken) {
221 // not using need because we want a custom error
224 // not using need because we want a custom error
222 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
225 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
223 }
226 }
@@ -228,13 +231,6 @@ export function useInvites() {
228 // "Auth key Error: A management mnemonic can't create invite codes"
231 // "Auth key Error: A management mnemonic can't create invite codes"
229 // );
232 // );
230 // }
233 // }
231− let planets: UnspawnedPoints = await api.getUnspawned(_point);
232−
233− const pendingTxs = await api.getPendingByShip(_point);
234−
235− const pendingSpawns = getPendingSpawns(pendingTxs);
236−
237− planets = planets.filter((point: number) => !pendingSpawns.has(point));
238
234
239 const invites: Invite[] = [];
235 const invites: Invite[] = [];
240
236
@@ -244,11 +240,11 @@ export function useInvites() {
244 throw new Error("Error: Address doesn't match proxy");
240 throw new Error("Error: Address doesn't match proxy");
245
241
246 const nonce = await api.getNonce({
242 const nonce = await api.getNonce({
247− ship: _point,
243+ ship: point.value,
248 proxy,
244 proxy,
249 });
245 });
250
246
251− for (let i = 0; i < numInvites && planets[i]; i++) {
247+ for (let i = 0; i < planets.length; i++) {
252 updateJob(point.value, { generatingNum: i + 1 });
248 updateJob(point.value, { generatingNum: i + 1 });
253 const planet = planets[i];
249 const planet = planets[i];
254 const nonceInc = i + nonce;
250 const nonceInc = i + nonce;
@@ -258,62 +254,72 @@ export function useInvites() {
258 _authToken
254 _authToken
259 );
255 );
260
256
257+ if (!spawn) {
258+ performL2Reticket({
259+ point: planet,
260+ to: inviteWallet.ownership.keys.address,
261+ manager: inviteWallet.management.keys.address,
262+ fromWallet: _wallet,
263+ toWallet: inviteWallet,
264+ });
265+ } else {
261− await submitL2Transaction({
266+ await submitL2Transaction({
262− api,
267+ api,
263− address: _wallet.address,
268+ address: _wallet.address,
264− wallet: _wallet,
269+ wallet: _wallet,
265− ship: _point,
270+ ship: point.value,
266− proxy,
271+ proxy,
267− nonce: nonceInc,
272+ nonce: nonceInc,
268− pointToSpawn: planet,
273+ pointToSpawn: planet,
269− type: 'spawn',
274+ type: 'spawn',
270− walletType,
275+ walletType,
271− web3: _web3,
276+ web3: _web3,
272− connector,
277+ connector,
273− });
278+ });
274−
279+
275− const networkSeed = await deriveNetworkSeedFromUrbitWallet(
280+ const networkSeed = await deriveNetworkSeedFromUrbitWallet(
276− inviteWallet,
281+ inviteWallet,
277− 1
282+ 1
278− );
283+ );
279− await submitL2Transaction({
284+ await submitL2Transaction({
280− api,
285+ api,
281− wallet: _wallet,
286+ wallet: _wallet,
282− ship: planet,
287+ ship: planet,
283− proxy: 'own',
288+ proxy: 'own',
284− nonce: 0,
289+ nonce: 0,
285− networkSeed,
290+ networkSeed,
286− type: 'configureKeys',
291+ type: 'configureKeys',
287− walletType,
292+ walletType,
288− web3: _web3,
293+ web3: _web3,
289− connector,
294+ connector,
290− });
295+ });
291−
296+
292− await registerProxyAddress(
297+ await registerProxyAddress(
293− api,
298+ api,
294− _wallet,
299+ _wallet,
295− planet,
300+ planet,
296− 'own',
301+ 'own',
297− 'manage',
302+ 'manage',
298− 1,
303+ 1,
299− inviteWallet.management.keys.address,
304+ inviteWallet.management.keys.address,
300− walletType,
305+ walletType,
301− _web3,
306+ _web3,
302− connector
307+ connector
303− );
308+ );
304−
309+
305− await submitL2Transaction({
310+ await submitL2Transaction({
306− api,
311+ api,
307− wallet: _wallet,
312+ wallet: _wallet,
308− ship: planet,
313+ ship: planet,
309− proxy: 'own',
314+ proxy: 'own',
310− type: 'transferPoint',
315+ type: 'transferPoint',
311− nonce: 2,
316+ nonce: 2,
312− address: inviteWallet.ownership.keys.address,
317+ address: inviteWallet.ownership.keys.address,
313− walletType,
318+ walletType,
314− web3: _web3,
319+ web3: _web3,
315− connector,
320+ connector,
316− });
321+ });
322+ }
317
323
318 invites.push({
324 invites.push({
319 hash: '',
325 hash: '',
@@ -344,6 +350,7 @@ export function useInvites() {
344 ls,
350 ls,
345 getInvites,
351 getInvites,
346 updateJob,
352 updateJob,
353+ performL2Reticket,
347 ]
354 ]
348 );
355 );
349
356
src/views/Points.tsx+6−1
@@ -256,7 +256,12 @@ export default function Points() {
256 pop={pop}
256 pop={pop}
257 hideBack
257 hideBack
258 header={
258 header={
259+ <L2PointHeader
260+ hideHome
261+ hideInvites
259− <L2PointHeader hideHome hideInvites showMigrate={!displayEmptyState} />
262+ showMigrate={!displayEmptyState}
263+ points={allPoints}
264+ />
260 }>
265 }>
261 <NavHeader>
266 <NavHeader>
262 <CopiableAddress
267 <CopiableAddress