on master help sign in
invites: splitting from roller, removing unused code, adding status
commit427900d14f7f42d66ba67c77349003a6bbe09c08 authorHunter Miller <hunter@hmiller.dev> dateFri 18 Feb 2022, 18:39 (5 years ago) parent8906361
13 files changed+480−996
src/components/L2/Headers/L2PointHeader.tsx+5−5 src/lib/useInviter.js−222 src/lib/useRoller.ts+2−272 src/lib/useRollerPoller.ts+3−1 src/store/rollerStore.ts−50 src/views/Invite/Cohort.tsx+41−38 src/views/Invite/Email.js−225 src/views/Invite/GeneratingModal.tsx+86 src/views/Invite/Inviter.js−54 src/views/Invite/Url.js−108 src/views/Invite/useInvites.ts+335 src/views/Point/Point.tsx+5−14 src/views/Points.tsx+3−7
src/components/L2/Headers/L2PointHeader.tsx+5−5
@@ -10,24 +10,24 @@ import HeaderButton from './HeaderButton';
1010import AccountsDropdown from '../Dropdowns/AccountsDropdown';
1111import './L2PointHeader.scss';
1212import LayerIndicator from '../LayerIndicator';
13+import { useInviteStore } from 'views/Invite/useInvites';
1314
1415export interface L2PointHeaderProps {
15− numInvites?: number;
1616 hideHome?: boolean;
17− hideTimer?: boolean;
1817 hideInvites?: boolean;
1918 showMigrate?: boolean;
2019}
2120
2221const L2PointHeader = ({
23− numInvites = 0,
2422 hideHome = false,
25− hideTimer = false,
2623 hideInvites = false,
2724 showMigrate = false,
2825}: L2PointHeaderProps) => {
29− const { point, pointList, invitesLoading } = useRollerStore();
26+ const { point, pointList } = useRollerStore();
27+ const { invites, loading: invitesLoading } = useInviteStore();
3028 const { pointCursor, setPointCursor }: any = usePointCursor();
29+ const numInvites = invites[point.value]?.length;
30+ debugger;
3131
3232 const { popTo, push, names }: any = useHistory();
3333
src/lib/useInviter.jsdeleted−222
@@ -1,222 +0,0 @@
1−import { useState, useCallback, useEffect } from 'react';
2−import { toBN } from 'web3-utils';
3−import * as azimuth from 'azimuth-js';
4−import { FORM_ERROR } from 'final-form';
5−
6−import * as wg from 'lib/walletgen';
7−import * as tank from 'lib/tank';
8−import * as need from 'lib/need';
9−import { GAS_LIMITS } from 'lib/constants';
10−import {
11− signTransaction,
12− sendSignedTransaction,
13− waitForTransactionConfirm,
14− hexify,
15−} from 'lib/txn';
16−import useGasPrice from 'lib/useGasPrice';
17−import { safeToWei } from 'lib/lib';
18−
19−import { useNetwork } from 'store/network';
20−import { useWallet } from 'store/wallet';
21−import { usePointCursor } from 'store/pointCursor';
22−import { usePointCache } from 'store/pointCache';
23−
24−import { WARNING } from 'form/helpers';
25−
26−const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
27−const STATUS = {
28− INPUT: 'INPUT',
29− SENDING: 'SENDING',
30− SUCCESS: 'SUCCESS',
31− FAILURE: 'FAILURE',
32−};
33−
34−const useInviter = () => {
35− const { contracts, web3, networkType } = useNetwork();
36− const { wallet, walletType, walletHdPath, authToken } = useWallet();
37− const { pointCursor } = usePointCursor();
38− const { syncInvites } = usePointCache();
39− const { gasPrice } = useGasPrice();
40−
41− const point = need.point(pointCursor);
42−
43− const [needFunds, setNeedFunds] = useState(null);
44− const [invites, setInvites] = useState([]);
45−
46− const [progress, setProgress] = useState(0);
47− const [txStatus, setTxStatus] = useState(STATUS.INPUT);
48−
49− const isDone = txStatus === STATUS.SUCCESS;
50− useEffect(() => {
51− if (isDone) {
52− syncInvites(point);
53− }
54− }, [isDone, syncInvites, point]);
55−
56− const resetInvites = useCallback(() => {
57− setProgress(0);
58− setInvites([]);
59− setTxStatus(STATUS.INPUT);
60− }, [setProgress, setInvites, setTxStatus]);
61−
62− //TODO split this out into smaller callbacks within useInviter
63− const generateInvites = useCallback(
64− async numInvites => {
65− const _contracts = contracts.getOrElse(null);
66− const _web3 = web3.getOrElse(null);
67− const _wallet = wallet.getOrElse(null);
68− const _authToken = authToken.getOrElse(null);
69− if (!_contracts || !_web3 || !_wallet || !_authToken) {
70− // not using need because we want a custom error
71− throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
72− }
73−
74− const nonce = await _web3.eth.getTransactionCount(_wallet.address);
75− const chainId = await _web3.eth.net.getId();
76− const planets = await azimuth.delegatedSending.getPlanetsToSend(
77− _contracts,
78− point,
79− numInvites
80− );
81−
82− setTxStatus(STATUS.SENDING);
83− // account for the race condition where invites got used up while we were
84− // composing our target list
85− if (planets.length < numInvites) {
86− // resync invites to the cache, since they're out of date
87− syncInvites(point);
88−
89− setTxStatus(STATUS.FAILURE);
90−
91− return {
92− errors: {
93− [FORM_ERROR]:
94− `Can currently only send ${planets.length} invites. ` +
95− `Please remove invites until you are within the limit.`,
96− },
97− };
98− }
99−
100− // NB(shrugs) - must be processed in serial because main thread, etc
101− let signedInvites = [];
102− for (let i = 0; i < numInvites; i++) {
103− setProgress(x => x + 1);
104− try {
105− const planet = planets[i];
106−
107− const {
108− ticket,
109− owner,
110− } = await wg.generateTemporaryDeterministicWallet(planet, _authToken);
111−
112− const inviteTx = azimuth.delegatedSending.sendPoint(
113− _contracts,
114− point,
115− planet,
116− owner.keys.address
117− );
118−
119− const signedTx = await signTransaction({
120− wallet: _wallet,
121− walletType,
122− walletHdPath,
123− networkType,
124− chainId,
125− nonce: nonce + i,
126− // TODO: ^ make a useTransactionSigner to encapsulate this logic
127− txn: inviteTx,
128− gasPrice: gasPrice.toFixed(), // expects string gwei
129− gasLimit: GAS_LIMIT.toFixed(),
130− });
131−
132− const rawTx = hexify(signedTx.serialize());
133−
134− signedInvites.push({ ticket, signedTx, rawTx });
135− } catch (error) {
136− console.error(error);
137− return {
138− errors: {
139− [WARNING]: `There was an error while generating wallets.`,
140− },
141− };
142− }
143− }
144−
145− const totalCost = toBN(GAS_LIMIT)
146− .mul(toBN(gasPrice))
147− .mul(toBN(numInvites));
148− const tankWasUsed = await tank.ensureFundsFor(
149− _web3,
150− point,
151− _wallet.address,
152− walletType,
153− safeToWei(totalCost, 'gwei'),
154− signedInvites.map(si => si.rawTx),
155− (address, minBalance, balance) =>
156− setNeedFunds({ address, minBalance, balance }),
157− () => setNeedFunds(undefined)
158− );
159−
160− let inviteErrors = [];
161− let confirmedInvites = [];
162− const txAndMailings = signedInvites.map(async invite => {
163− try {
164− const txHash = await sendSignedTransaction(
165− _web3,
166− invite.signedTx,
167− tankWasUsed
168− );
169−
170− await waitForTransactionConfirm(_web3, txHash);
171− confirmedInvites.push(invite);
172− } catch (error) {
173− console.error('Error sending invite tx:', error);
174− if (typeof error === 'string') {
175− inviteErrors.push(error);
176− } else if (typeof error === 'object' && error.message) {
177− inviteErrors.push(error.message);
178− }
179− return;
180− }
181− });
182−
183− await Promise.all(txAndMailings);
184−
185− setTxStatus(STATUS.SUCCESS);
186− setInvites(confirmedInvites);
187−
188− if (inviteErrors.length > 0) {
189− return {
190− errors: {
191− [FORM_ERROR]: ['Error sending invites: ', ...inviteErrors],
192− },
193− };
194− }
195−
196− return { invites: confirmedInvites };
197− },
198− [
199− authToken,
200− contracts,
201− gasPrice,
202− networkType,
203− point,
204− syncInvites,
205− wallet,
206− walletHdPath,
207− walletType,
208− web3,
209− ]
210− );
211−
212− return {
213− progress,
214− txStatus,
215− needFunds,
216− invites,
217− generateInvites,
218− resetInvites,
219− };
220−};
221−
222−export default useInviter;
src/lib/useRoller.ts+2−272
@@ -1,9 +1,8 @@
1−import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1+import { useCallback, useEffect, useMemo, useState } from 'react';
22import * as azimuth from 'azimuth-js';
33import * as need from 'lib/need';
44import SecureLS from 'secure-ls';
55
6−import { Invite } from 'lib/types/Invite';
76import {
87 deriveNetworkSeedFromUrbitWallet,
98 attemptNetworkSeedDerivation,
@@ -13,24 +12,17 @@ import { useNetwork } from 'store/network';
1312import { usePointCursor } from 'store/pointCursor';
1413import { EMPTY_POINT, useRollerStore } from 'store/rollerStore';
1514import { useWallet } from 'store/wallet';
16−import {
17− getStoredInvites,
18− setStoredInvite,
19− setStoredInvites,
20−} from 'store/storage/roller';
2115import { usePointCache } from 'store/pointCache';
2216
2317import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
2418import { convertToInt } from './convertToInt';
2519import { isDevelopment, isMainnet, isRopsten } from './flags';
2620import {
27− generateInviteWallet,
28− getPendingSpawns,
2921 submitL2Transaction,
3022 registerProxyAddress,
3123 isL2Spawn,
3224} from './utils/roller';
33−import { POINT_DOMINIONS, ROLLER_HOSTS, TEN_SECONDS } from './constants';
25+import { ROLLER_HOSTS, TEN_SECONDS } from './constants';
3426
3527import {
3628 Config,
@@ -39,7 +31,6 @@ import {
3931 RollerRPCAPI,
4032 Options,
4133 EthAddress,
42− UnspawnedPoints,
4334} from '@urbit/roller-api';
4435
4536import Point, { PointField, Points } from './types/Point';
@@ -60,18 +51,6 @@ interface UpdateParams {
6051 intervalTime?: number;
6152}
6253
63−const inviteTemplate = (
64− planet: number,
65− ticket = '',
66− owner = '',
67− hash = ''
68−): Invite => ({
69− planet,
70− ticket,
71− owner,
72− hash,
73−});
74−
7554export default function useRoller() {
7655 const {
7756 wallet,
@@ -89,22 +68,16 @@ export default function useRoller() {
8968 const {
9069 point,
9170 points,
92− pointList,
9371 nextQuotaTime,
94− addInvite,
9572 setNextBatchTime,
9673 setNextQuotaTime,
9774 setPendingTransactions,
98− setInvites,
99− setInviteGeneratingNum,
100− setInvitesLoading,
10175 setModalText,
10276 setPoints,
10377 updatePoint,
10478 storePendingL1Txn,
10579 deletePendingL1Txn,
10680 } = useRollerStore();
107− const generatingInvites = useRef(false);
10881 const [config, setConfig] = useState<Config | null>(null);
10982
11083 const options: Options = useMemo(() => {
@@ -344,247 +317,6 @@ export default function useRoller() {
344317 ]
345318 );
346319
347− const generateInviteInfo = async (planet: number, _authToken: string) => {
348− const { ticket, inviteWallet } = await generateInviteWallet(
349− planet,
350− _authToken
351− );
352−
353− return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
354− };
355−
356− const getInvites = useCallback(async () => {
357− if (generatingInvites.current) {
358− return;
359− }
360−
361− generatingInvites.current = true;
362− try {
363− setInvitesLoading(true);
364− const curPoint: number = Number(need.point(pointCursor));
365− const _authToken = authToken.getOrElse(null);
366− const _contracts = contracts.getOrElse(null);
367−
368− if (_authToken && _contracts) {
369− const spawnedPoints = point.isL2Spawn
370− ? await api.getSpawned(curPoint)
371− : [];
372−
373− const newPending = await api.getPendingByShip(curPoint);
374− setPendingTransactions(newPending);
375− const pendingSpawns = getPendingSpawns(newPending);
376−
377− const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
378− _contracts,
379− curPoint
380− );
381−
382− const outgoingPoints = pointList
383− .filter(
384− ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
385− )
386− .map(({ value }) => value);
387−
388− const invitePlanets = spawnedPoints
389− .concat(
390− outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
391− )
392− .filter((p: number) => !pendingSpawns.has(p))
393− .sort();
394−
395− setInvites(curPoint, []);
396−
397− const newInvites: Invite[] = [];
398− // Iterate over all of the stored invites, generating wallet info as necessary
399− const storedInvites = getStoredInvites(ls);
400−
401− for (let i = 0; i < invitePlanets.length; i++) {
402− setInviteGeneratingNum(i + 1);
403− const planet = invitePlanets[i];
404− const storedInvite = storedInvites[planet];
405− const invite =
406− storedInvite || (await generateInviteInfo(planet, _authToken));
407−
408− // TODO: check if the invite point's owner still matches the deterministic wallet address
409− const planetInfo = await api.getPoint(planet);
410− setStoredInvite(ls, invite);
411−
412− if (
413− invite.owner.toLowerCase() ===
414− planetInfo.ownership?.owner?.address &&
415− planetInfo.dominion === POINT_DOMINIONS.L2
416− ) {
417− addInvite(curPoint, invite);
418− newInvites.push(invite);
419− }
420−
421− if (storedInvite) {
422− setStoredInvite(ls, invite);
423− }
424− }
425−
426− setInvitesLoading(false);
427− return newInvites;
428− }
429− } catch (error) {
430− console.warn('ERROR GETTING INVITES', error);
431− }
432−
433− generatingInvites.current = false;
434− }, [
435− api,
436− authToken,
437− contracts,
438− pointCursor,
439− ls,
440− point,
441− pointList,
442− addInvite,
443− setInvites,
444− setInvitesLoading,
445− setInviteGeneratingNum,
446− setPendingTransactions,
447− ]);
448−
449− const generateInviteCodes = useCallback(
450− async (numInvites: number) => {
451− const _point = need.point(pointCursor);
452− const _contracts = contracts.getOrElse(null);
453− const _web3 = web3.getOrElse(null);
454− const _wallet = wallet.getOrElse(null);
455− const _authToken = authToken.getOrElse(null);
456− const _details = getDetails(_point);
457−
458− if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
459− // not using need because we want a custom error
460− throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
461− }
462−
463− // TODO: remove when UI disables management proxy from spawning invites
464− // if (Just.hasInstance(authMnemonic)) {
465− // throw new Error(
466− // "Auth key Error: A management mnemonic can't create invite codes"
467− // );
468− // }
469− let planets: UnspawnedPoints = await api.getUnspawned(_point);
470−
471− const pendingTxs = await api.getPendingByShip(_point);
472−
473− const pendingSpawns = getPendingSpawns(pendingTxs);
474−
475− planets = planets.filter((point: number) => !pendingSpawns.has(point));
476−
477− const invites: Invite[] = [];
478−
479− const proxy = point.getSpawnProxy();
480−
481− if (proxy === undefined)
482− throw new Error("Error: Address doesn't match proxy");
483−
484− const nonce = await api.getNonce({
485− ship: _point,
486− proxy,
487− });
488−
489− for (let i = 0; i < numInvites && planets[i]; i++) {
490− setInviteGeneratingNum(i + 1);
491− const planet = planets[i];
492− const nonceInc = i + nonce;
493−
494− const { ticket, inviteWallet } = await generateInviteWallet(
495− planet,
496− _authToken
497− );
498−
499− await submitL2Transaction({
500− api,
501− address: _wallet.address,
502− wallet: _wallet,
503− ship: _point,
504− proxy,
505− nonce: nonceInc,
506− pointToSpawn: planet,
507− type: 'spawn',
508− walletType,
509− web3: _web3,
510− connector,
511− });
512−
513− const networkSeed = await deriveNetworkSeedFromUrbitWallet(
514− inviteWallet,
515− 1
516− );
517− await submitL2Transaction({
518− api,
519− wallet: _wallet,
520− ship: planet,
521− proxy: 'own',
522− nonce: 0,
523− networkSeed,
524− type: 'configureKeys',
525− walletType,
526− web3: _web3,
527− connector,
528− });
529−
530− await registerProxyAddress(
531− api,
532− _wallet,
533− planet,
534− 'own',
535− 'manage',
536− 1,
537− inviteWallet.management.keys.address,
538− walletType,
539− _web3,
540− connector
541− );
542−
543− await submitL2Transaction({
544− api,
545− wallet: _wallet,
546− ship: planet,
547− proxy: 'own',
548− type: 'transferPoint',
549− nonce: 2,
550− address: inviteWallet.ownership.keys.address,
551− walletType,
552− web3: _web3,
553− connector,
554− });
555−
556− invites.push({
557− hash: '',
558− ticket,
559− planet,
560− owner: inviteWallet.ownership.keys.address,
561− });
562− }
563−
564− setStoredInvites(ls, invites);
565− console.log('getting invites from code generation');
566− getInvites();
567− showNotification(
568− `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
569− );
570− },
571− [
572− api,
573− authToken,
574− connector,
575− contracts,
576− pointCursor,
577− point,
578− wallet,
579− walletType,
580− web3,
581− getDetails,
582− ls,
583− getInvites,
584− setInviteGeneratingNum,
585− ]
586− );
587−
588320 const getPoints = useCallback(
589321 async (proxy: Proxy, address: EthAddress) => {
590322 const points: Ship[] =
@@ -1049,9 +781,7 @@ export default function useRoller() {
1049781 checkForUpdates,
1050782 config,
1051783 configureNetworkKeys,
1052− generateInviteCodes,
1053784 getAndUpdatePoint,
1054− getInvites,
1055785 getPoints,
1056786 getPointsDetails,
1057787 getPendingTransactions,
src/lib/useRollerPoller.ts+3−1
@@ -1,6 +1,7 @@
11import { useEffect, useState } from 'react';
22import { useRollerStore } from 'store/rollerStore';
33import { useTimerStore } from 'store/timerStore';
4+import { useInvites } from 'views/Invite/useInvites';
45import { ONE_SECOND, TEN_SECONDS } from './constants';
56import useRoller from './useRoller';
67import { getTimeToNextBatch } from './utils/roller';
@@ -8,7 +9,8 @@ import { getTimeToNextBatch } from './utils/roller';
89export function useRollerPoller() {
910 const { nextBatchTime, setNextBatchTime, setNextRoll } = useTimerStore();
1011 const { point } = useRollerStore();
11− const { api, getInvites, getPendingTransactions } = useRoller();
12+ const { api, getPendingTransactions } = useRoller();
13+ const { getInvites } = useInvites();
1214 const [time, setTime] = useState(ONE_SECOND);
1315
1416 useEffect(() => {
src/store/rollerStore.ts−50
@@ -4,7 +4,6 @@ import { toBN } from 'web3-utils';
44import BN from 'bn.js';
55
66import { HOUR } from 'lib/utils/roller';
7−import { Invite, Invites } from 'lib/types/Invite';
87import Point, { Points } from 'lib/types/Point';
98import { toL1Details } from 'lib/utils/point';
109import { PendingL1, PendingL1Txn } from 'lib/types/PendingL1Transaction';
@@ -30,25 +29,16 @@ export interface RollerStore {
3029 points: Points;
3130 pointList: Point[];
3231 pendingL1ByPoint: PendingL1;
33− invites: Invites;
3432 modalText?: string;
3533 recentlyCompleted: number;
36− inviteGeneratingNum: number;
37− invitesLoading: boolean;
3834 ethBalance: BN;
39− addInvite: (point: number, invite: Invite) => void;
4035 setLoading: (loading: boolean) => void;
41− removeInvite: (point: number, planet: number) => void;
42− setInvites: (points: number, invites: Invite[]) => void;
43− setInviteGeneratingNum: (numGenerating: number) => void;
44− setInvitesLoading: (invitesLoading: boolean) => void;
4536 setModalText: (modalText: string) => void;
4637 setNextBatchTime: (nextBatchTime: number) => void;
4738 setNextQuotaTime: (nextQuotaTime: number) => void;
4839 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
4940 setPoint: (point: number) => void;
5041 setPoints: (points: Points) => void;
51− updateInvite: (point: number, invite: Invite) => void;
5242 updatePoint: (point: Point) => void;
5343 storePendingL1Txn: (txn: PendingL1Txn) => void;
5444 deletePendingL1Txn: (txn: PendingL1Txn) => void;
@@ -64,10 +54,7 @@ export const useRollerStore = create<RollerStore>(set => ({
6454 pointList: [],
6555 points: {},
6656 pendingL1ByPoint: {},
67− invites: {},
6857 recentlyCompleted: 0,
69− inviteGeneratingNum: 0,
70− invitesLoading: false,
7158 ethBalance: toBN(0),
7259 setLoading: (loading: boolean) => set(() => ({ loading })),
7360 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
@@ -88,43 +75,6 @@ export const useRollerStore = create<RollerStore>(set => ({
8875 set(state => ({ point: state.points[point] || EMPTY_POINT })),
8976 setPoints: (points: Points) =>
9077 set(({ point }) => getPointsAndList(points, point)),
91− setInvites: (point: number, invites: Invite[]) =>
92− set(state => {
93− const newInvites: Invites = {};
94− newInvites[point] = invites;
95− return { invites: Object.assign(state.invites, newInvites) };
96− }),
97− setInviteGeneratingNum: (inviteGeneratingNum: number) =>
98− set(() => ({ inviteGeneratingNum })),
99− addInvite: (point: number, invite: Invite) => {
100− set(state => {
101− const newInvites: Invites = {};
102− const invites = state.invites[point];
103−
104− if (!invites.find(inv => inv.planet === invite.planet)) {
105− newInvites[point] = [...invites, invite];
106− }
107− return { invites: Object.assign(state.invites, newInvites) };
108− });
109− },
110− updateInvite: (point: number, invite: Invite) =>
111− set(state => {
112− const newInvites: Invites = {};
113− newInvites[point] = state.invites[point]?.map(i =>
114− i.planet === invite.planet ? invite : i
115− );
116− return { invites: Object.assign(state.invites, newInvites) };
117− }),
118− removeInvite: (point: number, planet: number) =>
119− set(state => {
120− const newInvites: Invites = {};
121− newInvites[point] = state.invites[point]?.filter(
122− i => i.planet !== planet
123− );
124− return { invites: Object.assign(state.invites, newInvites) };
125− }),
126− setInvitesLoading: (invitesLoading: boolean) =>
127− set(() => ({ invitesLoading })),
12878 updatePoint: (newPoint: Point) =>
12979 set(({ point, points }) => {
13080 const newPoints: Points = Object.assign({}, points);
src/views/Invite/Cohort.tsx+41−38
@@ -50,6 +50,12 @@ import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
5050
5151import './Cohort.scss';
5252import Modal from 'components/L2/Modal';
53+import {
54+ InviteGeneratingStatus,
55+ useInvites,
56+ useInviteStore,
57+} from './useInvites';
58+import { GeneratingModal } from './GeneratingModal';
5359
5460interface L1Invite {
5561 ticket: string;
@@ -59,27 +65,16 @@ interface L1Invite {
5965export default function InviteCohort() {
6066 const { pop }: any = useLocalRouter();
6167 const { authToken, walletType }: any = useWallet();
62− const {
63− point,
64− pendingTransactions,
65− invites,
66− inviteGeneratingNum,
67− nextQuotaTime,
68− setInviteGeneratingNum,
69− setInvites,
70− } = useRollerStore();
68+ const { point, pendingTransactions, nextQuotaTime } = useRollerStore();
7169
7270 const { nextRoll } = useTimerStore();
7371
74− const {
75− ls,
76− generateInviteCodes,
77− getPendingTransactions,
78− getInvites,
79− getAndUpdatePoint,
80− } = useRoller();
72+ const { ls, getPendingTransactions, getAndUpdatePoint } = useRoller();
8173 const { contracts }: any = useNetwork();
8274 const { syncControlledPoints }: any = usePointCache();
75+
76+ const { invites, generatingNum, setInvites } = useInviteStore();
77+ const { getInvites, generateInviteCodes } = useInvites();
8378 const currentL2 = Boolean(point.isL2Spawn);
8479
8580 const _contracts = need.contracts(contracts);
@@ -88,6 +83,9 @@ export default function InviteCohort() {
8883 currentL2 ? DEFAULT_NUM_INVITES : 1
8984 );
9085 const [showInviteForm, setShowInviteForm] = useState(false);
86+ const [generatingStatus, setGeneratingStatus] = useState<
87+ InviteGeneratingStatus
88+ >('initial');
9189 const [loading, setLoading] = useState(false);
9290 const [showCsvModal, setShowCsvModal] = useState(false);
9391 const [csvElement, setCsvElement] = useState<HTMLAnchorElement | null>();
@@ -128,7 +126,7 @@ export default function InviteCohort() {
128126 }
129127
130128 if (invitePoint) {
131− setInviteGeneratingNum(1);
129+ useInviteStore.setState({ generatingNum: 1 });
132130 const { ticket, inviteWallet } = await generateInviteWallet(
133131 invitePoint,
134132 _authToken
@@ -157,7 +155,6 @@ export default function InviteCohort() {
157155 showInviteForm,
158156 unconstruct,
159157 invitePoints,
160− setInviteGeneratingNum,
161158 ]);
162159
163160 useEffect(() => {
@@ -192,27 +189,29 @@ export default function InviteCohort() {
192189 const hasInvites = Boolean(invitePoints.length);
193190
194191 const createInvites = useCallback(async () => {
195− setLoading(true);
192+ setGeneratingStatus('generating');
196193 try {
197194 await generateInviteCodes(numInvites);
198195 getPendingTransactions();
199196 getAndUpdatePoint(point.value);
200− pop();
197+ setGeneratingStatus('finished');
201198 } catch (error) {
202− // TODO: error is not used anywhere on the UI
203− setError(error as string);
204− } finally {
205− setLoading(false);
206− setInviteGeneratingNum(0);
199+ if (typeof error === 'object' && (error as Error)?.message) {
200+ setError((error as Error).message);
201+ } else if (typeof error === 'string') {
202+ setError(error);
203+ } else {
204+ setError('Generating invites failed');
205+ }
206+ setGeneratingStatus('errored');
207207 }
208208 }, [
209209 generateInviteCodes,
210210 numInvites,
211211 getPendingTransactions,
212− pop,
213− setInviteGeneratingNum,
214212 getAndUpdatePoint,
215213 point,
214+ setGeneratingStatus,
216215 ]);
217216
218217 const generateCsv = useCallback(() => {
@@ -244,17 +243,10 @@ export default function InviteCohort() {
244243 setError('There was an error creating the CSV');
245244 }
246245 setLoading(false);
247− setInviteGeneratingNum(0);
246+ useInviteStore.setState({ generatingNum: 0 });
248247 };
249248 generateCsvElement();
250− }, [
251− getInvites,
252− setError,
253− setLoading,
254− setInviteGeneratingNum,
255− setShowCsvModal,
256− point,
257− ]);
249+ }, [getInvites, setError, setLoading, setShowCsvModal, point]);
258250
259251 const goNextPage = useCallback(() => {
260252 setPage(page + 1);
@@ -429,7 +421,18 @@ export default function InviteCohort() {
429421 </Box>
430422 </BodyPane>
431423 </Window>
432− <LoadingOverlay loading={loading} />
424+ <GeneratingModal
425+ status={generatingStatus}
426+ current={generatingNum}
427+ total={numInvites}
428+ error={error}
429+ hide={() => {
430+ setGeneratingStatus('initial');
431+ if (generatingStatus !== 'errored') {
432+ pop();
433+ }
434+ }}
435+ />
433436 </View>
434437 );
435438 }
@@ -437,7 +440,7 @@ export default function InviteCohort() {
437440 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
438441 const showAddMoreButton =
439442 (hasInvites || hasPending) && point.canSpawn && point.l2Quota > 0;
440− const generatingCodesText = `Generating ${inviteGeneratingNum} of ${invitePoints.length} codes...`;
443+ const generatingCodesText = `Generating ${generatingNum} of ${invitePoints.length} codes...`;
441444
442445 return (
443446 <View
src/views/Invite/Email.jsdeleted−225
@@ -1,225 +0,0 @@
1−import React, { useState, useCallback, useMemo } from 'react';
2−import { Grid } from 'indigo-react';
3−import { FORM_ERROR } from 'final-form';
4−import * as ob from 'urbit-ob';
5−import { uniq, zip, compact, isEqual } from 'lodash';
6−
7−import { usePointCursor } from 'store/pointCursor';
8−
9−import * as need from 'lib/need';
10−import useMailer from 'lib/useMailer';
11−import { pluralize } from 'lib/pluralize';
12−import useInviter from 'lib/useInviter';
13−
14−import {
15− composeValidator,
16− hasErrors,
17− buildEmailValidator,
18− buildArrayValidator,
19−} from 'form/validators';
20−import EmailChipInput from 'form/EmailChipInput';
21−import FormError from 'form/FormError';
22−import SubmitButton from 'form/SubmitButton';
23−import BridgeForm from 'form/BridgeForm';
24−
25−import NeedFundsNotice from 'components/NeedFundsNotice';
26−import ProgressButton from 'components/ProgressButton';
27−
28−const HAS_RECEIVED_TEXT = 'This email has already received an invite: ';
29−
30−const STATUS = {
31− INPUT: 'INPUT',
32− SENDING: 'SENDING',
33− SUCCESS: 'SUCCESS',
34− FAILURE: 'FAILURE',
35−};
36−
37−const InviteMail = ({ setSubmitting }) => {
38− const {
39− progress,
40− txStatus,
41− needFunds,
42− generateInvites,
43− resetInvites,
44− } = useInviter();
45−
46− const { sendMail, getHasReceived } = useMailer();
47−
48− const [status, setStatus] = useState(STATUS.INPUT);
49−
50− const [count, setCount] = useState(0);
51− const canInput = status === STATUS.INPUT;
52− const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
53− const isDone = status === STATUS.SUCCESS;
54− const isFailed = status === STATUS.FAILED;
55−
56− const { pointCursor } = usePointCursor();
57− const point = need.point(pointCursor);
58−
59− const sendInvites = useCallback(
60− async (emails, invites) => {
61− const mailingErrors = compact(
62− await Promise.all(
63− zip(emails, invites).map(([email, invite]) =>
64− sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
65− .then(() => null)
66− .catch(
67− () => `Sending ticket ${invite.ticket} to ${email} failed.`
68− )
69− )
70− )
71− ).join(', ');
72− if (mailingErrors.length) {
73− return { [FORM_ERROR]: mailingErrors };
74− }
75− },
76− [point, sendMail]
77− );
78−
79− const onSubmit = useCallback(
80− async (values, form) => {
81− const emailCount = values.emails.length;
82− setCount(emailCount);
83− setStatus(STATUS.SENDING);
84− setSubmitting(true);
85− const { errors, invites } = await generateInvites(emailCount);
86− if (errors) {
87− setStatus(STATUS.FAILED);
88− setSubmitting(false);
89− return errors;
90− }
91− const mailErrors = await sendInvites(values.emails, invites);
92− if (mailErrors) {
93− console.log(mailErrors);
94− setStatus(STATUS.FAILED);
95− setSubmitting(false);
96− return mailErrors;
97− }
98− setSubmitting(false);
99− setStatus(STATUS.SUCCESS);
100− setTimeout(() => {
101− setStatus(STATUS.INPUT);
102− resetInvites();
103− form.reset();
104− }, 1500);
105− },
106− [setSubmitting, generateInvites, sendInvites, resetInvites]
107− );
108−
109− const validateHasReceived = useCallback(
110− async email => {
111− const hasReceived = await getHasReceived(email);
112− if (hasReceived) {
113− return HAS_RECEIVED_TEXT + email;
114− }
115− },
116− [getHasReceived]
117− );
118−
119− const buttonText = useCallback(() => {
120− switch (status) {
121− case STATUS.INPUT:
122− return 'Add to Invite Group';
123− case STATUS.SENDING:
124− return `Sending ${pluralize(count, 'Invite')}...`;
125− case STATUS.SUCCESS:
126− return `✓ ${pluralize(count, 'Invite')} sent`;
127− case STATUS.FAILURE:
128− default:
129− return 'Error';
130− }
131− }, [status, count]);
132−
133− const renderButton = useCallback(
134− handleSubmit => {
135− if (canInput) {
136− return (
137− <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
138− {buttonText()}
139− </Grid.Item>
140− );
141− }
142−
143− if (isSending) {
144− const percentProgress = progress / count;
145− return (
146− <Grid.Item
147− full
148− as={ProgressButton}
149− className="mt4"
150− solid
151− disabled
152− success
153− progress={percentProgress}>
154− {buttonText(count)}
155− </Grid.Item>
156− );
157− }
158− if (isDone) {
159− return (
160− <Grid.Item full className="mt4 p3 bg-green3 white f5">
161− {buttonText(count)}
162− </Grid.Item>
163− );
164− }
165− if (isFailed) {
166− return (
167− <Grid.Item full className="mt4 p3 bg-red3 white f5">
168− {buttonText(count)}
169− </Grid.Item>
170− );
171− }
172−
173− return null;
174− },
175− [buttonText, isDone, isFailed, isSending, canInput, count, progress]
176− );
177−
178− const validateForm = useCallback((values, errors) => {
179− if (hasErrors(errors)) {
180− return errors;
181− }
182−
183− // check for email uniqenesss
184− const emails = values.emails.filter(d => !!d);
185− if (uniq(emails).length !== emails.length) {
186− return { [FORM_ERROR]: 'Duplicate email.' };
187− }
188− }, []);
189−
190− const validate = useMemo(
191− () =>
192− composeValidator(
193− {
194− emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
195− },
196− validateForm
197− ),
198− [validateHasReceived, validateForm]
199− );
200−
201− const initialValues = { emails: [] };
202− return (
203− <Grid.Item full as={Grid}>
204− <BridgeForm
205− validate={validate}
206− initialValues={initialValues}
207− initialValuesEqual={isEqual}
208− onSubmit={onSubmit}>
209− {({ handleSubmit, values }) => (
210− <>
211− <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
212− {renderButton(handleSubmit)}
213−
214− <Grid.Item full as={FormError} />
215− {needFunds && (
216− <Grid.Item full as={NeedFundsNotice} {...needFunds} />
217− )}
218− </>
219− )}
220− </BridgeForm>
221− </Grid.Item>
222− );
223−};
224−
225−export default InviteMail;
src/views/Invite/GeneratingModal.tsxnew file+86
@@ -0,0 +1,86 @@
1+import { Col, H3, LoadingSpinner, Row, Text } from '@tlon/indigo-react';
2+import Modal from 'components/L2/Modal';
3+import { Button } from 'indigo-react';
4+import { InviteGeneratingStatus } from './useInvites';
5+
6+interface GeneratingModalProps {
7+ status: InviteGeneratingStatus;
8+ current: number;
9+ total: number;
10+ error?: string;
11+ hide: () => void;
12+}
13+
14+export const GeneratingModal = ({
15+ status,
16+ current,
17+ total,
18+ hide,
19+ error,
20+}: GeneratingModalProps) => {
21+ const successfulInvites = current > 1 ? current - 1 : 0;
22+ return (
23+ <Modal
24+ show={status !== 'initial'}
25+ hide={hide}
26+ hideClose={status !== 'finished'}>
27+ <Col width="350px">
28+ {status === 'generating' && (
29+ <>
30+ <Row alignItems="center" marginBottom={2}>
31+ <LoadingSpinner dark />
32+ <H3 marginLeft={2}>
33+ Generating {current} of {total} Invites
34+ </H3>
35+ </Row>
36+ <Text>
37+ Each invite will require four transactions to be signed, unless
38+ your point is owned by a master ticket.
39+ </Text>
40+ </>
41+ )}
42+ {status === 'finished' && (
43+ <>
44+ <H3 marginBottom={2}>
45+ <span className="ph1 number-emphasis">{total}</span> Invites
46+ Generated
47+ </H3>
48+ <Text marginBottom={4}>
49+ Your invites have been sent to the roller for confirmation in the
50+ next batch. Check the point's page to see its status.
51+ </Text>
52+ {/*
53+ // @ts-ignore */}
54+ <Button solid onClick={hide}>
55+ Check Status
56+ </Button>
57+ </>
58+ )}
59+ {status === 'errored' && (
60+ <>
61+ <H3 marginBottom={2}>
62+ There was an error while generating your invites:
63+ </H3>
64+ {error && (
65+ <Text color="red" mono marginBottom={3}>
66+ {error}
67+ </Text>
68+ )}
69+ {!!successfulInvites && (
70+ <Text marginY={3}>
71+ At least {successfulInvites} invite
72+ {successfulInvites > 1 ? 's' : ''} were successfully generated.
73+ Go back to see them or try again.
74+ </Text>
75+ )}
76+ {/*
77+ // @ts-ignore */}
78+ <Button solid onClick={hide}>
79+ Back
80+ </Button>
81+ </>
82+ )}
83+ </Col>
84+ </Modal>
85+ );
86+};
src/views/Invite/Inviter.jsdeleted−54
@@ -1,54 +0,0 @@
1−import React, { useState, useCallback } from 'react';
2−import { Grid } from 'indigo-react';
3−
4−import { useConditionalBlockWindowClose } from 'lib/useBlockWindowClose';
5−
6−import Tabs from 'components/Tabs';
7−
8−import InviteMail from 'views/Invite/Email';
9−import InviteUrl from 'views/Invite/Url';
10−
11−const Inviter = () => {
12− const [tab, setTab] = useState(NAMES.MAIL);
13− const [submitting, setSubmitting] = useState(false);
14−
15− useConditionalBlockWindowClose(submitting);
16− const onTabChange = useCallback(
17− newTab => {
18− if (!submitting) {
19− setTab(newTab);
20− }
21− },
22− [submitting, setTab]
23− );
24− return (
25− <Grid.Item
26− full
27− as={Tabs}
28− center
29− className="flex1"
30− views={VIEWS}
31− options={OPTIONS}
32− currentTab={tab}
33− onTabChange={onTabChange}
34− setSubmitting={setSubmitting}
35− />
36− );
37−};
38−
39−const NAMES = {
40− MAIL: 'MAIL',
41− URL: 'URL',
42−};
43−
44−const VIEWS = {
45− [NAMES.MAIL]: InviteMail,
46− [NAMES.URL]: InviteUrl,
47−};
48−
49−const OPTIONS = [
50− { text: 'Email', value: NAMES.MAIL },
51− { text: 'URL', value: NAMES.URL },
52−];
53−
54−export default Inviter;
src/views/Invite/Url.jsdeleted−108
@@ -1,108 +0,0 @@
1−import React, { useCallback, useState } from 'react';
2−import { Grid, ErrorText, Flex } from 'indigo-react';
3−import { get } from 'lodash';
4−import { FORM_ERROR } from 'final-form';
5−
6−import useCopiable from 'lib/useCopiable';
7−import useInviter from 'lib/useInviter';
8−
9−import ProgressButton from 'components/ProgressButton';
10−import { ForwardButton } from 'components/Buttons';
11−import NeedFundsNotice from 'components/NeedFundsNotice';
12−
13−import { hasErrors } from 'form/validators';
14−import { WARNING } from 'form/helpers';
15−const STATUS = {
16− INPUT: 'INPUT',
17− SENDING: 'SENDING',
18− SUCCESS: 'SUCCESS',
19− FAILURE: 'FAILURE',
20−};
21−
22−const InviteUrl = ({ setSubmitting }) => {
23− const { txStatus, needFunds, invites, generateInvites } = useInviter();
24−
25− const [error, setError] = useState();
26−
27− const generateInvite = useCallback(async () => {
28− setSubmitting(true);
29− const { errors } = await generateInvites(1);
30− if (hasErrors(errors)) {
31− setError(errors);
32− }
33− setSubmitting(false);
34− }, [generateInvites, setSubmitting]);
35−
36− const renderGenerateButton = useCallback(() => {
37− if (error) {
38− return (
39− <Grid.Item full className="mt4 p3 bg-red3 white f5">
40− Error generating invite
41− </Grid.Item>
42− );
43− }
44− switch (txStatus) {
45− case STATUS.INPUT:
46− return (
47− <Grid.Item
48− solid
49− full
50− className="mt4"
51− as={ForwardButton}
52− onClick={() => generateInvite()}>
53− Generate Invite URL
54− </Grid.Item>
55− );
56− case STATUS.SENDING:
57− return (
58− <Grid.Item
59− full
60− as={ProgressButton}
61− className="mt4"
62− solid
63− disabled
64− success
65− progress={0.5}>
66− Generating Invite URL
67− </Grid.Item>
68− );
69− default:
70− return null;
71− }
72− }, [txStatus, error, generateInvite]);
73−
74− const url = `https://bridge.urbit.org/#${get(invites, '[0].ticket', '').slice(
75− 1
76− )}`;
77−
78− const [doCopy, didCopy] = useCopiable(url);
79−
80− return (
81− <Grid.Item full as={Grid} gap={3}>
82− {invites.length > 0 && (
83− <Grid.Item as={Flex} full col>
84− <Flex.Item full className="b-gray3 b1 mt4 mb1 p1 flex flex-center">
85− <div className="flex1 ml1 f6">{url}</div>
86− <Flex.Item
87− className="p2 bg-black white pointer-hover f6"
88− onClick={() => doCopy()}>
89− {didCopy ? 'Copied' : 'Copy'}
90− </Flex.Item>
91− </Flex.Item>
92− <Flex.Item full className="f6 mb4 gray5">
93− Your invite link expires after consumption.
94− </Flex.Item>
95− </Grid.Item>
96− )}
97− {invites.length === 0 && renderGenerateButton()}
98− {needFunds && <Grid.Item full as={NeedFundsNotice} {...needFunds} />}
99− {error && (
100− <Grid.Item as={ErrorText}>
101− {error[FORM_ERROR] || error[WARNING]}
102− </Grid.Item>
103− )}
104− </Grid.Item>
105− );
106−};
107−
108−export default InviteUrl;
src/views/Invite/useInvites.tsnew file+335
@@ -0,0 +1,335 @@
1+import { UnspawnedPoints } from '@urbit/roller-api';
2+import { POINT_DOMINIONS } from 'lib/constants';
3+import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
4+import * as need from 'lib/need';
5+import * as azimuth from 'azimuth-js';
6+import { Invite, Invites } from 'lib/types/Invite';
7+import useRoller from 'lib/useRoller';
8+import { useWalletConnect } from 'lib/useWalletConnect';
9+import { showNotification } from 'lib/utils/notifications';
10+import {
11+ generateInviteWallet,
12+ getPendingSpawns,
13+ submitL2Transaction,
14+ registerProxyAddress,
15+} from 'lib/utils/roller';
16+import { useCallback, useRef } from 'react';
17+import { useNetwork } from 'store/network';
18+import { usePointCursor } from 'store/pointCursor';
19+import { useRollerStore } from 'store/rollerStore';
20+import {
21+ getStoredInvites,
22+ setStoredInvite,
23+ setStoredInvites,
24+} from 'store/storage/roller';
25+import { useWallet } from 'store/wallet';
26+import { usePointCache } from 'store/pointCache';
27+import create from 'zustand';
28+
29+const inviteTemplate = (
30+ planet: number,
31+ ticket = '',
32+ owner = '',
33+ hash = ''
34+): Invite => ({
35+ planet,
36+ ticket,
37+ owner,
38+ hash,
39+});
40+
41+export type InviteGeneratingStatus =
42+ | 'initial'
43+ | 'generating'
44+ | 'finished'
45+ | 'errored';
46+
47+interface InviteStore {
48+ invites: Invites;
49+ generatingStatus: InviteGeneratingStatus;
50+ generatingNum: number;
51+ loading: boolean;
52+ addInvite: (point: number, invite: Invite) => void;
53+ setInvites: (points: number, invites: Invite[]) => void;
54+}
55+
56+export const useInviteStore = create<InviteStore>((set, get) => ({
57+ invites: {},
58+ generatingStatus: 'initial',
59+ generatingNum: 0,
60+ loading: false,
61+ addInvite: (point: number, invite: Invite) => {
62+ set(state => {
63+ const newInvites: Invites = {};
64+ const invites = state.invites[point];
65+
66+ if (!invites.find(inv => inv.planet === invite.planet)) {
67+ newInvites[point] = [...invites, invite];
68+ }
69+ return { invites: Object.assign(state.invites, newInvites) };
70+ });
71+ },
72+ setInvites: (point: number, invites: Invite[]) =>
73+ set(state => {
74+ const newInvites: Invites = {};
75+ newInvites[point] = invites;
76+ return { invites: Object.assign(state.invites, newInvites) };
77+ }),
78+}));
79+
80+export function useInvites() {
81+ const { wallet, authToken, walletType }: any = useWallet();
82+ const { connector } = useWalletConnect();
83+ const { pointCursor }: any = usePointCursor();
84+ const { web3, contracts }: any = useNetwork();
85+ const allPoints: any = usePointCache();
86+ const getDetails = allPoints?.getDetails;
87+ const { api, ls } = useRoller();
88+ const { point, pointList, setPendingTransactions } = useRollerStore();
89+ const { addInvite, setInvites } = useInviteStore();
90+ const updatingInvites = useRef(false);
91+
92+ const generateInviteInfo = async (planet: number, _authToken: string) => {
93+ const { ticket, inviteWallet } = await generateInviteWallet(
94+ planet,
95+ _authToken
96+ );
97+
98+ return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
99+ };
100+
101+ const getInvites = useCallback(async () => {
102+ if (updatingInvites.current) {
103+ return;
104+ }
105+
106+ updatingInvites.current = true;
107+ try {
108+ useInviteStore.setState({ loading: true });
109+ const curPoint: number = Number(need.point(pointCursor));
110+ const _authToken = authToken.getOrElse(null);
111+ const _contracts = contracts.getOrElse(null);
112+
113+ if (_authToken && _contracts) {
114+ const spawnedPoints = point.isL2Spawn
115+ ? await api.getSpawned(curPoint)
116+ : [];
117+
118+ const newPending = await api.getPendingByShip(curPoint);
119+ setPendingTransactions(newPending);
120+ const pendingSpawns = getPendingSpawns(newPending);
121+
122+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
123+ _contracts,
124+ curPoint
125+ );
126+
127+ const outgoingPoints = pointList
128+ .filter(
129+ ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
130+ )
131+ .map(({ value }) => value);
132+
133+ const invitePlanets = spawnedPoints
134+ .concat(
135+ outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
136+ )
137+ .filter((p: number) => !pendingSpawns.has(p))
138+ .sort();
139+
140+ setInvites(curPoint, []);
141+
142+ const newInvites: Invite[] = [];
143+ // Iterate over all of the stored invites, generating wallet info as necessary
144+ const storedInvites = getStoredInvites(ls);
145+
146+ for (let i = 0; i < invitePlanets.length; i++) {
147+ useInviteStore.setState({ generatingNum: i + 1 });
148+ const planet = invitePlanets[i];
149+ const storedInvite = storedInvites[planet];
150+ const invite =
151+ storedInvite || (await generateInviteInfo(planet, _authToken));
152+
153+ // TODO: check if the invite point's owner still matches the deterministic wallet address
154+ const planetInfo = await api.getPoint(planet);
155+ setStoredInvite(ls, invite);
156+
157+ if (
158+ invite.owner.toLowerCase() ===
159+ planetInfo.ownership?.owner?.address &&
160+ planetInfo.dominion === POINT_DOMINIONS.L2
161+ ) {
162+ addInvite(curPoint, invite);
163+ newInvites.push(invite);
164+ }
165+
166+ if (storedInvite) {
167+ setStoredInvite(ls, invite);
168+ }
169+ }
170+
171+ useInviteStore.setState({ loading: false, generatingNum: 0 });
172+ return newInvites;
173+ }
174+ } catch (error) {
175+ console.warn('ERROR GETTING INVITES', error);
176+ useInviteStore.setState({ loading: false, generatingNum: 0 });
177+ }
178+
179+ updatingInvites.current = false;
180+ }, [
181+ api,
182+ authToken,
183+ contracts,
184+ pointCursor,
185+ ls,
186+ point,
187+ pointList,
188+ addInvite,
189+ setInvites,
190+ setPendingTransactions,
191+ ]);
192+
193+ const generateInviteCodes = useCallback(
194+ async (numInvites: number) => {
195+ const _point = need.point(pointCursor);
196+ const _contracts = contracts.getOrElse(null);
197+ const _web3 = web3.getOrElse(null);
198+ const _wallet = wallet.getOrElse(null);
199+ const _authToken = authToken.getOrElse(null);
200+ const _details = getDetails(_point);
201+
202+ if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
203+ // not using need because we want a custom error
204+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
205+ }
206+
207+ // TODO: remove when UI disables management proxy from spawning invites
208+ // if (Just.hasInstance(authMnemonic)) {
209+ // throw new Error(
210+ // "Auth key Error: A management mnemonic can't create invite codes"
211+ // );
212+ // }
213+ let planets: UnspawnedPoints = await api.getUnspawned(_point);
214+
215+ const pendingTxs = await api.getPendingByShip(_point);
216+
217+ const pendingSpawns = getPendingSpawns(pendingTxs);
218+
219+ planets = planets.filter((point: number) => !pendingSpawns.has(point));
220+
221+ const invites: Invite[] = [];
222+
223+ const proxy = point.getSpawnProxy();
224+
225+ if (proxy === undefined)
226+ throw new Error("Error: Address doesn't match proxy");
227+
228+ const nonce = await api.getNonce({
229+ ship: _point,
230+ proxy,
231+ });
232+
233+ for (let i = 0; i < numInvites && planets[i]; i++) {
234+ useInviteStore.setState({ generatingNum: i + 1 });
235+ const planet = planets[i];
236+ const nonceInc = i + nonce;
237+
238+ const { ticket, inviteWallet } = await generateInviteWallet(
239+ planet,
240+ _authToken
241+ );
242+
243+ await submitL2Transaction({
244+ api,
245+ address: _wallet.address,
246+ wallet: _wallet,
247+ ship: _point,
248+ proxy,
249+ nonce: nonceInc,
250+ pointToSpawn: planet,
251+ type: 'spawn',
252+ walletType,
253+ web3: _web3,
254+ connector,
255+ });
256+
257+ const networkSeed = await deriveNetworkSeedFromUrbitWallet(
258+ inviteWallet,
259+ 1
260+ );
261+ await submitL2Transaction({
262+ api,
263+ wallet: _wallet,
264+ ship: planet,
265+ proxy: 'own',
266+ nonce: 0,
267+ networkSeed,
268+ type: 'configureKeys',
269+ walletType,
270+ web3: _web3,
271+ connector,
272+ });
273+
274+ await registerProxyAddress(
275+ api,
276+ _wallet,
277+ planet,
278+ 'own',
279+ 'manage',
280+ 1,
281+ inviteWallet.management.keys.address,
282+ walletType,
283+ _web3,
284+ connector
285+ );
286+
287+ await submitL2Transaction({
288+ api,
289+ wallet: _wallet,
290+ ship: planet,
291+ proxy: 'own',
292+ type: 'transferPoint',
293+ nonce: 2,
294+ address: inviteWallet.ownership.keys.address,
295+ walletType,
296+ web3: _web3,
297+ connector,
298+ });
299+
300+ invites.push({
301+ hash: '',
302+ ticket,
303+ planet,
304+ owner: inviteWallet.ownership.keys.address,
305+ });
306+ }
307+
308+ setStoredInvites(ls, invites);
309+ console.log('getting invites from code generation');
310+ getInvites();
311+ showNotification(
312+ `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
313+ );
314+ },
315+ [
316+ api,
317+ authToken,
318+ connector,
319+ contracts,
320+ pointCursor,
321+ point,
322+ wallet,
323+ walletType,
324+ web3,
325+ getDetails,
326+ ls,
327+ getInvites,
328+ ]
329+ );
330+
331+ return {
332+ getInvites,
333+ generateInviteCodes,
334+ };
335+}
src/views/Point/Point.tsx+5−14
@@ -25,18 +25,15 @@ import PendingTransaction from 'components/L2/PendingTransaction';
2525import Modal from 'components/L2/Modal';
2626import './Point.scss';
2727import { TransactionRow } from 'views/Transactions/TransactionRow';
28+import { useInvites } from 'views/Invite/useInvites';
2829
2930export default function Point() {
3031 const { pop, push, names }: any = useLocalRouter();
3132 const { wallet }: any = useWallet();
3233 const { syncExtras }: any = usePointCache();
33− const { getInvites, getPendingTransactions } = useRoller();
34− const {
35− pendingTransactions,
36− point,
37− invites,
38− pendingL1ByPoint,
39− } = useRollerStore();
34+ const { getPendingTransactions } = useRoller();
35+ const { getInvites } = useInvites();
36+ const { pendingTransactions, point, pendingL1ByPoint } = useRollerStore();
4037 const { nextRoll } = useTimerStore();
4138 const networkKeysSet = useHasNetworkKeysSet();
4239 const [seenMissingKeys, setSeenMissingKeys] = useSeenMissingKeys();
@@ -111,13 +108,7 @@ export default function Point() {
111108 inset
112109 className="point"
113110 hideBack
114− header={
115− <L2PointHeader
116− hideTimer={!!spawnedPending}
117− numInvites={invites[point.value]?.length}
118− hideInvites={!networkKeysSet}
119− />
120− }>
111+ header={<L2PointHeader hideInvites={!networkKeysSet} />}>
121112 <Greeting point={point.value} />
122113 {!!spawnedPending && (
123114 <PendingTransaction
src/views/Points.tsx+3−7
@@ -175,7 +175,8 @@ export default function Points() {
175175
176176 // If the user has any stars or galaxies, give a polite heads up that
177177 // it can take some time for the roller be aware of L1 TXs
178− const showMigrateNotice = !displayEmptyState &&
178+ const showMigrateNotice =
179+ !displayEmptyState &&
179180 allPoints.some(p => isStar(p.value) || isGalaxy(p.value));
180181
181182 const starReleasing = starReleaseDetails
@@ -255,12 +256,7 @@ export default function Points() {
255256 pop={pop}
256257 hideBack
257258 header={
258− <L2PointHeader
259− hideHome
260− hideTimer
261− hideInvites
262− showMigrate={!displayEmptyState}
263− />
259+ <L2PointHeader hideHome hideInvites showMigrate={!displayEmptyState} />
264260 }>
265261 <NavHeader>
266262 <CopiableAddress