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';
34import { POINT_DOMINIONS, 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';
54import { TRANSACTION_PROGRESS } from './reticket';
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();
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);
268 const check = async () => {
269 const updatedPoint = await initPoint(point);
270 const changedField = !points[point]
272 : updatedPoint.getChangedField(points[point], field);
275 console.log(`CHECKING FOR ${changedField} UPDATES:`, updatedPoint);
278 if (!updatedPoint.isPlaceholder && changedField) {
279 updatePoint(updatedPoint);
281 clearInterval(interval);
284 if (l1Txn) deletePendingL1Txn(l1Txn);
287 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
293 interval = setInterval(check, intervalTime);
296 [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
299 const spawnPoint = useCallback(
300 async (pointToSpawn: number, destinationAddress: string) => {
301 if (quotaReached()) {
305 const _point = need.point(pointCursor);
306 const _contracts = contracts.getOrElse(null);
307 const _web3 = web3.getOrElse(null);
308 const _wallet = wallet.getOrElse(null);
309 const _authToken = authToken.getOrElse(null);
311 if (!_contracts || !_web3 || !_wallet || !_authToken) {
312 // not using need because we want a custom error
313 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
316 const proxy = point.getSpawnProxy();
318 if (proxy === undefined)
319 throw new Error("Error: Address doesn't match proxy");
321 const nonce = await api.getNonce({
326 await submitL2Transaction({
328 address: destinationAddress,
354 const generateInviteInfo = async (planet: number, _authToken: string) => {
355 const { ticket, inviteWallet } = await generateInviteWallet(
360 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
363 const getInvites = useCallback(async () => {
364 if (invitesLoading) {
369 setInvitesLoading(true);
370 const curPoint: number = Number(need.point(pointCursor));
371 const _authToken = authToken.getOrElse(null);
372 const _contracts = contracts.getOrElse(null);
374 if (_authToken && _contracts) {
375 const spawnedPoints = point.isL2Spawn
376 ? await api.getSpawned(curPoint)
379 const newPending = await api.getPendingByShip(curPoint);
380 setPendingTransactions(newPending);
381 const pendingSpawns = getPendingSpawns(newPending);
383 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
388 const outgoingPoints = pointList
390 ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
392 .map(({ value }) => value);
394 const invitePlanets = spawnedPoints
396 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
398 .filter((p: number) => !pendingSpawns.has(p))
403 invitePlanets.map(p => inviteTemplate(p))
406 const newInvites: Invite[] = [];
407 // Iterate over all of the stored invites, generating wallet info as necessary
408 const storedInvites = getStoredInvites(ls);
410 for (let i = 0; i < invitePlanets.length; i++) {
411 setInviteGeneratingNum(i + 1);
412 const planet = invitePlanets[i];
413 const storedInvite = storedInvites[planet];
415 storedInvite || (await generateInviteInfo(planet, _authToken));
417 // TODO: check if the invite point's owner still matches the deterministic wallet address
418 const planetInfo = await api.getPoint(planet);
419 setStoredInvite(ls, invite);
422 invite.owner.toLowerCase() ===
423 planetInfo.ownership?.owner?.address &&
424 planetInfo.dominion === POINT_DOMINIONS.L2
426 newInvites.push(invite);
427 updateInvite(curPoint, invite);
429 removeInvite(curPoint, invite.planet);
433 setStoredInvite(ls, invite);
436 setInvites(curPoint, newInvites);
437 setInvitesLoading(false);
441 console.warn('ERROR GETTING INVITES', error);
454 setInviteGeneratingNum,
455 setPendingTransactions,
459 const generateInviteCodes = useCallback(
460 async (numInvites: number) => {
461 const _point = need.point(pointCursor);
462 const _contracts = contracts.getOrElse(null);
463 const _web3 = web3.getOrElse(null);
464 const _wallet = wallet.getOrElse(null);
465 const _authToken = authToken.getOrElse(null);
466 const _details = getDetails(_point);
468 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
469 // not using need because we want a custom error
470 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
473 // TODO: remove when UI disables management proxy from spawning invites
474 // if (Just.hasInstance(authMnemonic)) {
476 // "Auth key Error: A management mnemonic can't create invite codes"
479 let planets: UnspawnedPoints = await api.getUnspawned(_point);
481 const pendingTxs = await api.getPendingByShip(_point);
483 const pendingSpawns = getPendingSpawns(pendingTxs);
485 planets = planets.filter((point: number) => !pendingSpawns.has(point));
487 const invites: Invite[] = [];
489 const proxy = point.getSpawnProxy();
491 if (proxy === undefined)
492 throw new Error("Error: Address doesn't match proxy");
494 const nonce = await api.getNonce({
499 for (let i = 0; i < numInvites && planets[i]; i++) {
500 setInviteGeneratingNum(i + 1);
501 const planet = planets[i];
502 const nonceInc = i + nonce;
504 const { ticket, inviteWallet } = await generateInviteWallet(
509 await submitL2Transaction({
511 address: _wallet.address,
516 pointToSpawn: planet,
523 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
527 await submitL2Transaction({
534 type: 'configureKeys',
540 await registerProxyAddress(
547 inviteWallet.management.keys.address,
553 await submitL2Transaction({
558 type: 'transferPoint',
560 address: inviteWallet.ownership.keys.address,
570 owner: inviteWallet.ownership.keys.address,
574 setStoredInvites(ls, invites);
577 `Your invite${invites.length > 1 ? 's have' : ' has'} been generated!`
593 setInviteGeneratingNum,
597 const getPoints = useCallback(
598 async (proxy: Proxy, address: EthAddress) => {
599 const points: Ship[] =
601 ? await api.getOwnedPoints(address)
603 ? await api.getManagerFor(address)
605 ? await api.getVotingFor(address)
606 : proxy === 'transfer'
607 ? await api.getTransferringFor(address)
609 ? await api.getSpawningFor(address)
617 const getPendingTransactions = useCallback(async () => {
619 if (!point.isDefault) {
620 const newPending = await api.getPendingByShip(point.value);
621 setPendingTransactions(newPending);
624 console.warn('ERROR GETTING PENDING', error);
626 }, [api, setPendingTransactions, point]);
628 interface ConfigureKeysParams {
630 customNetworkSeed?: string;
633 const configureNetworkKeys = useCallback(
634 async ({ breach, customNetworkSeed }: ConfigureKeysParams) => {
635 if (quotaReached()) {
638 const _wallet = wallet.getOrElse(null);
639 const _web3 = web3.getOrElse(null);
640 if (!_wallet || !_web3 || point.isDefault) {
641 // not using need because we want a custom error
642 throw new Error('Internal Error: Missing Wallet/Details');
645 const proxy = point.getManagerProxy();
647 if (proxy === undefined)
648 throw new Error("Error: Address doesn't match proxy");
650 const nonce = await api.getNonce({
655 const nextRevision = point.keyRevisionNumber + 1;
656 const networkSeed = customNetworkSeed
658 : await attemptNetworkSeedDerivation({
665 revision: nextRevision,
667 const txHash = await submitL2Transaction({
674 type: 'configureKeys',
681 return api.getPendingTx(txHash);
684 // eslint-disable-line
698 const performL2Reticket = async ({
705 }: ReticketParams) => {
706 const azimuthPoint = await api.getPoint(point);
708 const _web3 = web3.getOrElse(null);
709 let nonce = await api.getNonce({ ship: point, proxy });
710 const progress = onUpdate
711 ? (state: number) => onUpdate({ type: 'progress', state })
716 // 1. Update networking keys
717 progress(TRANSACTION_PROGRESS.GENERATING);
718 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
719 const nextRevision = networkRevision + 1;
720 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
724 const configureKeysRequest = await submitL2Transaction({
732 type: 'configureKeys',
737 requests.push(configureKeysRequest);
739 // 2. Set Management Proxy
740 progress(TRANSACTION_PROGRESS.SIGNING);
741 const registerMgmtRequest = await registerProxyAddress(
754 requests.push(registerMgmtRequest);
756 // 3. Set Spawn Proxy
757 progress(TRANSACTION_PROGRESS.FUNDING);
758 if (!isPlanet(Number(point))) {
759 const registerSpawnRequest = await registerProxyAddress(
766 toWallet?.spawn?.keys?.address || to,
772 requests.push(registerSpawnRequest);
776 progress(TRANSACTION_PROGRESS.TRANSFERRING);
777 const transferTxRequest = await submitL2Transaction({
782 type: 'transferPoint',
789 requests.push(transferTxRequest);
791 progress(TRANSACTION_PROGRESS.CLEANING);
792 const hashes = await Promise.all(requests);
794 progress(TRANSACTION_PROGRESS.DONE);
798 const setProxyAddress = useCallback(
799 async (proxyType: Proxy, address: EthAddress) => {
800 if (quotaReached()) {
804 const _wallet = wallet.getOrElse(null);
805 const _web3 = web3.getOrElse(null);
808 // not using need because we want a custom error
809 throw new Error('Internal Error: Missing Wallet');
812 if (point.isDefault) {
813 // not using need because we want a custom error
814 throw new Error('Internal Error: Invalid point');
817 const proxy = point.getAddressProxy(proxyType);
819 if (proxy === undefined)
820 throw new Error("Error: Address doesn't match expected proxy");
822 const nonce = await api.getNonce({ ship: point.value, proxy });
824 const txHash = await registerProxyAddress(
837 return api.getPendingTx(txHash);
839 [api, connector, point, wallet, web3, walletType, quotaReached]
842 const changeSponsorship = useCallback(
845 type: 'adopt' | 'detach' | 'reject',
846 skipQuotaCheck = false
848 if (!skipQuotaCheck && quotaReached()) {
852 const _wallet = wallet.getOrElse(null);
853 const _web3 = web3.getOrElse(null);
854 const ship = point.value;
856 const proxy = point.getManagerProxy();
858 if (proxy === undefined)
859 throw new Error("Error: Address doesn't match expected proxy");
861 const nonce = await api.getNonce({ ship, proxy });
862 // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
863 const txHash = await submitL2Transaction({
870 address: _wallet.address,
877 return api.getPendingTx(txHash);
879 [api, point, wallet, walletType, web3, connector, quotaReached]
882 const transferPoint = useCallback(
883 async (address: EthAddress, reset?: boolean) => {
884 if (quotaReached()) {
888 const _point = need.point(pointCursor);
889 const _wallet = wallet.getOrElse(null);
890 const _web3 = web3.getOrElse(null);
893 // not using need because we want a custom error
894 throw new Error('Internal Error: Missing Wallet');
897 const proxy = point.getTransferProxy();
899 if (proxy === undefined)
900 throw new Error("Error: Address doesn't match proxy");
902 const nonce = await api.getNonce({ ship: _point, proxy });
904 const txHash = await submitL2Transaction({
911 type: 'transferPoint',
918 return api.getPendingTx(txHash);
920 [api, connector, point, pointCursor, wallet, web3, walletType, quotaReached]
923 const sendL2Transaction = useCallback(
924 async (params: SendL2Params) => {
925 if (quotaReached()) {
929 const _wallet = wallet.getOrElse(null);
930 const _web3 = web3.getOrElse(null);
932 if (!_wallet || point.isDefault) {
933 // not using need because we want a custom error
934 throw new Error('Internal Error: Missing Wallet or Point Info');
937 return submitL2Transaction({
947 [api, connector, point, wallet, web3, walletType, quotaReached]
950 const changeSponsor = useCallback(
951 async (newSponsor: number) => {
952 if (point.isDefault) {
953 throw new Error('Internal Error: point is default');
956 const curPoint = point.value;
957 const _wallet = wallet.getOrElse(null);
960 // not using need because we want a custom error
961 throw new Error('Internal Error: Missing Wallet');
964 const proxy = point.getManagerProxy();
966 if (proxy === undefined)
967 throw new Error("Error: Address doesn't match proxy");
969 const nonce = await api.getNonce({ ship: curPoint, proxy });
971 return sendL2Transaction({
979 [api, point, sendL2Transaction, wallet]
982 const cancelEscape = useCallback(async () => {
983 if (point.isDefault) {
984 throw new Error('Internal Error: point is default');
987 const curPoint = point.value;
988 const _wallet = wallet.getOrElse(null);
991 // not using need because we want a custom error
992 throw new Error('Internal Error: Missing Wallet');
995 const proxy = point.getManagerProxy();
997 if (proxy === undefined)
998 throw new Error("Error: Address doesn't match proxy");
1000 const nonce = await api.getNonce({ ship: curPoint, proxy });
1002 return sendL2Transaction({
1005 type: 'cancelEscape',
1008 }, [api, point, sendL2Transaction, wallet]);
1010 const getPointsDetails = useCallback(
1012 ownedPoints: number[],
1013 incomingPoints: number[],
1014 managingPoints: number[],
1015 votingPoints: number[],
1016 spawningPoints: number[]
1020 await Promise.all(ownedPoints.map(initPoint)),
1021 await Promise.all(incomingPoints.map(initPoint)),
1022 await Promise.all(managingPoints.map(initPoint)),
1023 await Promise.all(votingPoints.map(initPoint)),
1024 await Promise.all(spawningPoints.map(initPoint)),
1028 allPoints.reduce((acc: Points, cur: (Point | undefined)[]) => {
1041 [setPoints, initPoint]
1044 // On load, get initial config
1051 }, [config, fetchConfig]);
1054 const time = ONE_SECOND;
1056 const interval = setInterval(() => {
1057 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1058 setNextRoll(nextRoll);
1060 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1061 api.getRollerConfig().then(response => {
1062 setNextBatchTime(response.nextBatch);
1065 getPendingTransactions();
1068 if (!point.isDefault) {
1069 getInvites(); // This will also get pending txns
1071 }, TEN_SECONDS); // Should this be more like a minute?
1075 return () => clearInterval(interval);
1076 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1085 configureNetworkKeys,
1086 generateInviteCodes,
1091 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