on ea25a45 help sign in
bridge/src/lib/useRoller.ts
857 lines 22 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 {
7 deriveNetworkSeedFromUrbitWallet,
8 attemptNetworkSeedDerivation,
9} from 'lib/keys';
11import { useNetwork } from 'store/network';
12import { usePointCursor } from 'store/pointCursor';
13import { EMPTY_POINT, useRollerStore } from 'store/rollerStore';
14import { useWallet } from 'store/wallet';
16import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
17import { convertToInt } from './convertToInt';
18import { isDevelopment } from './flags';
19import {
20 submitL2Transaction,
21 registerProxyAddress,
22 isL2Spawn,
23} from './utils/roller';
24import { TEN_SECONDS } from './constants';
26import {
27 Config,
28 Ship,
29 Proxy,
30 RollerRPCAPI,
31 EthAddress,
32} from '@urbit/roller-api';
34import Point, { PointField, Points } from './types/Point';
35import { ReticketParams, SendL2Params } from './types/L2Transaction';
36import { L1Point } from './types/L1Point';
37import { ddmmmYYYY } from './utils/date';
38import { showNotification } from './utils/notifications';
39import { useWalletConnect } from './useWalletConnect';
40import { PendingL1Txn } from './types/PendingL1Transaction';
41import { TRANSACTION_PROGRESS } from './reticket';
42import { useRollerOptions } from './useRollerOptions';
43import { useTimerStore } from 'store/timerStore';
45interface UpdateParams {
46 point?: Point | number;
47 message?: string;
48 notify?: boolean;
49 field?: PointField;
50 l1Txn?: PendingL1Txn;
51 intervalTime?: number;
54export default function useRoller() {
55 const {
56 wallet,
57 authToken,
58 authMnemonic,
59 urbitWallet,
60 walletType,
61 }: any = useWallet();
62 const { connector, isConnected, signPersonalMessage } = useWalletConnect();
63 const { pointCursor }: any = usePointCursor();
64 const { web3, contracts }: any = useNetwork();
66 const {
67 point,
68 points,
69 nextQuotaTime,
70 setNextQuotaTime,
71 setPendingTransactions,
72 setModalText,
73 setPoints,
74 updatePoint,
75 storePendingL1Txn,
76 deletePendingL1Txn,
77 } = useRollerStore();
78 const { setNextBatchTime } = useTimerStore();
79 const [config, setConfig] = useState<Config | null>(null);
81 const { options } = useRollerOptions();
83 const api = useMemo(() => {
84 return new RollerRPCAPI(options);
85 }, [options]);
87 const ls = useMemo(
88 () =>
89 new SecureLS({
90 isCompression: false,
91 encryptionSecret: authToken.getOrElse('default'),
92 }),
93 [authToken]
94 );
96 const getNextQuotaTime = useCallback(
97 (nextSlice: number) => {
98 setTimeout(() => {
99 api
100 .whenNextSlice()
101 .then(next => {
102 setNextQuotaTime(next);
103 getNextQuotaTime(next);
104 })
105 .catch(err => console.warn(err));
106 }, nextSlice - new Date().getTime());
107 },
108 [api, setNextQuotaTime]
109 );
111 const fetchConfig = useCallback(async () => {
112 getNextQuotaTime(Date.now() + 1000);
113 api
114 .getRollerConfig()
115 .then(config => {
116 setConfig(config);
117 setNextBatchTime(config.nextBatch);
118 })
119 .catch(err => {
120 // TODO: more elegant error handling
121 console.warn(
122 '[fetchConfig:failed] check the roller connection?\n',
123 err
124 );
125 });
126 }, [api, getNextQuotaTime, setNextBatchTime]);
128 const quotaReached = useCallback(() => {
129 if (point.l2Quota <= 0) {
130 setModalText(
131 `You have reached your weekly L2 transaction limit. You will get another
132 ${point.l2Allowance} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
133 );
134 }
136 return point.l2Quota <= 0;
137 }, [nextQuotaTime, point, setModalText]);
139 const initPoint = useCallback(
140 async (point: string | number): Promise<Point> => {
141 const _wallet = wallet.getOrElse(null);
143 if (!_wallet) {
144 return EMPTY_POINT;
145 }
147 const pointNum = Number(point);
149 try {
150 // Try fetching from the roller
151 const rawDetails = await api.getPoint(pointNum); // this fails for newly spawned L1 point until roller updates
152 const details = toL1Details(rawDetails);
153 const isL2 = isL2Spawn(rawDetails?.dominion);
155 if (isL2) {
156 // Scenario 1: L2 point exists in roller state - use roller data
157 const l2Quota = await api.getRemainingQuota(pointNum);
158 const l2Allowance = await api.getAllowance(pointNum);
159 return new Point({
160 value: pointNum,
161 details,
162 address: _wallet.address,
163 l2Quota,
164 l2Allowance,
165 });
166 } else {
167 // Scenario 2: L1 point exists in roller state - use combination of chain and roller data
168 const _contracts = need.contracts(contracts);
169 const l1Details = await azimuth.azimuth.getPoint(
170 _contracts,
171 pointNum
172 );
173 // Override with roller data for accuracy, L1 data here could be stale
174 l1Details.sponsor = details.sponsor;
175 l1Details.escapeRequested = details.escapeRequested;
176 l1Details.escapeRequestedTo = details.escapeRequestedTo;
177 return new Point({
178 value: pointNum,
179 details: l1Details,
180 address: _wallet.address,
181 });
182 }
183 } catch (e) {
184 // roller failed to fetch point
185 try {
186 // Scenario 3: Newly spawned L1 point - not in roller state yet, use chain data only
187 const _contracts = need.contracts(contracts);
188 const l1Details = await azimuth.azimuth.getPoint(
189 _contracts,
190 pointNum
191 );
192 return new Point({
193 value: pointNum,
194 details: l1Details,
195 address: _wallet.address,
196 });
197 } catch (e2) {
198 // Scenario 4: Some other issue, point doesn’t exist on chain or in roller state. Just return placeholder.
199 console.warn('Failed to fetch point data:', e2);
200 const details: L1Point = toL1Details();
201 return new Point({
202 value: pointNum,
203 details,
204 address: _wallet.address,
205 isPlaceholder: true,
206 });
207 }
208 }
209 },
210 [api, wallet, contracts]
211 );
213 const getAndUpdatePoint = useCallback(
214 async (point: number) => {
215 const updatedPoint = await initPoint(point);
216 updatePoint(updatedPoint);
217 },
218 [initPoint, updatePoint]
219 );
221 const checkForUpdates = useCallback(
222 async ({
223 point,
224 message,
225 notify = true,
226 field,
227 l1Txn,
228 intervalTime = TEN_SECONDS,
229 }: UpdateParams) => {
230 const pVal = typeof point === 'number' ? point : point.value;
231 const p = await initPoint(pVal);
233 if (l1Txn) storePendingL1Txn(l1Txn);
235 let interval: any;
236 const check = async () => {
237 const changedField = !points[p.value]
238 ? 'newPoint'
239 : p.getChangedField(points[p.value], field);
241 if (isDevelopment) {
242 console.log(`CHECKING FOR ${changedField} UPDATES:`, p.patp);
243 }
245 if (!p.isPlaceholder && changedField) {
246 updatePoint(p);
247 if (interval) {
248 clearInterval(interval);
249 }
251 if (l1Txn) deletePendingL1Txn(l1Txn);
252 if (notify) {
253 showNotification(
254 `${message || getUpdatedPointMessage(p, changedField)}`
255 );
256 }
257 }
258 };
260 interval = setInterval(check, intervalTime);
261 check();
262 },
263 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
264 );
266 const spawnPoint = useCallback(
267 async (pointToSpawn: number, destinationAddress: string) => {
268 if (quotaReached()) {
269 return;
270 }
272 const _point = need.point(pointCursor);
273 const _contracts = contracts.getOrElse(null);
274 const _web3 = web3.getOrElse(null);
275 const _wallet = wallet.getOrElse(null);
276 const _authToken = authToken.getOrElse(null);
278 if (!_contracts || !_web3 || !_wallet || !_authToken) {
279 // not using need because we want a custom error
280 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
281 }
283 const proxy = point.getSpawnProxy();
285 if (proxy === undefined)
286 throw new Error("Error: Address doesn't match proxy");
288 const nonce = await api.getNonce({
289 ship: _point,
290 proxy,
291 });
293 await submitL2Transaction({
294 api,
295 address: destinationAddress,
296 wallet: _wallet,
297 ship: _point,
298 proxy,
299 nonce,
300 pointToSpawn,
301 type: 'spawn',
302 walletType,
303 web3: _web3,
304 connector,
305 isConnected,
306 signPersonalMessage,
307 });
308 },
309 [
310 api,
311 authToken,
312 connector,
313 contracts,
314 pointCursor,
315 point,
316 wallet,
317 walletType,
318 web3,
319 quotaReached,
320 signPersonalMessage,
321 isConnected,
322 ]
323 );
325 const getPoints = useCallback(
326 async (proxy: Proxy, address: EthAddress) => {
327 const points: Ship[] =
328 proxy === 'own'
329 ? await api.getOwnedPoints(address)
330 : proxy === 'manage'
331 ? await api.getManagerFor(address)
332 : proxy === 'vote'
333 ? await api.getVotingFor(address)
334 : proxy === 'transfer'
335 ? await api.getTransferringFor(address)
336 : proxy === 'spawn'
337 ? await api.getSpawningFor(address)
338 : [];
340 return points;
341 },
342 [api]
343 );
345 const getPendingTransactions = useCallback(async () => {
346 try {
347 if (!point.isDefault) {
348 const newPending = await api.getPendingByShip(point.value);
349 setPendingTransactions(newPending);
350 }
351 } catch (error) {
352 console.warn('ERROR GETTING PENDING', error);
353 }
354 }, [api, setPendingTransactions, point]);
356 interface ConfigureKeysParams {
357 breach: boolean;
358 customNetworkSeed?: string;
359 }
361 const configureNetworkKeys = useCallback(
362 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
363 if (quotaReached()) {
364 return;
365 }
366 const _wallet = wallet.getOrElse(null);
367 const _web3 = web3.getOrElse(null);
368 if (!_wallet || !_web3 || point.isDefault) {
369 // not using need because we want a custom error
370 throw new Error('Internal Error: Missing Wallet/Details');
371 }
373 const proxy = point.getManagerProxy();
375 if (proxy === undefined)
376 throw new Error("Error: Address doesn't match proxy");
378 const nonce = await api.getNonce({
379 ship: point.value,
380 proxy,
381 });
383 const nextRevision = point.keyRevisionNumber + 1;
384 const networkSeed = customNetworkSeed
385 ? customNetworkSeed
386 : await attemptNetworkSeedDerivation({
387 urbitWallet,
388 wallet,
389 authMnemonic,
390 details: point,
391 authToken,
392 point: point.value,
393 revision: nextRevision,
394 });
395 const txHash = await submitL2Transaction({
396 api,
397 wallet: _wallet,
398 ship: point.value,
399 proxy,
400 nonce,
401 networkSeed,
402 type: 'configureKeys',
403 walletType,
404 web3: _web3,
405 connector,
406 isConnected,
407 signPersonalMessage,
408 breach,
409 });
411 return api.getPendingTx(txHash);
412 },
413 [
414 // eslint-disable-line
415 api,
416 authMnemonic,
417 authToken,
418 point,
419 urbitWallet,
420 wallet,
421 walletType,
422 web3,
423 quotaReached,
424 connector,
425 isConnected,
426 signPersonalMessage,
427 ]
428 );
430 const performL2Reticket = async ({
431 point,
432 to,
433 manager,
434 fromWallet,
435 toWallet,
436 onUpdate,
437 }: ReticketParams) => {
438 const azimuthPoint = await api.getPoint(point);
439 const proxy = 'own';
440 const _web3 = web3.getOrElse(null);
441 let nonce = await api.getNonce({ ship: point, proxy });
442 const progress = onUpdate
443 ? (state: number) => onUpdate({ type: 'progress', state })
444 : () => { };
446 let requests = [];
448 // 1. Update networking keys
449 progress(TRANSACTION_PROGRESS.GENERATING);
450 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
451 const nextRevision = networkRevision + 1;
452 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
453 toWallet,
454 nextRevision
455 );
456 const configureKeysRequest = await submitL2Transaction({
457 api,
458 wallet: fromWallet,
459 ship: point,
460 proxy,
461 nonce,
462 networkSeed,
463 walletType,
464 type: 'configureKeys',
465 web3: _web3,
466 connector,
467 signPersonalMessage,
468 isConnected,
469 });
470 nonce = nonce + 1;
471 requests.push(configureKeysRequest);
473 // 2. Set Management Proxy
474 progress(TRANSACTION_PROGRESS.SIGNING);
475 const registerMgmtRequest = await registerProxyAddress(
476 api,
477 fromWallet,
478 point,
479 proxy,
480 'manage',
481 nonce,
482 manager,
483 walletType,
484 _web3,
485 connector,
486 signPersonalMessage,
487 isConnected
488 );
489 nonce = nonce + 1;
490 requests.push(registerMgmtRequest);
492 // 3. Set Spawn Proxy
493 progress(TRANSACTION_PROGRESS.FUNDING);
494 if (!isPlanet(Number(point))) {
495 const registerSpawnRequest = await registerProxyAddress(
496 api,
497 fromWallet,
498 point,
499 proxy,
500 'spawn',
501 nonce,
502 toWallet?.spawn?.keys?.address || to,
503 walletType,
504 _web3,
505 connector,
506 signPersonalMessage,
507 isConnected
508 );
509 nonce = nonce + 1;
510 requests.push(registerSpawnRequest);
511 }
513 // 4. Transfer point
514 progress(TRANSACTION_PROGRESS.TRANSFERRING);
515 const transferTxRequest = await submitL2Transaction({
516 api,
517 wallet: fromWallet,
518 ship: point,
519 proxy: 'own',
520 type: 'transferPoint',
521 nonce,
522 address: to,
523 walletType,
524 web3: _web3,
525 connector,
526 signPersonalMessage,
527 isConnected,
528 });
529 requests.push(transferTxRequest);
531 progress(TRANSACTION_PROGRESS.CLEANING);
532 const hashes = await Promise.all(requests);
534 progress(TRANSACTION_PROGRESS.DONE);
535 return hashes;
536 };
538 const setProxyAddress = useCallback(
539 async (proxyType: Proxy, address: EthAddress) => {
540 if (quotaReached()) {
541 return;
542 }
543 const _wallet = wallet.getOrElse(null);
544 const _web3 = web3.getOrElse(null);
546 if (!_wallet) {
547 // not using need because we want a custom error
548 throw new Error('Internal Error: Missing Wallet');
549 }
551 if (point.isDefault) {
552 // not using need because we want a custom error
553 throw new Error('Internal Error: Invalid point');
554 }
555 const proxy = point.getAddressProxy(proxyType);
557 if (proxy === undefined)
558 throw new Error("Error: Address doesn't match expected proxy");
560 const nonce = await api.getNonce({ ship: point.value, proxy });
561 const txHash = await registerProxyAddress(
562 api,
563 _wallet,
564 point.value,
565 proxy,
566 proxyType,
567 nonce,
568 address,
569 walletType,
570 _web3,
571 connector,
572 signPersonalMessage,
573 isConnected
574 );
575 return api.getPendingTx(txHash);
576 },
577 [
578 api,
579 connector,
580 signPersonalMessage,
581 isConnected,
582 point,
583 wallet,
584 web3,
585 walletType,
586 quotaReached,
587 ]
588 );
590 const changeSponsorship = useCallback(
591 async (
592 sponsee: Ship,
593 type: 'adopt' | 'detach' | 'reject',
594 skipQuotaCheck = false
595 ) => {
596 if (!skipQuotaCheck && quotaReached()) {
597 return;
598 }
600 const _wallet = wallet.getOrElse(null);
601 const _web3 = web3.getOrElse(null);
602 const ship = point.value;
604 const proxy = point.getManagerProxy();
606 if (proxy === undefined)
607 throw new Error("Error: Address doesn't match expected proxy");
609 const nonce = await api.getNonce({ ship, proxy });
610 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
611 const txHash = await submitL2Transaction({
612 api,
613 wallet: _wallet,
614 ship,
615 sponsee,
616 proxy,
617 nonce,
618 address: _wallet.address,
619 type,
620 walletType,
621 web3: _web3,
622 connector,
623 signPersonalMessage,
624 isConnected,
625 });
627 return api.getPendingTx(txHash);
628 },
629 [
630 api,
631 point,
632 wallet,
633 walletType,
634 web3,
635 connector,
636 quotaReached,
637 isConnected,
638 signPersonalMessage,
639 ]
640 );
642 const transferPoint = useCallback(
643 async (address: EthAddress, reset?: boolean) => {
644 if (quotaReached()) {
645 return;
646 }
648 const _point = need.point(pointCursor);
649 const _wallet = wallet.getOrElse(null);
650 const _web3 = web3.getOrElse(null);
652 if (!_wallet) {
653 // not using need because we want a custom error
654 throw new Error('Internal Error: Missing Wallet');
655 }
657 const proxy = point.getTransferProxy();
659 if (proxy === undefined)
660 throw new Error("Error: Address doesn't match proxy");
662 const nonce = await api.getNonce({ ship: _point, proxy });
664 const txHash = await submitL2Transaction({
665 api,
666 wallet: _wallet,
667 ship: _point,
668 proxy,
669 nonce,
670 address,
671 type: 'transferPoint',
672 walletType,
673 web3: _web3,
674 connector,
675 signPersonalMessage,
676 isConnected,
677 reset,
678 });
680 return api.getPendingTx(txHash);
681 },
682 [
683 api,
684 connector,
685 point,
686 pointCursor,
687 wallet,
688 web3,
689 walletType,
690 quotaReached,
691 isConnected,
692 signPersonalMessage,
693 ]
694 );
696 const sendL2Transaction = useCallback(
697 async (params: SendL2Params) => {
698 if (quotaReached()) {
699 return;
700 }
702 const _wallet = wallet.getOrElse(null);
703 const _web3 = web3.getOrElse(null);
705 if (!_wallet || point.isDefault) {
706 // not using need because we want a custom error
707 throw new Error('Internal Error: Missing Wallet or Point Info');
708 }
710 return submitL2Transaction({
711 ...params,
712 api,
713 wallet: _wallet,
714 walletType,
715 web3: _web3,
716 connector,
717 signPersonalMessage,
718 isConnected,
719 ship: point.value,
720 });
721 },
722 [
723 api,
724 connector,
725 point,
726 wallet,
727 web3,
728 walletType,
729 quotaReached,
730 isConnected,
731 signPersonalMessage,
732 ]
733 );
735 const changeSponsor = useCallback(
736 async (newSponsor: number) => {
737 if (point.isDefault) {
738 throw new Error('Internal Error: point is default');
739 }
741 const curPoint = point.value;
742 const _wallet = wallet.getOrElse(null);
744 if (!_wallet) {
745 // not using need because we want a custom error
746 throw new Error('Internal Error: Missing Wallet');
747 }
749 const proxy = point.getManagerProxy();
751 if (proxy === undefined)
752 throw new Error("Error: Address doesn't match proxy");
754 const nonce = await api.getNonce({ ship: curPoint, proxy });
756 return sendL2Transaction({
757 newSponsor,
758 nonce,
759 proxy,
760 type: 'escape',
761 ship: curPoint,
762 });
763 },
764 [api, point, sendL2Transaction, wallet]
765 );
767 const cancelEscape = useCallback(async () => {
768 if (point.isDefault) {
769 throw new Error('Internal Error: point is default');
770 }
772 const curPoint = point.value;
773 const _wallet = wallet.getOrElse(null);
775 if (!_wallet) {
776 // not using need because we want a custom error
777 throw new Error('Internal Error: Missing Wallet');
778 }
780 const proxy = point.getManagerProxy();
782 if (proxy === undefined)
783 throw new Error("Error: Address doesn't match proxy");
785 const nonce = await api.getNonce({ ship: curPoint, proxy });
787 return sendL2Transaction({
788 nonce,
789 proxy,
790 type: 'cancelEscape',
791 ship: curPoint,
792 });
793 }, [api, point, sendL2Transaction, wallet]);
795 const getPointsDetails = useCallback(
796 async (
797 ownedPoints: number[],
798 incomingPoints: number[],
799 managingPoints: number[],
800 votingPoints: number[],
801 spawningPoints: number[]
802 ) => {
803 try {
804 const allPoints = [
805 await Promise.all(ownedPoints.map(initPoint)),
806 await Promise.all(incomingPoints.map(initPoint)),
807 await Promise.all(managingPoints.map(initPoint)),
808 await Promise.all(votingPoints.map(initPoint)),
809 await Promise.all(spawningPoints.map(initPoint)),
810 ];
812 setPoints(
813 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
814 cur.forEach(p => {
815 if (p) {
816 acc[p.value] = p;
817 }
818 });
819 return acc;
820 }, {})
821 );
822 } catch (e) {
823 console.warn(e);
824 }
825 },
826 [setPoints, initPoint]
827 );
829 // On load, get initial config
830 useEffect(() => {
831 if (config) {
832 return;
833 }
835 fetchConfig();
836 }, [config, fetchConfig]);
838 return {
839 api,
840 changeSponsorship,
841 changeSponsor,
842 cancelEscape,
843 checkForUpdates,
844 config,
845 configureNetworkKeys,
846 getAndUpdatePoint,
847 getPoints,
848 getPointsDetails,
849 getPendingTransactions,
850 initPoint,
851 ls,
852 performL2Reticket,
853 setProxyAddress,
854 spawnPoint,
855 transferPoint,
856 };