on df0c310 help sign in
bridge/src/views/Invite/useInvites.ts
378 lines 11 kB history raw
1import { SpawnedPoints, 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 } from 'react';
17import { useNetwork } from 'store/network';
18import { usePointCursor } from 'store/pointCursor';
19import { useRollerStore } from 'store/rollerStore';
20import {
21 getStoredInvites,
22 removeStoredInvite,
23 setStoredInvite,
24 setStoredInvites,
25} from 'store/storage/roller';
26import { useWallet } from 'store/wallet';
27import { usePointCache } from 'store/pointCache';
28import create from 'zustand';
29import { generateCsvLine } from 'lib/utils/invite';
30import Point from 'lib/types/Point';
32const inviteTemplate = (
33 planet: number,
34 ticket = '',
35 owner = '',
36 hash = ''
37): Invite => ({
38 planet,
39 ticket,
40 owner,
41 hash,
42});
44export type InviteGeneratingStatus =
45 | 'initial'
46 | 'generating'
47 | 'finished'
48 | 'errored';
50interface InviteJob {
51 generatingNum: number;
52 status: InviteGeneratingStatus;
55interface InviteStore {
56 invites: Invites;
57 inviteJobs: {
58 [point: number]: InviteJob;
59 };
60 addInvite: (point: number, invite: Invite) => void;
61 setInvites: (points: number, invites: Invite[]) => void;
62 updateJob: (point: number, updates: Partial<InviteJob>) => void;
65export const useInviteStore = create<InviteStore>((set, get) => ({
66 invites: {},
67 inviteJobs: {},
68 addInvite: (point: number, invite: Invite) => {
69 set(state => {
70 const newInvites: Invites = {};
71 const invites = state.invites[point];
73 if (!invites.find(inv => inv.planet === invite.planet)) {
74 newInvites[point] = [...invites, invite];
75 }
76 return { invites: Object.assign(state.invites, newInvites) };
77 });
78 },
79 setInvites: (point: number, invites: Invite[]) =>
80 set(state => {
81 const newInvites: Invites = {};
82 newInvites[point] = invites;
83 return { invites: Object.assign(state.invites, newInvites) };
84 }),
85 updateJob: (point: number, updates: Partial<InviteJob>) => {
86 const jobs = get().inviteJobs;
87 const job = jobs[point] || { status: 'initial', generatingNum: 0 };
88 const newJob = Object.assign({}, job, updates);
89 jobs[point] = newJob;
91 set({ inviteJobs: jobs });
92 },
93}));
95export function useInvites() {
96 const { wallet, authToken, walletType }: any = useWallet();
97 const { connector } = useWalletConnect();
98 const { pointCursor }: any = usePointCursor();
99 const { web3, contracts }: any = useNetwork();
100 const allPoints: any = usePointCache();
101 const getDetails = allPoints?.getDetails;
102 const { api, ls, performL2Reticket } = useRoller();
103 const { point, pointList, setPendingTransactions } = useRollerStore();
104 const {
105 invites,
106 inviteJobs,
107 addInvite,
108 setInvites,
109 updateJob,
110 } = useInviteStore();
112 const generateInviteInfo = async (planet: number, _authToken: string) => {
113 const { ticket, inviteWallet } = await generateInviteWallet(
114 planet,
115 _authToken
116 );
118 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
119 };
121 const getInvites = useCallback(async () => {
122 const inviteJob = inviteJobs[point.value];
123 if (inviteJob?.status === 'generating') {
124 return;
125 }
127 try {
128 updateJob(point.value, { status: 'generating' });
130 const curPoint: number = Number(need.point(pointCursor));
131 const _authToken = authToken.getOrElse(null);
132 const _contracts = contracts.getOrElse(null);
134 if (_authToken && _contracts) {
135 const spawnedPoints = point.isL2Spawn
136 ? await api.getSpawned(curPoint)
137 : [];
139 const newPending = await api.getPendingByShip(curPoint);
140 setPendingTransactions(newPending);
141 const pendingSpawns = getPendingSpawns(newPending);
143 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
144 _contracts,
145 curPoint
146 );
148 const outgoingPoints = pointList
149 .filter(
150 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
151 )
152 .map(({ value }) => value);
154 const invitePlanets = spawnedPoints
155 .concat(
156 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
157 )
158 .filter((p: number) => !pendingSpawns.has(p))
159 .sort();
161 setInvites(curPoint, []);
163 const newInvites: Invite[] = [];
164 // Iterate over all of the stored invites, generating wallet info as necessary
165 const storedInvites = getStoredInvites(ls);
167 for (let i = 0; i < invitePlanets.length; i++) {
168 updateJob(point.value, { generatingNum: i + 1 });
169 const planet = invitePlanets[i];
170 const storedInvite = storedInvites[planet];
171 const invite =
172 storedInvite || (await generateInviteInfo(planet, _authToken));
174 const planetInfo = await api.getPoint(planet);
175 // unclaimed point: if still owned by the deterministic invite wallet
176 if (
177 invite.owner.toLowerCase() ===
178 planetInfo.ownership?.owner?.address &&
179 planetInfo.dominion === POINT_DOMINIONS.L2
180 ) {
181 setStoredInvite(ls, invite);
182 addInvite(curPoint, invite);
183 newInvites.push(invite);
184 } else {
185 // The point has been claimed, remove if it's in the cache
186 if (storedInvite) {
187 removeStoredInvite(ls, planet);
188 }
189 }
190 }
192 updateJob(point.value, { status: 'finished', generatingNum: 0 });
193 return newInvites;
194 }
195 } catch (error) {
196 console.warn('ERROR GETTING INVITES', error);
197 updateJob(point.value, { status: 'errored', generatingNum: 0 });
198 }
199 }, [
200 api,
201 authToken,
202 contracts,
203 pointCursor,
204 ls,
205 point,
206 pointList,
207 addInvite,
208 setInvites,
209 setPendingTransactions,
210 updateJob,
211 ]);
213 const generateInviteCodes = useCallback(
214 async (
215 point: Point,
216 planets: SpawnedPoints | UnspawnedPoints,
217 spawn: boolean = true
218 ) => {
219 const _contracts = contracts.getOrElse(null);
220 const _web3 = web3.getOrElse(null);
221 const _wallet = wallet.getOrElse(null);
222 const _authToken = authToken.getOrElse(null);
224 if (!_contracts || !_web3 || !_wallet || !_authToken) {
225 // not using need because we want a custom error
226 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
227 }
229 const invites: Invite[] = [];
230 const storedInvites = getStoredInvites(ls);
232 const proxy = point.getSpawnProxy();
234 if (proxy === undefined)
235 throw new Error("Error: Address doesn't match proxy");
237 const nonce = await api.getNonce({
238 ship: point.value,
239 proxy,
240 });
242 for (let i = 0; i < planets.length; i++) {
243 updateJob(point.value, { generatingNum: i + 1 });
244 const planet = planets[i];
245 const nonceInc = i + nonce;
247 // When reticketing a planet in a browser that previously generated
248 // the invite, we need to remove it from the cache.
249 const storedInvite = storedInvites[planet];
250 if (storedInvite) {
251 removeStoredInvite(ls, planet);
252 }
254 const { ticket, inviteWallet } = await generateInviteWallet(
255 planet,
256 _authToken
257 );
259 if (!spawn) {
260 await performL2Reticket({
261 point: planet,
262 to: inviteWallet.ownership.keys.address,
263 manager: inviteWallet.management.keys.address,
264 fromWallet: _wallet,
265 toWallet: inviteWallet,
266 });
267 } else {
268 await submitL2Transaction({
269 api,
270 address: _wallet.address,
271 wallet: _wallet,
272 ship: point.value,
273 proxy,
274 nonce: nonceInc,
275 pointToSpawn: planet,
276 type: 'spawn',
277 walletType,
278 web3: _web3,
279 connector,
280 });
282 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
283 inviteWallet,
284 1
285 );
286 await submitL2Transaction({
287 api,
288 wallet: _wallet,
289 ship: planet,
290 proxy: 'own',
291 nonce: 0,
292 networkSeed,
293 type: 'configureKeys',
294 walletType,
295 web3: _web3,
296 connector,
297 });
299 await registerProxyAddress(
300 api,
301 _wallet,
302 planet,
303 'own',
304 'manage',
305 1,
306 inviteWallet.management.keys.address,
307 walletType,
308 _web3,
309 connector
310 );
312 await submitL2Transaction({
313 api,
314 wallet: _wallet,
315 ship: planet,
316 proxy: 'own',
317 type: 'transferPoint',
318 nonce: 2,
319 address: inviteWallet.ownership.keys.address,
320 walletType,
321 web3: _web3,
322 connector,
323 });
324 }
326 invites.push({
327 hash: '',
328 ticket,
329 planet,
330 owner: inviteWallet.ownership.keys.address,
331 });
332 }
334 setStoredInvites(ls, invites);
335 console.log('getting invites from code generation');
336 getInvites();
337 showNotification(
338 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
339 );
340 },
341 [
342 api,
343 authToken,
344 connector,
345 contracts,
346 pointCursor,
347 point,
348 wallet,
349 walletType,
350 web3,
351 getDetails,
352 ls,
353 getInvites,
354 updateJob,
355 performL2Reticket,
356 ]
357 );
359 const generateCsv = useCallback(() => {
360 const pointInvites = invites[point.value];
362 if (!pointInvites) {
363 throw new Error('There was an error creating the CSV');
364 }
366 return pointInvites.reduce(
367 (csvData, { ticket, planet }, ind) =>
368 (csvData += generateCsvLine(ind, ticket, planet)),
369 'Number,Planet,Invite URL,Point,Ticket\n'
370 );
371 }, [invites, point]);
373 return {
374 getInvites,
375 generateCsv,
376 generateInviteCodes,
377 };