on 7675621 help sign in
bridge/src/lib/useRoller.ts
838 lines 21 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);
148 const rawDetails = await api.getPoint(pointNum);
149 const isL2 = isL2Spawn(rawDetails?.dominion);
151 const details = toL1Details(rawDetails);
153 try {
154 if (isL2) {
155 const l2Quota = isL2 ? await api.getRemainingQuota(pointNum) : 0;
156 const l2Allowance = isL2 ? await api.getAllowance(pointNum) : 0;
158 return new Point({
159 value: pointNum,
160 details,
161 address: _wallet.address,
162 l2Quota,
163 l2Allowance,
164 });
165 }
167 const _contracts = need.contracts(contracts);
168 const l1Details = await azimuth.azimuth.getPoint(_contracts, point);
170 l1Details.sponsor = details.sponsor;
171 l1Details.escapeRequested = details.escapeRequested;
172 l1Details.escapeRequestedTo = details.escapeRequestedTo;
174 return new Point({
175 value: pointNum,
176 details: l1Details,
177 address: _wallet.address,
178 });
179 } catch (e) {
180 console.warn(e);
181 // Just return a placeholder Point
182 const details: L1Point = toL1Details();
183 return new Point({
184 value: pointNum,
185 details,
186 address: _wallet.address,
187 isPlaceholder: true,
188 });
189 }
190 },
191 [api, wallet, contracts]
192 );
194 const getAndUpdatePoint = useCallback(
195 async (point: number) => {
196 const updatedPoint = await initPoint(point);
197 updatePoint(updatedPoint);
198 },
199 [initPoint, updatePoint]
200 );
202 const checkForUpdates = useCallback(
203 async ({
204 point,
205 message,
206 notify = true,
207 field,
208 l1Txn,
209 intervalTime = TEN_SECONDS,
210 }: UpdateParams) => {
211 const pVal = typeof point === 'number' ? point : point.value;
212 const p = await initPoint(pVal);
214 if (l1Txn) storePendingL1Txn(l1Txn);
216 let interval: any;
217 const check = async () => {
218 const changedField = !points[p.value]
219 ? 'newPoint'
220 : p.getChangedField(points[p.value], field);
222 if (isDevelopment) {
223 console.log(`CHECKING FOR ${changedField} UPDATES:`, p.patp);
224 }
226 if (!p.isPlaceholder && changedField) {
227 updatePoint(p);
228 if (interval) {
229 clearInterval(interval);
230 }
232 if (l1Txn) deletePendingL1Txn(l1Txn);
233 if (notify) {
234 showNotification(
235 `${message || getUpdatedPointMessage(p, changedField)}`
236 );
237 }
238 }
239 };
241 interval = setInterval(check, intervalTime);
242 check();
243 },
244 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
245 );
247 const spawnPoint = useCallback(
248 async (pointToSpawn: number, destinationAddress: string) => {
249 if (quotaReached()) {
250 return;
251 }
253 const _point = need.point(pointCursor);
254 const _contracts = contracts.getOrElse(null);
255 const _web3 = web3.getOrElse(null);
256 const _wallet = wallet.getOrElse(null);
257 const _authToken = authToken.getOrElse(null);
259 if (!_contracts || !_web3 || !_wallet || !_authToken) {
260 // not using need because we want a custom error
261 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
262 }
264 const proxy = point.getSpawnProxy();
266 if (proxy === undefined)
267 throw new Error("Error: Address doesn't match proxy");
269 const nonce = await api.getNonce({
270 ship: _point,
271 proxy,
272 });
274 await submitL2Transaction({
275 api,
276 address: destinationAddress,
277 wallet: _wallet,
278 ship: _point,
279 proxy,
280 nonce,
281 pointToSpawn,
282 type: 'spawn',
283 walletType,
284 web3: _web3,
285 connector,
286 isConnected,
287 signPersonalMessage,
288 });
289 },
290 [
291 api,
292 authToken,
293 connector,
294 contracts,
295 pointCursor,
296 point,
297 wallet,
298 walletType,
299 web3,
300 quotaReached,
301 signPersonalMessage,
302 isConnected,
303 ]
304 );
306 const getPoints = useCallback(
307 async (proxy: Proxy, address: EthAddress) => {
308 const points: Ship[] =
309 proxy === 'own'
310 ? await api.getOwnedPoints(address)
311 : proxy === 'manage'
312 ? await api.getManagerFor(address)
313 : proxy === 'vote'
314 ? await api.getVotingFor(address)
315 : proxy === 'transfer'
316 ? await api.getTransferringFor(address)
317 : proxy === 'spawn'
318 ? await api.getSpawningFor(address)
319 : [];
321 return points;
322 },
323 [api]
324 );
326 const getPendingTransactions = useCallback(async () => {
327 try {
328 if (!point.isDefault) {
329 const newPending = await api.getPendingByShip(point.value);
330 setPendingTransactions(newPending);
331 }
332 } catch (error) {
333 console.warn('ERROR GETTING PENDING', error);
334 }
335 }, [api, setPendingTransactions, point]);
337 interface ConfigureKeysParams {
338 breach: boolean;
339 customNetworkSeed?: string;
340 }
342 const configureNetworkKeys = useCallback(
343 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
344 if (quotaReached()) {
345 return;
346 }
347 const _wallet = wallet.getOrElse(null);
348 const _web3 = web3.getOrElse(null);
349 if (!_wallet || !_web3 || point.isDefault) {
350 // not using need because we want a custom error
351 throw new Error('Internal Error: Missing Wallet/Details');
352 }
354 const proxy = point.getManagerProxy();
356 if (proxy === undefined)
357 throw new Error("Error: Address doesn't match proxy");
359 const nonce = await api.getNonce({
360 ship: point.value,
361 proxy,
362 });
364 const nextRevision = point.keyRevisionNumber + 1;
365 const networkSeed = customNetworkSeed
366 ? customNetworkSeed
367 : await attemptNetworkSeedDerivation({
368 urbitWallet,
369 wallet,
370 authMnemonic,
371 details: point,
372 authToken,
373 point: point.value,
374 revision: nextRevision,
375 });
376 const txHash = await submitL2Transaction({
377 api,
378 wallet: _wallet,
379 ship: point.value,
380 proxy,
381 nonce,
382 networkSeed,
383 type: 'configureKeys',
384 walletType,
385 web3: _web3,
386 connector,
387 isConnected,
388 signPersonalMessage,
389 breach,
390 });
392 return api.getPendingTx(txHash);
393 },
394 [
395 // eslint-disable-line
396 api,
397 authMnemonic,
398 authToken,
399 point,
400 urbitWallet,
401 wallet,
402 walletType,
403 web3,
404 quotaReached,
405 connector,
406 isConnected,
407 signPersonalMessage,
408 ]
409 );
411 const performL2Reticket = async ({
412 point,
413 to,
414 manager,
415 fromWallet,
416 toWallet,
417 onUpdate,
418 }: ReticketParams) => {
419 const azimuthPoint = await api.getPoint(point);
420 const proxy = 'own';
421 const _web3 = web3.getOrElse(null);
422 let nonce = await api.getNonce({ ship: point, proxy });
423 const progress = onUpdate
424 ? (state: number) => onUpdate({ type: 'progress', state })
425 : () => {};
427 let requests = [];
429 // 1. Update networking keys
430 progress(TRANSACTION_PROGRESS.GENERATING);
431 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
432 const nextRevision = networkRevision + 1;
433 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
434 toWallet,
435 nextRevision
436 );
437 const configureKeysRequest = await submitL2Transaction({
438 api,
439 wallet: fromWallet,
440 ship: point,
441 proxy,
442 nonce,
443 networkSeed,
444 walletType,
445 type: 'configureKeys',
446 web3: _web3,
447 connector,
448 signPersonalMessage,
449 isConnected,
450 });
451 nonce = nonce + 1;
452 requests.push(configureKeysRequest);
454 // 2. Set Management Proxy
455 progress(TRANSACTION_PROGRESS.SIGNING);
456 const registerMgmtRequest = await registerProxyAddress(
457 api,
458 fromWallet,
459 point,
460 proxy,
461 'manage',
462 nonce,
463 manager,
464 walletType,
465 _web3,
466 connector,
467 signPersonalMessage,
468 isConnected
469 );
470 nonce = nonce + 1;
471 requests.push(registerMgmtRequest);
473 // 3. Set Spawn Proxy
474 progress(TRANSACTION_PROGRESS.FUNDING);
475 if (!isPlanet(Number(point))) {
476 const registerSpawnRequest = await registerProxyAddress(
477 api,
478 fromWallet,
479 point,
480 proxy,
481 'spawn',
482 nonce,
483 toWallet?.spawn?.keys?.address || to,
484 walletType,
485 _web3,
486 connector,
487 signPersonalMessage,
488 isConnected
489 );
490 nonce = nonce + 1;
491 requests.push(registerSpawnRequest);
492 }
494 // 4. Transfer point
495 progress(TRANSACTION_PROGRESS.TRANSFERRING);
496 const transferTxRequest = await submitL2Transaction({
497 api,
498 wallet: fromWallet,
499 ship: point,
500 proxy: 'own',
501 type: 'transferPoint',
502 nonce,
503 address: to,
504 walletType,
505 web3: _web3,
506 connector,
507 signPersonalMessage,
508 isConnected,
509 });
510 requests.push(transferTxRequest);
512 progress(TRANSACTION_PROGRESS.CLEANING);
513 const hashes = await Promise.all(requests);
515 progress(TRANSACTION_PROGRESS.DONE);
516 return hashes;
517 };
519 const setProxyAddress = useCallback(
520 async (proxyType: Proxy, address: EthAddress) => {
521 if (quotaReached()) {
522 return;
523 }
524 const _wallet = wallet.getOrElse(null);
525 const _web3 = web3.getOrElse(null);
527 if (!_wallet) {
528 // not using need because we want a custom error
529 throw new Error('Internal Error: Missing Wallet');
530 }
532 if (point.isDefault) {
533 // not using need because we want a custom error
534 throw new Error('Internal Error: Invalid point');
535 }
536 const proxy = point.getAddressProxy(proxyType);
538 if (proxy === undefined)
539 throw new Error("Error: Address doesn't match expected proxy");
541 const nonce = await api.getNonce({ ship: point.value, proxy });
542 const txHash = await registerProxyAddress(
543 api,
544 _wallet,
545 point.value,
546 proxy,
547 proxyType,
548 nonce,
549 address,
550 walletType,
551 _web3,
552 connector,
553 signPersonalMessage,
554 isConnected
555 );
556 return api.getPendingTx(txHash);
557 },
558 [
559 api,
560 connector,
561 signPersonalMessage,
562 isConnected,
563 point,
564 wallet,
565 web3,
566 walletType,
567 quotaReached,
568 ]
569 );
571 const changeSponsorship = useCallback(
572 async (
573 sponsee: Ship,
574 type: 'adopt' | 'detach' | 'reject',
575 skipQuotaCheck = false
576 ) => {
577 if (!skipQuotaCheck && quotaReached()) {
578 return;
579 }
581 const _wallet = wallet.getOrElse(null);
582 const _web3 = web3.getOrElse(null);
583 const ship = point.value;
585 const proxy = point.getManagerProxy();
587 if (proxy === undefined)
588 throw new Error("Error: Address doesn't match expected proxy");
590 const nonce = await api.getNonce({ ship, proxy });
591 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
592 const txHash = await submitL2Transaction({
593 api,
594 wallet: _wallet,
595 ship,
596 sponsee,
597 proxy,
598 nonce,
599 address: _wallet.address,
600 type,
601 walletType,
602 web3: _web3,
603 connector,
604 signPersonalMessage,
605 isConnected,
606 });
608 return api.getPendingTx(txHash);
609 },
610 [
611 api,
612 point,
613 wallet,
614 walletType,
615 web3,
616 connector,
617 quotaReached,
618 isConnected,
619 signPersonalMessage,
620 ]
621 );
623 const transferPoint = useCallback(
624 async (address: EthAddress, reset?: boolean) => {
625 if (quotaReached()) {
626 return;
627 }
629 const _point = need.point(pointCursor);
630 const _wallet = wallet.getOrElse(null);
631 const _web3 = web3.getOrElse(null);
633 if (!_wallet) {
634 // not using need because we want a custom error
635 throw new Error('Internal Error: Missing Wallet');
636 }
638 const proxy = point.getTransferProxy();
640 if (proxy === undefined)
641 throw new Error("Error: Address doesn't match proxy");
643 const nonce = await api.getNonce({ ship: _point, proxy });
645 const txHash = await submitL2Transaction({
646 api,
647 wallet: _wallet,
648 ship: _point,
649 proxy,
650 nonce,
651 address,
652 type: 'transferPoint',
653 walletType,
654 web3: _web3,
655 connector,
656 signPersonalMessage,
657 isConnected,
658 reset,
659 });
661 return api.getPendingTx(txHash);
662 },
663 [
664 api,
665 connector,
666 point,
667 pointCursor,
668 wallet,
669 web3,
670 walletType,
671 quotaReached,
672 isConnected,
673 signPersonalMessage,
674 ]
675 );
677 const sendL2Transaction = useCallback(
678 async (params: SendL2Params) => {
679 if (quotaReached()) {
680 return;
681 }
683 const _wallet = wallet.getOrElse(null);
684 const _web3 = web3.getOrElse(null);
686 if (!_wallet || point.isDefault) {
687 // not using need because we want a custom error
688 throw new Error('Internal Error: Missing Wallet or Point Info');
689 }
691 return submitL2Transaction({
692 ...params,
693 api,
694 wallet: _wallet,
695 walletType,
696 web3: _web3,
697 connector,
698 signPersonalMessage,
699 isConnected,
700 ship: point.value,
701 });
702 },
703 [
704 api,
705 connector,
706 point,
707 wallet,
708 web3,
709 walletType,
710 quotaReached,
711 isConnected,
712 signPersonalMessage,
713 ]
714 );
716 const changeSponsor = useCallback(
717 async (newSponsor: number) => {
718 if (point.isDefault) {
719 throw new Error('Internal Error: point is default');
720 }
722 const curPoint = point.value;
723 const _wallet = wallet.getOrElse(null);
725 if (!_wallet) {
726 // not using need because we want a custom error
727 throw new Error('Internal Error: Missing Wallet');
728 }
730 const proxy = point.getManagerProxy();
732 if (proxy === undefined)
733 throw new Error("Error: Address doesn't match proxy");
735 const nonce = await api.getNonce({ ship: curPoint, proxy });
737 return sendL2Transaction({
738 newSponsor,
739 nonce,
740 proxy,
741 type: 'escape',
742 ship: curPoint,
743 });
744 },
745 [api, point, sendL2Transaction, wallet]
746 );
748 const cancelEscape = useCallback(async () => {
749 if (point.isDefault) {
750 throw new Error('Internal Error: point is default');
751 }
753 const curPoint = point.value;
754 const _wallet = wallet.getOrElse(null);
756 if (!_wallet) {
757 // not using need because we want a custom error
758 throw new Error('Internal Error: Missing Wallet');
759 }
761 const proxy = point.getManagerProxy();
763 if (proxy === undefined)
764 throw new Error("Error: Address doesn't match proxy");
766 const nonce = await api.getNonce({ ship: curPoint, proxy });
768 return sendL2Transaction({
769 nonce,
770 proxy,
771 type: 'cancelEscape',
772 ship: curPoint,
773 });
774 }, [api, point, sendL2Transaction, wallet]);
776 const getPointsDetails = useCallback(
777 async (
778 ownedPoints: number[],
779 incomingPoints: number[],
780 managingPoints: number[],
781 votingPoints: number[],
782 spawningPoints: number[]
783 ) => {
784 try {
785 const allPoints = [
786 await Promise.all(ownedPoints.map(initPoint)),
787 await Promise.all(incomingPoints.map(initPoint)),
788 await Promise.all(managingPoints.map(initPoint)),
789 await Promise.all(votingPoints.map(initPoint)),
790 await Promise.all(spawningPoints.map(initPoint)),
791 ];
793 setPoints(
794 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
795 cur.forEach(p => {
796 if (p) {
797 acc[p.value] = p;
798 }
799 });
800 return acc;
801 }, {})
802 );
803 } catch (e) {
804 console.warn(e);
805 }
806 },
807 [setPoints, initPoint]
808 );
810 // On load, get initial config
811 useEffect(() => {
812 if (config) {
813 return;
814 }
816 fetchConfig();
817 }, [config, fetchConfig]);
819 return {
820 api,
821 changeSponsorship,
822 changeSponsor,
823 cancelEscape,
824 checkForUpdates,
825 config,
826 configureNetworkKeys,
827 getAndUpdatePoint,
828 getPoints,
829 getPointsDetails,
830 getPendingTransactions,
831 initPoint,
832 ls,
833 performL2Reticket,
834 setProxyAddress,
835 spawnPoint,
836 transferPoint,
837 };