on a2eaa1f help sign in
bridge/src/views/Invite/useInvites.ts
372 lines 10 kB history raw
1import { UnspawnedPoints } from '@urbit/roller-api';
2import { POINT_DOMINIONS } from 'lib/constants';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
4import * as need from 'lib/need';
5import * as azimuth from 'azimuth-js';
6import { Invite, Invites } from 'lib/types/Invite';
7import useRoller from 'lib/useRoller';
8import { useWalletConnect } from 'lib/useWalletConnect';
9import { showNotification } from 'lib/utils/notifications';
10import {
11 generateInviteWallet,
12 getPendingSpawns,
13 submitL2Transaction,
14 registerProxyAddress,
15} from 'lib/utils/roller';
16import { useCallback, useRef } from 'react';
17import { useNetwork } from 'store/network';
18import { usePointCursor } from 'store/pointCursor';
19import { useRollerStore } from 'store/rollerStore';
20import {
21 getStoredInvites,
22 setStoredInvite,
23 setStoredInvites,
24} from 'store/storage/roller';
25import { useWallet } from 'store/wallet';
26import { usePointCache } from 'store/pointCache';
27import create from 'zustand';
28import { generateCsvLine } from 'lib/utils/invite';
30const inviteTemplate = (
31 planet: number,
32 ticket = '',
33 owner = '',
34 hash = ''
35): Invite => ({
36 planet,
37 ticket,
38 owner,
39 hash,
40});
42export type InviteGeneratingStatus =
43 | 'initial'
44 | 'generating'
45 | 'finished'
46 | 'errored';
48interface InviteJob {
49 generatingNum: number;
50 status: InviteGeneratingStatus;
53interface InviteStore {
54 invites: Invites;
55 inviteJobs: {
56 [point: number]: InviteJob;
57 };
58 addInvite: (point: number, invite: Invite) => void;
59 setInvites: (points: number, invites: Invite[]) => void;
60 updateJob: (point: number, updates: Partial<InviteJob>) => void;
63export const useInviteStore = create<InviteStore>((set, get) => ({
64 invites: {},
65 inviteJobs: {},
66 addInvite: (point: number, invite: Invite) => {
67 set(state => {
68 const newInvites: Invites = {};
69 const invites = state.invites[point];
71 if (!invites.find(inv => inv.planet === invite.planet)) {
72 newInvites[point] = [...invites, invite];
73 }
74 return { invites: Object.assign(state.invites, newInvites) };
75 });
76 },
77 setInvites: (point: number, invites: Invite[]) =>
78 set(state => {
79 const newInvites: Invites = {};
80 newInvites[point] = invites;
81 return { invites: Object.assign(state.invites, newInvites) };
82 }),
83 updateJob: (point: number, updates: Partial<InviteJob>) => {
84 debugger;
85 const jobs = get().inviteJobs;
86 const job = jobs[point] || { status: 'initial', generatingNum: 0 };
87 const newJob = Object.assign({}, job, updates);
88 jobs[point] = newJob;
90 //console.log({ point, oldJob: job, updates, newJob });
92 set({ inviteJobs: jobs });
93 },
94}));
96export function useInvites() {
97 const { wallet, authToken, walletType }: any = useWallet();
98 const { connector } = useWalletConnect();
99 const { pointCursor }: any = usePointCursor();
100 const { web3, contracts }: any = useNetwork();
101 const allPoints: any = usePointCache();
102 const getDetails = allPoints?.getDetails;
103 const { api, ls } = useRoller();
104 const { point, pointList, setPendingTransactions } = useRollerStore();
105 const {
106 invites,
107 inviteJobs,
108 addInvite,
109 setInvites,
110 updateJob,
111 } = useInviteStore();
113 const generateInviteInfo = async (planet: number, _authToken: string) => {
114 const { ticket, inviteWallet } = await generateInviteWallet(
115 planet,
116 _authToken
117 );
119 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
120 };
122 const getInvites = useCallback(async () => {
123 const inviteJob = inviteJobs[point.value];
124 if (inviteJob?.status === 'generating') {
125 return;
126 }
128 try {
129 updateJob(point.value, { status: 'generating' });
131 const curPoint: number = Number(need.point(pointCursor));
132 const _authToken = authToken.getOrElse(null);
133 const _contracts = contracts.getOrElse(null);
135 if (_authToken && _contracts) {
136 const spawnedPoints = point.isL2Spawn
137 ? await api.getSpawned(curPoint)
138 : [];
140 const newPending = await api.getPendingByShip(curPoint);
141 setPendingTransactions(newPending);
142 const pendingSpawns = getPendingSpawns(newPending);
144 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
145 _contracts,
146 curPoint
147 );
149 const outgoingPoints = pointList
150 .filter(
151 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
152 )
153 .map(({ value }) => value);
155 const invitePlanets = spawnedPoints
156 .concat(
157 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
158 )
159 .filter((p: number) => !pendingSpawns.has(p))
160 .sort();
162 setInvites(curPoint, []);
164 const newInvites: Invite[] = [];
165 // Iterate over all of the stored invites, generating wallet info as necessary
166 const storedInvites = getStoredInvites(ls);
168 for (let i = 0; i < invitePlanets.length; i++) {
169 updateJob(point.value, { generatingNum: i + 1 });
170 const planet = invitePlanets[i];
171 const storedInvite = storedInvites[planet];
172 const invite =
173 storedInvite || (await generateInviteInfo(planet, _authToken));
175 // TODO: check if the invite point's owner still matches the deterministic wallet address
176 const planetInfo = await api.getPoint(planet);
177 setStoredInvite(ls, invite);
179 if (
180 invite.owner.toLowerCase() ===
181 planetInfo.ownership?.owner?.address &&
182 planetInfo.dominion === POINT_DOMINIONS.L2
183 ) {
184 addInvite(curPoint, invite);
185 newInvites.push(invite);
186 }
188 if (storedInvite) {
189 setStoredInvite(ls, invite);
190 }
191 }
193 updateJob(point.value, { status: 'finished', generatingNum: 0 });
194 return newInvites;
195 }
196 } catch (error) {
197 console.warn('ERROR GETTING INVITES', error);
198 updateJob(point.value, { status: 'errored', generatingNum: 0 });
199 }
200 }, [
201 api,
202 authToken,
203 contracts,
204 pointCursor,
205 ls,
206 point,
207 pointList,
208 addInvite,
209 setInvites,
210 setPendingTransactions,
211 updateJob,
212 ]);
214 const generateInviteCodes = useCallback(
215 async (numInvites: number) => {
216 const _point = need.point(pointCursor);
217 const _contracts = contracts.getOrElse(null);
218 const _web3 = web3.getOrElse(null);
219 const _wallet = wallet.getOrElse(null);
220 const _authToken = authToken.getOrElse(null);
221 const _details = getDetails(_point);
223 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
224 // not using need because we want a custom error
225 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
226 }
228 // TODO: remove when UI disables management proxy from spawning invites
229 // if (Just.hasInstance(authMnemonic)) {
230 // throw new Error(
231 // "Auth key Error: A management mnemonic can't create invite codes"
232 // );
233 // }
234 let planets: UnspawnedPoints = await api.getUnspawned(_point);
236 const pendingTxs = await api.getPendingByShip(_point);
238 const pendingSpawns = getPendingSpawns(pendingTxs);
240 planets = planets.filter((point: number) => !pendingSpawns.has(point));
242 const invites: Invite[] = [];
244 const proxy = point.getSpawnProxy();
246 if (proxy === undefined)
247 throw new Error("Error: Address doesn't match proxy");
249 const nonce = await api.getNonce({
250 ship: _point,
251 proxy,
252 });
254 for (let i = 0; i < numInvites && planets[i]; i++) {
255 updateJob(point.value, { generatingNum: i + 1 });
256 const planet = planets[i];
257 const nonceInc = i + nonce;
259 const { ticket, inviteWallet } = await generateInviteWallet(
260 planet,
261 _authToken
262 );
264 await submitL2Transaction({
265 api,
266 address: _wallet.address,
267 wallet: _wallet,
268 ship: _point,
269 proxy,
270 nonce: nonceInc,
271 pointToSpawn: planet,
272 type: 'spawn',
273 walletType,
274 web3: _web3,
275 connector,
276 });
278 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
279 inviteWallet,
280 1
281 );
282 await submitL2Transaction({
283 api,
284 wallet: _wallet,
285 ship: planet,
286 proxy: 'own',
287 nonce: 0,
288 networkSeed,
289 type: 'configureKeys',
290 walletType,
291 web3: _web3,
292 connector,
293 });
295 await registerProxyAddress(
296 api,
297 _wallet,
298 planet,
299 'own',
300 'manage',
301 1,
302 inviteWallet.management.keys.address,
303 walletType,
304 _web3,
305 connector
306 );
308 await submitL2Transaction({
309 api,
310 wallet: _wallet,
311 ship: planet,
312 proxy: 'own',
313 type: 'transferPoint',
314 nonce: 2,
315 address: inviteWallet.ownership.keys.address,
316 walletType,
317 web3: _web3,
318 connector,
319 });
321 invites.push({
322 hash: '',
323 ticket,
324 planet,
325 owner: inviteWallet.ownership.keys.address,
326 });
327 }
329 setStoredInvites(ls, invites);
330 console.log('getting invites from code generation');
331 getInvites();
332 showNotification(
333 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
334 );
335 },
336 [
337 api,
338 authToken,
339 connector,
340 contracts,
341 pointCursor,
342 point,
343 wallet,
344 walletType,
345 web3,
346 getDetails,
347 ls,
348 getInvites,
349 updateJob,
350 ]
351 );
353 const generateCsv = useCallback(() => {
354 const pointInvites = invites[point.value];
356 if (!pointInvites) {
357 throw new Error('There was an error creating the CSV');
358 }
360 return pointInvites.reduce(
361 (csvData, { ticket, planet }, ind) =>
362 (csvData += generateCsvLine(ind, ticket, planet)),
363 'Number,Planet,Invite URL,Point,Ticket\n'
364 );
365 }, [invites, point]);
367 return {
368 getInvites,
369 generateCsv,
370 generateInviteCodes,
371 };