on 46c79ea help sign in
bridge/src/lib/useRoller.ts
501 lines 14 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';
6import { Just } from 'folktale/maybe';
7import { randomHex } from 'web3-utils';
9import {
10 attemptNetworkSeedDerivation,
11 deriveNetworkKeys,
12 CRYPTO_SUITE_VERSION,
13} from 'lib/keys';
14import { addHexPrefix } from 'lib/utils/address';
16import {
17 Config,
18 Ship,
19 L2Point,
20 Proxy,
21 RollerRPCAPI,
22 Options,
23 EthAddress,
24 UnspawnedPoints,
25} from '@urbit/roller-api';
27import { isDevelopment, isRopsten } from './flags';
28import { ROLLER_HOSTS } from './constants';
29import { useRollerStore } from 'store/roller';
30import { getTimeToNextBatch } from './utils/roller';
31import { useWallet } from 'store/wallet';
32import { usePointCursor } from 'store/pointCursor';
33import { useNetwork } from 'store/network';
34import { signTransactionHash } from './authToken';
35import { Invite } from 'types/Invite';
36import {
37 getStoredInvites,
38 setPendingInvites,
39 setStoredInvites,
40} from 'store/storage/roller';
41import { usePointCache } from 'store/pointCache';
42import { getOutgoingPoints, maybeGetResult } from 'views/Points';
43import { isPlanet } from './utils/point';
45const spawn = async (
46 api: any,
47 _wallet: any,
48 _point: number,
49 proxy: string,
50 nonce: number,
51 planet: number
52) => {
53 const from = {
54 ship: _point, //ship that is spawning the planet
55 proxy,
56 };
58 const data = {
59 address: _wallet.address,
60 ship: planet, // ship to spawn
61 };
63 const hash = await api.hashTransaction(nonce, from, 'spawn', data);
64 const sSig = signTransactionHash(hash, _wallet.privateKey);
65 return api.spawn(sSig, from, _wallet.address, data);
66};
68const configureKeys = async (
69 api: any,
70 _wallet: any,
71 _point: number,
72 proxy: string,
73 nonce: number,
74 details: any,
75 authToken: string,
76 authMnemonic: string,
77 wallet: any,
78 urbitWallet: any
79) => {
80 const from = {
81 ship: _point, //ship that is spawning the planet
82 proxy,
83 };
85 const networkSeed = await attemptNetworkSeedDerivation({
86 urbitWallet,
87 wallet,
88 authMnemonic,
89 details,
90 point: _point,
91 authToken,
92 revision: 1,
93 });
95 const seed = Just.hasInstance(networkSeed)
96 ? networkSeed.value
97 : randomHex(32);
99 const pair = deriveNetworkKeys(seed);
101 const data = {
102 encrypt: addHexPrefix(pair.crypt.public),
103 auth: addHexPrefix(pair.auth.public),
104 cryptoSuite: String(CRYPTO_SUITE_VERSION),
105 breach: false,
106 };
108 const hash = await api.hashTransaction(nonce, from, 'configureKeys', data);
109 const sig = signTransactionHash(hash, _wallet.privateKey);
110 return api.configureKeys(sig, from, _wallet.address, data);
111};
113const transferPoint = async (
114 api: any,
115 _wallet: any,
116 _point: number,
117 proxy: string,
118 nonce: number,
119 address: string
120) => {
121 const from = {
122 ship: _point, //ship that is spawning the planet
123 proxy,
124 };
126 const data = {
127 address,
128 reset: false,
129 };
131 const hash = await api.hashTransaction(nonce, from, 'transferPoint', data);
132 const sig = signTransactionHash(hash, _wallet.privateKey);
133 return api.transferPoint(sig, from, _wallet.address, data);
134};
136const hasPoint = (point: number) => (invite: Invite) => invite.planet === point;
138const getProxyAndNonce = (
139 point: L2Point,
140 address: string
141): { proxy?: string; nonce?: number } =>
142 point.ownership?.managementProxy?.address === address
143 ? { proxy: 'manage', nonce: point.ownership?.managementProxy.nonce }
144 : point.ownership?.owner?.address === address
145 ? { proxy: 'own', nonce: point.ownership?.owner.nonce }
146 : point.ownership?.spawnProxy?.address === address
147 ? { proxy: 'spawn', nonce: point.ownership?.spawn?.owner.nonce }
148 : point.ownership?.votingProxy?.address === address
149 ? { proxy: 'vote', nonce: point.ownership?.votingProxy?.owner.nonce }
150 : point.ownership?.transferProxy?.address === address
151 ? {
152 proxy: 'transfer',
153 nonce: point.transferProxy?.votingProxy?.owner.nonce,
154 }
155 : { proxy: undefined, nonce: undefined };
157export default function useRoller() {
158 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
159 const { pointCursor }: any = usePointCursor();
160 const { web3, contracts }: any = useNetwork();
161 const allPoints: any = usePointCache();
162 const controlledPoints = allPoints?.controlledPoints;
163 const getDetails = allPoints?.getDetails;
165 const {
166 nextBatchTime,
167 setNextBatchTime,
168 setNextRoll,
169 setPendingTransactions,
170 setInvites,
171 currentL2,
172 } = useRollerStore();
173 const [config, setConfig] = useState<Config | null>(null);
175 const options: Options = useMemo(() => {
176 const type = isRopsten || !isDevelopment ? 'https' : 'http';
177 const host = isRopsten
178 ? ROLLER_HOSTS.ROPSTEN
179 : isDevelopment
180 ? ROLLER_HOSTS.LOCAL
181 : ROLLER_HOSTS.MAINNET;
182 const port = isDevelopment ? 8080 : 80;
183 const path = '/v1/roller';
185 return {
186 transport: {
187 type,
188 host,
189 port,
190 path,
191 },
192 };
193 }, []);
195 const api = useMemo(() => {
196 return new RollerRPCAPI(options);
197 }, [options]);
199 const fetchConfig = useCallback(async () => {
200 api
201 .getRollerConfig()
202 .then(response => {
203 setConfig(response);
204 setNextBatchTime(response.nextBatch);
205 })
206 .catch(err => {
207 // TODO: more elegant error handling
208 console.warn(
209 '[fetchConfig:failed] is roller running on localhost?\n',
210 err
211 );
212 });
213 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
215 const generateInviteCodes = useCallback(
216 async (numInvites: number) => {
217 const _point = need.point(pointCursor);
218 const _contracts = contracts.getOrElse(null);
219 const _web3 = web3.getOrElse(null);
220 const _wallet = wallet.getOrElse(null);
221 const _authToken = authToken.getOrElse(null);
222 const _details = getDetails(_point);
223 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
224 // not using need because we want a custom error
225 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
226 }
228 const planets: UnspawnedPoints = await api.getUnspawned(_point);
229 const starInfo = await api.getPoint(_point);
231 const tickets: { ticket: string; planet: number; owner: string }[] = [];
232 const requests: Promise<string>[] = [];
234 const { proxy, nonce } = getProxyAndNonce(starInfo, _wallet.address);
236 if (!(proxy === 'own' || proxy === 'spawn') || nonce === undefined)
237 throw new Error("Error: Address doesn't match proxy");
239 for (let i = 0; i < numInvites && planets[i]; i++) {
240 const planet = planets[i];
241 const nonceInc = i * 3;
243 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
244 planet,
245 _authToken
246 );
248 const spawnRequest = await spawn(
249 api,
250 _wallet,
251 _point,
252 proxy,
253 nonceInc,
254 planet
255 );
257 const configureKeysRequest = await configureKeys(
258 api,
259 _wallet,
260 _point,
261 proxy,
262 nonceInc + 1,
263 _details,
264 authToken,
265 authMnemonic,
266 wallet,
267 urbitWallet
268 );
270 const transferPointRequest = await transferPoint(
271 api,
272 _wallet,
273 _point,
274 proxy,
275 nonceInc + 2,
276 owner.keys.address
277 );
279 requests.push(spawnRequest, configureKeysRequest, transferPointRequest);
280 tickets.push({
281 ticket,
282 planet,
283 owner: owner.keys.address,
284 });
285 }
287 const hashes = await Promise.all(requests);
289 const pendingInvites = tickets.map((ticket, ind) => ({
290 ...ticket,
291 hash: hashes[ind * 3 + 2],
292 status: 'pending',
293 }));
295 setPendingInvites(_point, pendingInvites);
296 },
297 [
298 api,
299 authToken,
300 contracts,
301 pointCursor,
302 wallet,
303 // walletHdPath,
304 // walletType,
305 web3,
306 getDetails,
307 authMnemonic,
308 urbitWallet,
309 ]
310 );
312 const getPoints = useCallback(
313 async (proxy: Proxy, address: EthAddress) => {
314 const points: Ship[] =
315 proxy === 'own'
316 ? await api.getOwnedPoints(address)
317 : proxy === 'mange'
318 ? await api.getManagerFor(address)
319 : proxy === 'vote'
320 ? await api.getVotingFor(address)
321 : proxy === 'transfer'
322 ? await api.getTransferringFor(address)
323 : proxy === 'spawn'
324 ? await api.getSpawningFor(address)
325 : [];
327 return points;
328 },
329 [api]
330 );
332 const getPendingTransactions = useCallback(async () => {
333 try {
334 const curPoint = Number(need.point(pointCursor));
335 const newPending = await api.getPendingByShip(curPoint);
336 setPendingTransactions(newPending);
338 // const allTransactions = await api.getHistory()
339 } catch (error) {
340 console.warn('ERROR GETTING PENDING', error);
341 }
342 }, [api, setPendingTransactions, pointCursor]);
344 const getInvites = useCallback(
345 async (isL2: boolean) => {
346 try {
347 const curPoint: number = Number(need.point(pointCursor));
348 const invites = getStoredInvites(curPoint);
349 const availableInvites = invites.available;
351 const pendingTransactions = await api.getPendingByShip(curPoint);
352 if (isDevelopment) {
353 console.log('PENDING', pendingTransactions);
354 }
355 setPendingTransactions(pendingTransactions);
357 const stillPending = invites.pending.filter(invite => {
358 const completed = !pendingTransactions.find(
359 p => `~${p?.rawTx?.tx?.tx?.data?.ship}` === ob.patp(invite.planet)
360 );
362 if (
363 completed &&
364 !availableInvites.find(({ ship }) => invite.ship === ship)
365 ) {
366 availableInvites.push({ ...invite, status: 'available' });
367 }
369 return !completed;
370 });
372 setStoredInvites(curPoint, {
373 available: availableInvites,
374 pending: stillPending,
375 });
376 setInvites(availableInvites);
378 const _authToken = authToken.getOrElse(null);
379 const _contracts = contracts.getOrElse(null);
381 if (_authToken && _contracts) {
382 let possibleMissingInvites: number[] = [];
383 if (isL2) {
384 const allSpawned = await api.getSpawned(curPoint);
385 const ownedPoints = maybeGetResult(
386 controlledPoints,
387 'ownedPoints',
388 []
389 );
390 possibleMissingInvites = allSpawned.filter(
391 (p: number) => ownedPoints.includes(p) && isPlanet(p)
392 );
393 }
395 const outgoingPoints = getOutgoingPoints(
396 controlledPoints,
397 getDetails
398 );
400 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
401 _contracts,
402 curPoint
403 );
405 possibleMissingInvites = possibleMissingInvites.concat(
406 outgoingPoints.filter(
407 (p: number) => isPlanet(p) && availablePoints.includes(p)
408 )
409 );
411 // Iterate over all spawned and controlled planets
412 // If the planet is not in available invites, generate the ticket and add it
413 if (isDevelopment) {
414 console.log('POSSIBLE MISSING', possibleMissingInvites);
415 }
417 for (let i = 0; i < possibleMissingInvites.length; i++) {
418 const planet = possibleMissingInvites[i];
420 if (
421 !availableInvites.find(hasPoint(planet)) &&
422 !(await azimuth.azimuth.isActive(_contracts, planet))
423 ) {
424 console.log('MISSING IN AVAILABLE', planet);
425 const {
426 ticket,
427 owner,
428 } = await wg.generateTemporaryDeterministicWallet(
429 planet,
430 _authToken
431 );
433 availableInvites.push({
434 ticket,
435 status: 'available',
436 planet,
437 hash: '',
438 owner: owner.keys.address,
439 });
440 }
441 }
443 setStoredInvites(curPoint, {
444 available: availableInvites.filter(({ planet }) =>
445 possibleMissingInvites.includes(planet)
446 ),
447 pending: stillPending,
448 });
449 setInvites(availableInvites);
450 }
451 } catch (error) {
452 console.warn('ERROR GETTING INVITES', error);
453 }
454 },
455 [
456 api,
457 pointCursor,
458 setInvites,
459 setPendingTransactions,
460 authToken,
461 contracts,
462 controlledPoints,
463 getDetails,
464 ]
465 );
467 // On load, get initial config
468 useEffect(() => {
469 if (config) {
470 return;
471 }
473 fetchConfig();
474 }, [config, fetchConfig]);
476 useEffect(() => {
477 const interval = setInterval(() => {
478 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
479 setNextRoll(nextRoll);
481 if (nextBatchTime <= new Date().getTime()) {
482 api.getRollerConfig().then(response => {
483 setNextBatchTime(response.nextBatch);
484 });
486 getInvites(currentL2);
487 }
488 }, 1000);
490 return () => clearInterval(interval);
491 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
493 return {
494 api,
495 config,
496 getPoints,
497 getInvites,
498 getPendingTransactions,
499 generateInviteCodes,
500 };