on bab2fa8 help sign in
bridge/src/lib/useRoller.ts
339 lines 10 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 { getStoredInvites, setPendingInvites, setStoredInvites } from 'store/storage/roller';
28import { usePointCache } from 'store/pointCache';
29import { maybeGetResult } from 'views/Points';
31const hasPoint = (point: number) => (invite: Invite) => invite.planet === point;
33const getProxyAndNonce = (
34 point: L2Point,
35 address: string
36): { proxy?: string; nonce?: number } =>
37 point.ownership?.managementProxy?.address === address
38 ? { proxy: 'manage', nonce: point.ownership?.managementProxy.nonce }
39 : point.ownership?.owner?.address === address
40 ? { proxy: 'own', nonce: point.ownership?.owner.nonce }
41 : point.ownership?.spawnProxy?.address === address
42 ? { proxy: 'spawn', nonce: point.ownership?.spawn?.owner.nonce }
43 : point.ownership?.votingProxy?.address === address
44 ? { proxy: 'vote', nonce: point.ownership?.votingProxy?.owner.nonce }
45 : point.ownership?.transferProxy?.address === address
46 ? {
47 proxy: 'transfer',
48 nonce: point.transferProxy?.votingProxy?.owner.nonce,
49 }
50 : { proxy: undefined, nonce: undefined };
52export default function useRoller() {
53 const { wallet, walletType, walletHdPath, authToken }: any = useWallet();
54 const { pointCursor }: any = usePointCursor();
55 const { web3, contracts }: any = useNetwork();
56 const allPoints: any = usePointCache();
57 const controlledPoints = allPoints?.controlledPoints;
59 const {
60 nextBatchTime,
61 setNextBatchTime,
62 setNextRoll,
63 setPendingTransactions,
64 setInvites,
65 } = useRollerStore();
66 const [config, setConfig] = useState<Config | null>(null);
68 const options: Options = useMemo(() => {
69 const type = isRopsten || !isDevelopment ? 'https' : 'http';
70 const host = isRopsten
71 ? ROLLER_HOSTS.ROPSTEN
72 : isDevelopment
73 ? ROLLER_HOSTS.LOCAL
74 : ROLLER_HOSTS.MAINNET;
75 const port = isDevelopment ? 8080 : 80;
76 const path = '/v1/roller';
78 return {
79 transport: {
80 type,
81 host,
82 port,
83 path,
84 },
85 };
86 }, []);
88 const api = useMemo(() => {
89 return new RollerRPCAPI(options);
90 }, [options]);
92 const fetchConfig = useCallback(async () => {
93 api
94 .getRollerConfig()
95 .then(response => {
96 setConfig(response);
97 setNextBatchTime(response.nextBatch);
98 })
99 .catch(err => {
100 // TODO: more elegant error handling
101 console.warn(
102 '[fetchConfig:failed] is roller running on localhost?\n',
103 err
104 );
105 });
106 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
108 const generateInviteCodes = useCallback(
109 async (numInvites: number) => {
110 const _point = need.point(pointCursor);
111 const _contracts = contracts.getOrElse(null);
112 const _web3 = web3.getOrElse(null);
113 const _wallet = wallet.getOrElse(null);
114 const _authToken = authToken.getOrElse(null);
115 if (!_contracts || !_web3 || !_wallet || !_authToken) {
116 // not using need because we want a custom error
117 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
118 }
120 const planets: UnspawnedPoints = await api.getUnspawned(_point);
121 const starInfo = await api.getPoint(_point);
123 const tickets: { ticket: string; planet: number; owner: string }[] = [];
124 const requests: Promise<string>[] = [];
126 const { proxy, nonce } = getProxyAndNonce(starInfo, _wallet.address);
128 if (proxy === undefined || nonce === undefined)
129 throw new Error("Error: Address doesn't match proxy");
131 for (let i = 0; i < numInvites && planets[i]; i++) {
132 const planet = planets[i];
134 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
135 planet,
136 _authToken
137 );
139 const from = {
140 ship: _point, //ship that is spawning the planet
141 proxy, // TODO: check that this is either "own" or "spawn"
142 };
144 const data = {
145 address: owner.keys.address, // the new owner of the star (invite wallet)
146 ship: planet, // ship to spawn
147 };
149 const txHash = await api.hashTransaction(
150 nonce + i,
151 from,
152 'spawn',
153 data
154 );
156 const signature = signTransactionHash(txHash, _wallet.privateKey);
157 requests.push(api.spawn(signature, from, _wallet.address, data));
158 tickets.push({
159 ticket,
160 planet,
161 owner: owner.keys.address,
162 });
163 }
165 const hashes = await Promise.all(requests);
166 const pendingInvites = hashes.map(
167 (hash, i): Invite => ({
168 hash,
169 ...tickets[i],
170 status: 'pending',
171 })
172 );
173 setPendingInvites(_point, pendingInvites);
174 },
175 [
176 api,
177 authToken,
178 contracts,
179 pointCursor,
180 wallet,
181 // walletHdPath,
182 // walletType,
183 web3,
184 ]
185 );
187 const getPoints = useCallback(
188 async (proxy: Proxy, address: EthAddress) => {
189 const points: Ship[] =
190 proxy === 'own'
191 ? await api.getOwnedPoints(address)
192 : proxy === 'mange'
193 ? await api.getManagerFor(address)
194 : proxy === 'vote'
195 ? await api.getVotingFor(address)
196 : proxy === 'transfer'
197 ? await api.getTransferringFor(address)
198 : proxy === 'spawn'
199 ? await api.getSpawningFor(address)
200 : [];
202 return points;
203 },
204 [api]
205 );
207 const getPendingTransactions = useCallback(async () => {
208 try {
209 const curPoint = need.point(pointCursor);
210 const newPending = await api.getPendingByShip(Number(curPoint));
211 console.log('PENDING', newPending);
212 setPendingTransactions(newPending);
214 // const allTransactions = await api.getHistory()
215 } catch (error) {
216 console.warn('ERROR GETTING PENDING', error);
217 }
218 }, [api, setPendingTransactions, pointCursor]);
220 const getInvites = useCallback(async () => {
221 try {
222 const curPoint: string = need.point(pointCursor);
223 const invites = getStoredInvites(curPoint);
224 const availableInvites = invites.available;
226 const pendingTransactions = await api.getPendingByShip(curPoint);
227 console.log('PENDING', pendingTransactions);
228 setPendingTransactions(pendingTransactions);
230 const stillPending = invites.pending.filter(invite => {
231 const completed = !pendingTransactions.find(
232 ({ tx }) => tx.tx?.data?.ship === invite.planet
233 );
235 if (completed) {
236 availableInvites.push({ ...invite, status: 'available' });
237 }
239 return !completed;
240 });
242 setStoredInvites(curPoint, {
243 available: availableInvites,
244 pending: stillPending,
245 claimed: invites.claimed,
246 });
247 setInvites(availableInvites);
249 const allSpawned = await api.getSpawned(Number(curPoint));
250 const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
251 const spawnedAndOwned = ownedPoints.filter(
252 (p: number) => allSpawned.includes(p) && p > 2000
253 );
254 console.log('SPAWNED AND OWNED', spawnedAndOwned);
256 // Iterate over all spawned and controlled planets
257 // If the planet is not in available invites, generate the ticket and add it
258 const _authToken = authToken.getOrElse(null);
259 const _contracts = contracts.getOrElse(null);
261 if (_authToken && _contracts) {
262 for (let i = 0; i < spawnedAndOwned.length; i++) {
263 const point = spawnedAndOwned[i];
265 if (!availableInvites.find(hasPoint(point))) {
266 console.log('MISSING IN AVAILABLE', point);
267 const {
268 ticket,
269 owner,
270 } = await wg.generateTemporaryDeterministicWallet(
271 point,
272 _authToken
273 );
275 availableInvites.push({
276 ticket,
277 status: 'available',
278 planet: point,
279 hash: '',
280 owner: owner.keys.address,
281 });
282 }
283 }
285 setStoredInvites(curPoint, {
286 available: availableInvites,
287 pending: stillPending,
288 claimed: invites.claimed,
289 });
290 setInvites(availableInvites);
291 }
292 } catch (error) {
293 console.warn('ERROR GETTING INVITES', error);
294 }
295 }, [
296 api,
297 pointCursor,
298 setInvites,
299 setPendingTransactions,
300 authToken,
301 contracts,
302 controlledPoints,
303 ]);
305 // On load, get initial config
306 useEffect(() => {
307 if (config) {
308 return;
309 }
311 fetchConfig();
312 }, [config, fetchConfig]);
314 useEffect(() => {
315 const interval = setInterval(() => {
316 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
317 setNextRoll(nextRoll);
319 if (nextBatchTime <= new Date().getTime()) {
320 api.getRollerConfig().then(response => {
321 setNextBatchTime(response.nextBatch);
322 });
324 getInvites();
325 }
326 }, 1000);
328 return () => clearInterval(interval);
329 }, [nextBatchTime, getPendingTransactions]); // eslint-disable-line react-hooks/exhaustive-deps
331 return {
332 api,
333 config,
334 getPoints,
335 getInvites,
336 getPendingTransactions,
337 generateInviteCodes,
338 };