on 0957ef2 help sign in
bridge/src/views/Invite/useInvites.ts
368 lines 10 kB history raw
1import { UnspawnedPoints } from '@urbit/roller-api';
2import { DEFAULT_CSV_NAME, 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, generateCsvName } 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 InviteStore {
49 invites: Invites;
50 generatingStatus: InviteGeneratingStatus;
51 generatingNum: number;
52 loading: boolean;
53 addInvite: (point: number, invite: Invite) => void;
54 setInvites: (points: number, invites: Invite[]) => void;
57export const useInviteStore = create<InviteStore>((set, get) => ({
58 invites: {},
59 generatingStatus: 'initial',
60 generatingNum: 0,
61 loading: false,
62 addInvite: (point: number, invite: Invite) => {
63 set(state => {
64 const newInvites: Invites = {};
65 const invites = state.invites[point];
67 if (!invites.find(inv => inv.planet === invite.planet)) {
68 newInvites[point] = [...invites, invite];
69 }
70 return { invites: Object.assign(state.invites, newInvites) };
71 });
72 },
73 setInvites: (point: number, invites: Invite[]) =>
74 set(state => {
75 const newInvites: Invites = {};
76 newInvites[point] = invites;
77 return { invites: Object.assign(state.invites, newInvites) };
78 }),
79}));
81export function useInvites() {
82 const { wallet, authToken, walletType }: any = useWallet();
83 const { connector } = useWalletConnect();
84 const { pointCursor }: any = usePointCursor();
85 const { web3, contracts }: any = useNetwork();
86 const allPoints: any = usePointCache();
87 const getDetails = allPoints?.getDetails;
88 const { api, ls } = useRoller();
89 const { point, pointList, setPendingTransactions } = useRollerStore();
90 const { addInvite, setInvites } = useInviteStore();
91 const updatingInvites = useRef<number[]>([]);
93 const generateInviteInfo = async (planet: number, _authToken: string) => {
94 const { ticket, inviteWallet } = await generateInviteWallet(
95 planet,
96 _authToken
97 );
99 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
100 };
102 const getInvites = useCallback(async () => {
103 if (updatingInvites.current.includes(point.value)) {
104 return;
105 }
107 updatingInvites.current.push(point.value);
108 try {
109 useInviteStore.setState({ loading: true });
110 const curPoint: number = Number(need.point(pointCursor));
111 const _authToken = authToken.getOrElse(null);
112 const _contracts = contracts.getOrElse(null);
114 if (_authToken && _contracts) {
115 const spawnedPoints = point.isL2Spawn
116 ? await api.getSpawned(curPoint)
117 : [];
119 const newPending = await api.getPendingByShip(curPoint);
120 setPendingTransactions(newPending);
121 const pendingSpawns = getPendingSpawns(newPending);
123 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
124 _contracts,
125 curPoint
126 );
128 const outgoingPoints = pointList
129 .filter(
130 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
131 )
132 .map(({ value }) => value);
134 const invitePlanets = spawnedPoints
135 .concat(
136 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
137 )
138 .filter((p: number) => !pendingSpawns.has(p))
139 .sort();
141 setInvites(curPoint, []);
143 const newInvites: Invite[] = [];
144 // Iterate over all of the stored invites, generating wallet info as necessary
145 const storedInvites = getStoredInvites(ls);
147 for (let i = 0; i < invitePlanets.length; i++) {
148 useInviteStore.setState({ generatingNum: i + 1 });
149 const planet = invitePlanets[i];
150 const storedInvite = storedInvites[planet];
151 const invite =
152 storedInvite || (await generateInviteInfo(planet, _authToken));
154 // TODO: check if the invite point's owner still matches the deterministic wallet address
155 const planetInfo = await api.getPoint(planet);
156 setStoredInvite(ls, invite);
158 if (
159 invite.owner.toLowerCase() ===
160 planetInfo.ownership?.owner?.address &&
161 planetInfo.dominion === POINT_DOMINIONS.L2
162 ) {
163 addInvite(curPoint, invite);
164 newInvites.push(invite);
165 }
167 if (storedInvite) {
168 setStoredInvite(ls, invite);
169 }
170 }
172 useInviteStore.setState({ loading: false, generatingNum: 0 });
173 return newInvites;
174 }
175 } catch (error) {
176 console.warn('ERROR GETTING INVITES', error);
177 useInviteStore.setState({ loading: false, generatingNum: 0 });
178 }
180 const pointIndex = updatingInvites.current.indexOf(point.value);
181 updatingInvites.current.splice(pointIndex, 1);
182 }, [
183 api,
184 authToken,
185 contracts,
186 pointCursor,
187 ls,
188 point,
189 pointList,
190 addInvite,
191 setInvites,
192 setPendingTransactions,
193 ]);
195 const generateInviteCodes = useCallback(
196 async (numInvites: number) => {
197 const _point = need.point(pointCursor);
198 const _contracts = contracts.getOrElse(null);
199 const _web3 = web3.getOrElse(null);
200 const _wallet = wallet.getOrElse(null);
201 const _authToken = authToken.getOrElse(null);
202 const _details = getDetails(_point);
204 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
205 // not using need because we want a custom error
206 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
207 }
209 // TODO: remove when UI disables management proxy from spawning invites
210 // if (Just.hasInstance(authMnemonic)) {
211 // throw new Error(
212 // "Auth key Error: A management mnemonic can't create invite codes"
213 // );
214 // }
215 let planets: UnspawnedPoints = await api.getUnspawned(_point);
217 const pendingTxs = await api.getPendingByShip(_point);
219 const pendingSpawns = getPendingSpawns(pendingTxs);
221 planets = planets.filter((point: number) => !pendingSpawns.has(point));
223 const invites: Invite[] = [];
225 const proxy = point.getSpawnProxy();
227 if (proxy === undefined)
228 throw new Error("Error: Address doesn't match proxy");
230 const nonce = await api.getNonce({
231 ship: _point,
232 proxy,
233 });
235 for (let i = 0; i < numInvites && planets[i]; i++) {
236 useInviteStore.setState({ generatingNum: i + 1 });
237 const planet = planets[i];
238 const nonceInc = i + nonce;
240 const { ticket, inviteWallet } = await generateInviteWallet(
241 planet,
242 _authToken
243 );
245 await submitL2Transaction({
246 api,
247 address: _wallet.address,
248 wallet: _wallet,
249 ship: _point,
250 proxy,
251 nonce: nonceInc,
252 pointToSpawn: planet,
253 type: 'spawn',
254 walletType,
255 web3: _web3,
256 connector,
257 });
259 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
260 inviteWallet,
261 1
262 );
263 await submitL2Transaction({
264 api,
265 wallet: _wallet,
266 ship: planet,
267 proxy: 'own',
268 nonce: 0,
269 networkSeed,
270 type: 'configureKeys',
271 walletType,
272 web3: _web3,
273 connector,
274 });
276 await registerProxyAddress(
277 api,
278 _wallet,
279 planet,
280 'own',
281 'manage',
282 1,
283 inviteWallet.management.keys.address,
284 walletType,
285 _web3,
286 connector
287 );
289 await submitL2Transaction({
290 api,
291 wallet: _wallet,
292 ship: planet,
293 proxy: 'own',
294 type: 'transferPoint',
295 nonce: 2,
296 address: inviteWallet.ownership.keys.address,
297 walletType,
298 web3: _web3,
299 connector,
300 });
302 invites.push({
303 hash: '',
304 ticket,
305 planet,
306 owner: inviteWallet.ownership.keys.address,
307 });
308 }
310 setStoredInvites(ls, invites);
311 console.log('getting invites from code generation');
312 getInvites();
313 showNotification(
314 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
315 );
316 },
317 [
318 api,
319 authToken,
320 connector,
321 contracts,
322 pointCursor,
323 point,
324 wallet,
325 walletType,
326 web3,
327 getDetails,
328 ls,
329 getInvites,
330 ]
331 );
333 const generateCsv = useCallback(async () => {
334 const invites = await getInvites();
336 if (!invites) {
337 throw new Error('There was an error creating the CSV');
338 }
340 const csv = invites.reduce(
341 (csvData, { ticket, planet }, ind) =>
342 (csvData += generateCsvLine(ind, ticket, planet)),
343 'Number,Planet,Invite URL,Point,Ticket\n'
344 );
346 document.getElementById('csv')?.remove();
347 const hiddenElement = document.createElement('a');
348 hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
349 csv
350 )}`;
351 hiddenElement.target = '_blank';
352 hiddenElement.id = 'csv';
354 //provide the name for the CSV file to be downloaded
355 hiddenElement.download = generateCsvName(
356 DEFAULT_CSV_NAME,
357 point.patp?.slice(1) || 'bridge'
358 );
360 return hiddenElement;
361 }, [getInvites, point]);
363 return {
364 getInvites,
365 generateCsv,
366 generateInviteCodes,
367 };