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';
34} from './utils/roller';
35import { ETH_ZERO_ADDR, ROLLER_HOSTS, TEN_SECONDS } from './constants';
45} from '@urbit/roller-api';
47import Point, { PointField, Points } from './types/Point';
48import { useTimerStore } from 'store/timerStore';
49import { ReticketParams, SendL2Params } from './types/L2Transaction';
50import { L1Point } from './types/L1Point';
51import { ddmmmYYYY } from './utils/date';
52import { showNotification } from './utils/notifications';
53import { useWalletConnect } from './useWalletConnect';
54import { PendingL1Txn } from './types/PendingL1Transaction';
56const ONE_SECOND = 1000;
58interface UpdateParams {
64 intervalTime?: number;
67const inviteTemplate = (
79export default function useRoller() {
87 const { connector } = useWalletConnect();
88 const { pointCursor }: any = usePointCursor();
89 const { web3, contracts }: any = useNetwork();
90 const allPoints: any = usePointCache();
91 const getDetails = allPoints?.getDetails;
93 const { setNextRoll } = useTimerStore();
104 setPendingTransactions,
106 setInviteGeneratingNum,
114 } = useRollerStore();
115 const [config, setConfig] = useState<Config | null>(null);
117 const options: Options = useMemo(() => {
118 const type = isMainnet || isRopsten ? 'https' : 'http';
119 const host = isMainnet
120 ? ROLLER_HOSTS.MAINNET
122 ? ROLLER_HOSTS.ROPSTEN
123 : ROLLER_HOSTS.LOCAL;
124 const port = isMainnet || isRopsten ? 443 : 8080;
125 const path = '/v1/roller';
137 const api = useMemo(() => {
138 return new RollerRPCAPI(options);
144 isCompression: false,
145 encryptionSecret: authToken.getOrElse('default'),
150 const getNextQuotaTime = useCallback(
151 (nextSlice: number) => {
156 setNextQuotaTime(next);
157 getNextQuotaTime(next);
159 .catch(err => console.warn(err));
160 }, nextSlice - new Date().getTime());
162 [api, setNextQuotaTime]
165 const fetchConfig = useCallback(async () => {
171 setNextBatchTime(config.nextBatch);
174 // TODO: more elegant error handling
176 '[fetchConfig:failed] check the roller connection?\n',
180 }, [api, getNextQuotaTime, setNextBatchTime]);
182 const quotaReached = useCallback(() => {
183 if (point.l2Quota <= 0) {
185 `You have reached your weekly L2 transaction limit. You will get another
186 ${config?.rollerQuota} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
190 return point.l2Quota <= 0;
191 }, [config, nextQuotaTime, point, setModalText]);
193 const initPoint = useCallback(
194 async (point: string | number) => {
195 const _wallet = wallet.getOrElse(null);
196 const _contracts = contracts.getOrElse(null);
198 if (!_wallet || !_contracts) {
202 const pointNum = Number(point);
204 const rawDetails = await api.getPoint(pointNum);
205 const l2Quota = isL2Spawn(rawDetails?.dominion)
206 ? await api.getRemainingQuota(pointNum)
209 const details = isL2(rawDetails?.dominion)
210 ? toL1Details(rawDetails)
211 : await azimuth.azimuth.getPoint(_contracts, point);
216 address: _wallet.address,
222 // Try getting the details from L1
224 const _contracts = need.contracts(contracts);
225 const details = await azimuth.azimuth.getPoint(_contracts, point);
229 address: _wallet.address,
233 // Just return a placeholder Point
234 const details: L1Point = toL1Details();
238 address: _wallet.address,
244 [api, wallet, contracts]
247 const getAndUpdatePoint = useCallback(
248 async (point: number) => {
249 const updatedPoint = await initPoint(point);
250 updatePoint(updatedPoint);
252 [initPoint, updatePoint]
255 const checkForUpdates = useCallback(
262 intervalTime = TEN_SECONDS,
263 }: UpdateParams) => {
264 if (l1Txn) storePendingL1Txn(l1Txn);
266 const interval = setInterval(async () => {
267 const updatedPoint = await initPoint(point);
268 const changedField = !points[point]
270 : updatedPoint.getChangedField(points[point], field);
273 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
276 if (!updatedPoint.isPlaceholder && changedField) {
277 updatePoint(updatedPoint);
278 clearInterval(interval);
279 if (l1Txn) deletePendingL1Txn(l1Txn);
282 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
288 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
291 const spawnPoint = useCallback(
292 async (pointToSpawn: number, destinationAddress: string) => {
293 if (quotaReached()) {
297 const _point = need.point(pointCursor);
298 const _contracts = contracts.getOrElse(null);
299 const _web3 = web3.getOrElse(null);
300 const _wallet = wallet.getOrElse(null);
301 const _authToken = authToken.getOrElse(null);
303 if (!_contracts || !_web3 || !_wallet || !_authToken) {
304 // not using need because we want a custom error
305 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
308 const proxy = point.getSpawnProxy();
310 if (proxy === undefined)
311 throw new Error("Error: Address doesn't match proxy");
313 const nonce = await api.getNonce({
318 await submitL2Transaction({
320 address: destinationAddress,
346 const generateInviteInfo = async (planet: number, _authToken: string) => {
347 const { ticket, inviteWallet } = await generateInviteWallet(
352 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
355 const getInvites = useCallback(async () => {
357 setInvitesLoading(true);
358 const curPoint: number = Number(need.point(pointCursor));
359 const _authToken = authToken.getOrElse(null);
360 const _contracts = contracts.getOrElse(null);
362 if (_authToken && _contracts) {
363 const spawnedPoints = point.isL2Spawn
364 ? await api.getSpawned(curPoint)
367 const newPending = await api.getPendingByShip(curPoint);
368 setPendingTransactions(newPending);
369 const pendingSpawns = getPendingSpawns(newPending);
371 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
376 const outgoingPoints = pointList
378 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
380 .map(({ value }) => value);
382 const invitePlanets = spawnedPoints
384 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
386 .filter((p: number) => !pendingSpawns.has(p))
391 invitePlanets.map(p => inviteTemplate(p))
394 const newInvites: Invite[] = [];
395 // Iterate over all of the stored invites, generating wallet info as necessary
396 const storedInvites = getStoredInvites(ls);
398 for (let i = 0; i < invitePlanets.length; i++) {
399 setInviteGeneratingNum(i + 1);
400 const planet = invitePlanets[i];
401 const storedInvite = storedInvites[planet];
403 storedInvite || (await generateInviteInfo(planet, _authToken));
405 // TODO: check if the invite point's owner still matches the deterministic wallet address
406 const planetInfo = await api.getPoint(planet);
407 setStoredInvite(ls, invite);
410 invite.owner.toLowerCase() ===
411 planetInfo.ownership?.owner?.address ||
412 planetInfo.ownership?.transferProxy?.address !== ETH_ZERO_ADDR
414 newInvites.push(invite);
415 updateInvite(curPoint, invite);
417 removeInvite(curPoint, invite.planet);
421 setStoredInvite(ls, invite);
424 setInvites(curPoint, newInvites);
425 setInvitesLoading(false);
429 console.warn('ERROR GETTING INVITES', error);
442 setInviteGeneratingNum,
443 setPendingTransactions,
447 const generateInviteCodes = useCallback(
448 async (numInvites: number) => {
449 const _point = need.point(pointCursor);
450 const _contracts = contracts.getOrElse(null);
451 const _web3 = web3.getOrElse(null);
452 const _wallet = wallet.getOrElse(null);
453 const _authToken = authToken.getOrElse(null);
454 const _details = getDetails(_point);
456 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
457 // not using need because we want a custom error
458 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
461 // TODO: remove when UI disables management proxy from spawning invites
462 // if (Just.hasInstance(authMnemonic)) {
464 // "Auth key Error: A management mnemonic can't create invite codes"
467 let planets: UnspawnedPoints = await api.getUnspawned(_point);
469 const pendingTxs = await api.getPendingByShip(_point);
471 const pendingSpawns = getPendingSpawns(pendingTxs);
473 planets = planets.filter((point: number) => !pendingSpawns.has(point));
475 const invites: Invite[] = [];
477 const proxy = point.getSpawnProxy();
479 if (proxy === undefined)
480 throw new Error("Error: Address doesn't match proxy");
482 const nonce = await api.getNonce({
487 for (let i = 0; i < numInvites && planets[i]; i++) {
488 setInviteGeneratingNum(i + 1);
489 const planet = planets[i];
490 const nonceInc = i + nonce;
492 const { ticket, inviteWallet } = await generateInviteWallet(
497 await submitL2Transaction({
499 address: _wallet.address,
504 pointToSpawn: planet,
511 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
515 await submitL2Transaction({
522 type: 'configureKeys',
528 await registerProxyAddress(
535 inviteWallet.management.keys.address,
541 await submitL2Transaction({
546 type: 'transferPoint',
548 address: inviteWallet.ownership.keys.address,
558 owner: inviteWallet.ownership.keys.address,
562 setStoredInvites(ls, invites);
565 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
581 setInviteGeneratingNum,
585 const getPoints = useCallback(
586 async (proxy: Proxy, address: EthAddress) => {
587 const points: Ship[] =
589 ? await api.getOwnedPoints(address)
591 ? await api.getManagerFor(address)
593 ? await api.getVotingFor(address)
594 : proxy === 'transfer'
595 ? await api.getTransferringFor(address)
597 ? await api.getSpawningFor(address)
605 const getPendingTransactions = useCallback(async () => {
607 if (!point.isDefault) {
608 const newPending = await api.getPendingByShip(point.value);
609 setPendingTransactions(newPending);
612 console.warn('ERROR GETTING PENDING', error);
614 }, [api, setPendingTransactions, point]);
616 interface ConfigureKeysParams {
618 customNetworkSeed?: string;
621 const configureNetworkKeys = useCallback(
622 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
623 if (quotaReached()) {
626 const _wallet = wallet.getOrElse(null);
627 const _web3 = web3.getOrElse(null);
628 if (!_wallet || !_web3 || point.isDefault) {
629 // not using need because we want a custom error
630 throw new Error('Internal Error: Missing Wallet/Details');
633 const proxy = point.getManagerProxy();
635 if (proxy === undefined)
636 throw new Error("Error: Address doesn't match proxy");
638 const nonce = await api.getNonce({
643 const nextRevision = point.keyRevisionNumber + 1;
644 const networkSeed = customNetworkSeed
646 : await attemptNetworkSeedDerivation({
653 revision: nextRevision,
655 const txHash = await submitL2Transaction({
662 type: 'configureKeys',
669 return api.getPendingTx(txHash);
671 [// eslint-disable-line
685 const performL2Reticket = async ({
691 }: ReticketParams) => {
692 const azimuthPoint = await api.getPoint(point);
694 const _web3 = web3.getOrElse(null);
695 let nonce = await api.getNonce({ ship: point, proxy });
699 // 1. Update networking keys
700 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
701 const nextRevision = networkRevision + 1;
702 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
706 const configureKeysRequest = await submitL2Transaction({
714 type: 'configureKeys',
719 requests.push(configureKeysRequest);
721 // 2. Set Management Proxy
722 const registerMgmtRequest = await registerProxyAddress(
735 requests.push(registerMgmtRequest);
737 // 3. Set Spawn Proxy
738 if (!isPlanet(Number(point))) {
739 const registerSpawnRequest = await registerProxyAddress(
746 toWallet?.spawn?.keys?.address || to,
752 requests.push(registerSpawnRequest);
755 const transferTxRequest = await submitL2Transaction({
760 type: 'transferPoint',
767 requests.push(transferTxRequest);
769 const hashes = await Promise.all(requests);
773 const setProxyAddress = useCallback(
774 async (proxyType: Proxy, address: EthAddress) => {
775 if (quotaReached()) {
779 const _wallet = wallet.getOrElse(null);
780 const _web3 = web3.getOrElse(null);
783 // not using need because we want a custom error
784 throw new Error('Internal Error: Missing Wallet');
787 if (point.isDefault) {
788 // not using need because we want a custom error
789 throw new Error('Internal Error: Invalid point');
792 const proxy = point.getAddressProxy(proxyType);
794 if (proxy === undefined)
795 throw new Error("Error: Address doesn't match expected proxy");
797 const nonce = await api.getNonce({ ship: point.value, proxy });
799 const txHash = await registerProxyAddress(
812 return api.getPendingTx(txHash);
814 [api, connector, point, wallet, web3, walletType, quotaReached]
817 const changeSponsorship = useCallback(
818 async (sponsee: Ship, type: 'adopt' | 'detach' | 'reject') => {
819 if (quotaReached()) {
823 const _wallet = wallet.getOrElse(null);
824 const _web3 = web3.getOrElse(null);
825 const ship = point.value;
827 const proxy = point.getManagerProxy();
829 if (proxy === undefined)
830 throw new Error("Error: Address doesn't match expected proxy");
832 const nonce = await api.getNonce({ ship, proxy });
833 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
834 const txHash = await submitL2Transaction({
841 address: _wallet.address,
848 return api.getPendingTx(txHash);
850 [api, point, wallet, walletType, web3, connector, quotaReached]
853 const transferPoint = useCallback(
854 async (address: EthAddress, reset?: boolean) => {
855 if (quotaReached()) {
859 const _point = need.point(pointCursor);
860 const _wallet = wallet.getOrElse(null);
861 const _web3 = web3.getOrElse(null);
864 // not using need because we want a custom error
865 throw new Error('Internal Error: Missing Wallet');
868 const proxy = point.getTransferProxy();
870 if (proxy === undefined)
871 throw new Error("Error: Address doesn't match proxy");
873 const nonce = await api.getNonce({ ship: _point, proxy });
875 const txHash = await submitL2Transaction({
882 type: 'transferPoint',
889 return api.getPendingTx(txHash);
891 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
894 const sendL2Transaction = useCallback(
895 async (params: SendL2Params) => {
896 if (quotaReached()) {
900 const _wallet = wallet.getOrElse(null);
901 const _web3 = web3.getOrElse(null);
903 if (!_wallet || point.isDefault) {
904 // not using need because we want a custom error
905 throw new Error('Internal Error: Missing Wallet or Point Info');
908 return submitL2Transaction({
918 [api, connector, point, wallet, web3, walletType, quotaReached]
921 const changeSponsor = useCallback(
922 async (newSponsor: number) => {
923 if (point.isDefault) {
924 throw new Error('Internal Error: point is default');
927 const curPoint = point.value;
928 const _wallet = wallet.getOrElse(null);
931 // not using need because we want a custom error
932 throw new Error('Internal Error: Missing Wallet');
935 const proxy = point.getManagerProxy();
937 if (proxy === undefined)
938 throw new Error("Error: Address doesn't match proxy");
940 const nonce = await api.getNonce({ ship: curPoint, proxy });
942 return sendL2Transaction({
950 [api, point, sendL2Transaction, wallet]
953 const cancelEscape = useCallback(async () => {
954 if (point.isDefault) {
955 throw new Error('Internal Error: point is default');
958 const curPoint = point.value;
959 const _wallet = wallet.getOrElse(null);
962 // not using need because we want a custom error
963 throw new Error('Internal Error: Missing Wallet');
966 const proxy = point.getManagerProxy();
968 if (proxy === undefined)
969 throw new Error("Error: Address doesn't match proxy");
971 const nonce = await api.getNonce({ ship: curPoint, proxy });
973 return sendL2Transaction({
976 type: 'cancelEscape',
979 }, [api, point, sendL2Transaction, wallet]);
981 const getPointsDetails = useCallback(
983 ownedPoints: number[],
984 incomingPoints: number[],
985 managingPoints: number[],
986 votingPoints: number[],
987 spawningPoints: number[]
991 await Promise.all(ownedPoints.map(initPoint)),
992 await Promise.all(incomingPoints.map(initPoint)),
993 await Promise.all(managingPoints.map(initPoint)),
994 await Promise.all(votingPoints.map(initPoint)),
995 await Promise.all(spawningPoints.map(initPoint)),
999 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
1012 [setPoints, initPoint]
1015 // On load, get initial config
1022 }, [config, fetchConfig]);
1025 const time = ONE_SECOND;
1027 const interval = setInterval(() => {
1028 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1029 setNextRoll(nextRoll);
1031 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1032 api.getRollerConfig().then(response => {
1033 setNextBatchTime(response.nextBatch);
1036 getPendingTransactions();
1039 if (!point.isDefault) {
1040 getInvites(); // This will also get pending txns
1042 }, TEN_SECONDS); // Should this be more like a minute?
1046 return () => clearInterval(interval);
1047 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1056 configureNetworkKeys,
1057 generateInviteCodes,
1062 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