on 9155372 help sign in
bridge/src/lib/useRoller.ts
782 lines 20 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';
15import { usePointCache } from 'store/pointCache';
17import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
18import { convertToInt } from './convertToInt';
19import { isDevelopment } from './flags';
20import {
21 submitL2Transaction,
22 registerProxyAddress,
23 isL2Spawn,
24} from './utils/roller';
25import { TEN_SECONDS } from './constants';
27import {
28 Config,
29 Ship,
30 Proxy,
31 RollerRPCAPI,
32 EthAddress,
33} from '@urbit/roller-api';
35import Point, { PointField, Points } from './types/Point';
36import { ReticketParams, SendL2Params } from './types/L2Transaction';
37import { L1Point } from './types/L1Point';
38import { ddmmmYYYY } from './utils/date';
39import { showNotification } from './utils/notifications';
40import { useWalletConnect } from './useWalletConnect';
41import { PendingL1Txn } from './types/PendingL1Transaction';
42import { TRANSACTION_PROGRESS } from './reticket';
43import { useRollerOptions } from './useRollerOptions';
44import { useTimerStore } from 'store/timerStore';
46interface UpdateParams {
47 point?: Point | number;
48 message?: string;
49 notify?: boolean;
50 field?: PointField;
51 l1Txn?: PendingL1Txn;
52 intervalTime?: number;
55export default function useRoller() {
56 const {
57 wallet,
58 authToken,
59 authMnemonic,
60 urbitWallet,
61 walletType,
62 }: any = useWallet();
63 const { connector } = useWalletConnect();
64 const { pointCursor }: any = usePointCursor();
65 const { web3, contracts }: any = useNetwork();
66 const allPoints: any = usePointCache();
67 const getDetails = allPoints?.getDetails;
69 const {
70 point,
71 points,
72 nextQuotaTime,
73 setNextQuotaTime,
74 setPendingTransactions,
75 setModalText,
76 setPoints,
77 updatePoint,
78 storePendingL1Txn,
79 deletePendingL1Txn,
80 } = useRollerStore();
81 const { setNextBatchTime } = useTimerStore();
82 const [config, setConfig] = useState<Config | null>(null);
84 const { options } = useRollerOptions();
86 const api = useMemo(() => {
87 return new RollerRPCAPI(options);
88 }, [options]);
90 const ls = useMemo(
91 () =>
92 new SecureLS({
93 isCompression: false,
94 encryptionSecret: authToken.getOrElse('default'),
95 }),
96 [authToken]
97 );
99 const getNextQuotaTime = useCallback(
100 (nextSlice: number) => {
101 setTimeout(() => {
102 api
103 .whenNextSlice()
104 .then(next => {
105 setNextQuotaTime(next);
106 getNextQuotaTime(next);
107 })
108 .catch(err => console.warn(err));
109 }, nextSlice - new Date().getTime());
110 },
111 [api, setNextQuotaTime]
112 );
114 const fetchConfig = useCallback(async () => {
115 getNextQuotaTime(Date.now() + 1000);
116 api
117 .getRollerConfig()
118 .then(config => {
119 setConfig(config);
120 setNextBatchTime(config.nextBatch);
121 })
122 .catch(err => {
123 // TODO: more elegant error handling
124 console.warn(
125 '[fetchConfig:failed] check the roller connection?\n',
126 err
127 );
128 });
129 }, [api, getNextQuotaTime, setNextBatchTime]);
131 const quotaReached = useCallback(() => {
132 if (point.l2Quota <= 0) {
133 setModalText(
134 `You have reached your weekly L2 transaction limit. You will get another
135 ${point.l2Allowance} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
136 );
137 }
139 return point.l2Quota <= 0;
140 }, [nextQuotaTime, point, setModalText]);
142 const initPoint = useCallback(
143 async (point: string | number): Promise<Point> => {
144 const _wallet = wallet.getOrElse(null);
145 const _contracts = contracts.getOrElse(null);
147 if (!_wallet || !_contracts) {
148 return EMPTY_POINT;
149 }
151 const pointNum = Number(point);
152 try {
153 const rawDetails = await api.getPoint(pointNum);
154 const isL2 = isL2Spawn(rawDetails?.dominion);
155 const l2Quota = isL2 ? await api.getRemainingQuota(pointNum) : 0;
156 const l2Allowance = isL2 ? await api.getAllowance(pointNum) : 0;
158 const details = isL2Spawn(rawDetails?.dominion)
159 ? toL1Details(rawDetails)
160 : await azimuth.azimuth.getPoint(_contracts, point);
162 return new Point({
163 value: pointNum,
164 details,
165 address: _wallet.address,
166 l2Quota,
167 l2Allowance,
168 });
169 } catch (e) {
170 console.warn(e);
172 // Try getting the details from L1
173 try {
174 const _contracts = need.contracts(contracts);
175 const details = await azimuth.azimuth.getPoint(_contracts, point);
176 return new Point({
177 value: pointNum,
178 details,
179 address: _wallet.address,
180 });
181 } catch (e) {
182 console.warn(e);
183 // Just return a placeholder Point
184 const details: L1Point = toL1Details();
185 return new Point({
186 value: pointNum,
187 details,
188 address: _wallet.address,
189 isPlaceholder: true,
190 });
191 }
192 }
193 },
194 [api, wallet, contracts]
195 );
197 const getAndUpdatePoint = useCallback(
198 async (point: number) => {
199 const updatedPoint = await initPoint(point);
200 updatePoint(updatedPoint);
201 },
202 [initPoint, updatePoint]
203 );
205 const checkForUpdates = useCallback(
206 async ({
207 point,
208 message,
209 notify = true,
210 field,
211 l1Txn,
212 intervalTime = TEN_SECONDS,
213 }: UpdateParams) => {
216 const pVal = (typeof point === 'number' ? point : point.value);
217 const p = await initPoint(pVal);
219 if (l1Txn) storePendingL1Txn(l1Txn);
221 let interval: any;
222 const check = async () => {
223 const changedField = !points[p.value]
224 ? 'newPoint'
225 : p.getChangedField(points[p.value], field);
227 if (isDevelopment) {
228 console.log(`CHECKING FOR ${changedField} UPDATES:`, p.patp);
229 }
231 if (!p.isPlaceholder && changedField) {
232 updatePoint(p);
233 if (interval) {
234 clearInterval(interval);
235 }
237 if (l1Txn) deletePendingL1Txn(l1Txn);
238 if (notify) {
239 showNotification(
240 `${message || getUpdatedPointMessage(p, changedField)}`
241 );
242 }
243 }
244 };
246 interval = setInterval(check, intervalTime);
247 check();
248 },
249 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
250 );
252 const spawnPoint = useCallback(
253 async (pointToSpawn: number, destinationAddress: string) => {
254 if (quotaReached()) {
255 return;
256 }
258 const _point = need.point(pointCursor);
259 const _contracts = contracts.getOrElse(null);
260 const _web3 = web3.getOrElse(null);
261 const _wallet = wallet.getOrElse(null);
262 const _authToken = authToken.getOrElse(null);
264 if (!_contracts || !_web3 || !_wallet || !_authToken) {
265 // not using need because we want a custom error
266 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
267 }
269 const proxy = point.getSpawnProxy();
271 if (proxy === undefined)
272 throw new Error("Error: Address doesn't match proxy");
274 const nonce = await api.getNonce({
275 ship: _point,
276 proxy,
277 });
279 await submitL2Transaction({
280 api,
281 address: destinationAddress,
282 wallet: _wallet,
283 ship: _point,
284 proxy,
285 nonce,
286 pointToSpawn,
287 type: 'spawn',
288 walletType,
289 web3: _web3,
290 connector,
291 });
292 },
293 [
294 api,
295 authToken,
296 connector,
297 contracts,
298 pointCursor,
299 point,
300 wallet,
301 walletType,
302 web3,
303 quotaReached,
304 ]
305 );
307 const getPoints = useCallback(
308 async (proxy: Proxy, address: EthAddress) => {
309 const points: Ship[] =
310 proxy === 'own'
311 ? await api.getOwnedPoints(address)
312 : proxy === 'manage'
313 ? await api.getManagerFor(address)
314 : proxy === 'vote'
315 ? await api.getVotingFor(address)
316 : proxy === 'transfer'
317 ? await api.getTransferringFor(address)
318 : proxy === 'spawn'
319 ? await api.getSpawningFor(address)
320 : [];
322 return points;
323 },
324 [api]
325 );
327 const getPendingTransactions = useCallback(async () => {
328 try {
329 if (!point.isDefault) {
330 const newPending = await api.getPendingByShip(point.value);
331 setPendingTransactions(newPending);
332 }
333 } catch (error) {
334 console.warn('ERROR GETTING PENDING', error);
335 }
336 }, [api, setPendingTransactions, point]);
338 interface ConfigureKeysParams {
339 breach: boolean;
340 customNetworkSeed?: string;
341 }
343 const configureNetworkKeys = useCallback(
344 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
345 if (quotaReached()) {
346 return;
347 }
348 const _wallet = wallet.getOrElse(null);
349 const _web3 = web3.getOrElse(null);
350 if (!_wallet || !_web3 || point.isDefault) {
351 // not using need because we want a custom error
352 throw new Error('Internal Error: Missing Wallet/Details');
353 }
355 const proxy = point.getManagerProxy();
357 if (proxy === undefined)
358 throw new Error("Error: Address doesn't match proxy");
360 const nonce = await api.getNonce({
361 ship: point.value,
362 proxy,
363 });
365 const nextRevision = point.keyRevisionNumber + 1;
366 const networkSeed = customNetworkSeed
367 ? customNetworkSeed
368 : await attemptNetworkSeedDerivation({
369 urbitWallet,
370 wallet,
371 authMnemonic,
372 details: point,
373 authToken,
374 point: point.value,
375 revision: nextRevision,
376 });
377 const txHash = await submitL2Transaction({
378 api,
379 wallet: _wallet,
380 ship: point.value,
381 proxy,
382 nonce,
383 networkSeed,
384 type: 'configureKeys',
385 walletType,
386 web3: _web3,
387 connector,
388 breach,
389 });
391 return api.getPendingTx(txHash);
392 },
393 [
394 // eslint-disable-line
395 api,
396 authMnemonic,
397 authToken,
398 point,
399 urbitWallet,
400 wallet,
401 walletType,
402 web3,
403 getDetails,
404 quotaReached,
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 });
446 nonce = nonce + 1;
447 requests.push(configureKeysRequest);
449 // 2. Set Management Proxy
450 progress(TRANSACTION_PROGRESS.SIGNING);
451 const registerMgmtRequest = await registerProxyAddress(
452 api,
453 fromWallet,
454 point,
455 proxy,
456 'manage',
457 nonce,
458 manager,
459 walletType,
460 _web3,
461 connector
462 );
463 nonce = nonce + 1;
464 requests.push(registerMgmtRequest);
466 // 3. Set Spawn Proxy
467 progress(TRANSACTION_PROGRESS.FUNDING);
468 if (!isPlanet(Number(point))) {
469 const registerSpawnRequest = await registerProxyAddress(
470 api,
471 fromWallet,
472 point,
473 proxy,
474 'spawn',
475 nonce,
476 toWallet?.spawn?.keys?.address || to,
477 walletType,
478 _web3,
479 connector
480 );
481 nonce = nonce + 1;
482 requests.push(registerSpawnRequest);
483 }
485 // 4. Transfer point
486 progress(TRANSACTION_PROGRESS.TRANSFERRING);
487 const transferTxRequest = await submitL2Transaction({
488 api,
489 wallet: fromWallet,
490 ship: point,
491 proxy: 'own',
492 type: 'transferPoint',
493 nonce,
494 address: to,
495 walletType,
496 web3: _web3,
497 connector,
498 });
499 requests.push(transferTxRequest);
501 progress(TRANSACTION_PROGRESS.CLEANING);
502 const hashes = await Promise.all(requests);
504 progress(TRANSACTION_PROGRESS.DONE);
505 return hashes;
506 };
508 const setProxyAddress = useCallback(
509 async (proxyType: Proxy, address: EthAddress) => {
510 if (quotaReached()) {
511 return;
512 }
514 const _wallet = wallet.getOrElse(null);
515 const _web3 = web3.getOrElse(null);
517 if (!_wallet) {
518 // not using need because we want a custom error
519 throw new Error('Internal Error: Missing Wallet');
520 }
522 if (point.isDefault) {
523 // not using need because we want a custom error
524 throw new Error('Internal Error: Invalid point');
525 }
527 const proxy = point.getAddressProxy(proxyType);
529 if (proxy === undefined)
530 throw new Error("Error: Address doesn't match expected proxy");
532 const nonce = await api.getNonce({ ship: point.value, proxy });
534 const txHash = await registerProxyAddress(
535 api,
536 _wallet,
537 point.value,
538 proxy,
539 proxyType,
540 nonce,
541 address,
542 walletType,
543 _web3,
544 connector
545 );
547 return api.getPendingTx(txHash);
548 },
549 [api, connector, point, wallet, web3, walletType, quotaReached]
550 );
552 const changeSponsorship = useCallback(
553 async (
554 sponsee: Ship,
555 type: 'adopt' | 'detach' | 'reject',
556 skipQuotaCheck = false
557 ) => {
558 if (!skipQuotaCheck && quotaReached()) {
559 return;
560 }
562 const _wallet = wallet.getOrElse(null);
563 const _web3 = web3.getOrElse(null);
564 const ship = point.value;
566 const proxy = point.getManagerProxy();
568 if (proxy === undefined)
569 throw new Error("Error: Address doesn't match expected proxy");
571 const nonce = await api.getNonce({ ship, proxy });
572 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
573 const txHash = await submitL2Transaction({
574 api,
575 wallet: _wallet,
576 ship,
577 sponsee,
578 proxy,
579 nonce,
580 address: _wallet.address,
581 type,
582 walletType,
583 web3: _web3,
584 connector,
585 });
587 return api.getPendingTx(txHash);
588 },
589 [api, point, wallet, walletType, web3, connector, quotaReached]
590 );
592 const transferPoint = useCallback(
593 async (address: EthAddress, reset?: boolean) => {
594 if (quotaReached()) {
595 return;
596 }
598 const _point = need.point(pointCursor);
599 const _wallet = wallet.getOrElse(null);
600 const _web3 = web3.getOrElse(null);
602 if (!_wallet) {
603 // not using need because we want a custom error
604 throw new Error('Internal Error: Missing Wallet');
605 }
607 const proxy = point.getTransferProxy();
609 if (proxy === undefined)
610 throw new Error("Error: Address doesn't match proxy");
612 const nonce = await api.getNonce({ ship: _point, proxy });
614 const txHash = await submitL2Transaction({
615 api,
616 wallet: _wallet,
617 ship: _point,
618 proxy,
619 nonce,
620 address,
621 type: 'transferPoint',
622 walletType,
623 web3: _web3,
624 connector,
625 reset,
626 });
628 return api.getPendingTx(txHash);
629 },
630 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
631 );
633 const sendL2Transaction = useCallback(
634 async (params: SendL2Params) => {
635 if (quotaReached()) {
636 return;
637 }
639 const _wallet = wallet.getOrElse(null);
640 const _web3 = web3.getOrElse(null);
642 if (!_wallet || point.isDefault) {
643 // not using need because we want a custom error
644 throw new Error('Internal Error: Missing Wallet or Point Info');
645 }
647 return submitL2Transaction({
648 ...params,
649 api,
650 wallet: _wallet,
651 walletType,
652 web3: _web3,
653 connector,
654 ship: point.value,
655 });
656 },
657 [api, connector, point, wallet, web3, walletType, quotaReached]
658 );
660 const changeSponsor = useCallback(
661 async (newSponsor: number) => {
662 if (point.isDefault) {
663 throw new Error('Internal Error: point is default');
664 }
666 const curPoint = point.value;
667 const _wallet = wallet.getOrElse(null);
669 if (!_wallet) {
670 // not using need because we want a custom error
671 throw new Error('Internal Error: Missing Wallet');
672 }
674 const proxy = point.getManagerProxy();
676 if (proxy === undefined)
677 throw new Error("Error: Address doesn't match proxy");
679 const nonce = await api.getNonce({ ship: curPoint, proxy });
681 return sendL2Transaction({
682 newSponsor,
683 nonce,
684 proxy,
685 type: 'escape',
686 ship: curPoint,
687 });
688 },
689 [api, point, sendL2Transaction, wallet]
690 );
692 const cancelEscape = useCallback(async () => {
693 if (point.isDefault) {
694 throw new Error('Internal Error: point is default');
695 }
697 const curPoint = point.value;
698 const _wallet = wallet.getOrElse(null);
700 if (!_wallet) {
701 // not using need because we want a custom error
702 throw new Error('Internal Error: Missing Wallet');
703 }
705 const proxy = point.getManagerProxy();
707 if (proxy === undefined)
708 throw new Error("Error: Address doesn't match proxy");
710 const nonce = await api.getNonce({ ship: curPoint, proxy });
712 return sendL2Transaction({
713 nonce,
714 proxy,
715 type: 'cancelEscape',
716 ship: curPoint,
717 });
718 }, [api, point, sendL2Transaction, wallet]);
720 const getPointsDetails = useCallback(
721 async (
722 ownedPoints: number[],
723 incomingPoints: number[],
724 managingPoints: number[],
725 votingPoints: number[],
726 spawningPoints: number[]
727 ) => {
728 try {
729 const allPoints = [
730 await Promise.all(ownedPoints.map(initPoint)),
731 await Promise.all(incomingPoints.map(initPoint)),
732 await Promise.all(managingPoints.map(initPoint)),
733 await Promise.all(votingPoints.map(initPoint)),
734 await Promise.all(spawningPoints.map(initPoint)),
735 ];
737 setPoints(
738 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
739 cur.forEach(p => {
740 if (p) {
741 acc[p.value] = p;
742 }
743 });
744 return acc;
745 }, {})
746 );
747 } catch (e) {
748 console.warn(e);
749 }
750 },
751 [setPoints, initPoint]
752 );
754 // On load, get initial config
755 useEffect(() => {
756 if (config) {
757 return;
758 }
760 fetchConfig();
761 }, [config, fetchConfig]);
763 return {
764 api,
765 changeSponsorship,
766 changeSponsor,
767 cancelEscape,
768 checkForUpdates,
769 config,
770 configureNetworkKeys,
771 getAndUpdatePoint,
772 getPoints,
773 getPointsDetails,
774 getPendingTransactions,
775 initPoint,
776 ls,
777 performL2Reticket,
778 setProxyAddress,
779 spawnPoint,
780 transferPoint,
781 };