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