on master help sign in
invite-converter: first pass
commit2992104546a5f201a77b0f51ed75b48cbe9e408a authorHunter Miller <hunter@hmiller.dev> dateSat 5 Mar 2022, 18:57 (5 years ago) parent3fe131d
8 files changed+408−95
src/components/L2/Headers/L2PointHeader.tsx+12−6 src/components/L2/InviteConverter.tsx+266 src/components/L2/Window/BodyPane.scss−1 src/components/L2/Window/BodyPane.tsx+17−4 src/views/Invite/GenerateInvites.tsx+14−4 src/views/Invite/GeneratingModal.tsx+15−8 src/views/Invite/useInvites.ts+78−71 src/views/Points.tsx+6−1
src/components/L2/Headers/L2PointHeader.tsx+12−6
@@ -11,17 +11,20 @@ import AccountsDropdown from '../Dropdowns/AccountsDropdown';
1111import './L2PointHeader.scss';
1212import LayerIndicator from '../LayerIndicator';
1313import { useInviteStore } from 'views/Invite/useInvites';
14−
14+import { InviteConverter } from '../InviteConverter';
15+import Point from 'lib/types/Point';
1516export interface L2PointHeaderProps {
1617 hideHome?: boolean;
1718 hideInvites?: boolean;
1819 showMigrate?: boolean;
20+ points?: Point[];
1921}
2022
2123const L2PointHeader = ({
2224 hideHome = false,
2325 hideInvites = false,
2426 showMigrate = false,
27+ points,
2528}: L2PointHeaderProps) => {
2629 const { point, pointList } = useRollerStore();
2730 const { invites, inviteJobs } = useInviteStore();
@@ -86,11 +89,14 @@ const L2PointHeader = ({
8689 {/* uncomment for L2 spawn badge */}
8790 {/* {point.isL1 && point.isL2Spawn && <Box className="l2-spawn">L2 Spawn</Box>} */}
8891 </Row>
89− <Row className="info">
90− {renderInviteButton()}
91− <Box className="history" onClick={goToHistory}>
92− History
93− </Box>
92+ <Row>
93+ <Row className="info" mr={3}>
94+ {renderInviteButton()}
95+ <Box className="history" onClick={goToHistory}>
96+ History
97+ </Box>
98+ </Row>
99+ {points && <InviteConverter points={points} />}
94100 </Row>
95101 </Row>
96102 );
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 @@
11@import 'style/shared/variables';
22
33.body-pane {
4− padding: 16px;
54 display: flex;
65 flex-direction: column;
76 align-items: center;
src/components/L2/Window/BodyPane.tsx+17−4
@@ -1,15 +1,28 @@
11import React from 'react';
2−import { Box } from '@tlon/indigo-react';
2+import { Box, BoxProps } from '@tlon/indigo-react';
33
44import './BodyPane.scss';
55
6−export interface BodyPaneProps {
6+export interface BodyPaneProps extends BoxProps {
77 children: React.ReactNode;
88 className?: string;
99}
1010
11−const BodyPane = ({ children, className = '' }: BodyPaneProps) => {
12− return <Box className={`body-pane ${className}`}>{children}</Box>;
11+const BodyPane = ({
12+ children,
13+ className = '',
14+ color,
15+ ...props
16+}: BodyPaneProps) => {
17+ return (
18+ <Box
19+ className={`body-pane ${className}`}
20+ color={color as string & BoxProps['color']}
21+ p={3}
22+ {...props}>
23+ {children}
24+ </Box>
25+ );
1326};
1427
1528export default BodyPane;
src/views/Invite/GenerateInvites.tsx+14−4
@@ -20,12 +20,14 @@ import {
2020import { DEFAULT_NUM_INVITES } from 'lib/constants';
2121import { useTimerStore } from 'store/timerStore';
2222import { useWallet } from 'store/wallet';
23+import { UnspawnedPoints } from '@urbit/roller-api';
24+import { getPendingSpawns } from 'lib/utils/roller';
2325
2426export const GenerateInvites = () => {
2527 const { pop } = useLocalRouter();
2628 const { walletType }: any = useWallet();
2729 const { point, nextQuotaTime } = useRollerStore();
28− const { getPendingTransactions, getAndUpdatePoint } = useRoller();
30+ const { api, getPendingTransactions, getAndUpdatePoint } = useRoller();
2931 const { nextRoll } = useTimerStore();
3032 const { inviteJobs } = useInviteStore();
3133 const { generateInviteCodes } = useInvites();
@@ -38,7 +40,14 @@ export const GenerateInvites = () => {
3840 const createInvites = useCallback(async () => {
3941 setGeneratingStatus('generating');
4042 try {
41− await generateInviteCodes(numInvites);
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+
50+ await generateInviteCodes(point, planets);
4251 getPendingTransactions();
4352 getAndUpdatePoint(point.value);
4453 setGeneratingStatus('finished');
@@ -53,11 +62,12 @@ export const GenerateInvites = () => {
5362 setGeneratingStatus('errored');
5463 }
5564 }, [
56− generateInviteCodes,
65+ api,
66+ point,
5767 numInvites,
68+ generateInviteCodes,
5869 getPendingTransactions,
5970 getAndUpdatePoint,
60− point,
6171 setGeneratingStatus,
6272 ]);
6373
src/views/Invite/GeneratingModal.tsx+15−8
@@ -7,7 +7,9 @@ interface GeneratingModalProps {
77 status: InviteGeneratingStatus;
88 current: number;
99 total: number;
10+ fromStar?: string;
1011 error?: string;
12+ spawn?: boolean;
1113 hide: () => void;
1214}
1315
@@ -17,6 +19,8 @@ export const GeneratingModal = ({
1719 total,
1820 hide,
1921 error,
22+ fromStar,
23+ spawn = true,
2024}: GeneratingModalProps) => {
2125 const successfulInvites = current > 1 ? current - 1 : 0;
2226 return (
@@ -27,15 +31,18 @@ export const GeneratingModal = ({
2731 <Col width="350px">
2832 {status === 'generating' && (
2933 <>
30− <Row alignItems="center" marginBottom={2}>
31− <LoadingSpinner dark />
32− <H3 marginLeft={2}>
33− Generating {current} of {total} Invites
34− </H3>
35− </Row>
34+ <Col mb={3}>
35+ <Row alignItems="center">
36+ <LoadingSpinner dark />
37+ <H3 marginLeft={2}>
38+ Generating {current} of {total} Invites
39+ </H3>
40+ </Row>
41+ {fromStar && <Text ml={4}>from {fromStar}</Text>}
42+ </Col>
3643 <Text>
37− Each invite will require four transactions to be signed, unless
38− your point is owned by a master ticket.
44+ Each invite will require {spawn ? 'four' : 'three'} transactions
45+ to be signed, unless your point is owned by a master ticket.
3946 </Text>
4047 </>
4148 )}
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';
22import { POINT_DOMINIONS } from 'lib/constants';
33import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
44import * as need from 'lib/need';
@@ -26,6 +26,7 @@ import { useWallet } from 'store/wallet';
2626import { usePointCache } from 'store/pointCache';
2727import create from 'zustand';
2828import { generateCsvLine } from 'lib/utils/invite';
29+import Point from 'lib/types/Point';
2930
3031const inviteTemplate = (
3132 planet: number,
@@ -97,7 +98,7 @@ export function useInvites() {
9798 const { web3, contracts }: any = useNetwork();
9899 const allPoints: any = usePointCache();
99100 const getDetails = allPoints?.getDetails;
100− const { api, ls } = useRoller();
101+ const { api, ls, performL2Reticket } = useRoller();
101102 const { point, pointList, setPendingTransactions } = useRollerStore();
102103 const {
103104 invites,
@@ -209,15 +210,17 @@ export function useInvites() {
209210 ]);
210211
211212 const generateInviteCodes = useCallback(
212− async (numInvites: number) => {
213− const _point = need.point(pointCursor);
213+ async (
214+ point: Point,
215+ planets: SpawnedPoints | UnspawnedPoints,
216+ spawn: boolean = true
217+ ) => {
214218 const _contracts = contracts.getOrElse(null);
215219 const _web3 = web3.getOrElse(null);
216220 const _wallet = wallet.getOrElse(null);
217221 const _authToken = authToken.getOrElse(null);
218− const _details = getDetails(_point);
219222
220− if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
223+ if (!_contracts || !_web3 || !_wallet || !_authToken) {
221224 // not using need because we want a custom error
222225 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
223226 }
@@ -228,13 +231,6 @@ export function useInvites() {
228231 // "Auth key Error: A management mnemonic can't create invite codes"
229232 // );
230233 // }
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));
238234
239235 const invites: Invite[] = [];
240236
@@ -244,11 +240,11 @@ export function useInvites() {
244240 throw new Error("Error: Address doesn't match proxy");
245241
246242 const nonce = await api.getNonce({
247− ship: _point,
243+ ship: point.value,
248244 proxy,
249245 });
250246
251− for (let i = 0; i < numInvites && planets[i]; i++) {
247+ for (let i = 0; i < planets.length; i++) {
252248 updateJob(point.value, { generatingNum: i + 1 });
253249 const planet = planets[i];
254250 const nonceInc = i + nonce;
@@ -258,62 +254,72 @@ export function useInvites() {
258254 _authToken
259255 );
260256
261− await submitL2Transaction({
262− api,
263− address: _wallet.address,
264− wallet: _wallet,
265− ship: _point,
266− proxy,
267− nonce: nonceInc,
268− pointToSpawn: planet,
269− type: 'spawn',
270− walletType,
271− web3: _web3,
272− connector,
273− });
274−
275− const networkSeed = await deriveNetworkSeedFromUrbitWallet(
276− inviteWallet,
277− 1
278− );
279− await submitL2Transaction({
280− api,
281− wallet: _wallet,
282− ship: planet,
283− proxy: 'own',
284− nonce: 0,
285− networkSeed,
286− type: 'configureKeys',
287− walletType,
288− web3: _web3,
289− connector,
290− });
291−
292− await registerProxyAddress(
293− api,
294− _wallet,
295− planet,
296− 'own',
297− 'manage',
298− 1,
299− inviteWallet.management.keys.address,
300− walletType,
301− _web3,
302− connector
303− );
304−
305− await submitL2Transaction({
306− api,
307− wallet: _wallet,
308− ship: planet,
309− proxy: 'own',
310− type: 'transferPoint',
311− nonce: 2,
312− address: inviteWallet.ownership.keys.address,
313− walletType,
314− web3: _web3,
315− connector,
316− });
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 {
266+ await submitL2Transaction({
267+ api,
268+ address: _wallet.address,
269+ wallet: _wallet,
270+ ship: point.value,
271+ proxy,
272+ nonce: nonceInc,
273+ pointToSpawn: planet,
274+ type: 'spawn',
275+ walletType,
276+ web3: _web3,
277+ connector,
278+ });
279+
280+ const networkSeed = await deriveNetworkSeedFromUrbitWallet(
281+ inviteWallet,
282+ 1
283+ );
284+ await submitL2Transaction({
285+ api,
286+ wallet: _wallet,
287+ ship: planet,
288+ proxy: 'own',
289+ nonce: 0,
290+ networkSeed,
291+ type: 'configureKeys',
292+ walletType,
293+ web3: _web3,
294+ connector,
295+ });
296+
297+ await registerProxyAddress(
298+ api,
299+ _wallet,
300+ planet,
301+ 'own',
302+ 'manage',
303+ 1,
304+ inviteWallet.management.keys.address,
305+ walletType,
306+ _web3,
307+ connector
308+ );
309+
310+ await submitL2Transaction({
311+ api,
312+ wallet: _wallet,
313+ ship: planet,
314+ proxy: 'own',
315+ type: 'transferPoint',
316+ nonce: 2,
317+ address: inviteWallet.ownership.keys.address,
318+ walletType,
319+ web3: _web3,
320+ connector,
321+ });
322+ }
317323
318324 invites.push({
319325 hash: '',
@@ -344,6 +350,7 @@ export function useInvites() {
344350 ls,
345351 getInvites,
346352 updateJob,
353+ performL2Reticket,
347354 ]
348355 );
349356
src/views/Points.tsx+6−1
@@ -256,7 +256,12 @@ export default function Points() {
256256 pop={pop}
257257 hideBack
258258 header={
259− <L2PointHeader hideHome hideInvites showMigrate={!displayEmptyState} />
259+ <L2PointHeader
260+ hideHome
261+ hideInvites
262+ showMigrate={!displayEmptyState}
263+ points={allPoints}
264+ />
260265 }>
261266 <NavHeader>
262267 <CopiableAddress