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