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