on master help sign in
bridge/src/views/Invite/useInvites.ts
383 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 create from 'zustand';
28import { generateCsvLine } from 'lib/utils/invite';
29import Point from 'lib/types/Point';
31const inviteTemplate = (
32 planet: number,
33 ticket = '',
34 owner = '',
35 hash = ''
36): Invite => ({
37 planet,
38 ticket,
39 owner,
40 hash,
41});
43export type InviteGeneratingStatus =
44 | 'initial'
45 | 'generating'
46 | 'finished'
47 | 'errored';
49interface InviteJob {
50 generatingNum: number;
51 status: InviteGeneratingStatus;
54interface InviteStore {
55 invites: Invites;
56 inviteJobs: {
57 [point: number]: InviteJob;
58 };
59 addInvite: (point: number, invite: Invite) => void;
60 setInvites: (points: number, invites: Invite[]) => void;
61 updateJob: (point: number, updates: Partial<InviteJob>) => void;
64export const useInviteStore = create<InviteStore>((set, get) => ({
65 invites: {},
66 inviteJobs: {},
67 addInvite: (point: number, invite: Invite) => {
68 set(state => {
69 const newInvites: Invites = {};
70 const invites = state.invites[point];
72 if (!invites.find(inv => inv.planet === invite.planet)) {
73 newInvites[point] = [...invites, invite];
74 }
75 return { invites: Object.assign(state.invites, newInvites) };
76 });
77 },
78 setInvites: (point: number, invites: Invite[]) =>
79 set(state => {
80 const newInvites: Invites = {};
81 newInvites[point] = invites;
82 return { invites: Object.assign(state.invites, newInvites) };
83 }),
84 updateJob: (point: number, updates: Partial<InviteJob>) => {
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 set({ inviteJobs: jobs });
91 },
92}));
94export function useInvites() {
95 const { wallet, authToken, walletType }: any = useWallet();
96 const { connector, isConnected, signPersonalMessage } = useWalletConnect();
97 const { pointCursor }: any = usePointCursor();
98 const { web3, contracts }: any = useNetwork();
99 const { api, ls, performL2Reticket } = useRoller();
100 const { point, pointList, setPendingTransactions } = useRollerStore();
101 const {
102 invites,
103 inviteJobs,
104 addInvite,
105 setInvites,
106 updateJob,
107 } = useInviteStore();
109 const generateInviteInfo = async (planet: number, _authToken: string) => {
110 const { ticket, inviteWallet } = await generateInviteWallet(
111 planet,
112 _authToken
113 );
115 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
116 };
118 const getInvites = useCallback(async () => {
119 const inviteJob = inviteJobs[point.value];
120 if (inviteJob?.status === 'generating') {
121 return;
122 }
124 try {
125 updateJob(point.value, { status: 'generating' });
127 const curPoint: number = Number(need.point(pointCursor));
128 const _authToken = authToken.getOrElse(null);
129 const _contracts = contracts.getOrElse(null);
131 if (_authToken && _contracts) {
132 const spawnedPoints = point.isL2Spawn
133 ? await api.getSpawned(curPoint)
134 : [];
136 const newPending = await api.getPendingByShip(curPoint);
137 setPendingTransactions(newPending);
138 const pendingSpawns = getPendingSpawns(newPending);
140 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
141 _contracts,
142 curPoint
143 );
145 const outgoingPoints = pointList
146 .filter(
147 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
148 )
149 .map(({ value }) => value);
151 const invitePlanets = spawnedPoints
152 .concat(
153 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
154 )
155 .filter((p: number) => !pendingSpawns.has(p))
156 .sort();
158 setInvites(curPoint, []);
160 const newInvites: Invite[] = [];
161 // Iterate over all of the stored invites, generating wallet info as necessary
162 const storedInvites = getStoredInvites(ls);
164 for (let i = 0; i < invitePlanets.length; i++) {
165 updateJob(point.value, { generatingNum: i + 1 });
166 const planet = invitePlanets[i];
167 const storedInvite = storedInvites[planet];
168 const invite =
169 storedInvite || (await generateInviteInfo(planet, _authToken));
171 const planetInfo = await api.getPoint(planet);
172 // unclaimed point: if still owned by the deterministic invite wallet
173 if (
174 invite.owner.toLowerCase() ===
175 planetInfo.ownership?.owner?.address &&
176 planetInfo.dominion === POINT_DOMINIONS.L2
177 ) {
178 setStoredInvite(ls, invite);
179 addInvite(curPoint, invite);
180 newInvites.push(invite);
181 } else {
182 // The point has been claimed, remove if it's in the cache
183 if (storedInvite) {
184 removeStoredInvite(ls, planet);
185 }
186 }
187 }
189 updateJob(point.value, { status: 'finished', generatingNum: 0 });
190 return newInvites;
191 }
192 } catch (error) {
193 console.warn('ERROR GETTING INVITES', error);
194 updateJob(point.value, { status: 'errored', generatingNum: 0 });
195 }
196 }, [
197 api,
198 authToken,
199 contracts,
200 pointCursor,
201 ls,
202 point,
203 pointList,
204 addInvite,
205 setInvites,
206 setPendingTransactions,
207 updateJob,
208 inviteJobs,
209 ]);
211 const generateInviteCodes = useCallback(
212 async (
213 point: Point,
214 planets: SpawnedPoints | UnspawnedPoints,
215 spawn: boolean = true
216 ) => {
217 const _contracts = contracts.getOrElse(null);
218 const _web3 = web3.getOrElse(null);
219 const _wallet = wallet.getOrElse(null);
220 const _authToken = authToken.getOrElse(null);
222 if (!_contracts || !_web3 || !_wallet || !_authToken) {
223 // not using need because we want a custom error
224 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
225 }
227 const invites: Invite[] = [];
228 const storedInvites = getStoredInvites(ls);
230 const proxy = point.getSpawnProxy();
232 if (proxy === undefined)
233 throw new Error("Error: Address doesn't match proxy");
235 const nonce = await api.getNonce({
236 ship: point.value,
237 proxy,
238 });
240 for (let i = 0; i < planets.length; i++) {
241 updateJob(point.value, { generatingNum: i + 1 });
242 const planet = planets[i];
243 const nonceInc = i + nonce;
245 // When reticketing a planet in a browser that previously generated
246 // the invite, we need to remove it from the cache.
247 const storedInvite = storedInvites[planet];
248 if (storedInvite) {
249 removeStoredInvite(ls, planet);
250 }
252 const { ticket, inviteWallet } = await generateInviteWallet(
253 planet,
254 _authToken
255 );
257 if (!spawn) {
258 await 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 signPersonalMessage,
279 isConnected,
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 signPersonalMessage,
298 isConnected,
299 });
301 await registerProxyAddress(
302 api,
303 _wallet,
304 planet,
305 'own',
306 'manage',
307 1,
308 inviteWallet.management.keys.address,
309 walletType,
310 _web3,
311 connector,
312 signPersonalMessage,
313 isConnected
314 );
316 await submitL2Transaction({
317 api,
318 wallet: _wallet,
319 ship: planet,
320 proxy: 'own',
321 type: 'transferPoint',
322 nonce: 2,
323 address: inviteWallet.ownership.keys.address,
324 walletType,
325 web3: _web3,
326 connector,
327 signPersonalMessage,
328 isConnected,
329 });
330 }
332 invites.push({
333 hash: '',
334 ticket,
335 planet,
336 owner: inviteWallet.ownership.keys.address,
337 });
338 }
340 setStoredInvites(ls, invites);
341 console.log('getting invites from code generation');
342 getInvites();
343 showNotification(
344 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
345 );
346 },
347 [
348 api,
349 authToken,
350 connector,
351 isConnected,
352 signPersonalMessage,
353 contracts,
354 wallet,
355 walletType,
356 web3,
357 ls,
358 getInvites,
359 updateJob,
360 performL2Reticket,
361 ]
362 );
364 const generateCsv = useCallback(() => {
365 const pointInvites = invites[point.value];
367 if (!pointInvites) {
368 throw new Error('There was an error creating the CSV');
369 }
371 return pointInvites.reduce(
372 (csvData, { ticket, planet }, ind) =>
373 (csvData += generateCsvLine(ind, ticket, planet)),
374 'Number,Planet,Invite URL,Point,Ticket\n'
375 );
376 }, [invites, point]);
378 return {
379 getInvites,
380 generateCsv,
381 generateInviteCodes,
382 };