on be08dee help sign in
bridge/src/lib/useRoller.ts
777 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';
45interface UpdateParams {
46 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 } = useWalletConnect();
63 const { pointCursor }: any = usePointCursor();
64 const { web3, contracts }: any = useNetwork();
65 const allPoints: any = usePointCache();
66 const getDetails = allPoints?.getDetails;
68 const {
69 point,
70 points,
71 nextQuotaTime,
72 setNextBatchTime,
73 setNextQuotaTime,
74 setPendingTransactions,
75 setModalText,
76 setPoints,
77 updatePoint,
78 storePendingL1Txn,
79 deletePendingL1Txn,
80 } = useRollerStore();
81 const [config, setConfig] = useState<Config | null>(null);
83 const { options } = useRollerOptions();
85 const api = useMemo(() => {
86 return new RollerRPCAPI(options);
87 }, [options]);
89 const ls = useMemo(
90 () =>
91 new SecureLS({
92 isCompression: false,
93 encryptionSecret: authToken.getOrElse('default'),
94 }),
95 [authToken]
96 );
98 const getNextQuotaTime = useCallback(
99 (nextSlice: number) => {
100 setTimeout(() => {
101 api
102 .whenNextSlice()
103 .then(next => {
104 setNextQuotaTime(next);
105 getNextQuotaTime(next);
106 })
107 .catch(err => console.warn(err));
108 }, nextSlice - new Date().getTime());
109 },
110 [api, setNextQuotaTime]
111 );
113 const fetchConfig = useCallback(async () => {
114 getNextQuotaTime(Date.now() + 1000);
115 api
116 .getRollerConfig()
117 .then(config => {
118 setConfig(config);
119 setNextBatchTime(config.nextBatch);
120 })
121 .catch(err => {
122 // TODO: more elegant error handling
123 console.warn(
124 '[fetchConfig:failed] check the roller connection?\n',
125 err
126 );
127 });
128 }, [api, getNextQuotaTime, setNextBatchTime]);
130 const quotaReached = useCallback(() => {
131 if (point.l2Quota <= 0) {
132 setModalText(
133 `You have reached your weekly L2 transaction limit. You will get another
134 ${point.l2Allowance} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
135 );
136 }
138 return point.l2Quota <= 0;
139 }, [nextQuotaTime, point, setModalText]);
141 const initPoint = useCallback(
142 async (point: string | number) => {
143 const _wallet = wallet.getOrElse(null);
144 const _contracts = contracts.getOrElse(null);
146 if (!_wallet || !_contracts) {
147 return EMPTY_POINT;
148 }
150 const pointNum = Number(point);
151 try {
152 const rawDetails = await api.getPoint(pointNum);
153 const isL2 = isL2Spawn(rawDetails?.dominion);
154 const l2Quota = isL2 ? await api.getRemainingQuota(pointNum) : 0;
155 const l2Allowance = isL2 ? await api.getAllowance(pointNum) : 0;
157 const details = isL2Spawn(rawDetails?.dominion)
158 ? toL1Details(rawDetails)
159 : await azimuth.azimuth.getPoint(_contracts, point);
161 return new Point({
162 value: pointNum,
163 details,
164 address: _wallet.address,
165 l2Quota,
166 l2Allowance,
167 });
168 } catch (e) {
169 console.warn(e);
171 // Try getting the details from L1
172 try {
173 const _contracts = need.contracts(contracts);
174 const details = await azimuth.azimuth.getPoint(_contracts, point);
175 return new Point({
176 value: pointNum,
177 details,
178 address: _wallet.address,
179 });
180 } catch (e) {
181 console.warn(e);
182 // Just return a placeholder Point
183 const details: L1Point = toL1Details();
184 return new Point({
185 value: pointNum,
186 details,
187 address: _wallet.address,
188 isPlaceholder: true,
189 });
190 }
191 }
192 },
193 [api, wallet, contracts]
194 );
196 const getAndUpdatePoint = useCallback(
197 async (point: number) => {
198 const updatedPoint = await initPoint(point);
199 updatePoint(updatedPoint);
200 },
201 [initPoint, updatePoint]
202 );
204 const checkForUpdates = useCallback(
205 async ({
206 point,
207 message,
208 notify = true,
209 field,
210 l1Txn,
211 intervalTime = TEN_SECONDS,
212 }: UpdateParams) => {
213 if (l1Txn) storePendingL1Txn(l1Txn);
215 let interval: any;
216 const check = async () => {
217 const updatedPoint = await initPoint(point);
218 const changedField = !points[point]
219 ? 'newPoint'
220 : updatedPoint.getChangedField(points[point], field);
222 if (isDevelopment) {
223 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
224 }
226 if (!updatedPoint.isPlaceholder && changedField) {
227 updatePoint(updatedPoint);
228 if (interval) {
229 clearInterval(interval);
230 }
232 if (l1Txn) deletePendingL1Txn(l1Txn);
233 if (notify) {
234 showNotification(
235 `${message || getUpdatedPointMessage(updatedPoint, 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 });
287 },
288 [
289 api,
290 authToken,
291 connector,
292 contracts,
293 pointCursor,
294 point,
295 wallet,
296 walletType,
297 web3,
298 quotaReached,
299 ]
300 );
302 const getPoints = useCallback(
303 async (proxy: Proxy, address: EthAddress) => {
304 const points: Ship[] =
305 proxy === 'own'
306 ? await api.getOwnedPoints(address)
307 : proxy === 'manage'
308 ? await api.getManagerFor(address)
309 : proxy === 'vote'
310 ? await api.getVotingFor(address)
311 : proxy === 'transfer'
312 ? await api.getTransferringFor(address)
313 : proxy === 'spawn'
314 ? await api.getSpawningFor(address)
315 : [];
317 return points;
318 },
319 [api]
320 );
322 const getPendingTransactions = useCallback(async () => {
323 try {
324 if (!point.isDefault) {
325 const newPending = await api.getPendingByShip(point.value);
326 setPendingTransactions(newPending);
327 }
328 } catch (error) {
329 console.warn('ERROR GETTING PENDING', error);
330 }
331 }, [api, setPendingTransactions, point]);
333 interface ConfigureKeysParams {
334 breach: boolean;
335 customNetworkSeed?: string;
336 }
338 const configureNetworkKeys = useCallback(
339 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
340 if (quotaReached()) {
341 return;
342 }
343 const _wallet = wallet.getOrElse(null);
344 const _web3 = web3.getOrElse(null);
345 if (!_wallet || !_web3 || point.isDefault) {
346 // not using need because we want a custom error
347 throw new Error('Internal Error: Missing Wallet/Details');
348 }
350 const proxy = point.getManagerProxy();
352 if (proxy === undefined)
353 throw new Error("Error: Address doesn't match proxy");
355 const nonce = await api.getNonce({
356 ship: point.value,
357 proxy,
358 });
360 const nextRevision = point.keyRevisionNumber + 1;
361 const networkSeed = customNetworkSeed
362 ? customNetworkSeed
363 : await attemptNetworkSeedDerivation({
364 urbitWallet,
365 wallet,
366 authMnemonic,
367 details: point,
368 authToken,
369 point: point.value,
370 revision: nextRevision,
371 });
372 const txHash = await submitL2Transaction({
373 api,
374 wallet: _wallet,
375 ship: point.value,
376 proxy,
377 nonce,
378 networkSeed,
379 type: 'configureKeys',
380 walletType,
381 web3: _web3,
382 connector,
383 breach,
384 });
386 return api.getPendingTx(txHash);
387 },
388 [
389 // eslint-disable-line
390 api,
391 authMnemonic,
392 authToken,
393 point,
394 urbitWallet,
395 wallet,
396 walletType,
397 web3,
398 getDetails,
399 quotaReached,
400 ]
401 );
403 const performL2Reticket = async ({
404 point,
405 to,
406 manager,
407 fromWallet,
408 toWallet,
409 onUpdate,
410 }: ReticketParams) => {
411 const azimuthPoint = await api.getPoint(point);
412 const proxy = 'own';
413 const _web3 = web3.getOrElse(null);
414 let nonce = await api.getNonce({ ship: point, proxy });
415 const progress = onUpdate
416 ? (state: number) => onUpdate({ type: 'progress', state })
417 : () => {};
419 let requests = [];
421 // 1. Update networking keys
422 progress(TRANSACTION_PROGRESS.GENERATING);
423 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
424 const nextRevision = networkRevision + 1;
425 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
426 toWallet,
427 nextRevision
428 );
429 const configureKeysRequest = await submitL2Transaction({
430 api,
431 wallet: fromWallet,
432 ship: point,
433 proxy,
434 nonce,
435 networkSeed,
436 walletType,
437 type: 'configureKeys',
438 web3: _web3,
439 connector,
440 });
441 nonce = nonce + 1;
442 requests.push(configureKeysRequest);
444 // 2. Set Management Proxy
445 progress(TRANSACTION_PROGRESS.SIGNING);
446 const registerMgmtRequest = await registerProxyAddress(
447 api,
448 fromWallet,
449 point,
450 proxy,
451 'manage',
452 nonce,
453 manager,
454 walletType,
455 _web3,
456 connector
457 );
458 nonce = nonce + 1;
459 requests.push(registerMgmtRequest);
461 // 3. Set Spawn Proxy
462 progress(TRANSACTION_PROGRESS.FUNDING);
463 if (!isPlanet(Number(point))) {
464 const registerSpawnRequest = await registerProxyAddress(
465 api,
466 fromWallet,
467 point,
468 proxy,
469 'spawn',
470 nonce,
471 toWallet?.spawn?.keys?.address || to,
472 walletType,
473 _web3,
474 connector
475 );
476 nonce = nonce + 1;
477 requests.push(registerSpawnRequest);
478 }
480 // 4. Transfer point
481 progress(TRANSACTION_PROGRESS.TRANSFERRING);
482 const transferTxRequest = await submitL2Transaction({
483 api,
484 wallet: fromWallet,
485 ship: point,
486 proxy: 'own',
487 type: 'transferPoint',
488 nonce,
489 address: to,
490 walletType,
491 web3: _web3,
492 connector,
493 });
494 requests.push(transferTxRequest);
496 progress(TRANSACTION_PROGRESS.CLEANING);
497 const hashes = await Promise.all(requests);
499 progress(TRANSACTION_PROGRESS.DONE);
500 return hashes;
501 };
503 const setProxyAddress = useCallback(
504 async (proxyType: Proxy, address: EthAddress) => {
505 if (quotaReached()) {
506 return;
507 }
509 const _wallet = wallet.getOrElse(null);
510 const _web3 = web3.getOrElse(null);
512 if (!_wallet) {
513 // not using need because we want a custom error
514 throw new Error('Internal Error: Missing Wallet');
515 }
517 if (point.isDefault) {
518 // not using need because we want a custom error
519 throw new Error('Internal Error: Invalid point');
520 }
522 const proxy = point.getAddressProxy(proxyType);
524 if (proxy === undefined)
525 throw new Error("Error: Address doesn't match expected proxy");
527 const nonce = await api.getNonce({ ship: point.value, proxy });
529 const txHash = await registerProxyAddress(
530 api,
531 _wallet,
532 point.value,
533 proxy,
534 proxyType,
535 nonce,
536 address,
537 walletType,
538 _web3,
539 connector
540 );
542 return api.getPendingTx(txHash);
543 },
544 [api, connector, point, wallet, web3, walletType, quotaReached]
545 );
547 const changeSponsorship = useCallback(
548 async (
549 sponsee: Ship,
550 type: 'adopt' | 'detach' | 'reject',
551 skipQuotaCheck = false
552 ) => {
553 if (!skipQuotaCheck && quotaReached()) {
554 return;
555 }
557 const _wallet = wallet.getOrElse(null);
558 const _web3 = web3.getOrElse(null);
559 const ship = point.value;
561 const proxy = point.getManagerProxy();
563 if (proxy === undefined)
564 throw new Error("Error: Address doesn't match expected proxy");
566 const nonce = await api.getNonce({ ship, proxy });
567 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
568 const txHash = await submitL2Transaction({
569 api,
570 wallet: _wallet,
571 ship,
572 sponsee,
573 proxy,
574 nonce,
575 address: _wallet.address,
576 type,
577 walletType,
578 web3: _web3,
579 connector,
580 });
582 return api.getPendingTx(txHash);
583 },
584 [api, point, wallet, walletType, web3, connector, quotaReached]
585 );
587 const transferPoint = useCallback(
588 async (address: EthAddress, reset?: boolean) => {
589 if (quotaReached()) {
590 return;
591 }
593 const _point = need.point(pointCursor);
594 const _wallet = wallet.getOrElse(null);
595 const _web3 = web3.getOrElse(null);
597 if (!_wallet) {
598 // not using need because we want a custom error
599 throw new Error('Internal Error: Missing Wallet');
600 }
602 const proxy = point.getTransferProxy();
604 if (proxy === undefined)
605 throw new Error("Error: Address doesn't match proxy");
607 const nonce = await api.getNonce({ ship: _point, proxy });
609 const txHash = await submitL2Transaction({
610 api,
611 wallet: _wallet,
612 ship: _point,
613 proxy,
614 nonce,
615 address,
616 type: 'transferPoint',
617 walletType,
618 web3: _web3,
619 connector,
620 reset,
621 });
623 return api.getPendingTx(txHash);
624 },
625 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
626 );
628 const sendL2Transaction = useCallback(
629 async (params: SendL2Params) => {
630 if (quotaReached()) {
631 return;
632 }
634 const _wallet = wallet.getOrElse(null);
635 const _web3 = web3.getOrElse(null);
637 if (!_wallet || point.isDefault) {
638 // not using need because we want a custom error
639 throw new Error('Internal Error: Missing Wallet or Point Info');
640 }
642 return submitL2Transaction({
643 ...params,
644 api,
645 wallet: _wallet,
646 walletType,
647 web3: _web3,
648 connector,
649 ship: point.value,
650 });
651 },
652 [api, connector, point, wallet, web3, walletType, quotaReached]
653 );
655 const changeSponsor = useCallback(
656 async (newSponsor: number) => {
657 if (point.isDefault) {
658 throw new Error('Internal Error: point is default');
659 }
661 const curPoint = point.value;
662 const _wallet = wallet.getOrElse(null);
664 if (!_wallet) {
665 // not using need because we want a custom error
666 throw new Error('Internal Error: Missing Wallet');
667 }
669 const proxy = point.getManagerProxy();
671 if (proxy === undefined)
672 throw new Error("Error: Address doesn't match proxy");
674 const nonce = await api.getNonce({ ship: curPoint, proxy });
676 return sendL2Transaction({
677 newSponsor,
678 nonce,
679 proxy,
680 type: 'escape',
681 ship: curPoint,
682 });
683 },
684 [api, point, sendL2Transaction, wallet]
685 );
687 const cancelEscape = useCallback(async () => {
688 if (point.isDefault) {
689 throw new Error('Internal Error: point is default');
690 }
692 const curPoint = point.value;
693 const _wallet = wallet.getOrElse(null);
695 if (!_wallet) {
696 // not using need because we want a custom error
697 throw new Error('Internal Error: Missing Wallet');
698 }
700 const proxy = point.getManagerProxy();
702 if (proxy === undefined)
703 throw new Error("Error: Address doesn't match proxy");
705 const nonce = await api.getNonce({ ship: curPoint, proxy });
707 return sendL2Transaction({
708 nonce,
709 proxy,
710 type: 'cancelEscape',
711 ship: curPoint,
712 });
713 }, [api, point, sendL2Transaction, wallet]);
715 const getPointsDetails = useCallback(
716 async (
717 ownedPoints: number[],
718 incomingPoints: number[],
719 managingPoints: number[],
720 votingPoints: number[],
721 spawningPoints: number[]
722 ) => {
723 try {
724 const allPoints = [
725 await Promise.all(ownedPoints.map(initPoint)),
726 await Promise.all(incomingPoints.map(initPoint)),
727 await Promise.all(managingPoints.map(initPoint)),
728 await Promise.all(votingPoints.map(initPoint)),
729 await Promise.all(spawningPoints.map(initPoint)),
730 ];
732 setPoints(
733 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
734 cur.forEach(p => {
735 if (p) {
736 acc[p.value] = p;
737 }
738 });
739 return acc;
740 }, {})
741 );
742 } catch (e) {
743 console.warn(e);
744 }
745 },
746 [setPoints, initPoint]
747 );
749 // On load, get initial config
750 useEffect(() => {
751 if (config) {
752 return;
753 }
755 fetchConfig();
756 }, [config, fetchConfig]);
758 return {
759 api,
760 changeSponsorship,
761 changeSponsor,
762 cancelEscape,
763 checkForUpdates,
764 config,
765 configureNetworkKeys,
766 getAndUpdatePoint,
767 getPoints,
768 getPointsDetails,
769 getPendingTransactions,
770 initPoint,
771 ls,
772 performL2Reticket,
773 setProxyAddress,
774 spawnPoint,
775 transferPoint,
776 };