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';
53import { PendingL1Txn } from './types/PendingL1Transaction';
55const ONE_SECOND = 1000;
57interface UpdateParams {
65const inviteTemplate = (
77export default function useRoller() {
85 const { connector } = useWalletConnect();
86 const { pointCursor }: any = usePointCursor();
87 const { web3, contracts }: any = useNetwork();
88 const allPoints: any = usePointCache();
89 const getDetails = allPoints?.getDetails;
91 const { setNextRoll } = useTimerStore();
102 setPendingTransactions,
104 setInviteGeneratingNum,
112 } = useRollerStore();
113 const [config, setConfig] = useState<Config | null>(null);
115 const options: Options = useMemo(() => {
116 const type = isRopsten || !isDevelopment ? 'https' : 'http';
117 const host = isRopsten
118 ? ROLLER_HOSTS.ROPSTEN
121 : ROLLER_HOSTS.MAINNET;
122 const port = isDevelopment && !isRopsten ? 8080 : 443;
123 const path = '/v1/roller';
135 const api = useMemo(() => {
136 return new RollerRPCAPI(options);
142 isCompression: false,
143 encryptionSecret: authToken.getOrElse('default'),
148 const getNextQuotaTime = useCallback(
149 (nextSlice: number) => {
154 setNextQuotaTime(next);
155 getNextQuotaTime(next);
157 .catch(err => console.warn(err));
158 }, nextSlice - new Date().getTime());
160 [api, setNextQuotaTime]
163 const fetchConfig = useCallback(async () => {
169 setNextBatchTime(config.nextBatch);
172 // TODO: more elegant error handling
174 '[fetchConfig:failed] check the roller connection?\n',
178 }, [api, getNextQuotaTime, setNextBatchTime]);
180 const quotaReached = useCallback(() => {
181 if (point.l2Quota <= 0) {
183 `You have reached your weekly L2 transaction limit. You will get another
184 ${config?.rollerQuota} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
188 return point.l2Quota <= 0;
189 }, [config, nextQuotaTime, point, setModalText]);
191 const initPoint = useCallback(
192 async (point: string | number) => {
193 const _wallet = wallet.getOrElse(null);
199 const pointNum = Number(point);
201 const rawDetails = await api.getPoint(pointNum);
202 const l2Quota = isL2Spawn(rawDetails?.dominion)
203 ? await api.getRemainingQuota(pointNum)
205 const details = toL1Details(rawDetails);
210 address: _wallet.address,
216 // Try getting the details from L1
218 const _contracts = need.contracts(contracts);
219 const details = await azimuth.azimuth.getPoint(_contracts, point);
223 address: _wallet.address,
227 // Just return a placeholder Point
228 const details: L1Point = toL1Details();
232 address: _wallet.address,
238 [api, wallet, contracts]
241 const checkForUpdates = useCallback(
242 async ({ point, message, notify = true, field, l1Txn }: UpdateParams) => {
243 if (l1Txn) storePendingL1Txn(l1Txn);
245 const interval = setInterval(async () => {
246 const updatedPoint = await initPoint(point);
247 const changedField = !points[point]
249 : updatedPoint.getChangedField(points[point], field);
252 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
255 if (!updatedPoint.isPlaceholder && changedField) {
256 updatePoint(updatedPoint);
257 clearInterval(interval);
258 if (l1Txn) deletePendingL1Txn(l1Txn);
261 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
267 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
270 const spawnPoint = useCallback(
271 async (pointToSpawn: number, destinationAddress: string) => {
272 if (quotaReached()) {
276 const _point = need.point(pointCursor);
277 const _contracts = contracts.getOrElse(null);
278 const _web3 = web3.getOrElse(null);
279 const _wallet = wallet.getOrElse(null);
280 const _authToken = authToken.getOrElse(null);
282 if (!_contracts || !_web3 || !_wallet || !_authToken) {
283 // not using need because we want a custom error
284 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
287 const proxy = point.getSpawnProxy();
289 if (proxy === undefined)
290 throw new Error("Error: Address doesn't match proxy");
292 const nonce = await api.getNonce({
297 await submitL2Transaction({
299 address: destinationAddress,
325 const generateInviteInfo = async (planet: number, _authToken: string) => {
326 const { ticket, inviteWallet } = await generateInviteWallet(
331 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
334 const getInvites = useCallback(async () => {
336 setInvitesLoading(true);
337 const curPoint: number = Number(need.point(pointCursor));
338 const _authToken = authToken.getOrElse(null);
339 const _contracts = contracts.getOrElse(null);
341 if (_authToken && _contracts) {
342 const spawnedPoints = point.isL2Spawn
343 ? await api.getSpawned(curPoint)
346 const newPending = await api.getPendingByShip(curPoint);
347 setPendingTransactions(newPending);
348 const pendingSpawns = getPendingSpawns(newPending);
350 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
355 const outgoingPoints = pointList
357 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
359 .map(({ value }) => value);
361 const invitePlanets = spawnedPoints
363 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
365 .filter((p: number) => !pendingSpawns.has(p))
370 invitePlanets.map(p => inviteTemplate(p))
373 const newInvites: Invite[] = [];
374 // Iterate over all of the stored invites, generating wallet info as necessary
375 const storedInvites = getStoredInvites(ls);
377 for (let i = 0; i < invitePlanets.length; i++) {
378 setInviteGeneratingNum(i + 1);
379 const planet = invitePlanets[i];
380 const storedInvite = storedInvites[planet];
382 storedInvite || (await generateInviteInfo(planet, _authToken));
384 // TODO: check if the invite point's owner still matches the deterministic wallet address
385 const planetInfo = await api.getPoint(planet);
386 setStoredInvite(ls, invite);
389 invite.owner.toLowerCase() ===
390 planetInfo.ownership?.owner?.address ||
391 planetInfo.ownership?.transferProxy?.address !== ETH_ZERO_ADDR
393 newInvites.push(invite);
394 updateInvite(curPoint, invite);
396 removeInvite(curPoint, invite.planet);
400 setStoredInvite(ls, invite);
403 setInvites(curPoint, newInvites);
404 setInvitesLoading(false);
408 console.warn('ERROR GETTING INVITES', error);
421 setInviteGeneratingNum,
422 setPendingTransactions,
426 const generateInviteCodes = useCallback(
427 async (numInvites: number) => {
428 const _point = need.point(pointCursor);
429 const _contracts = contracts.getOrElse(null);
430 const _web3 = web3.getOrElse(null);
431 const _wallet = wallet.getOrElse(null);
432 const _authToken = authToken.getOrElse(null);
433 const _details = getDetails(_point);
435 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
436 // not using need because we want a custom error
437 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
440 // TODO: remove when UI disables management proxy from spawning invites
441 // if (Just.hasInstance(authMnemonic)) {
443 // "Auth key Error: A management mnemonic can't create invite codes"
446 let planets: UnspawnedPoints = await api.getUnspawned(_point);
448 const pendingTxs = await api.getPendingByShip(_point);
450 const pendingSpawns = getPendingSpawns(pendingTxs);
452 planets = planets.filter((point: number) => !pendingSpawns.has(point));
454 const invites: Invite[] = [];
456 const proxy = point.getSpawnProxy();
458 if (proxy === undefined)
459 throw new Error("Error: Address doesn't match proxy");
461 const nonce = await api.getNonce({
466 for (let i = 0; i < numInvites && planets[i]; i++) {
467 setInviteGeneratingNum(i + 1);
468 const planet = planets[i];
469 const nonceInc = i + nonce;
471 const { ticket, inviteWallet } = await generateInviteWallet(
476 await submitL2Transaction({
478 address: _wallet.address,
483 pointToSpawn: planet,
490 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
494 await submitL2Transaction({
501 type: 'configureKeys',
507 await registerProxyAddress(
514 inviteWallet.management.keys.address,
520 await submitL2Transaction({
525 type: 'transferPoint',
527 address: inviteWallet.ownership.keys.address,
537 owner: inviteWallet.ownership.keys.address,
541 setStoredInvites(ls, invites);
544 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
560 setInviteGeneratingNum,
564 const getPoints = useCallback(
565 async (proxy: Proxy, address: EthAddress) => {
566 const points: Ship[] =
568 ? await api.getOwnedPoints(address)
570 ? await api.getManagerFor(address)
572 ? await api.getVotingFor(address)
573 : proxy === 'transfer'
574 ? await api.getTransferringFor(address)
576 ? await api.getSpawningFor(address)
584 const getPendingTransactions = useCallback(async () => {
586 if (!point.isDefault) {
587 const newPending = await api.getPendingByShip(point.value);
588 setPendingTransactions(newPending);
591 console.warn('ERROR GETTING PENDING', error);
593 }, [api, setPendingTransactions, point]);
595 interface ConfigureKeysParams {
597 customNetworkSeed?: string;
600 const configureNetworkKeys = useCallback(
601 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
602 if (quotaReached()) {
605 const _wallet = wallet.getOrElse(null);
606 const _web3 = web3.getOrElse(null);
607 const _details = getDetails(point.value);
608 if (!_wallet || !_web3 || !_details || point.isDefault) {
609 // not using need because we want a custom error
610 throw new Error('Internal Error: Missing Wallet/Details');
613 const proxy = point.getManagerProxy();
615 if (proxy === undefined)
616 throw new Error("Error: Address doesn't match proxy");
618 const nonce = await api.getNonce({
623 const nextRevision = point.keyRevisionNumber + 1;
624 const networkSeed = customNetworkSeed
626 : await attemptNetworkSeedDerivation({
633 revision: nextRevision,
635 const txHash = await submitL2Transaction({
642 type: 'configureKeys',
649 return api.getPendingTx(txHash);
651 [// eslint-disable-line
665 const performL2Reticket = async ({
671 }: ReticketParams) => {
672 const azimuthPoint = await api.getPoint(point);
674 const _web3 = web3.getOrElse(null);
675 let nonce = await api.getNonce({ ship: point, proxy });
679 // 1. Update networking keys
680 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
681 const nextRevision = networkRevision + 1;
682 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
686 const configureKeysRequest = await submitL2Transaction({
694 type: 'configureKeys',
699 requests.push(configureKeysRequest);
701 // 2. Set Management Proxy
702 const registerMgmtRequest = await registerProxyAddress(
715 requests.push(registerMgmtRequest);
717 // 3. Set Spawn Proxy
718 if (!isPlanet(Number(point))) {
719 const registerSpawnRequest = await registerProxyAddress(
726 toWallet.spawn.keys.address,
732 requests.push(registerSpawnRequest);
735 const transferTxRequest = await submitL2Transaction({
740 type: 'transferPoint',
747 requests.push(transferTxRequest);
749 const hashes = await Promise.all(requests);
753 const setProxyAddress = useCallback(
754 async (proxyType: Proxy, address: EthAddress) => {
755 if (quotaReached()) {
759 const _wallet = wallet.getOrElse(null);
760 const _web3 = web3.getOrElse(null);
763 // not using need because we want a custom error
764 throw new Error('Internal Error: Missing Wallet');
767 if (point.isDefault) {
768 // not using need because we want a custom error
769 throw new Error('Internal Error: Invalid point');
772 const proxy = point.getAddressProxy(proxyType);
774 if (proxy === undefined)
775 throw new Error("Error: Address doesn't match expected proxy");
777 const nonce = await api.getNonce({ ship: point.value, proxy });
779 const txHash = await registerProxyAddress(
792 return api.getPendingTx(txHash);
794 [api, connector, point, wallet, web3, walletType, quotaReached]
797 const changeSponsorship = useCallback(
798 async (sponsee: Ship, type: 'adopt' | 'detach' | 'reject') => {
799 if (quotaReached()) {
803 const _wallet = wallet.getOrElse(null);
804 const _web3 = web3.getOrElse(null);
805 const ship = point.value;
807 const proxy = point.getManagerProxy();
809 if (proxy === undefined)
810 throw new Error("Error: Address doesn't match expected proxy");
812 const nonce = await api.getNonce({ ship, proxy });
813 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
814 const txHash = await submitL2Transaction({
821 address: _wallet.address,
828 return api.getPendingTx(txHash);
830 [api, point, wallet, walletType, web3, connector, quotaReached]
833 const transferPoint = useCallback(
834 async (address: EthAddress, reset?: boolean) => {
835 if (quotaReached()) {
839 const _point = need.point(pointCursor);
840 const _wallet = wallet.getOrElse(null);
841 const _web3 = web3.getOrElse(null);
844 // not using need because we want a custom error
845 throw new Error('Internal Error: Missing Wallet');
848 const proxy = point.getTransferProxy();
850 if (proxy === undefined)
851 throw new Error("Error: Address doesn't match proxy");
853 const nonce = await api.getNonce({ ship: _point, proxy });
855 const txHash = await submitL2Transaction({
862 type: 'transferPoint',
869 return api.getPendingTx(txHash);
871 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
874 const sendL2Transaction = useCallback(
875 async (params: SendL2Params) => {
876 if (quotaReached()) {
880 const _wallet = wallet.getOrElse(null);
881 const _web3 = web3.getOrElse(null);
883 if (!_wallet || point.isDefault) {
884 // not using need because we want a custom error
885 throw new Error('Internal Error: Missing Wallet or Point Info');
888 return submitL2Transaction({
898 [api, connector, point, wallet, web3, walletType, quotaReached]
901 const changeSponsor = useCallback(
902 async (newSponsor: number) => {
903 if (point.isDefault) {
904 throw new Error('Internal Error: point is default');
907 const curPoint = point.value;
908 const _wallet = wallet.getOrElse(null);
911 // not using need because we want a custom error
912 throw new Error('Internal Error: Missing Wallet');
915 const proxy = point.getManagerProxy();
917 if (proxy === undefined)
918 throw new Error("Error: Address doesn't match proxy");
920 const nonce = await api.getNonce({ ship: curPoint, proxy });
922 return sendL2Transaction({
930 [api, point, sendL2Transaction, wallet]
933 const cancelEscape = useCallback(async () => {
934 if (point.isDefault) {
935 throw new Error('Internal Error: point is default');
938 const curPoint = point.value;
939 const _wallet = wallet.getOrElse(null);
942 // not using need because we want a custom error
943 throw new Error('Internal Error: Missing Wallet');
946 const proxy = point.getManagerProxy();
948 if (proxy === undefined)
949 throw new Error("Error: Address doesn't match proxy");
951 const nonce = await api.getNonce({ ship: curPoint, proxy });
953 return sendL2Transaction({
956 type: 'cancelEscape',
959 }, [api, point, sendL2Transaction, wallet]);
961 const getPointsDetails = useCallback(
963 ownedPoints: number[],
964 incomingPoints: number[],
965 managingPoints: number[],
966 votingPoints: number[],
967 spawningPoints: number[]
971 await Promise.all(ownedPoints.map(initPoint)),
972 await Promise.all(incomingPoints.map(initPoint)),
973 await Promise.all(managingPoints.map(initPoint)),
974 await Promise.all(votingPoints.map(initPoint)),
975 await Promise.all(spawningPoints.map(initPoint)),
979 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
992 [setPoints, initPoint]
995 // On load, get initial config
1002 }, [config, fetchConfig]);
1005 const time = ONE_SECOND;
1007 const interval = setInterval(() => {
1008 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1009 setNextRoll(nextRoll);
1011 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1012 api.getRollerConfig().then(response => {
1013 setNextBatchTime(response.nextBatch);
1016 getPendingTransactions();
1019 if (!point.isDefault) {
1020 getInvites(); // This will also get pending txns
1022 }, TEN_SECONDS); // Should this be more like a minute?
1026 return () => clearInterval(interval);
1027 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1036 configureNetworkKeys,
1037 generateInviteCodes,
1041 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