on 1ac970d help sign in
bridge/src/lib/useRoller.ts
388 lines 11 kB history raw
1import { useCallback, useEffect, useMemo, useState } from 'react';
2import * as wg from 'lib/walletgen';
3import * as need from 'lib/need';
4import * as azimuth from 'azimuth-js';
6import {
7 Config,
8 Ship,
9 L2Point,
10 Proxy,
11 RollerRPCAPI,
12 Options,
13 EthAddress,
14 UnspawnedPoints,
15} from '@urbit/roller-api';
17import { isDevelopment, isRopsten } from './flags';
18import { ROLLER_HOSTS } from './constants';
19import { useRollerStore } from 'store/roller';
20import { getTimeToNextBatch } from './utils/roller';
21import { useWallet } from 'store/wallet';
22import { usePointCursor } from 'store/pointCursor';
23import { useNetwork } from 'store/network';
24import { signTransactionHash } from './authToken';
25import { Invite } from 'types/Invite';
26import {
27 getStoredInvites,
28 setPendingInvites,
29 setStoredInvites,
30} from 'store/storage/roller';
31import { usePointCache } from 'store/pointCache';
32import { getOutgoingPoints, maybeGetResult } from 'views/Points';
34const hasPoint = (point: number) => (invite: Invite) => invite.planet === point;
36const getProxyAndNonce = (
37 point: L2Point,
38 address: string
39): { proxy?: string; nonce?: number } =>
40 point.ownership?.managementProxy?.address === address
41 ? { proxy: 'manage', nonce: point.ownership?.managementProxy.nonce }
42 : point.ownership?.owner?.address === address
43 ? { proxy: 'own', nonce: point.ownership?.owner.nonce }
44 : point.ownership?.spawnProxy?.address === address
45 ? { proxy: 'spawn', nonce: point.ownership?.spawn?.owner.nonce }
46 : point.ownership?.votingProxy?.address === address
47 ? { proxy: 'vote', nonce: point.ownership?.votingProxy?.owner.nonce }
48 : point.ownership?.transferProxy?.address === address
49 ? {
50 proxy: 'transfer',
51 nonce: point.transferProxy?.votingProxy?.owner.nonce,
52 }
53 : { proxy: undefined, nonce: undefined };
55export default function useRoller() {
56 const { wallet, authToken }: any = useWallet();
57 const { pointCursor }: any = usePointCursor();
58 const { web3, contracts }: any = useNetwork();
59 const allPoints: any = usePointCache();
60 const controlledPoints = allPoints?.controlledPoints;
61 const getDetails = allPoints?.getDetails;
63 const {
64 nextBatchTime,
65 setNextBatchTime,
66 setNextRoll,
67 setPendingTransactions,
68 setInvites,
69 currentL2,
70 } = useRollerStore();
71 const [config, setConfig] = useState<Config | null>(null);
73 const options: Options = useMemo(() => {
74 const type = isRopsten || !isDevelopment ? 'https' : 'http';
75 const host = isRopsten
76 ? ROLLER_HOSTS.ROPSTEN
77 : isDevelopment
78 ? ROLLER_HOSTS.LOCAL
79 : ROLLER_HOSTS.MAINNET;
80 const port = isDevelopment ? 8080 : 80;
81 const path = '/v1/roller';
83 return {
84 transport: {
85 type,
86 host,
87 port,
88 path,
89 },
90 };
91 }, []);
93 const api = useMemo(() => {
94 return new RollerRPCAPI(options);
95 }, [options]);
97 const fetchConfig = useCallback(async () => {
98 api
99 .getRollerConfig()
100 .then(response => {
101 setConfig(response);
102 setNextBatchTime(response.nextBatch);
103 })
104 .catch(err => {
105 // TODO: more elegant error handling
106 console.warn(
107 '[fetchConfig:failed] is roller running on localhost?\n',
108 err
109 );
110 });
111 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
113 const generateInviteCodes = useCallback(
114 async (numInvites: number) => {
115 const _point = need.point(pointCursor);
116 const _contracts = contracts.getOrElse(null);
117 const _web3 = web3.getOrElse(null);
118 const _wallet = wallet.getOrElse(null);
119 const _authToken = authToken.getOrElse(null);
120 if (!_contracts || !_web3 || !_wallet || !_authToken) {
121 // not using need because we want a custom error
122 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
123 }
125 const planets: UnspawnedPoints = await api.getUnspawned(_point);
126 const starInfo = await api.getPoint(_point);
128 const tickets: { ticket: string; planet: number; owner: string }[] = [];
129 const requests: Promise<string>[] = [];
131 const { proxy, nonce } = getProxyAndNonce(starInfo, _wallet.address);
133 if (!(proxy === 'own' || proxy === 'spawn') || nonce === undefined)
134 throw new Error("Error: Address doesn't match proxy");
136 for (let i = 0; i < numInvites && planets[i]; i++) {
137 const planet = planets[i];
139 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
140 planet,
141 _authToken
142 );
144 const from = {
145 ship: _point, //ship that is spawning the planet
146 proxy,
147 };
149 const data = {
150 address: owner.keys.address, // the new owner of the star (invite wallet)
151 ship: planet, // ship to spawn
152 };
154 const txHash = await api.hashTransaction(
155 nonce + i,
156 from,
157 'spawn',
158 data
159 );
161 const signature = signTransactionHash(txHash, _wallet.privateKey);
162 requests.push(api.spawn(signature, from, _wallet.address, data));
163 tickets.push({
164 ticket,
165 planet,
166 owner: owner.keys.address,
167 });
168 }
170 const hashes = await Promise.all(requests);
171 const pendingInvites = hashes.map(
172 (hash, i): Invite => ({
173 hash,
174 ...tickets[i],
175 status: 'pending',
176 })
177 );
178 setPendingInvites(_point, pendingInvites);
179 },
180 [
181 api,
182 authToken,
183 contracts,
184 pointCursor,
185 wallet,
186 // walletHdPath,
187 // walletType,
188 web3,
189 ]
190 );
192 const getPoints = useCallback(
193 async (proxy: Proxy, address: EthAddress) => {
194 const points: Ship[] =
195 proxy === 'own'
196 ? await api.getOwnedPoints(address)
197 : proxy === 'mange'
198 ? await api.getManagerFor(address)
199 : proxy === 'vote'
200 ? await api.getVotingFor(address)
201 : proxy === 'transfer'
202 ? await api.getTransferringFor(address)
203 : proxy === 'spawn'
204 ? await api.getSpawningFor(address)
205 : [];
207 return points;
208 },
209 [api]
210 );
212 const getPendingTransactions = useCallback(async () => {
213 try {
214 const curPoint = need.point(pointCursor);
215 const newPending = await api.getPendingByShip(Number(curPoint));
216 setPendingTransactions(newPending);
218 // const allTransactions = await api.getHistory()
219 } catch (error) {
220 console.warn('ERROR GETTING PENDING', error);
221 }
222 }, [api, setPendingTransactions, pointCursor]);
224 const getInvites = useCallback(
225 async (isL2: boolean) => {
226 try {
227 const curPoint: string = need.point(pointCursor);
228 const invites = getStoredInvites(curPoint);
229 const availableInvites = invites.available;
231 const pendingTransactions = await api.getPendingByShip(curPoint);
232 if (isDevelopment) {
233 console.log('PENDING', pendingTransactions);
234 }
235 setPendingTransactions(pendingTransactions);
237 const stillPending = invites.pending.filter(invite => {
238 const completed = !pendingTransactions.find(
239 p => p?.rawTx?.tx?.tx?.data?.ship === invite.planet
240 );
242 if (completed) {
243 availableInvites.push({ ...invite, status: 'available' });
244 }
246 return !completed;
247 });
249 setStoredInvites(curPoint, {
250 available: availableInvites,
251 pending: stillPending,
252 claimed: invites.claimed,
253 });
254 setInvites(availableInvites);
256 const _authToken = authToken.getOrElse(null);
257 const _contracts = contracts.getOrElse(null);
259 if (_authToken && _contracts) {
260 let possibleMissingInvites: number[] = [];
261 if (isL2) {
262 const allSpawned = await api.getSpawned(Number(curPoint));
263 const ownedPoints = maybeGetResult(
264 controlledPoints,
265 'ownedPoints',
266 []
267 );
268 possibleMissingInvites = allSpawned.filter(
269 (p: number) =>
270 ownedPoints.includes(p) &&
271 azimuth.azimuth.getPointSize(p) ===
272 azimuth.azimuth.PointSize.Planet
273 );
274 } else {
275 const outgoingPoints = getOutgoingPoints(
276 controlledPoints,
277 getDetails
278 );
280 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
281 _contracts,
282 curPoint
283 );
285 possibleMissingInvites = outgoingPoints.filter(
286 (p: number) =>
287 azimuth.azimuth.getPointSize(p) ===
288 azimuth.azimuth.PointSize.Planet &&
289 availablePoints.includes(p)
290 );
291 }
293 // Iterate over all spawned and controlled planets
294 // If the planet is not in available invites, generate the ticket and add it
295 if (isDevelopment) {
296 console.log('POSSIBLE MISSING', possibleMissingInvites);
297 }
299 const newClaimed = availableInvites.filter(
300 ({ planet }) => !possibleMissingInvites.includes(planet)
301 );
303 for (let i = 0; i < possibleMissingInvites.length; i++) {
304 const planet = possibleMissingInvites[i];
306 if (
307 !availableInvites.find(hasPoint(planet)) &&
308 !(await azimuth.azimuth.isActive(_contracts, planet))
309 ) {
310 console.log('MISSING IN AVAILABLE', planet);
311 const {
312 ticket,
313 owner,
314 } = await wg.generateTemporaryDeterministicWallet(
315 planet,
316 _authToken
317 );
319 availableInvites.push({
320 ticket,
321 status: 'available',
322 planet,
323 hash: '',
324 owner: owner.keys.address,
325 });
326 }
327 }
329 setStoredInvites(curPoint, {
330 available: availableInvites.filter(({ planet }) =>
331 possibleMissingInvites.includes(planet)
332 ),
333 pending: stillPending,
334 claimed: invites.claimed.concat(newClaimed),
335 });
336 setInvites(availableInvites);
337 }
338 } catch (error) {
339 console.warn('ERROR GETTING INVITES', error);
340 }
341 },
342 [
343 api,
344 pointCursor,
345 setInvites,
346 setPendingTransactions,
347 authToken,
348 contracts,
349 controlledPoints,
350 getDetails,
351 ]
352 );
354 // On load, get initial config
355 useEffect(() => {
356 if (config) {
357 return;
358 }
360 fetchConfig();
361 }, [config, fetchConfig]);
363 useEffect(() => {
364 const interval = setInterval(() => {
365 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
366 setNextRoll(nextRoll);
368 if (nextBatchTime <= new Date().getTime()) {
369 api.getRollerConfig().then(response => {
370 setNextBatchTime(response.nextBatch);
371 });
373 getInvites(currentL2);
374 }
375 }, 1000);
377 return () => clearInterval(interval);
378 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
380 return {
381 api,
382 config,
383 getPoints,
384 getInvites,
385 getPendingTransactions,
386 generateInviteCodes,
387 };