on f33e9cd help sign in
bridge/src/lib/useRoller.ts
677 lines 18 kB history raw
1import { useCallback, useEffect, useMemo, useState } from 'react';
2import * as azimuth from 'azimuth-js';
3import * as need from 'lib/need';
4import SecureLS from 'secure-ls';
6import { Invite } from 'lib/types/Invite';
7import { convertToInt } from './convertToInt';
8import { isDevelopment, isRopsten } from './flags';
9import { generateInviteWallet, getPendingSpawns } from './utils/roller';
10import { ROLLER_HOSTS } from './constants';
12import { useNetwork } from 'store/network';
13import { usePointCursor } from 'store/pointCursor';
14import { useRollerStore } from 'store/roller';
15import { useWallet } from 'store/wallet';
16import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
17import { usePointCache } from 'store/pointCache';
18import { getOutgoingPoints } from 'views/Points';
19import {
20 getSpawnProxy,
21 getManagerProxy,
22 getAddressProxy,
23 getTransferProxy,
24} from './utils/proxy';
25import { isPlanet } from './utils/point';
27import {
28 deriveNetworkSeedFromUrbitWallet,
29 attemptNetworkSeedDerivation,
30} from 'lib/keys';
32import {
33 Config,
34 Ship,
35 Proxy,
36 RollerRPCAPI,
37 Options,
38 EthAddress,
39 UnspawnedPoints,
40} from '@urbit/roller-api';
42import {
43 configureKeys,
44 getTimeToNextBatch,
45 spawn,
46 transferPointRequest,
47 registerProxyAddress,
48} from './utils/roller';
50const ONE_SECOND = 1000;
52const inviteTemplate = (
53 planet: number,
54 ticket = '',
55 owner = '',
56 hash = ''
57): Invite => ({
58 planet,
59 ticket,
60 owner,
61 hash,
62});
64export default function useRoller() {
65 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
66 const { pointCursor }: any = usePointCursor();
67 const { web3, contracts }: any = useNetwork();
68 const allPoints: any = usePointCache();
69 const controlledPoints = allPoints?.controlledPoints;
70 const getDetails = allPoints?.getDetails;
72 const {
73 nextBatchTime,
74 currentL2,
75 invitePoints,
76 invites,
77 setNextBatchTime,
78 setNextRoll,
79 setPendingTransactions,
80 setInvites,
81 setInvitePoints,
82 setInviteGeneratingNum,
83 } = useRollerStore();
84 const [config, setConfig] = useState<Config | null>(null);
86 const options: Options = useMemo(() => {
87 const type = isRopsten || !isDevelopment ? 'https' : 'http';
88 const host = isRopsten
89 ? ROLLER_HOSTS.ROPSTEN
90 : isDevelopment
91 ? ROLLER_HOSTS.LOCAL
92 : ROLLER_HOSTS.MAINNET;
93 const port = isRopsten ? 443 : 8080;
94 const path = '/v1/roller';
96 return {
97 transport: {
98 type,
99 host,
100 port,
101 path,
102 },
103 };
104 }, []);
106 const api = useMemo(() => {
107 return new RollerRPCAPI(options);
108 }, [options]);
110 const fetchConfig = useCallback(async () => {
111 api
112 .getRollerConfig()
113 .then(response => {
114 setConfig(response);
115 setNextBatchTime(response.nextBatch);
116 })
117 .catch(err => {
118 // TODO: more elegant error handling
119 console.warn(
120 '[fetchConfig:failed] check the roller connection?\n',
121 err
122 );
123 });
124 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
126 const ls = new SecureLS({
127 isCompression: false,
128 encryptionSecret: authToken.getOrElse('default'),
129 });
131 const generateInviteCodes = useCallback(
132 async (numInvites: number) => {
133 const _point = need.point(pointCursor);
134 const _contracts = contracts.getOrElse(null);
135 const _web3 = web3.getOrElse(null);
136 const _wallet = wallet.getOrElse(null);
137 const _authToken = authToken.getOrElse(null);
138 const _details = getDetails(_point);
140 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
141 // not using need because we want a custom error
142 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
143 }
145 // if (Just.hasInstance(authMnemonic)) {
146 // throw new Error(
147 // "Auth key Error: A management mnemonic can't create invite codes"
148 // );
149 // }
150 let planets: UnspawnedPoints = await api.getUnspawned(_point);
152 const pendingTxs = await api.getPendingByShip(_point);
154 const pendingSpawns = getPendingSpawns(pendingTxs);
156 planets = planets.filter((point: number) => !pendingSpawns.has(point));
158 const starInfo = await api.getPoint(_point);
159 const invites: Invite[] = [];
160 const requests: Promise<string>[] = [];
162 const proxy = getSpawnProxy(starInfo.ownership!, _wallet.address);
164 if (proxy === undefined)
165 throw new Error("Error: Address doesn't match proxy");
167 const nonce = await api.getNonce({
168 ship: _point,
169 proxy,
170 });
172 for (let i = 0; i < numInvites && planets[i]; i++) {
173 setInviteGeneratingNum(i + 1);
174 const planet = planets[i];
175 const nonceInc = i + nonce;
177 const { ticket, inviteWallet } = await generateInviteWallet(
178 planet,
179 _authToken
180 );
182 const spawnRequest = spawn(
183 api,
184 _wallet,
185 _point,
186 proxy,
187 nonceInc,
188 planet
189 );
191 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
192 inviteWallet,
193 1
194 );
195 const configureKeysRequest = configureKeys(
196 api,
197 _wallet,
198 planet,
199 'own',
200 0,
201 networkSeed
202 );
204 const setManagementProxyRequest = registerProxyAddress(
205 api,
206 _wallet,
207 planet,
208 'own',
209 'manage',
210 1,
211 inviteWallet.management.keys.address
212 );
214 const transferRequest = transferPointRequest(
215 api,
216 _wallet,
217 planet,
218 'own',
219 2,
220 inviteWallet.ownership.keys.address
221 );
223 requests.push(
224 spawnRequest,
225 configureKeysRequest,
226 setManagementProxyRequest,
227 transferRequest
228 );
229 await Promise.all(requests);
231 invites.push({
232 hash: '',
233 ticket,
234 planet,
235 owner: inviteWallet.ownership.keys.address,
236 });
237 }
239 setStoredInvites(ls, invites);
240 },
241 [
242 api,
243 authToken,
244 contracts,
245 pointCursor,
246 wallet,
247 web3,
248 getDetails,
249 ls,
250 setInviteGeneratingNum,
251 ]
252 );
254 const getPoints = useCallback(
255 async (proxy: Proxy, address: EthAddress) => {
256 const points: Ship[] =
257 proxy === 'own'
258 ? await api.getOwnedPoints(address)
259 : proxy === 'manage'
260 ? await api.getManagerFor(address)
261 : proxy === 'vote'
262 ? await api.getVotingFor(address)
263 : proxy === 'transfer'
264 ? await api.getTransferringFor(address)
265 : proxy === 'spawn'
266 ? await api.getSpawningFor(address)
267 : [];
269 return points;
270 },
271 [api]
272 );
274 const getPendingTransactions = useCallback(async () => {
275 try {
276 const curPoint = Number(need.point(pointCursor));
277 const newPending = await api.getPendingByShip(curPoint);
279 setPendingTransactions(newPending);
280 } catch (error) {
281 console.warn('ERROR GETTING PENDING', error);
282 }
283 }, [api, setPendingTransactions, pointCursor]);
285 const generateInviteInfo = async (planet: number, _authToken: string) => {
286 const { ticket, inviteWallet } = await generateInviteWallet(
287 planet,
288 _authToken
289 );
291 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
292 };
294 const getInvites = useCallback(
295 async (isL2: boolean, getAll = false) => {
296 try {
297 const curPoint: number = Number(need.point(pointCursor));
298 setInvites(invitePoints.map(p => inviteTemplate(p)));
300 const _authToken = authToken.getOrElse(null);
301 const _contracts = contracts.getOrElse(null);
303 if (_authToken && _contracts) {
304 const spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
306 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
307 _contracts,
308 curPoint
309 );
311 const outgoingPoints = getOutgoingPoints(
312 controlledPoints,
313 getDetails
314 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
316 const invitePlanets = spawnedPoints.concat(outgoingPoints);
318 if (isDevelopment) {
319 console.log('INVITED PLANETS', invitePlanets);
320 }
322 const newInvites: Invite[] = [];
323 // Iterate over all of the stored invites, generating wallet info as necessary
324 const storedInvites = getStoredInvites(ls);
326 for (let i = 0; i < invitePlanets.length; i++) {
327 setInviteGeneratingNum(i + 1);
328 const planet = invitePlanets[i];
329 const invite = storedInvites[planet];
331 if (invite?.ticket) {
332 newInvites.push(invite);
333 } else if (getAll) {
334 if (isDevelopment) {
335 console.log('MISSING INVITE INFO', planet);
336 }
337 const newInvite = await generateInviteInfo(planet, _authToken);
338 newInvites.push(newInvite);
339 } else {
340 newInvites.push(inviteTemplate(planet));
341 }
342 }
344 setStoredInvites(ls, newInvites);
345 setInvites(newInvites);
347 return newInvites;
348 }
349 } catch (error) {
350 console.warn('ERROR GETTING INVITES', error);
351 }
352 },
353 [
354 api,
355 authToken,
356 contracts,
357 controlledPoints,
358 getDetails,
359 pointCursor,
360 invitePoints,
361 ls,
362 setInvites,
363 setInviteGeneratingNum,
364 ]
365 );
367 const showInvite = useCallback(
368 async (planet: number) => {
369 const _authToken = authToken.getOrElse(null);
371 if (_authToken) {
372 const newInvite = await generateInviteInfo(planet, _authToken);
373 const newInvites = invites.map(invite =>
374 invite.planet === planet ? newInvite : invite
375 );
376 setInvites(newInvites);
377 setStoredInvites(ls, newInvites);
378 }
379 },
380 [authToken, invites, ls, setInvites]
381 );
383 const getNumInvites = useCallback(
384 async (isL2: boolean) => {
385 try {
386 const curPoint: number = Number(need.point(pointCursor));
387 const _authToken = authToken.getOrElse(null);
388 const _contracts = contracts.getOrElse(null);
390 if (_authToken && _contracts) {
391 let invitePoints = isL2 ? await api.getSpawned(curPoint) : [];
393 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
394 _contracts,
395 curPoint
396 );
398 const newPending = await api.getPendingByShip(curPoint);
399 const pendingSpawns = getPendingSpawns(newPending);
401 const outgoingPoints = getOutgoingPoints(
402 controlledPoints,
403 getDetails
404 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
406 setPendingTransactions(newPending);
407 setInvitePoints(
408 invitePoints
409 .concat(outgoingPoints)
410 .filter(p => !pendingSpawns.has(p))
411 );
412 }
413 } catch (e) {
414 if (isDevelopment) {
415 console.warn(e);
416 }
417 }
418 },
419 [
420 api,
421 pointCursor,
422 authToken,
423 contracts,
424 controlledPoints,
425 getDetails,
426 setInvitePoints,
427 setPendingTransactions,
428 ]
429 );
431 interface ConfigureKeysParams {
432 breach: boolean;
433 customNetworkSeed?: string;
434 }
436 const configureNetworkingKeys = async ({
437 breach,
438 customNetworkSeed,
439 }: ConfigureKeysParams) => {
440 const _point = need.point(pointCursor);
441 const azimuthPoint = await api.getPoint(_point);
442 const _wallet = wallet.getOrElse(null);
443 const _details = getDetails(_point);
444 if (!_wallet || !_details) {
445 // not using need because we want a custom error
446 throw new Error('Internal Error: Missing Wallet/Details');
447 }
449 const proxy = getManagerProxy(azimuthPoint.ownership!, _wallet.address);
451 if (proxy === undefined)
452 throw new Error("Error: Address doesn't match proxy");
454 const nonce = await api.getNonce({
455 ship: _point,
456 proxy,
457 });
459 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
460 const nextRevision = networkRevision + 1;
461 const networkSeed = customNetworkSeed
462 ? customNetworkSeed
463 : await attemptNetworkSeedDerivation({
464 urbitWallet,
465 wallet,
466 authMnemonic,
467 details: _details,
468 point: _point,
469 authToken,
470 revision: nextRevision,
471 });
472 const txHash = await configureKeys(
473 api,
474 _wallet,
475 _point,
476 proxy!,
477 nonce,
478 networkSeed,
479 breach
480 );
481 const pendingTx = await api.getPendingTx(txHash);
483 return pendingTx;
484 };
486 interface ReticketParams {
487 point: Ship;
488 to: EthAddress;
489 manager: EthAddress;
490 fromWallet: any; // TODO: wallet type
491 toWallet: any; // TODO: wallet type
492 }
494 const performL2Reticket = async ({
495 point,
496 to,
497 manager,
498 fromWallet,
499 toWallet,
500 }: ReticketParams) => {
501 const azimuthPoint = await api.getPoint(point);
502 const proxy = 'own';
503 let nonce = await api.getNonce({ ship: point, proxy });
505 let requests = [];
507 // 1. Update networking keys
508 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
509 const nextRevision = networkRevision + 1;
510 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
511 toWallet,
512 nextRevision
513 );
514 const configureKeysRequest = await configureKeys(
515 api,
516 fromWallet,
517 point,
518 proxy,
519 nonce,
520 networkSeed
521 );
522 nonce = nonce + 1;
523 requests.push(configureKeysRequest);
525 // 2. Set Management Proxy
526 const registerMgmtRequest = await registerProxyAddress(
527 api,
528 fromWallet,
529 point,
530 proxy,
531 'manage',
532 nonce,
533 manager
534 );
535 nonce = nonce + 1;
536 requests.push(registerMgmtRequest);
538 // 3. Set Spawn Proxy
539 if (!isPlanet(Number(point))) {
540 const registerSpawnRequest = await registerProxyAddress(
541 api,
542 fromWallet,
543 point,
544 proxy,
545 'spawn',
546 nonce,
547 toWallet.spawn.keys.address
548 );
549 nonce = nonce + 1;
550 requests.push(registerSpawnRequest);
551 }
552 // 4. Transfer point
553 const transferTxRequest = await transferPointRequest(
554 api,
555 fromWallet,
556 point,
557 proxy,
558 nonce,
559 to
560 );
561 requests.push(transferTxRequest);
563 const hashes = await Promise.all(requests);
564 return hashes;
565 };
567 const setProxyAddress = useCallback(
568 async (proxyType: Proxy, address: EthAddress) => {
569 const _point = need.point(pointCursor);
570 const _wallet = wallet.getOrElse(null);
572 if (!_wallet) {
573 // not using need because we want a custom error
574 throw new Error('Internal Error: Missing Wallet');
575 }
577 const pointDetails = await api.getPoint(_point);
578 const ownership = pointDetails.ownership!;
579 const proxy = getAddressProxy(ownership, _wallet.address, proxyType);
581 if (proxy === undefined)
582 throw new Error("Error: Address doesn't match expected proxy");
584 const nonce = await api.getNonce({ ship: _point, proxy });
586 const txHash = await registerProxyAddress(
587 api,
588 _wallet,
589 _point,
590 proxy,
591 proxyType,
592 nonce,
593 address
594 );
596 const pendingTx = await api.getPendingTx(txHash);
598 return pendingTx;
599 },
600 [api, pointCursor, wallet]
601 );
603 const transferPoint = useCallback(
604 async (address: EthAddress, reset?: boolean) => {
605 const _point = need.point(pointCursor);
606 const _wallet = wallet.getOrElse(null);
608 if (!_wallet) {
609 // not using need because we want a custom error
610 throw new Error('Internal Error: Missing Wallet');
611 }
613 const pointDetails = await api.getPoint(_point);
614 const proxy = getTransferProxy(pointDetails.ownership!, _wallet.address);
616 if (proxy === undefined)
617 throw new Error("Error: Address doesn't match proxy");
619 const nonce = await api.getNonce({ ship: _point, proxy });
621 const txHash = await transferPointRequest(
622 api,
623 _wallet,
624 _point,
625 proxy,
626 nonce,
627 address,
628 reset || false
629 );
630 const pendingTx = await api.getPendingTx(txHash);
632 return pendingTx;
633 },
634 [api, pointCursor, wallet]
635 );
637 // On load, get initial config
638 useEffect(() => {
639 if (config) {
640 return;
641 }
643 fetchConfig();
644 }, [config, fetchConfig]);
646 useEffect(() => {
647 const interval = setInterval(() => {
648 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
649 setNextRoll(nextRoll);
651 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
652 api.getRollerConfig().then(response => {
653 setNextBatchTime(response.nextBatch);
654 getNumInvites(currentL2);
655 });
656 }
657 }, 1000);
659 return () => clearInterval(interval);
660 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
662 return {
663 api,
664 performL2Reticket,
665 config,
666 getPoints,
667 getInvites,
668 getPendingTransactions,
669 generateInviteCodes,
670 transferPoint,
671 setProxyAddress,
672 configureNetworkingKeys,
673 getNumInvites,
674 showInvite,
675 ls,
676 };