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 { Invite } from 'lib/types/Invite';
8 deriveNetworkSeedFromUrbitWallet,
9 attemptNetworkSeedDerivation,
12import { useNetwork } from 'store/network';
13import { usePointCursor } from 'store/pointCursor';
14import { EMPTY_POINT, useRollerStore } from 'store/rollerStore';
15import { useWallet } from 'store/wallet';
20} from 'store/storage/roller';
21import { usePointCache } from 'store/pointCache';
23import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
24import { convertToInt } from './convertToInt';
25import { isDevelopment, isMainnet, isRopsten } from './flags';
33} from './utils/roller';
49} from '@urbit/roller-api';
51import Point, { PointField, Points } from './types/Point';
52import { useTimerStore } from 'store/timerStore';
53import { ReticketParams, SendL2Params } from './types/L2Transaction';
54import { L1Point } from './types/L1Point';
55import { ddmmmYYYY } from './utils/date';
56import { showNotification } from './utils/notifications';
57import { useWalletConnect } from './useWalletConnect';
58import { PendingL1Txn } from './types/PendingL1Transaction';
60const ONE_SECOND = 1000;
62interface UpdateParams {
68 intervalTime?: number;
71const inviteTemplate = (
83export default function useRoller() {
91 const { connector } = useWalletConnect();
92 const { pointCursor }: any = usePointCursor();
93 const { web3, contracts }: any = useNetwork();
94 const allPoints: any = usePointCache();
95 const getDetails = allPoints?.getDetails;
97 const { setNextRoll } = useTimerStore();
108 setPendingTransactions,
110 setInviteGeneratingNum,
118 } = useRollerStore();
119 const [config, setConfig] = useState<Config | null>(null);
121 const options: Options = useMemo(() => {
122 const type = isMainnet || isRopsten ? 'https' : 'http';
123 const host = isMainnet
124 ? ROLLER_HOSTS.MAINNET
126 ? ROLLER_HOSTS.ROPSTEN
127 : ROLLER_HOSTS.LOCAL;
128 const port = isMainnet || isRopsten ? 443 : 8080;
129 const path = '/v1/roller';
141 const api = useMemo(() => {
142 return new RollerRPCAPI(options);
148 isCompression: false,
149 encryptionSecret: authToken.getOrElse('default'),
154 const getNextQuotaTime = useCallback(
155 (nextSlice: number) => {
160 setNextQuotaTime(next);
161 getNextQuotaTime(next);
163 .catch(err => console.warn(err));
164 }, nextSlice - new Date().getTime());
166 [api, setNextQuotaTime]
169 const fetchConfig = useCallback(async () => {
175 setNextBatchTime(config.nextBatch);
178 // TODO: more elegant error handling
180 '[fetchConfig:failed] check the roller connection?\n',
184 }, [api, getNextQuotaTime, setNextBatchTime]);
186 const quotaReached = useCallback(() => {
187 if (point.l2Quota <= 0) {
189 `You have reached your weekly L2 transaction limit. You will get another
190 ${config?.rollerQuota} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
194 return point.l2Quota <= 0;
195 }, [config, nextQuotaTime, point, setModalText]);
197 const initPoint = useCallback(
198 async (point: string | number) => {
199 const _wallet = wallet.getOrElse(null);
200 const _contracts = contracts.getOrElse(null);
202 if (!_wallet || !_contracts) {
206 const pointNum = Number(point);
208 const rawDetails = await api.getPoint(pointNum);
209 const l2Quota = isL2Spawn(rawDetails?.dominion)
210 ? await api.getRemainingQuota(pointNum)
213 const details = isL2Spawn(rawDetails?.dominion)
214 ? toL1Details(rawDetails)
215 : await azimuth.azimuth.getPoint(_contracts, point);
220 address: _wallet.address,
226 // Try getting the details from L1
228 const _contracts = need.contracts(contracts);
229 const details = await azimuth.azimuth.getPoint(_contracts, point);
233 address: _wallet.address,
237 // Just return a placeholder Point
238 const details: L1Point = toL1Details();
242 address: _wallet.address,
248 [api, wallet, contracts]
251 const getAndUpdatePoint = useCallback(
252 async (point: number) => {
253 const updatedPoint = await initPoint(point);
254 updatePoint(updatedPoint);
256 [initPoint, updatePoint]
259 const checkForUpdates = useCallback(
266 intervalTime = TEN_SECONDS,
267 }: UpdateParams) => {
268 if (l1Txn) storePendingL1Txn(l1Txn);
270 const interval = setInterval(async () => {
271 const updatedPoint = await initPoint(point);
272 const changedField = !points[point]
274 : updatedPoint.getChangedField(points[point], field);
277 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
280 if (!updatedPoint.isPlaceholder && changedField) {
281 updatePoint(updatedPoint);
282 clearInterval(interval);
283 if (l1Txn) deletePendingL1Txn(l1Txn);
286 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
292 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
295 const spawnPoint = useCallback(
296 async (pointToSpawn: number, destinationAddress: string) => {
297 if (quotaReached()) {
301 const _point = need.point(pointCursor);
302 const _contracts = contracts.getOrElse(null);
303 const _web3 = web3.getOrElse(null);
304 const _wallet = wallet.getOrElse(null);
305 const _authToken = authToken.getOrElse(null);
307 if (!_contracts || !_web3 || !_wallet || !_authToken) {
308 // not using need because we want a custom error
309 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
312 const proxy = point.getSpawnProxy();
314 if (proxy === undefined)
315 throw new Error("Error: Address doesn't match proxy");
317 const nonce = await api.getNonce({
322 await submitL2Transaction({
324 address: destinationAddress,
350 const generateInviteInfo = async (planet: number, _authToken: string) => {
351 const { ticket, inviteWallet } = await generateInviteWallet(
356 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
359 const getInvites = useCallback(async () => {
361 setInvitesLoading(true);
362 const curPoint: number = Number(need.point(pointCursor));
363 const _authToken = authToken.getOrElse(null);
364 const _contracts = contracts.getOrElse(null);
366 if (_authToken && _contracts) {
367 const spawnedPoints = point.isL2Spawn
368 ? await api.getSpawned(curPoint)
371 const newPending = await api.getPendingByShip(curPoint);
372 setPendingTransactions(newPending);
373 const pendingSpawns = getPendingSpawns(newPending);
375 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
380 const outgoingPoints = pointList
382 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
384 .map(({ value }) => value);
386 const invitePlanets = spawnedPoints
388 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
390 .filter((p: number) => !pendingSpawns.has(p))
395 invitePlanets.map(p => inviteTemplate(p))
398 const newInvites: Invite[] = [];
399 // Iterate over all of the stored invites, generating wallet info as necessary
400 const storedInvites = getStoredInvites(ls);
402 for (let i = 0; i < invitePlanets.length; i++) {
403 setInviteGeneratingNum(i + 1);
404 const planet = invitePlanets[i];
405 const storedInvite = storedInvites[planet];
407 storedInvite || (await generateInviteInfo(planet, _authToken));
409 // TODO: check if the invite point's owner still matches the deterministic wallet address
410 const planetInfo = await api.getPoint(planet);
411 setStoredInvite(ls, invite);
414 invite.owner.toLowerCase() ===
415 planetInfo.ownership?.owner?.address &&
416 planetInfo.dominion === POINT_DOMINIONS.L2
418 newInvites.push(invite);
419 updateInvite(curPoint, invite);
421 removeInvite(curPoint, invite.planet);
425 setStoredInvite(ls, invite);
428 setInvites(curPoint, newInvites);
429 setInvitesLoading(false);
433 console.warn('ERROR GETTING INVITES', error);
446 setInviteGeneratingNum,
447 setPendingTransactions,
451 const generateInviteCodes = useCallback(
452 async (numInvites: number) => {
453 const _point = need.point(pointCursor);
454 const _contracts = contracts.getOrElse(null);
455 const _web3 = web3.getOrElse(null);
456 const _wallet = wallet.getOrElse(null);
457 const _authToken = authToken.getOrElse(null);
458 const _details = getDetails(_point);
460 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
461 // not using need because we want a custom error
462 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
465 // TODO: remove when UI disables management proxy from spawning invites
466 // if (Just.hasInstance(authMnemonic)) {
468 // "Auth key Error: A management mnemonic can't create invite codes"
471 let planets: UnspawnedPoints = await api.getUnspawned(_point);
473 const pendingTxs = await api.getPendingByShip(_point);
475 const pendingSpawns = getPendingSpawns(pendingTxs);
477 planets = planets.filter((point: number) => !pendingSpawns.has(point));
479 const invites: Invite[] = [];
481 const proxy = point.getSpawnProxy();
483 if (proxy === undefined)
484 throw new Error("Error: Address doesn't match proxy");
486 const nonce = await api.getNonce({
491 for (let i = 0; i < numInvites && planets[i]; i++) {
492 setInviteGeneratingNum(i + 1);
493 const planet = planets[i];
494 const nonceInc = i + nonce;
496 const { ticket, inviteWallet } = await generateInviteWallet(
501 await submitL2Transaction({
503 address: _wallet.address,
508 pointToSpawn: planet,
515 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
519 await submitL2Transaction({
526 type: 'configureKeys',
532 await registerProxyAddress(
539 inviteWallet.management.keys.address,
545 await submitL2Transaction({
550 type: 'transferPoint',
552 address: inviteWallet.ownership.keys.address,
562 owner: inviteWallet.ownership.keys.address,
566 setStoredInvites(ls, invites);
569 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
585 setInviteGeneratingNum,
589 const getPoints = useCallback(
590 async (proxy: Proxy, address: EthAddress) => {
591 const points: Ship[] =
593 ? await api.getOwnedPoints(address)
595 ? await api.getManagerFor(address)
597 ? await api.getVotingFor(address)
598 : proxy === 'transfer'
599 ? await api.getTransferringFor(address)
601 ? await api.getSpawningFor(address)
609 const getPendingTransactions = useCallback(async () => {
611 if (!point.isDefault) {
612 const newPending = await api.getPendingByShip(point.value);
613 setPendingTransactions(newPending);
616 console.warn('ERROR GETTING PENDING', error);
618 }, [api, setPendingTransactions, point]);
620 interface ConfigureKeysParams {
622 customNetworkSeed?: string;
625 const configureNetworkKeys = useCallback(
626 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
627 if (quotaReached()) {
630 const _wallet = wallet.getOrElse(null);
631 const _web3 = web3.getOrElse(null);
632 if (!_wallet || !_web3 || point.isDefault) {
633 // not using need because we want a custom error
634 throw new Error('Internal Error: Missing Wallet/Details');
637 const proxy = point.getManagerProxy();
639 if (proxy === undefined)
640 throw new Error("Error: Address doesn't match proxy");
642 const nonce = await api.getNonce({
647 const nextRevision = point.keyRevisionNumber + 1;
648 const networkSeed = customNetworkSeed
650 : await attemptNetworkSeedDerivation({
657 revision: nextRevision,
659 const txHash = await submitL2Transaction({
666 type: 'configureKeys',
673 return api.getPendingTx(txHash);
675 [// eslint-disable-line
689 const performL2Reticket = async ({
695 }: ReticketParams) => {
696 const azimuthPoint = await api.getPoint(point);
698 const _web3 = web3.getOrElse(null);
699 let nonce = await api.getNonce({ ship: point, proxy });
703 // 1. Update networking keys
704 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
705 const nextRevision = networkRevision + 1;
706 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
710 const configureKeysRequest = await submitL2Transaction({
718 type: 'configureKeys',
723 requests.push(configureKeysRequest);
725 // 2. Set Management Proxy
726 const registerMgmtRequest = await registerProxyAddress(
739 requests.push(registerMgmtRequest);
741 // 3. Set Spawn Proxy
742 if (!isPlanet(Number(point))) {
743 const registerSpawnRequest = await registerProxyAddress(
750 toWallet?.spawn?.keys?.address || to,
756 requests.push(registerSpawnRequest);
759 const transferTxRequest = await submitL2Transaction({
764 type: 'transferPoint',
771 requests.push(transferTxRequest);
773 const hashes = await Promise.all(requests);
777 const setProxyAddress = useCallback(
778 async (proxyType: Proxy, address: EthAddress) => {
779 if (quotaReached()) {
783 const _wallet = wallet.getOrElse(null);
784 const _web3 = web3.getOrElse(null);
787 // not using need because we want a custom error
788 throw new Error('Internal Error: Missing Wallet');
791 if (point.isDefault) {
792 // not using need because we want a custom error
793 throw new Error('Internal Error: Invalid point');
796 const proxy = point.getAddressProxy(proxyType);
798 if (proxy === undefined)
799 throw new Error("Error: Address doesn't match expected proxy");
801 const nonce = await api.getNonce({ ship: point.value, proxy });
803 const txHash = await registerProxyAddress(
816 return api.getPendingTx(txHash);
818 [api, connector, point, wallet, web3, walletType, quotaReached]
821 const changeSponsorship = useCallback(
824 type: 'adopt' | 'detach' | 'reject',
825 skipQuotaCheck = false
827 if (!skipQuotaCheck && quotaReached()) {
831 const _wallet = wallet.getOrElse(null);
832 const _web3 = web3.getOrElse(null);
833 const ship = point.value;
835 const proxy = point.getManagerProxy();
837 if (proxy === undefined)
838 throw new Error("Error: Address doesn't match expected proxy");
840 const nonce = await api.getNonce({ ship, proxy });
841 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
842 const txHash = await submitL2Transaction({
849 address: _wallet.address,
856 return api.getPendingTx(txHash);
858 [api, point, wallet, walletType, web3, connector, quotaReached]
861 const transferPoint = useCallback(
862 async (address: EthAddress, reset?: boolean) => {
863 if (quotaReached()) {
867 const _point = need.point(pointCursor);
868 const _wallet = wallet.getOrElse(null);
869 const _web3 = web3.getOrElse(null);
872 // not using need because we want a custom error
873 throw new Error('Internal Error: Missing Wallet');
876 const proxy = point.getTransferProxy();
878 if (proxy === undefined)
879 throw new Error("Error: Address doesn't match proxy");
881 const nonce = await api.getNonce({ ship: _point, proxy });
883 const txHash = await submitL2Transaction({
890 type: 'transferPoint',
897 return api.getPendingTx(txHash);
899 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
902 const sendL2Transaction = useCallback(
903 async (params: SendL2Params) => {
904 if (quotaReached()) {
908 const _wallet = wallet.getOrElse(null);
909 const _web3 = web3.getOrElse(null);
911 if (!_wallet || point.isDefault) {
912 // not using need because we want a custom error
913 throw new Error('Internal Error: Missing Wallet or Point Info');
916 return submitL2Transaction({
926 [api, connector, point, wallet, web3, walletType, quotaReached]
929 const changeSponsor = useCallback(
930 async (newSponsor: number) => {
931 if (point.isDefault) {
932 throw new Error('Internal Error: point is default');
935 const curPoint = point.value;
936 const _wallet = wallet.getOrElse(null);
939 // not using need because we want a custom error
940 throw new Error('Internal Error: Missing Wallet');
943 const proxy = point.getManagerProxy();
945 if (proxy === undefined)
946 throw new Error("Error: Address doesn't match proxy");
948 const nonce = await api.getNonce({ ship: curPoint, proxy });
950 return sendL2Transaction({
958 [api, point, sendL2Transaction, wallet]
961 const cancelEscape = useCallback(async () => {
962 if (point.isDefault) {
963 throw new Error('Internal Error: point is default');
966 const curPoint = point.value;
967 const _wallet = wallet.getOrElse(null);
970 // not using need because we want a custom error
971 throw new Error('Internal Error: Missing Wallet');
974 const proxy = point.getManagerProxy();
976 if (proxy === undefined)
977 throw new Error("Error: Address doesn't match proxy");
979 const nonce = await api.getNonce({ ship: curPoint, proxy });
981 return sendL2Transaction({
984 type: 'cancelEscape',
987 }, [api, point, sendL2Transaction, wallet]);
989 const getPointsDetails = useCallback(
991 ownedPoints: number[],
992 incomingPoints: number[],
993 managingPoints: number[],
994 votingPoints: number[],
995 spawningPoints: number[]
999 await Promise.all(ownedPoints.map(initPoint)),
1000 await Promise.all(incomingPoints.map(initPoint)),
1001 await Promise.all(managingPoints.map(initPoint)),
1002 await Promise.all(votingPoints.map(initPoint)),
1003 await Promise.all(spawningPoints.map(initPoint)),
1007 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
1020 [setPoints, initPoint]
1023 // On load, get initial config
1030 }, [config, fetchConfig]);
1033 const time = ONE_SECOND;
1035 const interval = setInterval(() => {
1036 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1037 setNextRoll(nextRoll);
1039 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1040 api.getRollerConfig().then(response => {
1041 setNextBatchTime(response.nextBatch);
1044 getPendingTransactions();
1047 if (!point.isDefault) {
1048 getInvites(); // This will also get pending txns
1050 }, TEN_SECONDS); // Should this be more like a minute?
1054 return () => clearInterval(interval);
1055 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1064 configureNetworkKeys,
1065 generateInviteCodes,
1070 getPendingTransactions,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list