on afdb680 help sign in
bridge/src/lib/useRoller.ts
860 lines 23 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 {
8 deriveNetworkSeedFromUrbitWallet,
9 attemptNetworkSeedDerivation,
10} from 'lib/keys';
12import { useNetwork } from 'store/network';
13import { usePointCursor } from 'store/pointCursor';
14import { EMPTY_POINT, useRollerStore } from 'store/rollerStore';
15import { useWallet } from 'store/wallet';
16import {
17 getStoredInvites,
18 setStoredInvite,
19 setStoredInvites,
20} from 'store/storage/roller';
21import { usePointCache } from 'store/pointCache';
22import { getOutgoingPoints } from 'views/Points';
24import {
25 getSpawnProxy,
26 getManagerProxy,
27 getAddressProxy,
28 getTransferProxy,
29} from './utils/proxy';
30import { isPlanet, toL1Details } from './utils/point';
31import { convertToInt } from './convertToInt';
32import { isDevelopment, isRopsten } from './flags';
33import {
34 generateInviteWallet,
35 getPendingSpawns,
36 submitL2Transaction,
37 getTimeToNextBatch,
38 registerProxyAddress,
39 adopt,
40 detach,
41 reject,
42} from './utils/roller';
43import { ETH_ZERO_ADDR, ROLLER_HOSTS } from './constants';
45import {
46 Config,
47 Ship,
48 Proxy,
49 RollerRPCAPI,
50 Options,
51 EthAddress,
52 UnspawnedPoints,
53} from '@urbit/roller-api';
55import Point, { Points, Relationship } from './types/Point';
56import { useTimerStore } from 'store/timerStore';
57import { ReticketParams, SendL2Params } from './types/L2Transaction';
58import { L1Point } from './types/L1Point';
60const ONE_SECOND = 1000;
62const inviteTemplate = (
63 planet: number,
64 ticket = '',
65 owner = '',
66 hash = ''
67): Invite => ({
68 planet,
69 ticket,
70 owner,
71 hash,
72});
74export default function useRoller() {
75 const {
76 wallet,
77 authToken,
78 authMnemonic,
79 urbitWallet,
80 walletType,
81 }: any = useWallet();
82 const { pointCursor }: any = usePointCursor();
83 const { web3, contracts }: any = useNetwork();
84 const allPoints: any = usePointCache();
85 const controlledPoints = allPoints?.controlledPoints;
86 const getDetails = allPoints?.getDetails;
88 const { setNextRoll } = useTimerStore();
90 const {
91 nextBatchTime,
92 point,
93 removeInvite,
94 setNextBatchTime,
95 setPendingTransactions,
96 setInvites,
97 setInviteGeneratingNum,
98 setInvitesLoading,
99 setPoints,
100 updateInvite,
101 } = useRollerStore();
102 const [config, setConfig] = useState<Config | null>(null);
104 const options: Options = useMemo(() => {
105 const type = isRopsten || !isDevelopment ? 'https' : 'http';
106 const host = isRopsten
107 ? ROLLER_HOSTS.ROPSTEN
108 : isDevelopment
109 ? ROLLER_HOSTS.LOCAL
110 : ROLLER_HOSTS.MAINNET;
111 const port = isDevelopment && !isRopsten ? 8080 : 443;
112 const path = '/v1/roller';
114 return {
115 transport: {
116 type,
117 host,
118 port,
119 path,
120 },
121 };
122 }, []);
124 const api = useMemo(() => {
125 return new RollerRPCAPI(options);
126 }, [options]);
128 const ls = useMemo(
129 () =>
130 new SecureLS({
131 isCompression: false,
132 encryptionSecret: authToken.getOrElse('default'),
133 }),
134 [authToken]
135 );
137 const fetchConfig = useCallback(async () => {
138 api
139 .getRollerConfig()
140 .then(response => {
141 // console.log('CONFIG', response)
142 setConfig(response);
143 setNextBatchTime(response.nextBatch);
144 })
145 .catch(err => {
146 // TODO: more elegant error handling
147 console.warn(
148 '[fetchConfig:failed] check the roller connection?\n',
149 err
150 );
151 });
152 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
154 const generateInviteCodes = useCallback(
155 async (numInvites: number) => {
156 const _point = need.point(pointCursor);
157 const _contracts = contracts.getOrElse(null);
158 const _web3 = web3.getOrElse(null);
159 const _wallet = wallet.getOrElse(null);
160 const _authToken = authToken.getOrElse(null);
161 const _details = getDetails(_point);
163 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
164 // not using need because we want a custom error
165 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
166 }
168 // TODO: remove when UI disables management proxy from spawning invites
169 // if (Just.hasInstance(authMnemonic)) {
170 // throw new Error(
171 // "Auth key Error: A management mnemonic can't create invite codes"
172 // );
173 // }
174 let planets: UnspawnedPoints = await api.getUnspawned(_point);
176 const pendingTxs = await api.getPendingByShip(_point);
178 const pendingSpawns = getPendingSpawns(pendingTxs);
180 planets = planets.filter((point: number) => !pendingSpawns.has(point));
182 const starInfo = await api.getPoint(_point);
183 const invites: Invite[] = [];
185 const proxy = getSpawnProxy(starInfo.ownership!, _wallet.address);
187 if (proxy === undefined)
188 throw new Error("Error: Address doesn't match proxy");
190 const nonce = await api.getNonce({
191 ship: _point,
192 proxy,
193 });
195 for (let i = 0; i < numInvites && planets[i]; i++) {
196 setInviteGeneratingNum(i + 1);
197 const planet = planets[i];
198 const nonceInc = i + nonce;
200 const { ticket, inviteWallet } = await generateInviteWallet(
201 planet,
202 _authToken
203 );
205 await submitL2Transaction({
206 api,
207 address: _wallet.address,
208 wallet: _wallet,
209 ship: _point,
210 proxy,
211 nonce: nonceInc,
212 pointToSpawn: planet,
213 type: 'spawn',
214 walletType,
215 web3: _web3,
216 });
218 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
219 inviteWallet,
220 1
221 );
222 await submitL2Transaction({
223 api,
224 wallet: _wallet,
225 ship: planet,
226 proxy: 'own',
227 nonce: 0,
228 networkSeed,
229 type: 'configureKeys',
230 walletType,
231 web3: _web3,
232 });
234 await registerProxyAddress(
235 api,
236 _wallet,
237 planet,
238 'own',
239 'manage',
240 1,
241 inviteWallet.management.keys.address,
242 walletType,
243 _web3
244 );
246 await submitL2Transaction({
247 api,
248 wallet: _wallet,
249 ship: planet,
250 proxy: 'own',
251 type: 'transferPoint',
252 nonce: 2,
253 address: inviteWallet.ownership.keys.address,
254 walletType,
255 web3: _web3,
256 });
258 invites.push({
259 hash: '',
260 ticket,
261 planet,
262 owner: inviteWallet.ownership.keys.address,
263 });
264 }
266 setStoredInvites(ls, invites);
267 },
268 [
269 api,
270 authToken,
271 contracts,
272 pointCursor,
273 wallet,
274 walletType,
275 web3,
276 getDetails,
277 ls,
278 setInviteGeneratingNum,
279 ]
280 );
282 const getPoints = useCallback(
283 async (proxy: Proxy, address: EthAddress) => {
284 const points: Ship[] =
285 proxy === 'own'
286 ? await api.getOwnedPoints(address)
287 : proxy === 'manage'
288 ? await api.getManagerFor(address)
289 : proxy === 'vote'
290 ? await api.getVotingFor(address)
291 : proxy === 'transfer'
292 ? await api.getTransferringFor(address)
293 : proxy === 'spawn'
294 ? await api.getSpawningFor(address)
295 : [];
297 return points;
298 },
299 [api]
300 );
302 const getPendingTransactions = useCallback(async () => {
303 try {
304 const curPoint = Number(need.point(pointCursor));
305 const newPending = await api.getPendingByShip(curPoint);
307 setPendingTransactions(newPending);
308 } catch (error) {
309 console.warn('ERROR GETTING PENDING', error);
310 }
311 }, [api, setPendingTransactions, pointCursor]);
313 const generateInviteInfo = async (planet: number, _authToken: string) => {
314 const { ticket, inviteWallet } = await generateInviteWallet(
315 planet,
316 _authToken
317 );
319 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
320 };
322 const getInvites = useCallback(async () => {
323 try {
324 setInvitesLoading(true);
325 const curPoint: number = Number(need.point(pointCursor));
326 const _authToken = authToken.getOrElse(null);
327 const _contracts = contracts.getOrElse(null);
329 if (_authToken && _contracts) {
330 const spawnedPoints = point.isL2Spawn
331 ? await api.getSpawned(curPoint)
332 : [];
334 const newPending = await api.getPendingByShip(curPoint);
335 setPendingTransactions(newPending);
336 const pendingSpawns = getPendingSpawns(newPending);
338 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
339 _contracts,
340 curPoint
341 );
343 const outgoingPoints = getOutgoingPoints(
344 controlledPoints,
345 getDetails
346 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
348 const invitePlanets = spawnedPoints
349 .concat(
350 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
351 )
352 .filter((p: number) => !pendingSpawns.has(p))
353 .sort();
355 setInvites(
356 curPoint,
357 invitePlanets.map(p => inviteTemplate(p))
358 );
360 const newInvites: Invite[] = [];
361 // Iterate over all of the stored invites, generating wallet info as necessary
362 const storedInvites = getStoredInvites(ls);
364 for (let i = 0; i < invitePlanets.length; i++) {
365 setInviteGeneratingNum(i + 1);
366 const planet = invitePlanets[i];
367 const storedInvite = storedInvites[planet];
368 const invite =
369 storedInvite || (await generateInviteInfo(planet, _authToken));
371 // TODO: check if the invite point's owner still matches the deterministic wallet address
372 const planetInfo = await api.getPoint(planet);
373 setStoredInvite(ls, invite);
375 if (
376 invite.owner.toLowerCase() ===
377 planetInfo.ownership?.owner?.address ||
378 planetInfo.ownership?.transferProxy?.address !== ETH_ZERO_ADDR
379 ) {
380 newInvites.push(invite);
381 updateInvite(curPoint, invite);
382 } else {
383 removeInvite(curPoint, invite.planet);
384 }
386 if (storedInvite) {
387 setStoredInvite(ls, invite);
388 }
389 }
390 setInvites(curPoint, newInvites);
391 setInvitesLoading(false);
392 return newInvites;
393 }
394 } catch (error) {
395 console.warn('ERROR GETTING INVITES', error);
396 }
397 }, [
398 api,
399 authToken,
400 contracts,
401 controlledPoints,
402 getDetails,
403 pointCursor,
404 ls,
405 point,
406 removeInvite,
407 setInvites,
408 setInvitesLoading,
409 setInviteGeneratingNum,
410 setPendingTransactions,
411 updateInvite,
412 ]);
414 interface ConfigureKeysParams {
415 breach: boolean;
416 customNetworkSeed?: string;
417 }
419 const configureNetworkingKeys = async ({
420 breach,
421 customNetworkSeed,
422 }: ConfigureKeysParams) => {
423 const _point = need.point(pointCursor);
424 const azimuthPoint = await api.getPoint(_point);
425 const _wallet = wallet.getOrElse(null);
426 const _web3 = web3.getOrElse(null);
427 const _details = getDetails(_point);
428 if (!_wallet || !_details || !_web3) {
429 // not using need because we want a custom error
430 throw new Error('Internal Error: Missing Wallet/Details');
431 }
433 const proxy = getManagerProxy(azimuthPoint.ownership!, _wallet.address);
435 if (proxy === undefined)
436 throw new Error("Error: Address doesn't match proxy");
438 const nonce = await api.getNonce({
439 ship: _point,
440 proxy,
441 });
443 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
444 const nextRevision = networkRevision + 1;
445 const networkSeed = customNetworkSeed
446 ? customNetworkSeed
447 : await attemptNetworkSeedDerivation({
448 urbitWallet,
449 wallet,
450 authMnemonic,
451 details: _details,
452 point: _point,
453 authToken,
454 revision: nextRevision,
455 });
456 const txHash = await submitL2Transaction({
457 api,
458 wallet: _wallet,
459 ship: _point,
460 proxy: proxy!,
461 nonce,
462 networkSeed,
463 type: 'configureKeys',
464 walletType,
465 web3: _web3,
466 breach,
467 });
468 const pendingTx = await api.getPendingTx(txHash);
470 return pendingTx;
471 };
473 const performL2Reticket = async ({
474 point,
475 to,
476 manager,
477 fromWallet,
478 toWallet,
479 }: ReticketParams) => {
480 const azimuthPoint = await api.getPoint(point);
481 const proxy = 'own';
482 const _web3 = web3.getOrElse(null);
483 let nonce = await api.getNonce({ ship: point, proxy });
485 let requests = [];
487 // 1. Update networking keys
488 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
489 const nextRevision = networkRevision + 1;
490 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
491 toWallet,
492 nextRevision
493 );
494 const configureKeysRequest = await submitL2Transaction({
495 api,
496 wallet: fromWallet,
497 ship: point,
498 proxy,
499 nonce,
500 networkSeed,
501 walletType,
502 type: 'configureKeys',
503 web3: _web3,
504 });
505 nonce = nonce + 1;
506 requests.push(configureKeysRequest);
508 // 2. Set Management Proxy
509 const registerMgmtRequest = await registerProxyAddress(
510 api,
511 fromWallet,
512 point,
513 proxy,
514 'manage',
515 nonce,
516 manager,
517 walletType,
518 _web3
519 );
520 nonce = nonce + 1;
521 requests.push(registerMgmtRequest);
523 // 3. Set Spawn Proxy
524 if (!isPlanet(Number(point))) {
525 const registerSpawnRequest = await registerProxyAddress(
526 api,
527 fromWallet,
528 point,
529 proxy,
530 'spawn',
531 nonce,
532 toWallet.spawn.keys.address,
533 walletType,
534 _web3
535 );
536 nonce = nonce + 1;
537 requests.push(registerSpawnRequest);
538 }
539 // 4. Transfer point
540 const transferTxRequest = await submitL2Transaction({
541 api,
542 wallet: fromWallet,
543 ship: point,
544 proxy: 'own',
545 type: 'transferPoint',
546 nonce,
547 address: to,
548 walletType,
549 web3: _web3,
550 });
551 requests.push(transferTxRequest);
553 const hashes = await Promise.all(requests);
554 return hashes;
555 };
557 const setProxyAddress = useCallback(
558 async (proxyType: Proxy, address: EthAddress) => {
559 const _point = need.point(pointCursor);
560 const _wallet = wallet.getOrElse(null);
561 const _web3 = web3.getOrElse(null);
563 if (!_wallet) {
564 // not using need because we want a custom error
565 throw new Error('Internal Error: Missing Wallet');
566 }
568 const pointDetails = await api.getPoint(_point);
569 const ownership = pointDetails.ownership!;
570 const proxy = getAddressProxy(ownership, _wallet.address, proxyType);
572 if (proxy === undefined)
573 throw new Error("Error: Address doesn't match expected proxy");
575 const nonce = await api.getNonce({ ship: _point, proxy });
577 const txHash = await registerProxyAddress(
578 api,
579 _wallet,
580 _point,
581 proxy,
582 proxyType,
583 nonce,
584 address,
585 walletType,
586 _web3
587 );
589 const pendingTx = await api.getPendingTx(txHash);
591 return pendingTx;
592 },
593 [api, pointCursor, wallet, web3, walletType]
594 );
596 const adoptPoint = useCallback(
597 async (point: Ship) => {
598 const sponsor = need.point(pointCursor);
599 const _wallet = wallet.getOrElse(null);
601 const sponsorInfo = await api.getPoint(sponsor);
602 const ownership = sponsorInfo.ownership!;
603 const proxy = getManagerProxy(ownership, _wallet.address);
605 if (proxy === undefined)
606 throw new Error("Error: Address doesn't match expected proxy");
608 const nonce = await api.getNonce({ ship: sponsor, proxy });
609 const txHash = await adopt(api, _wallet, sponsor, proxy, nonce, point);
610 const pendingTx = await api.getPendingTx(txHash);
612 return pendingTx;
613 },
614 [api, pointCursor, wallet]
615 );
617 const kickPoint = useCallback(
618 async (point: Ship) => {
619 const sponsor = need.point(pointCursor);
620 const _wallet = wallet.getOrElse(null);
622 const sponsorInfo = await api.getPoint(sponsor);
623 const ownership = sponsorInfo.ownership!;
624 const proxy = getManagerProxy(ownership, _wallet.address);
626 if (proxy === undefined)
627 throw new Error("Error: Address doesn't match expected proxy");
629 const nonce = await api.getNonce({ ship: sponsor, proxy });
630 const txHash = await detach(api, _wallet, sponsor, proxy, nonce, point);
631 const pendingTx = await api.getPendingTx(txHash);
633 return pendingTx;
634 },
635 [api, pointCursor, wallet]
636 );
638 const rejectPoint = useCallback(
639 async (point: Ship) => {
640 const sponsor = need.point(pointCursor);
641 const _wallet = wallet.getOrElse(null);
643 const sponsorInfo = await api.getPoint(sponsor);
644 const ownership = sponsorInfo.ownership!;
645 const proxy = getManagerProxy(ownership, _wallet.address);
647 if (proxy === undefined)
648 throw new Error("Error: Address doesn't match expected proxy");
650 const nonce = await api.getNonce({ ship: sponsor, proxy });
651 const txHash = await reject(api, _wallet, sponsor, proxy, nonce, point);
652 const pendingTx = await api.getPendingTx(txHash);
654 return pendingTx;
655 },
656 [api, pointCursor, wallet]
657 );
659 const transferPoint = useCallback(
660 async (address: EthAddress, reset?: boolean) => {
661 const _point = need.point(pointCursor);
662 const _wallet = wallet.getOrElse(null);
663 const _web3 = web3.getOrElse(null);
665 if (!_wallet) {
666 // not using need because we want a custom error
667 throw new Error('Internal Error: Missing Wallet');
668 }
670 const pointDetails = await api.getPoint(_point);
671 const proxy = getTransferProxy(pointDetails.ownership!, _wallet.address);
673 if (proxy === undefined)
674 throw new Error("Error: Address doesn't match proxy");
676 const nonce = await api.getNonce({ ship: _point, proxy });
678 const txHash = await submitL2Transaction({
679 api,
680 wallet: _wallet,
681 ship: _point,
682 proxy,
683 nonce,
684 address,
685 type: 'transferPoint',
686 walletType,
687 web3: _web3,
688 reset,
689 });
691 const pendingTx = await api.getPendingTx(txHash);
693 return pendingTx;
694 },
695 [api, pointCursor, wallet, web3, walletType]
696 );
698 const sendL2Transaction = useCallback(
699 async (params: SendL2Params) => {
700 const _point = need.point(pointCursor);
701 const _wallet = wallet.getOrElse(null);
702 const _web3 = web3.getOrElse(null);
704 if (!_wallet) {
705 // not using need because we want a custom error
706 throw new Error('Internal Error: Missing Wallet');
707 }
709 return submitL2Transaction({
710 ...params,
711 api,
712 wallet: _wallet,
713 walletType,
714 web3: _web3,
715 ship: _point,
716 });
717 },
718 [api, pointCursor, wallet, web3, walletType]
719 );
721 const changeSponsor = useCallback(
722 async (newSponsor: number) => {
723 const _wallet = wallet.getOrElse(null);
724 const _point = need.point(pointCursor);
726 if (!_wallet) {
727 // not using need because we want a custom error
728 throw new Error('Internal Error: Missing Wallet');
729 }
731 const pointDetails = await api.getPoint(_point);
732 const proxy = getManagerProxy(pointDetails.ownership!, _wallet.address);
734 if (proxy === undefined)
735 throw new Error("Error: Address doesn't match proxy");
737 const nonce = await api.getNonce({ ship: _point, proxy });
739 sendL2Transaction({
740 newSponsor,
741 nonce,
742 proxy,
743 type: 'escape',
744 ship: _point,
745 });
746 },
747 [api, sendL2Transaction, wallet, pointCursor]
748 );
750 const initPoint = useCallback(
751 (relationship: Relationship) => async (point: string | number) => {
752 const _wallet = wallet.getOrElse(null);
754 if (!_wallet) {
755 return EMPTY_POINT;
756 }
758 const pointNum = Number(point);
759 try {
760 const rawDetails = await api.getPoint(pointNum);
761 const details = toL1Details(rawDetails);
763 return new Point(pointNum, relationship, details, _wallet.address);
764 } catch (e) {
765 console.warn(e);
767 // try getting the details from L1
768 if (getDetails) {
769 const details: L1Point = getDetails(point);
770 return new Point(pointNum, relationship, details, _wallet.address);
771 } else {
772 const details: L1Point = toL1Details();
773 return new Point(pointNum, relationship, details, _wallet.address);
774 }
775 }
776 },
777 [api, wallet, getDetails]
778 );
780 const getPointDetails = useCallback(
781 async (
782 ownedPoints: number[],
783 incomingPoints: number[],
784 managingPoints: number[],
785 votingPoints: number[],
786 spawningPoints: number[]
787 ) => {
788 try {
789 const allPoints = [
790 await Promise.all(ownedPoints.map(initPoint(Relationship.own))),
791 await Promise.all(
792 incomingPoints.map(initPoint(Relationship.transfer))
793 ),
794 await Promise.all(managingPoints.map(initPoint(Relationship.manage))),
795 await Promise.all(votingPoints.map(initPoint(Relationship.vote))),
796 await Promise.all(spawningPoints.map(initPoint(Relationship.spawn))),
797 ];
799 setPoints(
800 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
801 cur.forEach(p => {
802 if (p) {
803 acc[p.value] = p;
804 }
805 });
806 return acc;
807 }, {})
808 );
809 } catch (e) {
810 console.warn(e);
811 }
812 },
813 [setPoints, initPoint]
814 );
816 // On load, get initial config
817 useEffect(() => {
818 if (config) {
819 return;
820 }
822 fetchConfig();
823 }, [config, fetchConfig]);
825 useEffect(() => {
826 const time = isDevelopment ? 10000 : ONE_SECOND;
828 const interval = setInterval(() => {
829 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
830 setNextRoll(nextRoll);
832 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
833 api.getRollerConfig().then(response => {
834 setNextBatchTime(response.nextBatch);
835 });
836 }
837 }, time);
839 return () => clearInterval(interval);
840 }, [nextBatchTime, getPendingTransactions, point]); // eslint-disable-line react-hooks/exhaustive-deps
842 return {
843 adoptPoint,
844 api,
845 changeSponsor,
846 config,
847 configureNetworkingKeys,
848 getInvites,
849 getPoints,
850 getPointDetails,
851 getPendingTransactions,
852 generateInviteCodes,
853 kickPoint,
854 rejectPoint,
855 transferPoint,
856 ls,
857 performL2Reticket,
858 setProxyAddress,
859 };