on 368f024 help sign in
bridge/src/lib/useRoller.ts
881 lines 24 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 { 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';
59const ONE_SECOND = 1000;
61const inviteTemplate = (
62 planet: number,
63 ticket = '',
64 owner = '',
65 hash = ''
66): Invite => ({
67 planet,
68 ticket,
69 owner,
70 hash,
71});
73export default function useRoller() {
74 const {
75 wallet,
76 authToken,
77 authMnemonic,
78 urbitWallet,
79 walletType,
80 }: any = useWallet();
81 const { pointCursor }: any = usePointCursor();
82 const { web3, contracts }: any = useNetwork();
83 const allPoints: any = usePointCache();
84 const controlledPoints = allPoints?.controlledPoints;
85 const getDetails = allPoints?.getDetails;
87 const { setNextRoll } = useTimerStore();
89 const {
90 nextBatchTime,
91 point,
92 invitePoints,
93 removeInvite,
94 setNextBatchTime,
95 setPendingTransactions,
96 setInvites,
97 setInvitePoints,
98 setInviteGeneratingNum,
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 setConfig(response);
142 setNextBatchTime(response.nextBatch);
143 })
144 .catch(err => {
145 // TODO: more elegant error handling
146 console.warn(
147 '[fetchConfig:failed] check the roller connection?\n',
148 err
149 );
150 });
151 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
153 const generateInviteCodes = useCallback(
154 async (numInvites: number) => {
155 const _point = need.point(pointCursor);
156 const _contracts = contracts.getOrElse(null);
157 const _web3 = web3.getOrElse(null);
158 const _wallet = wallet.getOrElse(null);
159 const _authToken = authToken.getOrElse(null);
160 const _details = getDetails(_point);
162 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
163 // not using need because we want a custom error
164 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
165 }
167 // TODO: remove when UI disables management proxy from spawning invites
168 // if (Just.hasInstance(authMnemonic)) {
169 // throw new Error(
170 // "Auth key Error: A management mnemonic can't create invite codes"
171 // );
172 // }
173 let planets: UnspawnedPoints = await api.getUnspawned(_point);
175 const pendingTxs = await api.getPendingByShip(_point);
177 const pendingSpawns = getPendingSpawns(pendingTxs);
179 planets = planets.filter((point: number) => !pendingSpawns.has(point));
181 const starInfo = await api.getPoint(_point);
182 const invites: Invite[] = [];
184 const proxy = getSpawnProxy(starInfo.ownership!, _wallet.address);
186 if (proxy === undefined)
187 throw new Error("Error: Address doesn't match proxy");
189 const nonce = await api.getNonce({
190 ship: _point,
191 proxy,
192 });
194 for (let i = 0; i < numInvites && planets[i]; i++) {
195 setInviteGeneratingNum(i + 1);
196 const planet = planets[i];
197 const nonceInc = i + nonce;
199 const { ticket, inviteWallet } = await generateInviteWallet(
200 planet,
201 _authToken
202 );
204 await submitL2Transaction({
205 api,
206 address: _wallet.address,
207 wallet: _wallet,
208 ship: _point,
209 proxy,
210 nonce: nonceInc,
211 pointToSpawn: planet,
212 type: 'spawn',
213 walletType,
214 web3: _web3,
215 });
217 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
218 inviteWallet,
219 1
220 );
221 await submitL2Transaction({
222 api,
223 wallet: _wallet,
224 ship: planet,
225 proxy: 'own',
226 nonce: 0,
227 networkSeed,
228 type: 'configureKeys',
229 walletType,
230 web3: _web3,
231 });
233 await registerProxyAddress(
234 api,
235 _wallet,
236 planet,
237 'own',
238 'manage',
239 1,
240 inviteWallet.management.keys.address,
241 walletType,
242 _web3
243 );
245 await submitL2Transaction({
246 api,
247 wallet: _wallet,
248 ship: planet,
249 proxy: 'own',
250 type: 'transferPoint',
251 nonce: 2,
252 address: inviteWallet.ownership.keys.address,
253 walletType,
254 web3: _web3,
255 });
257 invites.push({
258 hash: '',
259 ticket,
260 planet,
261 owner: inviteWallet.ownership.keys.address,
262 });
263 }
265 setStoredInvites(ls, invites);
266 },
267 [
268 api,
269 authToken,
270 contracts,
271 pointCursor,
272 wallet,
273 walletType,
274 web3,
275 getDetails,
276 ls,
277 setInviteGeneratingNum,
278 ]
279 );
281 const getPoints = useCallback(
282 async (proxy: Proxy, address: EthAddress) => {
283 const points: Ship[] =
284 proxy === 'own'
285 ? await api.getOwnedPoints(address)
286 : proxy === 'manage'
287 ? await api.getManagerFor(address)
288 : proxy === 'vote'
289 ? await api.getVotingFor(address)
290 : proxy === 'transfer'
291 ? await api.getTransferringFor(address)
292 : proxy === 'spawn'
293 ? await api.getSpawningFor(address)
294 : [];
296 return points;
297 },
298 [api]
299 );
301 const getPendingTransactions = useCallback(async () => {
302 try {
303 const curPoint = Number(need.point(pointCursor));
304 const newPending = await api.getPendingByShip(curPoint);
306 setPendingTransactions(newPending);
307 } catch (error) {
308 console.warn('ERROR GETTING PENDING', error);
309 }
310 }, [api, setPendingTransactions, pointCursor]);
312 const generateInviteInfo = async (planet: number, _authToken: string) => {
313 const { ticket, inviteWallet } = await generateInviteWallet(
314 planet,
315 _authToken
316 );
318 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
319 };
321 const getInvites = useCallback(
322 async (isL2: boolean, getAll = false) => {
323 try {
324 const curPoint: number = Number(need.point(pointCursor));
325 setInvites(invitePoints.map(p => inviteTemplate(p)));
327 const _authToken = authToken.getOrElse(null);
328 const _contracts = contracts.getOrElse(null);
330 if (_authToken && _contracts) {
331 const spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
333 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
334 _contracts,
335 curPoint
336 );
338 const outgoingPoints = getOutgoingPoints(
339 controlledPoints,
340 getDetails
341 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
343 const invitePlanets = spawnedPoints.concat(
344 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
345 );
347 const newInvites: Invite[] = [];
348 // Iterate over all of the stored invites, generating wallet info as necessary
349 const storedInvites = getStoredInvites(ls);
351 for (let i = 0; i < invitePlanets.length; i++) {
352 setInviteGeneratingNum(i + 1);
353 const planet = invitePlanets[i];
354 const invite =
355 storedInvites[planet] ||
356 (await generateInviteInfo(planet, _authToken));
358 // TODO: check if the invite point's owner still matches the deterministic wallet address
359 const planetInfo = await api.getPoint(planet);
360 if (invite.owner === planetInfo.ownership?.transferProxy?.address) {
361 newInvites.push(invite);
362 updateInvite(invite);
363 setStoredInvite(ls, invite);
364 } else {
365 removeInvite(planet);
366 }
367 }
368 setStoredInvites(ls, newInvites);
369 setInvites(newInvites);
371 return newInvites;
372 }
373 } catch (error) {
374 console.warn('ERROR GETTING INVITES', error);
375 }
376 },
377 [
378 api,
379 authToken,
380 contracts,
381 controlledPoints,
382 getDetails,
383 pointCursor,
384 invitePoints,
385 ls,
386 removeInvite,
387 setInvites,
388 setInviteGeneratingNum,
389 updateInvite,
390 ]
391 );
393 const getNumInvites = useCallback(
394 async (isL2: boolean) => {
395 try {
396 const curPoint: number = Number(need.point(pointCursor));
397 const _authToken = authToken.getOrElse(null);
398 const _contracts = contracts.getOrElse(null);
400 if (_authToken && _contracts) {
401 let spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
403 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
404 _contracts,
405 curPoint
406 );
408 const newPending = await api.getPendingByShip(curPoint);
409 const pendingSpawns = getPendingSpawns(newPending);
411 const outgoingPoints = getOutgoingPoints(
412 controlledPoints,
413 getDetails
414 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
416 const invitePoints = spawnedPoints
417 .concat(
418 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
419 )
420 .filter((p: number) => !pendingSpawns.has(p));
422 setPendingTransactions(newPending);
423 setInvitePoints(invitePoints);
425 return invitePoints;
426 }
427 } catch (e) {
428 if (isDevelopment) {
429 console.warn(e);
430 }
431 }
432 },
433 [
434 api,
435 pointCursor,
436 authToken,
437 contracts,
438 controlledPoints,
439 getDetails,
440 setInvitePoints,
441 setPendingTransactions,
442 ]
443 );
445 interface ConfigureKeysParams {
446 breach: boolean;
447 customNetworkSeed?: string;
448 }
450 const configureNetworkingKeys = async ({
451 breach,
452 customNetworkSeed,
453 }: ConfigureKeysParams) => {
454 const _point = need.point(pointCursor);
455 const azimuthPoint = await api.getPoint(_point);
456 const _wallet = wallet.getOrElse(null);
457 const _web3 = web3.getOrElse(null);
458 const _details = getDetails(_point);
459 if (!_wallet || !_details || !_web3) {
460 // not using need because we want a custom error
461 throw new Error('Internal Error: Missing Wallet/Details');
462 }
464 const proxy = getManagerProxy(azimuthPoint.ownership!, _wallet.address);
466 if (proxy === undefined)
467 throw new Error("Error: Address doesn't match proxy");
469 const nonce = await api.getNonce({
470 ship: _point,
471 proxy,
472 });
474 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
475 const nextRevision = networkRevision + 1;
476 const networkSeed = customNetworkSeed
477 ? customNetworkSeed
478 : await attemptNetworkSeedDerivation({
479 urbitWallet,
480 wallet,
481 authMnemonic,
482 details: _details,
483 point: _point,
484 authToken,
485 revision: nextRevision,
486 });
487 const txHash = await submitL2Transaction({
488 api,
489 wallet: _wallet,
490 ship: _point,
491 proxy: proxy!,
492 nonce,
493 networkSeed,
494 type: 'configureKeys',
495 walletType,
496 web3: _web3,
497 breach,
498 });
499 const pendingTx = await api.getPendingTx(txHash);
501 return pendingTx;
502 };
504 const performL2Reticket = async ({
505 point,
506 to,
507 manager,
508 fromWallet,
509 toWallet,
510 }: ReticketParams) => {
511 const azimuthPoint = await api.getPoint(point);
512 const proxy = 'own';
513 const _web3 = web3.getOrElse(null);
514 let nonce = await api.getNonce({ ship: point, proxy });
516 let requests = [];
518 // 1. Update networking keys
519 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
520 const nextRevision = networkRevision + 1;
521 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
522 toWallet,
523 nextRevision
524 );
525 const configureKeysRequest = await submitL2Transaction({
526 api,
527 wallet: fromWallet,
528 ship: point,
529 proxy,
530 nonce,
531 networkSeed,
532 walletType,
533 type: 'configureKeys',
534 web3: _web3,
535 });
536 nonce = nonce + 1;
537 requests.push(configureKeysRequest);
539 // 2. Set Management Proxy
540 const registerMgmtRequest = await registerProxyAddress(
541 api,
542 fromWallet,
543 point,
544 proxy,
545 'manage',
546 nonce,
547 manager,
548 walletType,
549 _web3
550 );
551 nonce = nonce + 1;
552 requests.push(registerMgmtRequest);
554 // 3. Set Spawn Proxy
555 if (!isPlanet(Number(point))) {
556 const registerSpawnRequest = await registerProxyAddress(
557 api,
558 fromWallet,
559 point,
560 proxy,
561 'spawn',
562 nonce,
563 toWallet.spawn.keys.address,
564 walletType,
565 _web3
566 );
567 nonce = nonce + 1;
568 requests.push(registerSpawnRequest);
569 }
570 // 4. Transfer point
571 const transferTxRequest = await submitL2Transaction({
572 api,
573 wallet: fromWallet,
574 ship: point,
575 proxy: 'own',
576 type: 'transferPoint',
577 nonce,
578 address: to,
579 walletType,
580 web3: _web3,
581 });
582 requests.push(transferTxRequest);
584 const hashes = await Promise.all(requests);
585 return hashes;
586 };
588 const setProxyAddress = useCallback(
589 async (proxyType: Proxy, address: EthAddress) => {
590 const _point = need.point(pointCursor);
591 const _wallet = wallet.getOrElse(null);
592 const _web3 = web3.getOrElse(null);
594 if (!_wallet) {
595 // not using need because we want a custom error
596 throw new Error('Internal Error: Missing Wallet');
597 }
599 const pointDetails = await api.getPoint(_point);
600 const ownership = pointDetails.ownership!;
601 const proxy = getAddressProxy(ownership, _wallet.address, proxyType);
603 if (proxy === undefined)
604 throw new Error("Error: Address doesn't match expected proxy");
606 const nonce = await api.getNonce({ ship: _point, proxy });
608 const txHash = await registerProxyAddress(
609 api,
610 _wallet,
611 _point,
612 proxy,
613 proxyType,
614 nonce,
615 address,
616 walletType,
617 _web3
618 );
620 const pendingTx = await api.getPendingTx(txHash);
622 return pendingTx;
623 },
624 [api, pointCursor, wallet, web3, walletType]
625 );
627 const adoptPoint = useCallback(
628 async (point: Ship) => {
629 const sponsor = need.point(pointCursor);
630 const _wallet = wallet.getOrElse(null);
632 const sponsorInfo = await api.getPoint(sponsor);
633 const ownership = sponsorInfo.ownership!;
634 const proxy = getManagerProxy(ownership, _wallet.address);
636 if (proxy === undefined)
637 throw new Error("Error: Address doesn't match expected proxy");
639 const nonce = await api.getNonce({ ship: sponsor, proxy });
640 const txHash = await adopt(api, _wallet, sponsor, proxy, nonce, point);
641 const pendingTx = await api.getPendingTx(txHash);
643 return pendingTx;
644 },
645 [api, pointCursor, wallet]
646 );
648 const kickPoint = useCallback(
649 async (point: Ship) => {
650 const sponsor = need.point(pointCursor);
651 const _wallet = wallet.getOrElse(null);
653 const sponsorInfo = await api.getPoint(sponsor);
654 const ownership = sponsorInfo.ownership!;
655 const proxy = getManagerProxy(ownership, _wallet.address);
657 if (proxy === undefined)
658 throw new Error("Error: Address doesn't match expected proxy");
660 const nonce = await api.getNonce({ ship: sponsor, proxy });
661 const txHash = await detach(api, _wallet, sponsor, proxy, nonce, point);
662 const pendingTx = await api.getPendingTx(txHash);
664 return pendingTx;
665 },
666 [api, pointCursor, wallet]
667 );
669 const rejectPoint = useCallback(
670 async (point: Ship) => {
671 const sponsor = need.point(pointCursor);
672 const _wallet = wallet.getOrElse(null);
674 const sponsorInfo = await api.getPoint(sponsor);
675 const ownership = sponsorInfo.ownership!;
676 const proxy = getManagerProxy(ownership, _wallet.address);
678 if (proxy === undefined)
679 throw new Error("Error: Address doesn't match expected proxy");
681 const nonce = await api.getNonce({ ship: sponsor, proxy });
682 const txHash = await reject(api, _wallet, sponsor, proxy, nonce, point);
683 const pendingTx = await api.getPendingTx(txHash);
685 return pendingTx;
686 },
687 [api, pointCursor, wallet]
688 );
690 const transferPoint = useCallback(
691 async (address: EthAddress, reset?: boolean) => {
692 const _point = need.point(pointCursor);
693 const _wallet = wallet.getOrElse(null);
694 const _web3 = web3.getOrElse(null);
696 if (!_wallet) {
697 // not using need because we want a custom error
698 throw new Error('Internal Error: Missing Wallet');
699 }
701 const pointDetails = await api.getPoint(_point);
702 const proxy = getTransferProxy(pointDetails.ownership!, _wallet.address);
704 if (proxy === undefined)
705 throw new Error("Error: Address doesn't match proxy");
707 const nonce = await api.getNonce({ ship: _point, proxy });
709 const txHash = await submitL2Transaction({
710 api,
711 wallet: _wallet,
712 ship: _point,
713 proxy,
714 nonce,
715 address,
716 type: 'transferPoint',
717 walletType,
718 web3: _web3,
719 reset,
720 });
722 const pendingTx = await api.getPendingTx(txHash);
724 return pendingTx;
725 },
726 [api, pointCursor, wallet, web3, walletType]
727 );
729 const sendL2Transaction = useCallback(
730 async (params: SendL2Params) => {
731 const _point = need.point(pointCursor);
732 const _wallet = wallet.getOrElse(null);
733 const _web3 = web3.getOrElse(null);
735 if (!_wallet) {
736 // not using need because we want a custom error
737 throw new Error('Internal Error: Missing Wallet');
738 }
740 return submitL2Transaction({
741 ...params,
742 api,
743 wallet: _wallet,
744 walletType,
745 web3: _web3,
746 ship: _point,
747 });
748 },
749 [api, pointCursor, wallet, web3, walletType]
750 );
752 const changeSponsor = useCallback(
753 async (newSponsor: number) => {
754 const _wallet = wallet.getOrElse(null);
755 const _point = need.point(pointCursor);
757 if (!_wallet) {
758 // not using need because we want a custom error
759 throw new Error('Internal Error: Missing Wallet');
760 }
762 const pointDetails = await api.getPoint(_point);
763 const proxy = getManagerProxy(pointDetails.ownership!, _wallet.address);
765 if (proxy === undefined)
766 throw new Error("Error: Address doesn't match proxy");
768 const nonce = await api.getNonce({ ship: _point, proxy });
770 sendL2Transaction({
771 newSponsor,
772 nonce,
773 proxy,
774 type: 'escape',
775 ship: _point,
776 });
777 },
778 [api, sendL2Transaction, wallet, pointCursor]
779 );
781 const initPoint = useCallback(
782 (relationship: Relationship) => async (point: string | number) => {
783 const _wallet = wallet.getOrElse(null);
785 if (!_wallet) {
786 return EMPTY_POINT;
787 }
788 try {
789 const pointNum = Number(point);
790 const rawDetails = await api.getPoint(pointNum);
791 const details = toL1Details(rawDetails);
793 return new Point(pointNum, relationship, details, _wallet.address);
794 } catch (e) {
795 console.warn(e);
796 }
797 },
798 [api, wallet]
799 );
801 const getPointDetails = useCallback(
802 async (
803 ownedPoints: number[],
804 incomingPoints: number[],
805 managingPoints: number[],
806 votingPoints: number[],
807 spawningPoints: number[]
808 ) => {
809 try {
810 const allPoints = [
811 await Promise.all(ownedPoints.map(initPoint(Relationship.own))),
812 await Promise.all(
813 incomingPoints.map(initPoint(Relationship.transfer))
814 ),
815 await Promise.all(managingPoints.map(initPoint(Relationship.manage))),
816 await Promise.all(votingPoints.map(initPoint(Relationship.vote))),
817 await Promise.all(spawningPoints.map(initPoint(Relationship.spawn))),
818 ];
820 setPoints(
821 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
822 cur.forEach(p => {
823 if (p) {
824 acc[p.value] = p;
825 }
826 });
827 return acc;
828 }, {})
829 );
830 } catch (e) {
831 console.warn(e);
832 }
833 },
834 [setPoints, initPoint]
835 );
837 // On load, get initial config
838 useEffect(() => {
839 if (config) {
840 return;
841 }
843 fetchConfig();
844 }, [config, fetchConfig]);
846 useEffect(() => {
847 const interval = setInterval(() => {
848 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
849 setNextRoll(nextRoll);
851 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
852 api.getRollerConfig().then(response => {
853 setNextBatchTime(response.nextBatch);
854 getNumInvites(!!point?.isL2);
855 });
856 }
857 }, 10000000);
859 return () => clearInterval(interval);
860 }, [nextBatchTime, getPendingTransactions, point]); // eslint-disable-line react-hooks/exhaustive-deps
862 return {
863 adoptPoint,
864 api,
865 changeSponsor,
866 config,
867 configureNetworkingKeys,
868 getInvites,
869 getNumInvites,
870 getPoints,
871 getPointDetails,
872 getPendingTransactions,
873 generateInviteCodes,
874 kickPoint,
875 rejectPoint,
876 transferPoint,
877 ls,
878 performL2Reticket,
879 setProxyAddress,
880 };