on 2fec545 help sign in
bridge/src/lib/useRoller.ts
778 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: 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) => {
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) => {
214 if (l1Txn) storePendingL1Txn(l1Txn);
216 let interval: any;
217 const check = async () => {
218 const updatedPoint = await initPoint(point);
219 const changedField = !points[point]
220 ? 'newPoint'
221 : updatedPoint.getChangedField(points[point], field);
223 if (isDevelopment) {
224 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
225 }
227 if (!updatedPoint.isPlaceholder && changedField) {
228 updatePoint(updatedPoint);
229 if (interval) {
230 clearInterval(interval);
231 }
233 if (l1Txn) deletePendingL1Txn(l1Txn);
234 if (notify) {
235 showNotification(
236 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
237 );
238 }
239 }
240 };
242 interval = setInterval(check, intervalTime);
243 check();
244 },
245 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
246 );
248 const spawnPoint = useCallback(
249 async (pointToSpawn: number, destinationAddress: string) => {
250 if (quotaReached()) {
251 return;
252 }
254 const _point = need.point(pointCursor);
255 const _contracts = contracts.getOrElse(null);
256 const _web3 = web3.getOrElse(null);
257 const _wallet = wallet.getOrElse(null);
258 const _authToken = authToken.getOrElse(null);
260 if (!_contracts || !_web3 || !_wallet || !_authToken) {
261 // not using need because we want a custom error
262 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
263 }
265 const proxy = point.getSpawnProxy();
267 if (proxy === undefined)
268 throw new Error("Error: Address doesn't match proxy");
270 const nonce = await api.getNonce({
271 ship: _point,
272 proxy,
273 });
275 await submitL2Transaction({
276 api,
277 address: destinationAddress,
278 wallet: _wallet,
279 ship: _point,
280 proxy,
281 nonce,
282 pointToSpawn,
283 type: 'spawn',
284 walletType,
285 web3: _web3,
286 connector,
287 });
288 },
289 [
290 api,
291 authToken,
292 connector,
293 contracts,
294 pointCursor,
295 point,
296 wallet,
297 walletType,
298 web3,
299 quotaReached,
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 breach,
385 });
387 return api.getPendingTx(txHash);
388 },
389 [
390 // eslint-disable-line
391 api,
392 authMnemonic,
393 authToken,
394 point,
395 urbitWallet,
396 wallet,
397 walletType,
398 web3,
399 getDetails,
400 quotaReached,
401 ]
402 );
404 const performL2Reticket = async ({
405 point,
406 to,
407 manager,
408 fromWallet,
409 toWallet,
410 onUpdate,
411 }: ReticketParams) => {
412 const azimuthPoint = await api.getPoint(point);
413 const proxy = 'own';
414 const _web3 = web3.getOrElse(null);
415 let nonce = await api.getNonce({ ship: point, proxy });
416 const progress = onUpdate
417 ? (state: number) => onUpdate({ type: 'progress', state })
418 : () => {};
420 let requests = [];
422 // 1. Update networking keys
423 progress(TRANSACTION_PROGRESS.GENERATING);
424 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
425 const nextRevision = networkRevision + 1;
426 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
427 toWallet,
428 nextRevision
429 );
430 const configureKeysRequest = await submitL2Transaction({
431 api,
432 wallet: fromWallet,
433 ship: point,
434 proxy,
435 nonce,
436 networkSeed,
437 walletType,
438 type: 'configureKeys',
439 web3: _web3,
440 connector,
441 });
442 nonce = nonce + 1;
443 requests.push(configureKeysRequest);
445 // 2. Set Management Proxy
446 progress(TRANSACTION_PROGRESS.SIGNING);
447 const registerMgmtRequest = await registerProxyAddress(
448 api,
449 fromWallet,
450 point,
451 proxy,
452 'manage',
453 nonce,
454 manager,
455 walletType,
456 _web3,
457 connector
458 );
459 nonce = nonce + 1;
460 requests.push(registerMgmtRequest);
462 // 3. Set Spawn Proxy
463 progress(TRANSACTION_PROGRESS.FUNDING);
464 if (!isPlanet(Number(point))) {
465 const registerSpawnRequest = await registerProxyAddress(
466 api,
467 fromWallet,
468 point,
469 proxy,
470 'spawn',
471 nonce,
472 toWallet?.spawn?.keys?.address || to,
473 walletType,
474 _web3,
475 connector
476 );
477 nonce = nonce + 1;
478 requests.push(registerSpawnRequest);
479 }
481 // 4. Transfer point
482 progress(TRANSACTION_PROGRESS.TRANSFERRING);
483 const transferTxRequest = await submitL2Transaction({
484 api,
485 wallet: fromWallet,
486 ship: point,
487 proxy: 'own',
488 type: 'transferPoint',
489 nonce,
490 address: to,
491 walletType,
492 web3: _web3,
493 connector,
494 });
495 requests.push(transferTxRequest);
497 progress(TRANSACTION_PROGRESS.CLEANING);
498 const hashes = await Promise.all(requests);
500 progress(TRANSACTION_PROGRESS.DONE);
501 return hashes;
502 };
504 const setProxyAddress = useCallback(
505 async (proxyType: Proxy, address: EthAddress) => {
506 if (quotaReached()) {
507 return;
508 }
510 const _wallet = wallet.getOrElse(null);
511 const _web3 = web3.getOrElse(null);
513 if (!_wallet) {
514 // not using need because we want a custom error
515 throw new Error('Internal Error: Missing Wallet');
516 }
518 if (point.isDefault) {
519 // not using need because we want a custom error
520 throw new Error('Internal Error: Invalid point');
521 }
523 const proxy = point.getAddressProxy(proxyType);
525 if (proxy === undefined)
526 throw new Error("Error: Address doesn't match expected proxy");
528 const nonce = await api.getNonce({ ship: point.value, proxy });
530 const txHash = await registerProxyAddress(
531 api,
532 _wallet,
533 point.value,
534 proxy,
535 proxyType,
536 nonce,
537 address,
538 walletType,
539 _web3,
540 connector
541 );
543 return api.getPendingTx(txHash);
544 },
545 [api, connector, point, wallet, web3, walletType, quotaReached]
546 );
548 const changeSponsorship = useCallback(
549 async (
550 sponsee: Ship,
551 type: 'adopt' | 'detach' | 'reject',
552 skipQuotaCheck = false
553 ) => {
554 if (!skipQuotaCheck && quotaReached()) {
555 return;
556 }
558 const _wallet = wallet.getOrElse(null);
559 const _web3 = web3.getOrElse(null);
560 const ship = point.value;
562 const proxy = point.getManagerProxy();
564 if (proxy === undefined)
565 throw new Error("Error: Address doesn't match expected proxy");
567 const nonce = await api.getNonce({ ship, proxy });
568 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
569 const txHash = await submitL2Transaction({
570 api,
571 wallet: _wallet,
572 ship,
573 sponsee,
574 proxy,
575 nonce,
576 address: _wallet.address,
577 type,
578 walletType,
579 web3: _web3,
580 connector,
581 });
583 return api.getPendingTx(txHash);
584 },
585 [api, point, wallet, walletType, web3, connector, quotaReached]
586 );
588 const transferPoint = useCallback(
589 async (address: EthAddress, reset?: boolean) => {
590 if (quotaReached()) {
591 return;
592 }
594 const _point = need.point(pointCursor);
595 const _wallet = wallet.getOrElse(null);
596 const _web3 = web3.getOrElse(null);
598 if (!_wallet) {
599 // not using need because we want a custom error
600 throw new Error('Internal Error: Missing Wallet');
601 }
603 const proxy = point.getTransferProxy();
605 if (proxy === undefined)
606 throw new Error("Error: Address doesn't match proxy");
608 const nonce = await api.getNonce({ ship: _point, proxy });
610 const txHash = await submitL2Transaction({
611 api,
612 wallet: _wallet,
613 ship: _point,
614 proxy,
615 nonce,
616 address,
617 type: 'transferPoint',
618 walletType,
619 web3: _web3,
620 connector,
621 reset,
622 });
624 return api.getPendingTx(txHash);
625 },
626 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
627 );
629 const sendL2Transaction = useCallback(
630 async (params: SendL2Params) => {
631 if (quotaReached()) {
632 return;
633 }
635 const _wallet = wallet.getOrElse(null);
636 const _web3 = web3.getOrElse(null);
638 if (!_wallet || point.isDefault) {
639 // not using need because we want a custom error
640 throw new Error('Internal Error: Missing Wallet or Point Info');
641 }
643 return submitL2Transaction({
644 ...params,
645 api,
646 wallet: _wallet,
647 walletType,
648 web3: _web3,
649 connector,
650 ship: point.value,
651 });
652 },
653 [api, connector, point, wallet, web3, walletType, quotaReached]
654 );
656 const changeSponsor = useCallback(
657 async (newSponsor: number) => {
658 if (point.isDefault) {
659 throw new Error('Internal Error: point is default');
660 }
662 const curPoint = point.value;
663 const _wallet = wallet.getOrElse(null);
665 if (!_wallet) {
666 // not using need because we want a custom error
667 throw new Error('Internal Error: Missing Wallet');
668 }
670 const proxy = point.getManagerProxy();
672 if (proxy === undefined)
673 throw new Error("Error: Address doesn't match proxy");
675 const nonce = await api.getNonce({ ship: curPoint, proxy });
677 return sendL2Transaction({
678 newSponsor,
679 nonce,
680 proxy,
681 type: 'escape',
682 ship: curPoint,
683 });
684 },
685 [api, point, sendL2Transaction, wallet]
686 );
688 const cancelEscape = useCallback(async () => {
689 if (point.isDefault) {
690 throw new Error('Internal Error: point is default');
691 }
693 const curPoint = point.value;
694 const _wallet = wallet.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 proxy = point.getManagerProxy();
703 if (proxy === undefined)
704 throw new Error("Error: Address doesn't match proxy");
706 const nonce = await api.getNonce({ ship: curPoint, proxy });
708 return sendL2Transaction({
709 nonce,
710 proxy,
711 type: 'cancelEscape',
712 ship: curPoint,
713 });
714 }, [api, point, sendL2Transaction, wallet]);
716 const getPointsDetails = useCallback(
717 async (
718 ownedPoints: number[],
719 incomingPoints: number[],
720 managingPoints: number[],
721 votingPoints: number[],
722 spawningPoints: number[]
723 ) => {
724 try {
725 const allPoints = [
726 await Promise.all(ownedPoints.map(initPoint)),
727 await Promise.all(incomingPoints.map(initPoint)),
728 await Promise.all(managingPoints.map(initPoint)),
729 await Promise.all(votingPoints.map(initPoint)),
730 await Promise.all(spawningPoints.map(initPoint)),
731 ];
733 setPoints(
734 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
735 cur.forEach(p => {
736 if (p) {
737 acc[p.value] = p;
738 }
739 });
740 return acc;
741 }, {})
742 );
743 } catch (e) {
744 console.warn(e);
745 }
746 },
747 [setPoints, initPoint]
748 );
750 // On load, get initial config
751 useEffect(() => {
752 if (config) {
753 return;
754 }
756 fetchConfig();
757 }, [config, fetchConfig]);
759 return {
760 api,
761 changeSponsorship,
762 changeSponsor,
763 cancelEscape,
764 checkForUpdates,
765 config,
766 configureNetworkKeys,
767 getAndUpdatePoint,
768 getPoints,
769 getPointsDetails,
770 getPendingTransactions,
771 initPoint,
772 ls,
773 performL2Reticket,
774 setProxyAddress,
775 spawnPoint,
776 transferPoint,
777 };