1import { useCallback, useEffect, useMemo, useState } from 'react';
2import * as azimuth from 'azimuth-js';
3import * as need from 'lib/need';
4import * as ob from 'urbit-ob';
5import SecureLS from 'secure-ls';
7import { Invite } from 'lib/types/Invite';
9 deriveNetworkSeedFromUrbitWallet,
10 attemptNetworkSeedDerivation,
13import { useNetwork } from 'store/network';
14import { usePointCursor } from 'store/pointCursor';
15import { EMPTY_POINT, useRollerStore } from 'store/rollerStore';
16import { useWallet } from 'store/wallet';
21} from 'store/storage/roller';
22import { usePointCache } from 'store/pointCache';
24import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
25import { convertToInt } from './convertToInt';
26import { isDevelopment, isMainnet, isRopsten } from './flags';
35} from './utils/roller';
36import { ETH_ZERO_ADDR, ROLLER_HOSTS, TEN_SECONDS } from './constants';
46} from '@urbit/roller-api';
48import Point, { PointField, Points } from './types/Point';
49import { useTimerStore } from 'store/timerStore';
50import { ReticketParams, SendL2Params } from './types/L2Transaction';
51import { L1Point } from './types/L1Point';
52import { ddmmmYYYY } from './utils/date';
53import { showNotification } from './utils/notifications';
54import { useWalletConnect } from './useWalletConnect';
55import { PendingL1Txn } from './types/PendingL1Transaction';
57const ONE_SECOND = 1000;
59interface UpdateParams {
65 intervalTime?: number;
68const inviteTemplate = (
80export default function useRoller() {
88 const { connector } = useWalletConnect();
89 const { pointCursor }: any = usePointCursor();
90 const { web3, contracts }: any = useNetwork();
91 const allPoints: any = usePointCache();
92 const getDetails = allPoints?.getDetails;
94 const { setNextRoll } = useTimerStore();
105 setPendingTransactions,
107 setInviteGeneratingNum,
115 } = useRollerStore();
116 const [config, setConfig] = useState<Config | null>(null);
118 const options: Options = useMemo(() => {
119 const type = isMainnet || isRopsten ? 'https' : 'http';
120 const host = isMainnet
121 ? ROLLER_HOSTS.MAINNET
123 ? ROLLER_HOSTS.ROPSTEN
124 : ROLLER_HOSTS.LOCAL;
125 const port = isMainnet || isRopsten ? 443 : 8080;
126 const path = '/v1/roller';
138 const api = useMemo(() => {
139 return new RollerRPCAPI(options);
145 isCompression: false,
146 encryptionSecret: authToken.getOrElse('default'),
151 const getNextQuotaTime = useCallback(
152 (nextSlice: number) => {
157 setNextQuotaTime(next);
158 getNextQuotaTime(next);
160 .catch(err => console.warn(err));
161 }, nextSlice - new Date().getTime());
163 [api, setNextQuotaTime]
166 const fetchConfig = useCallback(async () => {
172 setNextBatchTime(config.nextBatch);
175 // TODO: more elegant error handling
177 '[fetchConfig:failed] check the roller connection?\n',
181 }, [api, getNextQuotaTime, setNextBatchTime]);
183 const quotaReached = useCallback(() => {
184 if (point.l2Quota <= 0) {
186 `You have reached your weekly L2 transaction limit. You will get another
187 ${config?.rollerQuota} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
191 return point.l2Quota <= 0;
192 }, [config, nextQuotaTime, point, setModalText]);
194 const initPoint = useCallback(
195 async (point: string | number) => {
196 const _wallet = wallet.getOrElse(null);
197 const _contracts = contracts.getOrElse(null);
199 if (!_wallet || !_contracts) {
203 const pointNum = Number(point);
205 const rawDetails = await api.getPoint(pointNum);
206 const l2Quota = isL2Spawn(rawDetails?.dominion)
207 ? await api.getRemainingQuota(pointNum)
210 const details = isL2Spawn(rawDetails?.dominion)
211 ? toL1Details(rawDetails)
212 : await azimuth.azimuth.getPoint(_contracts, point);
217 address: _wallet.address,
223 // Try getting the details from L1
225 const _contracts = need.contracts(contracts);
226 const details = await azimuth.azimuth.getPoint(_contracts, point);
230 address: _wallet.address,
234 // Just return a placeholder Point
235 const details: L1Point = toL1Details();
239 address: _wallet.address,
245 [api, wallet, contracts]
248 const getAndUpdatePoint = useCallback(
249 async (point: number) => {
250 const updatedPoint = await initPoint(point);
251 updatePoint(updatedPoint);
253 [initPoint, updatePoint]
256 const checkForUpdates = useCallback(
263 intervalTime = TEN_SECONDS,
264 }: UpdateParams) => {
265 if (l1Txn) storePendingL1Txn(l1Txn);
267 const interval = setInterval(async () => {
268 const updatedPoint = await initPoint(point);
269 const changedField = !points[point]
271 : updatedPoint.getChangedField(points[point], field);
274 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
277 if (!updatedPoint.isPlaceholder && changedField) {
278 updatePoint(updatedPoint);
279 clearInterval(interval);
280 if (l1Txn) deletePendingL1Txn(l1Txn);
283 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
289 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
292 const spawnPoint = useCallback(
293 async (pointToSpawn: number, destinationAddress: string) => {
294 if (quotaReached()) {
298 const _point = need.point(pointCursor);
299 const _contracts = contracts.getOrElse(null);
300 const _web3 = web3.getOrElse(null);
301 const _wallet = wallet.getOrElse(null);
302 const _authToken = authToken.getOrElse(null);
304 if (!_contracts || !_web3 || !_wallet || !_authToken) {
305 // not using need because we want a custom error
306 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
309 const proxy = point.getSpawnProxy();
311 if (proxy === undefined)
312 throw new Error("Error: Address doesn't match proxy");
314 const nonce = await api.getNonce({
319 await submitL2Transaction({
321 address: destinationAddress,
347 const generateInviteInfo = async (planet: number, _authToken: string) => {
348 const { ticket, inviteWallet } = await generateInviteWallet(
353 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
356 const getInvites = useCallback(async () => {
358 setInvitesLoading(true);
359 const curPoint: number = Number(need.point(pointCursor));
360 const _authToken = authToken.getOrElse(null);
361 const _contracts = contracts.getOrElse(null);
363 if (_authToken && _contracts) {
364 const spawnedPoints = point.isL2Spawn
365 ? await api.getSpawned(curPoint)
368 const newPending = await api.getPendingByShip(curPoint);
369 setPendingTransactions(newPending);
370 const pendingSpawns = getPendingSpawns(newPending);
372 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
377 const outgoingPoints = pointList
379 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
381 .map(({ value }) => value);
383 const invitePlanets = spawnedPoints
385 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
387 .filter((p: number) => !pendingSpawns.has(p))
392 invitePlanets.map(p => inviteTemplate(p))
395 const newInvites: Invite[] = [];
396 // Iterate over all of the stored invites, generating wallet info as necessary
397 const storedInvites = getStoredInvites(ls);
399 for (let i = 0; i < invitePlanets.length; i++) {
400 setInviteGeneratingNum(i + 1);
401 const planet = invitePlanets[i];
402 const storedInvite = storedInvites[planet];
404 storedInvite || (await generateInviteInfo(planet, _authToken));
406 // TODO: check if the invite point's owner still matches the deterministic wallet address
407 const planetInfo = await api.getPoint(planet);
408 setStoredInvite(ls, invite);
412 `planet: ${planet} dominion: ${planetInfo.dominion} (${ob.patp(
416 console.log(`invite owner: ${invite.owner.toLowerCase()}`);
418 `planetInfo owner: ${planetInfo.ownership?.owner?.address}`
421 `planetInfo transfer: ${planetInfo.ownership?.transferProxy?.address}`
424 `invite unclaimed: ${invite.owner.toLowerCase() ===
425 planetInfo.ownership?.owner?.address ||
426 planetInfo.ownership?.transferProxy?.address !== ETH_ZERO_ADDR}`
431 invite.owner.toLowerCase() ===
432 planetInfo.ownership?.owner?.address ||
433 planetInfo.ownership?.transferProxy?.address !== ETH_ZERO_ADDR
435 newInvites.push(invite);
436 updateInvite(curPoint, invite);
438 removeInvite(curPoint, invite.planet);
442 setStoredInvite(ls, invite);
445 setInvites(curPoint, newInvites);
446 setInvitesLoading(false);
450 console.warn('ERROR GETTING INVITES', error);
463 setInviteGeneratingNum,
464 setPendingTransactions,
468 const generateInviteCodes = useCallback(
469 async (numInvites: number) => {
470 const _point = need.point(pointCursor);
471 const _contracts = contracts.getOrElse(null);
472 const _web3 = web3.getOrElse(null);
473 const _wallet = wallet.getOrElse(null);
474 const _authToken = authToken.getOrElse(null);
475 const _details = getDetails(_point);
477 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
478 // not using need because we want a custom error
479 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
482 // TODO: remove when UI disables management proxy from spawning invites
483 // if (Just.hasInstance(authMnemonic)) {
485 // "Auth key Error: A management mnemonic can't create invite codes"
488 let planets: UnspawnedPoints = await api.getUnspawned(_point);
490 const pendingTxs = await api.getPendingByShip(_point);
492 const pendingSpawns = getPendingSpawns(pendingTxs);
494 planets = planets.filter((point: number) => !pendingSpawns.has(point));
496 const invites: Invite[] = [];
498 const proxy = point.getSpawnProxy();
500 if (proxy === undefined)
501 throw new Error("Error: Address doesn't match proxy");
503 const nonce = await api.getNonce({
508 for (let i = 0; i < numInvites && planets[i]; i++) {
509 setInviteGeneratingNum(i + 1);
510 const planet = planets[i];
511 const nonceInc = i + nonce;
513 const { ticket, inviteWallet } = await generateInviteWallet(
518 await submitL2Transaction({
520 address: _wallet.address,
525 pointToSpawn: planet,
532 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
536 await submitL2Transaction({
543 type: 'configureKeys',
549 await registerProxyAddress(
556 inviteWallet.management.keys.address,
562 await submitL2Transaction({
567 type: 'transferPoint',
569 address: inviteWallet.ownership.keys.address,
579 owner: inviteWallet.ownership.keys.address,
583 setStoredInvites(ls, invites);
586 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
602 setInviteGeneratingNum,
606 const getPoints = useCallback(
607 async (proxy: Proxy, address: EthAddress) => {
608 const points: Ship[] =
610 ? await api.getOwnedPoints(address)
612 ? await api.getManagerFor(address)
614 ? await api.getVotingFor(address)
615 : proxy === 'transfer'
616 ? await api.getTransferringFor(address)
618 ? await api.getSpawningFor(address)
626 const getPendingTransactions = useCallback(async () => {
628 if (!point.isDefault) {
629 const newPending = await api.getPendingByShip(point.value);
630 setPendingTransactions(newPending);
633 console.warn('ERROR GETTING PENDING', error);
635 }, [api, setPendingTransactions, point]);
637 interface ConfigureKeysParams {
639 customNetworkSeed?: string;
642 const configureNetworkKeys = useCallback(
643 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
644 if (quotaReached()) {
647 const _wallet = wallet.getOrElse(null);
648 const _web3 = web3.getOrElse(null);
649 if (!_wallet || !_web3 || point.isDefault) {
650 // not using need because we want a custom error
651 throw new Error('Internal Error: Missing Wallet/Details');
654 const proxy = point.getManagerProxy();
656 if (proxy === undefined)
657 throw new Error("Error: Address doesn't match proxy");
659 const nonce = await api.getNonce({
664 const nextRevision = point.keyRevisionNumber + 1;
665 const networkSeed = customNetworkSeed
667 : await attemptNetworkSeedDerivation({
674 revision: nextRevision,
676 const txHash = await submitL2Transaction({
683 type: 'configureKeys',
690 return api.getPendingTx(txHash);
692 [// eslint-disable-line
706 const performL2Reticket = async ({
712 }: ReticketParams) => {
713 const azimuthPoint = await api.getPoint(point);
715 const _web3 = web3.getOrElse(null);
716 let nonce = await api.getNonce({ ship: point, proxy });
720 // 1. Update networking keys
721 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
722 const nextRevision = networkRevision + 1;
723 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
727 const configureKeysRequest = await submitL2Transaction({
735 type: 'configureKeys',
740 requests.push(configureKeysRequest);
742 // 2. Set Management Proxy
743 const registerMgmtRequest = await registerProxyAddress(
756 requests.push(registerMgmtRequest);
758 // 3. Set Spawn Proxy
759 if (!isPlanet(Number(point))) {
760 const registerSpawnRequest = await registerProxyAddress(
767 toWallet?.spawn?.keys?.address || to,
773 requests.push(registerSpawnRequest);
776 const transferTxRequest = await submitL2Transaction({
781 type: 'transferPoint',
788 requests.push(transferTxRequest);
790 const hashes = await Promise.all(requests);
794 const setProxyAddress = useCallback(
795 async (proxyType: Proxy, address: EthAddress) => {
796 if (quotaReached()) {
800 const _wallet = wallet.getOrElse(null);
801 const _web3 = web3.getOrElse(null);
804 // not using need because we want a custom error
805 throw new Error('Internal Error: Missing Wallet');
808 if (point.isDefault) {
809 // not using need because we want a custom error
810 throw new Error('Internal Error: Invalid point');
813 const proxy = point.getAddressProxy(proxyType);
815 if (proxy === undefined)
816 throw new Error("Error: Address doesn't match expected proxy");
818 const nonce = await api.getNonce({ ship: point.value, proxy });
820 const txHash = await registerProxyAddress(
833 return api.getPendingTx(txHash);
835 [api, connector, point, wallet, web3, walletType, quotaReached]
838 const changeSponsorship = useCallback(
841 type: 'adopt' | 'detach' | 'reject',
842 skipQuotaCheck = false
844 if (!skipQuotaCheck && quotaReached()) {
848 const _wallet = wallet.getOrElse(null);
849 const _web3 = web3.getOrElse(null);
850 const ship = point.value;
852 const proxy = point.getManagerProxy();
854 if (proxy === undefined)
855 throw new Error("Error: Address doesn't match expected proxy");
857 const nonce = await api.getNonce({ ship, proxy });
858 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
859 const txHash = await submitL2Transaction({
866 address: _wallet.address,
873 return api.getPendingTx(txHash);
875 [api, point, wallet, walletType, web3, connector, quotaReached]
878 const transferPoint = useCallback(
879 async (address: EthAddress, reset?: boolean) => {
880 if (quotaReached()) {
884 const _point = need.point(pointCursor);
885 const _wallet = wallet.getOrElse(null);
886 const _web3 = web3.getOrElse(null);
889 // not using need because we want a custom error
890 throw new Error('Internal Error: Missing Wallet');
893 const proxy = point.getTransferProxy();
895 if (proxy === undefined)
896 throw new Error("Error: Address doesn't match proxy");
898 const nonce = await api.getNonce({ ship: _point, proxy });
900 const txHash = await submitL2Transaction({
907 type: 'transferPoint',
914 return api.getPendingTx(txHash);
916 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
919 const sendL2Transaction = useCallback(
920 async (params: SendL2Params) => {
921 if (quotaReached()) {
925 const _wallet = wallet.getOrElse(null);
926 const _web3 = web3.getOrElse(null);
928 if (!_wallet || point.isDefault) {
929 // not using need because we want a custom error
930 throw new Error('Internal Error: Missing Wallet or Point Info');
933 return submitL2Transaction({
943 [api, connector, point, wallet, web3, walletType, quotaReached]
946 const changeSponsor = useCallback(
947 async (newSponsor: number) => {
948 if (point.isDefault) {
949 throw new Error('Internal Error: point is default');
952 const curPoint = point.value;
953 const _wallet = wallet.getOrElse(null);
956 // not using need because we want a custom error
957 throw new Error('Internal Error: Missing Wallet');
960 const proxy = point.getManagerProxy();
962 if (proxy === undefined)
963 throw new Error("Error: Address doesn't match proxy");
965 const nonce = await api.getNonce({ ship: curPoint, proxy });
967 return sendL2Transaction({
975 [api, point, sendL2Transaction, wallet]
978 const cancelEscape = useCallback(async () => {
979 if (point.isDefault) {
980 throw new Error('Internal Error: point is default');
983 const curPoint = point.value;
984 const _wallet = wallet.getOrElse(null);
987 // not using need because we want a custom error
988 throw new Error('Internal Error: Missing Wallet');
991 const proxy = point.getManagerProxy();
993 if (proxy === undefined)
994 throw new Error("Error: Address doesn't match proxy");
996 const nonce = await api.getNonce({ ship: curPoint, proxy });
998 return sendL2Transaction({
1001 type: 'cancelEscape',
1004 }, [api, point, sendL2Transaction, wallet]);
1006 const getPointsDetails = useCallback(
1008 ownedPoints: number[],
1009 incomingPoints: number[],
1010 managingPoints: number[],
1011 votingPoints: number[],
1012 spawningPoints: number[]
1016 await Promise.all(ownedPoints.map(initPoint)),
1017 await Promise.all(incomingPoints.map(initPoint)),
1018 await Promise.all(managingPoints.map(initPoint)),
1019 await Promise.all(votingPoints.map(initPoint)),
1020 await Promise.all(spawningPoints.map(initPoint)),
1024 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
1037 [setPoints, initPoint]
1040 // On load, get initial config
1047 }, [config, fetchConfig]);
1050 const time = ONE_SECOND;
1052 const interval = setInterval(() => {
1053 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1054 setNextRoll(nextRoll);
1056 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1057 api.getRollerConfig().then(response => {
1058 setNextBatchTime(response.nextBatch);
1061 getPendingTransactions();
1064 if (!point.isDefault) {
1065 getInvites(); // This will also get pending txns
1067 }, TEN_SECONDS); // Should this be more like a minute?
1071 return () => clearInterval(interval);
1072 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1081 configureNetworkKeys,
1082 generateInviteCodes,
1087 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