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, isRopsten } from './flags';
33} from './utils/roller';
34import { ETH_ZERO_ADDR, ROLLER_HOSTS, TEN_SECONDS } from './constants';
44} from '@urbit/roller-api';
46import Point, { PointField, Points } from './types/Point';
47import { useTimerStore } from 'store/timerStore';
48import { ReticketParams, SendL2Params } from './types/L2Transaction';
49import { L1Point } from './types/L1Point';
50import { ddmmmYYYY } from './utils/date';
51import { showNotification } from './utils/notifications';
52import { useWalletConnect } from './useWalletConnect';
54const ONE_SECOND = 1000;
56const inviteTemplate = (
68export default function useRoller() {
76 const { connector } = useWalletConnect();
77 const { pointCursor }: any = usePointCursor();
78 const { web3, contracts }: any = useNetwork();
79 const allPoints: any = usePointCache();
80 const getDetails = allPoints?.getDetails;
82 const { setNextRoll } = useTimerStore();
93 setPendingTransactions,
95 setInviteGeneratingNum,
101 } = useRollerStore();
102 const [config, setConfig] = useState<Config | null>(null);
104 const options: Options = useMemo(() => {
105 const type = isRopsten || !isDevelopment ? 'https' : 'http';
106 const host = isRopsten
107 ? ROLLER_HOSTS.ROPSTEN
110 : ROLLER_HOSTS.MAINNET;
111 const port = isDevelopment && !isRopsten ? 8080 : 443;
112 const path = '/v1/roller';
124 const api = useMemo(() => {
125 return new RollerRPCAPI(options);
131 isCompression: false,
132 encryptionSecret: authToken.getOrElse('default'),
137 const getNextQuotaTime = useCallback(
138 (nextSlice: number) => {
143 setNextQuotaTime(next);
144 getNextQuotaTime(next);
146 .catch(err => console.warn(err));
147 }, nextSlice - new Date().getTime());
149 [api, setNextQuotaTime]
152 const fetchConfig = useCallback(async () => {
158 setNextBatchTime(config.nextBatch);
161 // TODO: more elegant error handling
163 '[fetchConfig:failed] check the roller connection?\n',
167 }, [api, getNextQuotaTime, setNextBatchTime]);
169 const quotaReached = useCallback(() => {
170 if (point.l2Quota <= 0) {
172 `You have reached your weekly L2 transaction limit. You will get another
173 ${config?.rollerQuota} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
177 return point.l2Quota <= 0;
178 }, [config, nextQuotaTime, point, setModalText]);
180 const initPoint = useCallback(
181 async (point: string | number) => {
182 const _wallet = wallet.getOrElse(null);
188 const pointNum = Number(point);
190 const rawDetails = await api.getPoint(pointNum);
191 const l2Quota = isL2Spawn(rawDetails?.dominion)
192 ? await api.getRemainingQuota(pointNum)
194 const details = toL1Details(rawDetails);
199 address: _wallet.address,
205 // Try getting the details from L1
207 const _contracts = need.contracts(contracts);
208 const details = await azimuth.azimuth.getPoint(_contracts, point);
212 address: _wallet.address,
216 // Just return a placeholder Point
217 const details: L1Point = toL1Details();
221 address: _wallet.address,
227 [api, wallet, contracts]
230 const checkForUpdates = useCallback(
237 const interval = setInterval(async () => {
238 const updatedPoint = await initPoint(point);
239 const changedField = !points[point]
241 : updatedPoint.getChangedField(points[point], field);
244 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
247 if (!updatedPoint.isPlaceholder && changedField) {
248 updatePoint(updatedPoint);
249 clearInterval(interval);
252 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
258 [points, initPoint, updatePoint]
261 const spawnPoint = useCallback(
262 async (pointToSpawn: number, destinationAddress: string) => {
263 if (quotaReached()) {
267 const _point = need.point(pointCursor);
268 const _contracts = contracts.getOrElse(null);
269 const _web3 = web3.getOrElse(null);
270 const _wallet = wallet.getOrElse(null);
271 const _authToken = authToken.getOrElse(null);
273 if (!_contracts || !_web3 || !_wallet || !_authToken) {
274 // not using need because we want a custom error
275 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
278 const proxy = point.getSpawnProxy();
280 if (proxy === undefined)
281 throw new Error("Error: Address doesn't match proxy");
283 const nonce = await api.getNonce({
288 await submitL2Transaction({
290 address: destinationAddress,
316 const generateInviteInfo = async (planet: number, _authToken: string) => {
317 const { ticket, inviteWallet } = await generateInviteWallet(
322 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
325 const getInvites = useCallback(async () => {
327 setInvitesLoading(true);
328 const curPoint: number = Number(need.point(pointCursor));
329 const _authToken = authToken.getOrElse(null);
330 const _contracts = contracts.getOrElse(null);
332 if (_authToken && _contracts) {
333 const spawnedPoints = point.isL2Spawn
334 ? await api.getSpawned(curPoint)
337 const newPending = await api.getPendingByShip(curPoint);
338 setPendingTransactions(newPending);
339 const pendingSpawns = getPendingSpawns(newPending);
341 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
346 const outgoingPoints = pointList
348 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
350 .map(({ value }) => value);
352 const invitePlanets = spawnedPoints
354 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
356 .filter((p: number) => !pendingSpawns.has(p))
361 invitePlanets.map(p => inviteTemplate(p))
364 const newInvites: Invite[] = [];
365 // Iterate over all of the stored invites, generating wallet info as necessary
366 const storedInvites = getStoredInvites(ls);
368 for (let i = 0; i < invitePlanets.length; i++) {
369 setInviteGeneratingNum(i + 1);
370 const planet = invitePlanets[i];
371 const storedInvite = storedInvites[planet];
373 storedInvite || (await generateInviteInfo(planet, _authToken));
375 // TODO: check if the invite point's owner still matches the deterministic wallet address
376 const planetInfo = await api.getPoint(planet);
377 setStoredInvite(ls, invite);
380 invite.owner.toLowerCase() ===
381 planetInfo.ownership?.owner?.address ||
382 planetInfo.ownership?.transferProxy?.address !== ETH_ZERO_ADDR
384 newInvites.push(invite);
385 updateInvite(curPoint, invite);
387 removeInvite(curPoint, invite.planet);
391 setStoredInvite(ls, invite);
394 setInvites(curPoint, newInvites);
395 setInvitesLoading(false);
399 console.warn('ERROR GETTING INVITES', error);
412 setInviteGeneratingNum,
413 setPendingTransactions,
417 const generateInviteCodes = useCallback(
418 async (numInvites: number) => {
419 const _point = need.point(pointCursor);
420 const _contracts = contracts.getOrElse(null);
421 const _web3 = web3.getOrElse(null);
422 const _wallet = wallet.getOrElse(null);
423 const _authToken = authToken.getOrElse(null);
424 const _details = getDetails(_point);
426 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
427 // not using need because we want a custom error
428 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
431 // TODO: remove when UI disables management proxy from spawning invites
432 // if (Just.hasInstance(authMnemonic)) {
434 // "Auth key Error: A management mnemonic can't create invite codes"
437 let planets: UnspawnedPoints = await api.getUnspawned(_point);
439 const pendingTxs = await api.getPendingByShip(_point);
441 const pendingSpawns = getPendingSpawns(pendingTxs);
443 planets = planets.filter((point: number) => !pendingSpawns.has(point));
445 const invites: Invite[] = [];
447 const proxy = point.getSpawnProxy();
449 if (proxy === undefined)
450 throw new Error("Error: Address doesn't match proxy");
452 const nonce = await api.getNonce({
457 for (let i = 0; i < numInvites && planets[i]; i++) {
458 setInviteGeneratingNum(i + 1);
459 const planet = planets[i];
460 const nonceInc = i + nonce;
462 const { ticket, inviteWallet } = await generateInviteWallet(
467 await submitL2Transaction({
469 address: _wallet.address,
474 pointToSpawn: planet,
481 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
485 await submitL2Transaction({
492 type: 'configureKeys',
498 await registerProxyAddress(
505 inviteWallet.management.keys.address,
511 await submitL2Transaction({
516 type: 'transferPoint',
518 address: inviteWallet.ownership.keys.address,
528 owner: inviteWallet.ownership.keys.address,
532 setStoredInvites(ls, invites);
535 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
551 setInviteGeneratingNum,
555 const getPoints = useCallback(
556 async (proxy: Proxy, address: EthAddress) => {
557 const points: Ship[] =
559 ? await api.getOwnedPoints(address)
561 ? await api.getManagerFor(address)
563 ? await api.getVotingFor(address)
564 : proxy === 'transfer'
565 ? await api.getTransferringFor(address)
567 ? await api.getSpawningFor(address)
575 const getPendingTransactions = useCallback(async () => {
577 if (!point.isDefault) {
578 const newPending = await api.getPendingByShip(point.value);
579 setPendingTransactions(newPending);
582 console.warn('ERROR GETTING PENDING', error);
584 }, [api, setPendingTransactions, point]);
586 interface ConfigureKeysParams {
588 customNetworkSeed?: string;
591 const configureNetworkKeys = useCallback(
592 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
593 if (quotaReached()) {
596 const _wallet = wallet.getOrElse(null);
597 const _web3 = web3.getOrElse(null);
598 const _details = getDetails(point.value);
599 if (!_wallet || !_web3 || !_details || point.isDefault) {
600 // not using need because we want a custom error
601 throw new Error('Internal Error: Missing Wallet/Details');
604 const proxy = point.getManagerProxy();
606 if (proxy === undefined)
607 throw new Error("Error: Address doesn't match proxy");
609 const nonce = await api.getNonce({
614 const nextRevision = point.keyRevisionNumber + 1;
615 const networkSeed = customNetworkSeed
617 : await attemptNetworkSeedDerivation({
624 revision: nextRevision,
626 const txHash = await submitL2Transaction({
633 type: 'configureKeys',
640 return api.getPendingTx(txHash);
642 [// eslint-disable-line
656 const performL2Reticket = async ({
662 }: ReticketParams) => {
663 const azimuthPoint = await api.getPoint(point);
665 const _web3 = web3.getOrElse(null);
666 let nonce = await api.getNonce({ ship: point, proxy });
670 // 1. Update networking keys
671 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
672 const nextRevision = networkRevision + 1;
673 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
677 const configureKeysRequest = await submitL2Transaction({
685 type: 'configureKeys',
690 requests.push(configureKeysRequest);
692 // 2. Set Management Proxy
693 const registerMgmtRequest = await registerProxyAddress(
706 requests.push(registerMgmtRequest);
708 // 3. Set Spawn Proxy
709 if (!isPlanet(Number(point))) {
710 const registerSpawnRequest = await registerProxyAddress(
717 toWallet.spawn.keys.address,
723 requests.push(registerSpawnRequest);
726 const transferTxRequest = await submitL2Transaction({
731 type: 'transferPoint',
738 requests.push(transferTxRequest);
740 const hashes = await Promise.all(requests);
744 const setProxyAddress = useCallback(
745 async (proxyType: Proxy, address: EthAddress) => {
746 if (quotaReached()) {
750 const _wallet = wallet.getOrElse(null);
751 const _web3 = web3.getOrElse(null);
754 // not using need because we want a custom error
755 throw new Error('Internal Error: Missing Wallet');
758 if (point.isDefault) {
759 // not using need because we want a custom error
760 throw new Error('Internal Error: Invalid point');
763 const proxy = point.getAddressProxy(proxyType);
765 if (proxy === undefined)
766 throw new Error("Error: Address doesn't match expected proxy");
768 const nonce = await api.getNonce({ ship: point.value, proxy });
770 const txHash = await registerProxyAddress(
783 return api.getPendingTx(txHash);
785 [api, connector, point, wallet, web3, walletType, quotaReached]
788 const changeSponsorship = useCallback(
789 async (sponsee: Ship, type: 'adopt' | 'detach' | 'reject') => {
790 if (quotaReached()) {
794 const _wallet = wallet.getOrElse(null);
795 const _web3 = web3.getOrElse(null);
796 const ship = point.value;
798 const proxy = point.getManagerProxy();
800 if (proxy === undefined)
801 throw new Error("Error: Address doesn't match expected proxy");
803 const nonce = await api.getNonce({ ship, proxy });
804 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
805 const txHash = await submitL2Transaction({
812 address: _wallet.address,
819 return api.getPendingTx(txHash);
821 [api, point, wallet, walletType, web3, connector, quotaReached]
824 const transferPoint = useCallback(
825 async (address: EthAddress, reset?: boolean) => {
826 if (quotaReached()) {
830 const _point = need.point(pointCursor);
831 const _wallet = wallet.getOrElse(null);
832 const _web3 = web3.getOrElse(null);
835 // not using need because we want a custom error
836 throw new Error('Internal Error: Missing Wallet');
839 const proxy = point.getTransferProxy();
841 if (proxy === undefined)
842 throw new Error("Error: Address doesn't match proxy");
844 const nonce = await api.getNonce({ ship: _point, proxy });
846 const txHash = await submitL2Transaction({
853 type: 'transferPoint',
860 return api.getPendingTx(txHash);
862 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
865 const sendL2Transaction = useCallback(
866 async (params: SendL2Params) => {
867 if (quotaReached()) {
871 const _wallet = wallet.getOrElse(null);
872 const _web3 = web3.getOrElse(null);
874 if (!_wallet || point.isDefault) {
875 // not using need because we want a custom error
876 throw new Error('Internal Error: Missing Wallet or Point Info');
879 return submitL2Transaction({
889 [api, connector, point, wallet, web3, walletType, quotaReached]
892 const changeSponsor = useCallback(
893 async (newSponsor: number) => {
894 if (point.isDefault) {
895 throw new Error('Internal Error: point is default');
898 const curPoint = point.value;
899 const _wallet = wallet.getOrElse(null);
902 // not using need because we want a custom error
903 throw new Error('Internal Error: Missing Wallet');
906 const proxy = point.getManagerProxy();
908 if (proxy === undefined)
909 throw new Error("Error: Address doesn't match proxy");
911 const nonce = await api.getNonce({ ship: curPoint, proxy });
913 return sendL2Transaction({
921 [api, point, sendL2Transaction, wallet]
924 const cancelEscape = useCallback(async () => {
925 if (point.isDefault) {
926 throw new Error('Internal Error: point is default');
929 const curPoint = point.value;
930 const _wallet = wallet.getOrElse(null);
933 // not using need because we want a custom error
934 throw new Error('Internal Error: Missing Wallet');
937 const proxy = point.getManagerProxy();
939 if (proxy === undefined)
940 throw new Error("Error: Address doesn't match proxy");
942 const nonce = await api.getNonce({ ship: curPoint, proxy });
944 return sendL2Transaction({
947 type: 'cancelEscape',
950 }, [api, point, sendL2Transaction, wallet]);
952 const getPointsDetails = useCallback(
954 ownedPoints: number[],
955 incomingPoints: number[],
956 managingPoints: number[],
957 votingPoints: number[],
958 spawningPoints: number[]
962 await Promise.all(ownedPoints.map(initPoint)),
963 await Promise.all(incomingPoints.map(initPoint)),
964 await Promise.all(managingPoints.map(initPoint)),
965 await Promise.all(votingPoints.map(initPoint)),
966 await Promise.all(spawningPoints.map(initPoint)),
970 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
983 [setPoints, initPoint]
986 // On load, get initial config
993 }, [config, fetchConfig]);
996 const time = ONE_SECOND;
998 const interval = setInterval(() => {
999 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1000 setNextRoll(nextRoll);
1002 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1003 api.getRollerConfig().then(response => {
1004 setNextBatchTime(response.nextBatch);
1007 getPendingTransactions();
1010 if (!point.isDefault) {
1011 getInvites(); // This will also get pending txns
1013 }, TEN_SECONDS); // Should this be more like a minute?
1017 return () => clearInterval(interval);
1018 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1027 configureNetworkKeys,
1028 generateInviteCodes,
1032 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