diff --git a/package.json b/package.json index b3f80624..55cb7d2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-dex", - "version": "1.17.45", + "version": "1.18.0", "description": "The Itheum Data DEX enables you to trade your data using web3 tech", "dependencies": { "@chakra-ui/icons": "2.1.1", @@ -8,7 +8,7 @@ "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", "@hookform/resolvers": "3.6.0", - "@itheum/sdk-mx-data-nft": "3.8.0-alpha.2", + "@itheum/sdk-mx-data-nft": "^3.8.0-alpha.12", "@itheum/sdk-mx-enterprise": "0.3.0", "@multiversx/sdk-core": "13.9.0", "@multiversx/sdk-dapp": "2.33.3", diff --git a/src/components/ListDataNFTModal.tsx b/src/components/ListDataNFTModal.tsx index f250da59..2d1f54ef 100644 --- a/src/components/ListDataNFTModal.tsx +++ b/src/components/ListDataNFTModal.tsx @@ -22,7 +22,7 @@ import axios from "axios"; import BigNumber from "bignumber.js"; import DataNFTLiveUptime from "components/UtilComps/DataNFTLiveUptime"; import { contractsForChain } from "libs/config"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { DataNftMarketContract } from "libs/MultiversX/dataNftMarket"; import { sleep, printPrice, convertToLocalString, getTokenWantedRepresentation, backendApi, getApiDataMarshal, convertWeiToEsdt } from "libs/utils"; import { useMarketStore } from "store"; @@ -106,7 +106,7 @@ export default function ListDataNFTModal({ isOpen, onClose, sellerFee, nftData, maxQuantityPerAddress = maxPerAddress ) { const indexResponse = await axios.get( - `https://${getApi(chainID)}/accounts/${contractsForChain(chainID).market}/transactions?hashes=${txHash}&status=success&withScResults=true&withLogs=true` + `https://${getMvxRpcApi(chainID)}/accounts/${contractsForChain(chainID).market}/transactions?hashes=${txHash}&status=success&withScResults=true&withLogs=true` ); const results = indexResponse.data[0].results; diff --git a/src/components/MyListedDataNFT.tsx b/src/components/MyListedDataNFT.tsx index d9661de4..e3b30f1e 100644 --- a/src/components/MyListedDataNFT.tsx +++ b/src/components/MyListedDataNFT.tsx @@ -24,7 +24,7 @@ import BigNumber from "bignumber.js"; import moment from "moment/moment"; import ShortAddress from "components/UtilComps/ShortAddress"; import { CHAIN_TX_VIEWER, uxConfig } from "libs/config"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { convertWeiToEsdt, convertToLocalString, getTokenWantedRepresentation, hexZero, tokenDecimals } from "libs/utils"; import { useMarketStore, useMintStore } from "store"; import FrozenOverlay from "./FrozenOverlay"; @@ -81,7 +81,7 @@ const MyListedDataNFT: FC = (props) => { diff --git a/src/components/Sections/AppFooter.tsx b/src/components/Sections/AppFooter.tsx index f747b937..86bd91f7 100644 --- a/src/components/Sections/AppFooter.tsx +++ b/src/components/Sections/AppFooter.tsx @@ -3,7 +3,7 @@ import { ExternalLinkIcon } from "@chakra-ui/icons"; import { Box, Text, Flex, HStack, Link, useColorMode } from "@chakra-ui/react"; import { ApiNetworkProvider } from "@multiversx/sdk-core/out"; import { useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks"; -import { getApi, getNetworkProvider, getNetworkProviderCodification } from "libs/MultiversX/api"; +import { getMvxRpcApi, getNetworkProvider, getNetworkProviderCodification } from "libs/MultiversX/api"; import { getSentryProfile } from "libs/utils"; const dataDexVersion = import.meta.env.VITE_APP_VERSION ?? "version number unknown"; @@ -12,7 +12,7 @@ const nonProdEnv = `env:${getSentryProfile()}`; export default function () { const { colorMode } = useColorMode(); const { chainID } = useGetNetworkConfig(); - const isPublicApi = getApi(chainID).includes("api.multiversx.com"); + const isPublicApi = getMvxRpcApi(chainID).includes("api.multiversx.com"); const isPublicNetworkProvider = getNetworkProviderCodification(chainID).includes(".multiversx.com"); const isApiNetworkProvider = getNetworkProvider(chainID) instanceof ApiNetworkProvider; diff --git a/src/components/Sections/NoDataHere.tsx b/src/components/Sections/NoDataHere.tsx index 1acff8a4..bc0fd578 100644 --- a/src/components/Sections/NoDataHere.tsx +++ b/src/components/Sections/NoDataHere.tsx @@ -2,10 +2,10 @@ import React from "react"; import { Text, Flex, Icon } from "@chakra-ui/react"; import { FaEyeSlash } from "react-icons/fa"; -export const NoDataHere = ({ imgFromTop = "10rem" }: { imgFromTop?: string }) => { +export const NoDataHere = ({ imgFromTop = "10rem", customMsg = "Nothing here yet..." }: { imgFromTop?: string; customMsg?: string }) => { return ( - Nothing here yet... + {customMsg} ); diff --git a/src/components/Sections/RecentDataNFTs.tsx b/src/components/Sections/RecentDataNFTs.tsx index cf1a62f9..7d2d1cde 100644 --- a/src/components/Sections/RecentDataNFTs.tsx +++ b/src/components/Sections/RecentDataNFTs.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Card, CardBody, Heading, Image, Link, SimpleGrid, Skeleton, Stack, Text } from "@chakra-ui/react"; +import { Card, CardBody, Heading, Link, SimpleGrid, Skeleton, Stack, Text } from "@chakra-ui/react"; import { DataNft, Offer, createTokenIdentifier } from "@itheum/sdk-mx-data-nft/out"; import { Address } from "@multiversx/sdk-core/out"; import { useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks"; @@ -7,10 +7,9 @@ import { useGetLoginInfo } from "@multiversx/sdk-dapp/hooks/account"; import { Link as ReactRouterLink } from "react-router-dom"; import NftMediaComponent from "components/NftMediaComponent"; import { IS_DEVNET, getFavoritesFromBackendApi, getHealthCheckFromBackendApi, getRecentOffersFromBackendApi } from "libs/MultiversX"; -import { getApi, getNftsByIds } from "libs/MultiversX/api"; -import { DataNftMarketContract } from "libs/MultiversX/dataNftMarket"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { RecentDataNFTType } from "libs/types"; -import { convertWeiToEsdt, hexZero, sleep } from "libs/utils"; +import { convertWeiToEsdt } from "libs/utils"; import { useAccountStore, useMarketStore } from "store"; import { NoDataHere } from "./NoDataHere"; import { Favourite } from "../Favourite/Favourite"; @@ -46,7 +45,7 @@ const RecentDataNFTs = ({ headingText, headingSize }: { headingText: string; hea const marketRequirements = useMarketStore((state) => state.marketRequirements); const favoriteNfts = useAccountStore((state) => state.favoriteNfts); const updateFavoriteNfts = useAccountStore((state) => state.updateFavoriteNfts); - const marketContract = new DataNftMarketContract(chainID); + const [web2ApiDown, setWeb2ApiDown] = useState(false); useEffect(() => { apiWrapper(); @@ -69,7 +68,7 @@ const RecentDataNFTs = ({ headingText, headingSize }: { headingText: string; hea }; const apiWrapper = async () => { - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); try { const isApiUp = await getHealthCheckFromBackendApi(chainID); @@ -77,8 +76,9 @@ const RecentDataNFTs = ({ headingText, headingSize }: { headingText: string; hea if (isApiUp) { const offers = await getRecentOffersFromBackendApi(chainID); const recentNonces = offers.map((nft: any) => ({ nonce: nft.offeredTokenNonce })); - const dataNfts: DataNft[] = await DataNft.createManyFromApi(recentNonces); + console.log("Debug ABOUT TO HIT RecentDataNFTs:createManyFromApi"); + const dataNfts: DataNft[] = await DataNft.createManyFromApi(recentNonces); const _latestOffers: RecentDataNFTType[] = []; offers.forEach((offer: Offer) => { @@ -105,54 +105,17 @@ const RecentDataNFTs = ({ headingText, headingSize }: { headingText: string; hea }); } }); + setLatestOffers(_latestOffers); setLoadedOffers(true); } else { throw new Error("API is down"); } } catch (error) { - const highestOfferIndex = await marketContract.getLastValidOfferId(); - - // get latest 10 offers from the SC - const startIndex = Math.max(highestOfferIndex - 40, 0); - const stopIndex = highestOfferIndex; - - const offers = await marketContract.viewOffers(startIndex, stopIndex); - const slicedOffers = offers.slice(0, 10); - // get these offers metadata from the API - const nftIds = slicedOffers.map((offer) => `${offer.offeredTokenIdentifier}-${hexZero(offer.offeredTokenNonce)}`); - const dataNfts = await getNftsByIds(nftIds, chainID); - - // merge the offer data and meta data - const _latestOffers: RecentDataNFTType[] = []; - - slicedOffers.forEach((offer, idx) => { - const _nft = dataNfts.find((nft) => createTokenIdentifier(nft.collection, nft.nonce) === nft.identifier); - - if (_nft !== undefined) { - const _nftMetaData = DataNft.decodeAttributes(_nft.attributes); - - _latestOffers.push({ - creator: new Address(_nftMetaData.creator ?? ""), - owner: new Address(offer.owner), - offeredTokenIdentifier: offer.offeredTokenIdentifier, - offeredTokenNonce: offer.offeredTokenNonce, - offeredTokenAmount: offer.offeredTokenAmount, - index: idx, - wantedTokenIdentifier: offer.wantedTokenIdentifier, - wantedTokenNonce: offer.wantedTokenNonce, - wantedTokenAmount: offer.wantedTokenAmount, - quantity: offer.quantity, - tokenName: _nftMetaData.tokenName, - title: _nftMetaData.title, - nftImgUrl: "https://" + getApi(chainID) + "/nfts/" + _nft.identifier + "/thumbnail", - royalties: _nftMetaData.royalties, - media: _nftMetaData.media, - }); - } - }); - await sleep(1); - setLatestOffers(_latestOffers); + console.log("Web2 API is down so gracefully handle it"); + console.error(error); + setWeb2ApiDown(true); + setLatestOffers([]); setLoadedOffers(true); } }; @@ -169,7 +132,9 @@ const RecentDataNFTs = ({ headingText, headingSize }: { headingText: string; hea {headingText} - {loadedOffers && latestOffers.length === 0 && } + {loadedOffers && latestOffers.length === 0 && ( + + )} {latestOffers.map((item: RecentDataNFTType, idx: number) => { diff --git a/src/components/Tables/InteractionTxTable.tsx b/src/components/Tables/InteractionTxTable.tsx index ab3acfd0..9a795ae9 100644 --- a/src/components/Tables/InteractionTxTable.tsx +++ b/src/components/Tables/InteractionTxTable.tsx @@ -9,7 +9,7 @@ import { ColumnDef } from "@tanstack/react-table"; import axios from "axios"; import ShortAddress from "components/UtilComps/ShortAddress"; import { CHAIN_TX_VIEWER, contractsForChain, uxConfig } from "libs/config"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { convertWeiToEsdt } from "libs/utils"; import { useMarketStore } from "store"; import { DataTable } from "./Components/DataTable"; @@ -132,7 +132,7 @@ export const getInteractionTransactions = async ( chainID: string, marketRequirements: MarketplaceRequirements ) => { - const api = getApi(chainID); + const api = getMvxRpcApi(chainID); try { const minterTxs = `https://${api}/accounts/${address}/transactions?size=50&status=success&senderOrReceiver=${minterSmartContractAddress}&withOperations=true`; const marketTxs = `https://${api}/accounts/${address}/transactions?size=50&status=success&senderOrReceiver=${marketSmartContractAddress}&withOperations=true`; diff --git a/src/components/Tables/TokenTxTable.tsx b/src/components/Tables/TokenTxTable.tsx index 4d9fb42d..ee02e164 100644 --- a/src/components/Tables/TokenTxTable.tsx +++ b/src/components/Tables/TokenTxTable.tsx @@ -6,7 +6,7 @@ import { ColumnDef } from "@tanstack/react-table"; import axios from "axios"; import ShortAddress from "components/UtilComps/ShortAddress"; import { CHAIN_TX_VIEWER } from "libs/config"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { backendApi } from "libs/utils"; import { useMarketStore } from "store"; import { DataTable } from "./Components/DataTable"; @@ -87,7 +87,7 @@ export default function TokenTxTable(props: TokenTableProps) { useEffect(() => { async function getInteractions() { - const response = await axios.get(`https://${getApi(chainID)}/nfts/${props.tokenId}/transactions?size=50&status=success`); + const response = await axios.get(`https://${getMvxRpcApi(chainID)}/nfts/${props.tokenId}/transactions?size=50&status=success`); const interactions = response.data; const dataTemp: TransactionInTable[] = []; diff --git a/src/components/UtilComps/AppSettings.tsx b/src/components/UtilComps/AppSettings.tsx index f47a68ae..1a9b4247 100644 --- a/src/components/UtilComps/AppSettings.tsx +++ b/src/components/UtilComps/AppSettings.tsx @@ -4,7 +4,7 @@ import { ApiNetworkProvider } from "@multiversx/sdk-core/out"; import { useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks"; import { PREVIEW_DATA_ON_DEVNET_SESSION_KEY } from "libs/config"; import { useLocalStorage } from "libs/hooks"; -import { getApi, getNetworkProvider, getNetworkProviderCodification } from "libs/MultiversX/api"; +import { getMvxRpcApi, getNetworkProvider, getNetworkProviderCodification } from "libs/MultiversX/api"; import { getApiDataDex, getApiDataMarshal, getSentryProfile } from "libs/utils"; const dataDexVersion = import.meta.env.VITE_APP_VERSION ? `v${import.meta.env.VITE_APP_VERSION}` : "version number unknown"; @@ -12,7 +12,7 @@ const nonProdEnv = `${getSentryProfile()}`; export default function () { const { chainID } = useGetNetworkConfig(); - const isPublicApi = getApi(chainID).includes("api.multiversx.com"); + const isPublicApi = getMvxRpcApi(chainID).includes("api.multiversx.com"); const isPublicNetworkProvider = getNetworkProviderCodification(chainID).includes(".multiversx.com"); const isApiNetworkProvider = getNetworkProvider(chainID) instanceof ApiNetworkProvider; const [previewDataOnDevnetSession, setPreviewDataOnDevnetSession] = useLocalStorage(PREVIEW_DATA_ON_DEVNET_SESSION_KEY, null); @@ -81,7 +81,7 @@ export default function () { Dynamic Settings - MultiversX API being used : {getApi(chainID)} + MultiversX API being used : {getMvxRpcApi(chainID)} MultiversX Gateway being used : {getNetworkProviderCodification(chainID)} Web2 Data DEX API : {getApiDataDex(chainID)} Web2 Data Marshal API : {getApiDataMarshal(chainID)} diff --git a/src/components/WalletDataNFTMX/WalletDataNFTMX.tsx b/src/components/WalletDataNFTMX/WalletDataNFTMX.tsx index b5ba116f..d128495f 100644 --- a/src/components/WalletDataNFTMX/WalletDataNFTMX.tsx +++ b/src/components/WalletDataNFTMX/WalletDataNFTMX.tsx @@ -41,7 +41,7 @@ import ShortAddress from "components/UtilComps/ShortAddress"; import { CHAIN_TX_VIEWER, PREVIEW_DATA_ON_DEVNET_SESSION_KEY, contractsForChain, uxConfig } from "libs/config"; import { useLocalStorage } from "libs/hooks"; import { labels } from "libs/language"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { DataNftMarketContract } from "libs/MultiversX/dataNftMarket"; import { backendApi, @@ -59,6 +59,7 @@ import AccessDataStreamModal from "./AccessDatastreamModal"; import BurnDataNFTModal from "./BurnDataNFTModal"; import ListDataNFTModal from "../ListDataNFTModal"; import { isNFMeIDVaultClassDataNFT } from "libs/utils"; +import { IS_DEVNET } from "libs/MultiversX"; export default function WalletDataNFTMX(item: any) { const { chainID, network } = useGetNetworkConfig(); @@ -160,7 +161,7 @@ export default function WalletDataNFTMX(item: any) { // Use a loop with a boolean condition while (!success) { indexResponse = await axios.get( - `https://${getApi(chainID)}/accounts/${contractsForChain(chainID).market}/transactions?hashes=${txHash}&withScResults=true&withLogs=true` + `https://${getMvxRpcApi(chainID)}/accounts/${contractsForChain(chainID).market}/transactions?hashes=${txHash}&withScResults=true&withLogs=true` ); if (indexResponse.data[0].status === "success" && typeof indexResponse.data[0].pendingResults === "undefined") { @@ -238,7 +239,8 @@ export default function WalletDataNFTMX(item: any) { throw Error(labels.ERR_NATIVE_AUTH_TOKEN_MISSING); } - DataNft.setNetworkConfig(network.id); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + const dataNft = await DataNft.createFromApi({ tokenIdentifier, nonce }); const arg = { mvxNativeAuthOrigins: [decodeNativeAuthToken(tokenLogin.nativeAuthToken).origin], diff --git a/src/libs/MultiversX/api.ts b/src/libs/MultiversX/api.ts index 435cfd26..a752e19e 100644 --- a/src/libs/MultiversX/api.ts +++ b/src/libs/MultiversX/api.ts @@ -4,12 +4,66 @@ import { AccountType } from "@multiversx/sdk-dapp/types"; import { NftType, TokenType } from "@multiversx/sdk-dapp/types/tokens.types"; import axios from "axios"; import { IS_DEVNET, contractsForChain, uxConfig } from "libs/config"; +import { getDataFromClientSessionCache, setDataToClientSessionCache } from "libs/utils/util"; + +declare const window: { + ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION: string; + ITH_GLOBAL_MVX_RPC_API_SESSION: string; +} & Window; + +window.ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION = ""; +window.ITH_GLOBAL_MVX_RPC_API_SESSION = ""; + +/* +We already return the public MVX API here, but we store and load it from the window object +so we don't have to keep running the ENV variable logic + +getMvxRpcPublicOnlyApi can be used for non-critical use cases for fetching data in the app +e.g. recent data nft, trending data data, +*/ +export const getMvxRpcPublicOnlyApi = (chainID: string) => { + if (window.ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION !== "") { + console.log("ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION served from session ", window.ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION); + return window.ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION; + } -export const getApi = (chainID: string) => { - const envKey = chainID === "1" ? "VITE_ENV_API_MAINNET_KEY" : "VITE_ENV_API_DEVNET_KEY"; const defaultUrl = chainID === "1" ? "api.multiversx.com" : "devnet-api.multiversx.com"; + window.ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION = defaultUrl; - return import.meta.env[envKey] || defaultUrl; + return window.ITH_GLOBAL_MVX_PUBLIC_RPC_API_SESSION; +}; + +/* +Here we load the private RPC or public RPC based on usage randomization - i. 30% of time load public / 70% of time private +this will help load balance performance and costs + +getMvxRpcApi SHOULD be used for critical use cases for fetching data in the app +e.g. offers, market etc +*/ +export const getMvxRpcApi = (chainID: string) => { + if (window.ITH_GLOBAL_MVX_RPC_API_SESSION !== "") { + console.log("ITH_GLOBAL_MVX_RPC_API_SESSION served from session ", window.ITH_GLOBAL_MVX_RPC_API_SESSION); + return window.ITH_GLOBAL_MVX_RPC_API_SESSION; + } + + const defaultUrl = chainID === "1" ? "api.multiversx.com" : "devnet-api.multiversx.com"; + + // 30% of chance, default to the Public API + const defaultToPublic = Math.random() < 0.3; // math random gives you close to even distribution from 0 - 1 + + if (defaultToPublic) { + window.ITH_GLOBAL_MVX_RPC_API_SESSION = defaultUrl; + + console.log("ITH_GLOBAL_MVX_RPC_API_SESSION defaulted based on chance to public ", window.ITH_GLOBAL_MVX_RPC_API_SESSION); + } else { + // else, we revert to original logic of using ENV variable + const envKey = chainID === "1" ? "VITE_ENV_API_MAINNET_KEY" : "VITE_ENV_API_DEVNET_KEY"; + + window.ITH_GLOBAL_MVX_RPC_API_SESSION = import.meta.env[envKey] || defaultUrl; + console.log("ITH_GLOBAL_MVX_RPC_API_SESSION fetched rom ENV ", window.ITH_GLOBAL_MVX_RPC_API_SESSION); + } + + return window.ITH_GLOBAL_MVX_RPC_API_SESSION; }; export const getNetworkProvider = (chainID: string) => { @@ -19,8 +73,8 @@ export const getNetworkProvider = (chainID: string) => { const envValue = import.meta.env[envKey]; const isApi = envValue && envValue.includes("api"); return isApi - ? new ApiNetworkProvider(envValue, { timeout: uxConfig.mxAPITimeoutMs, clientName: "ithuemDataDex" }) - : new ProxyNetworkProvider(envValue || defaultUrl, { timeout: uxConfig.mxAPITimeoutMs, clientName: "ithuemDataDex" }); + ? new ApiNetworkProvider(envValue, { timeout: uxConfig.mxAPITimeoutMs, clientName: "itheumDataDex" }) + : new ProxyNetworkProvider(envValue || defaultUrl, { timeout: uxConfig.mxAPITimeoutMs, clientName: "itheumDataDex" }); }; export const getNetworkProviderCodification = (chainID: string) => { @@ -38,7 +92,7 @@ export const getExplorer = (chainID: string) => { }; export const getClaimTransactions = async (address: string, chainID: string) => { - const api = getApi(chainID); + const api = getMvxRpcApi(chainID); const claimsContractAddress = contractsForChain(chainID).claims; try { const allTxs = `https://${api}/accounts/${address}/transactions?size=25&receiver=${claimsContractAddress}&function=claim&withOperations=true`; @@ -106,7 +160,8 @@ export const getClaimTransactions = async (address: string, chainID: string) => }; export const getNftsOfACollectionForAnAddress = async (address: string, collectionTickers: string[], chainID: string): Promise => { - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + try { const ownerByAddress = await DataNft.ownedByAddress(address, collectionTickers); return ownerByAddress; @@ -116,18 +171,34 @@ export const getNftsOfACollectionForAnAddress = async (address: string, collecti } }; +/* +Ideally this should be in the Data NFT SDK so it can be client caches, but for now we can keep it here @TODO +*/ export const getNftsByIds = async (nftIds: string[], chainID: string): Promise => { - const api = getApi(chainID); + const api = getMvxRpcApi(chainID); try { - const url = `https://${api}/nfts?withSupply=true&identifiers=${nftIds.join(",")}`; - const { data } = await axios.get(url, { - timeout: uxConfig.mxAPITimeoutMs, - }); + const fetchUrl = `https://${api}/nfts?withSupply=true&identifiers=${nftIds.join(",")}`; + + // check if its in session cache + let jsonDataPayload = null; + const getFromSessionCache = getDataFromClientSessionCache(fetchUrl); + + if (!getFromSessionCache) { + const { data } = await axios.get(fetchUrl, { + timeout: uxConfig.mxAPITimeoutMs, + }); + + jsonDataPayload = data; + + setDataToClientSessionCache(fetchUrl, jsonDataPayload, 5 * 60 * 1000); + } else { + jsonDataPayload = getFromSessionCache; + } // match input and output order const sorted: NftType[] = []; for (const nftId of nftIds) { - for (const nft of data) { + for (const nft of jsonDataPayload) { if (nftId === nft.identifier) { sorted.push(nft); break; @@ -149,7 +220,7 @@ export const getNftsByIds = async (nftIds: string[], chainID: string): Promise => { try { - const api = getApi(chainID); + const api = getMvxRpcApi(chainID); const url = `https://${api}/accounts/${address}/tokens/${tokenId}`; const { data } = await axios.get(url, { timeout: uxConfig.mxAPITimeoutMs, @@ -178,7 +249,7 @@ export const getItheumPriceFromApi = async (): Promise => { export const getAccountDetailFromApi = async (address: string, chainID: string): Promise => { try { - const api = getApi(chainID); + const api = getMvxRpcApi(chainID); const url = `https://${api}/accounts/${address}`; const { data } = await axios.get(url, { timeout: uxConfig.mxAPITimeoutMs, @@ -192,7 +263,7 @@ export const getAccountDetailFromApi = async (address: string, chainID: string): }; export const getTokenDecimalsRequest = async (tokenIdentifier: string | undefined, chainID: string) => { - const tokenIdentifierUrl = `https://${getApi(chainID)}/tokens/${tokenIdentifier}`; + const tokenIdentifierUrl = `https://${getMvxRpcApi(chainID)}/tokens/${tokenIdentifier}`; try { const { data } = await axios.get(tokenIdentifierUrl); if (tokenIdentifier !== undefined) { diff --git a/src/libs/utils/util.ts b/src/libs/utils/util.ts index 77b7a76f..f7fdf497 100644 --- a/src/libs/utils/util.ts +++ b/src/libs/utils/util.ts @@ -427,3 +427,43 @@ export function computeMaxBuyForOfferForAddress( } return mboa; } + +/* +Simple Caching Module helps throttle frequently used calls to RPC that fetch data +this helps speed up the client side app and also reduces calls to the RPC +we allow consumer to set a custom TTL in MS for how long data is stored in cache +*/ +const sessionCache: Record = {}; + +export function getDataFromClientSessionCache(cacheKey: string) { + const cacheObject = sessionCache[cacheKey]; + + if (!cacheObject) { + console.log("getDataFromClientSessionCache: not found"); + return false; + } else { + // did it expire? is so, delete it from the cache + if (Date.now() - cacheObject.addedOn > cacheObject.expireAfter) { + console.log("getDataFromClientSessionCache: expired"); + delete sessionCache[cacheKey]; // remove it from cache as its expired + return false; + } else { + console.log("getDataFromClientSessionCache: available"); + return cacheObject.payload; + } + } +} + +export function setDataToClientSessionCache(cacheKey: string, jsonData: any, ttlInMs?: number) { + const howManyMsToCacheFor = ttlInMs || 120000; // 120000 is 2 min default TTL + + sessionCache[cacheKey] = { + payload: jsonData, + addedOn: Date.now(), + expireAfter: howManyMsToCacheFor, + }; + + console.log("setDataToClientSessionCache: cached for ms ", howManyMsToCacheFor); + + return true; +} diff --git a/src/pages/AdvertiseData/components/TradeForm.tsx b/src/pages/AdvertiseData/components/TradeForm.tsx index 72817f83..dfaac795 100644 --- a/src/pages/AdvertiseData/components/TradeForm.tsx +++ b/src/pages/AdvertiseData/components/TradeForm.tsx @@ -63,7 +63,7 @@ import ChainSupportedInput from "components/UtilComps/ChainSupportedInput"; import { PopoverTooltip } from "components/UtilComps/PopoverTooltip"; import { IS_DEVNET, MENU, PRINT_UI_DEBUG_PANELS } from "libs/config"; import { labels } from "libs/language"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { UserDataType } from "libs/MultiversX/types"; import { getApiDataMarshal, isValidNumericCharacter, sleep, timeUntil } from "libs/utils"; import { useAccountStore, useMintStore } from "store"; @@ -637,7 +637,7 @@ export const TradeForm: React.FC = (props) => { // if we have to auto-vault -- i.e most likely user first nfme id, then we can auto vault it with this TX if ((dataToPrefill?.shouldAutoVault ?? false) && !bondVaultNonce) { const dataNftTokenId = IS_DEVNET ? dataNftTokenIdentifier.devnet : dataNftTokenIdentifier.mainnet; - const nonceToVault = (await axios.get(`https://${getApi(IS_DEVNET ? "D" : "1")}/nfts/count?search=${dataNftTokenId}`)).data; + const nonceToVault = (await axios.get(`https://${getMvxRpcApi(IS_DEVNET ? "D" : "1")}/nfts/count?search=${dataNftTokenId}`)).data; const bondContract = new BondContract(IS_DEVNET ? "devnet" : "mainnet"); const vaultTx = bondContract.setVaultNonce(new Address(mxAddress), nonceToVault, dataNftTokenId); diff --git a/src/pages/App/Launcher.tsx b/src/pages/App/Launcher.tsx index de17900d..0fbf9a48 100644 --- a/src/pages/App/Launcher.tsx +++ b/src/pages/App/Launcher.tsx @@ -1,14 +1,18 @@ import React, { useState } from "react"; +import { useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks"; import { TransactionsToastList, SignTransactionsModals, NotificationModal } from "@multiversx/sdk-dapp/UI"; import { DappProvider } from "@multiversx/sdk-dapp/wrappers"; import { TermsChangedNoticeModal } from "components/TermsChangedNoticeModal"; import { uxConfig } from "libs/config"; import { useLocalStorage } from "libs/hooks"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { walletConnectV2ProjectId, MX_TOAST_LIFETIME_IN_MS } from "libs/mxConstants"; import { clearAppSessionsLaunchMode } from "libs/utils"; import AppMx from "./AppMultiversX"; import ModalAuthPickerMx from "./ModalAuthPickerMultiversX"; + function Launcher() { + const { chainID } = useGetNetworkConfig(); const [launchModeSession, setLaunchModeSession] = useLocalStorage("itm-launch-mode", null); const [launchMode, setLaunchMode] = useState(launchModeSession || "no-auth"); const [redirectToRoute, setRedirectToRoute] = useState(null); @@ -33,9 +37,10 @@ function Launcher() { { + const { chainID } = useGetNetworkConfig(); const { address } = useGetAccountInfo(); const { hasPendingTransactions } = useGetPendingTransactions(); const bondContractAdminDevnet = import.meta.env.VITE_ENV_BONDING_ADMIN_DEVNET; const bondContractAdminMainnet = import.meta.env.VITE_ENV_BONDING_ADMIN_MAINNET; const bondContract = new BondContract(IS_DEVNET ? "devnet" : "mainnet"); - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + const [bondingDataNfts, setBondingDataNfts] = useState>([]); const [compensationDataNfts, setCompensationDataNfts] = useState>([]); const [contractBonds, setContractBonds] = useState([]); diff --git a/src/pages/DataNFT/DataNFTDetails.tsx b/src/pages/DataNFT/DataNFTDetails.tsx index 84b764e9..b489c7cc 100644 --- a/src/pages/DataNFT/DataNFTDetails.tsx +++ b/src/pages/DataNFT/DataNFTDetails.tsx @@ -54,7 +54,7 @@ import ShortAddress from "components/UtilComps/ShortAddress"; import { CHAIN_TX_VIEWER, REPORTED_TO_BE_BAD_DATA_NFTS, uxConfig } from "libs/config"; import { labels } from "libs/language"; import { getFavoritesFromBackendApi, getOffersByIdAndNoncesFromBackendApi, getVolumes } from "libs/MultiversX"; -import { getApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { DataNftMarketContract } from "libs/MultiversX/dataNftMarket"; import { computeMaxBuyForOfferForAddress, @@ -225,7 +225,7 @@ export default function DataNFTDetails(props: DataNFTDetailsProps) { const getAddressTokenInformation = async () => { if (isMxLoggedIn) { - const apiLink = getApi(chainID); + const apiLink = getMvxRpcApi(chainID); const nftApiLink = `https://${apiLink}/accounts/${address}/nfts/${tokenId}`; axios .get(nftApiLink) @@ -243,7 +243,7 @@ export default function DataNFTDetails(props: DataNFTDetailsProps) { }; function getTokenDetails() { - const apiLink = getApi(chainID); + const apiLink = getMvxRpcApi(chainID); const nftApiLink = `https://${apiLink}/nfts/${tokenId}`; axios diff --git a/src/pages/DataNFT/DataNFTMarketplaceMultiversX.tsx b/src/pages/DataNFT/DataNFTMarketplaceMultiversX.tsx index 2f807e37..4bb2627c 100644 --- a/src/pages/DataNFT/DataNFTMarketplaceMultiversX.tsx +++ b/src/pages/DataNFT/DataNFTMarketplaceMultiversX.tsx @@ -49,7 +49,7 @@ import ConditionalRender from "components/UtilComps/ApiWrapper"; import UpperCardComponent from "components/UtilComps/UpperCardComponent"; import useThrottle from "components/UtilComps/UseThrottle"; import { getOfersAsCollectionFromBackendApi, getOffersCountFromBackendApi, getOffersFromBackendApi } from "libs/MultiversX"; -import { getApi, getNetworkProvider, getNftsByIds } from "libs/MultiversX/api"; +import { getMvxRpcApi, getNetworkProvider, getNftsByIds } from "libs/MultiversX/api"; import { DataNftMarketContract } from "libs/MultiversX/dataNftMarket"; import { DataNftMintContract } from "libs/MultiversX/dataNftMint"; import { DataNftCollectionType } from "libs/MultiversX/types"; @@ -489,7 +489,7 @@ export const Marketplace: FC = ({ tabState }) => { = ({ tabState }) => { { const [allInfoLoading, setAllInfoLoading] = useState(true); const [withdrawBondConfirmationWorkflow, setWithdrawBondConfirmationWorkflow] = useState(null); - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + const [bondingOffers, setBondingOffers] = useState>([]); const [dataNftsWithNoBond, setDataNftsWithNoBond] = useState>([]); diff --git a/src/pages/DataNFT/components/FavoriteCards.tsx b/src/pages/DataNFT/components/FavoriteCards.tsx index 19a0f863..81ed75a5 100644 --- a/src/pages/DataNFT/components/FavoriteCards.tsx +++ b/src/pages/DataNFT/components/FavoriteCards.tsx @@ -5,6 +5,7 @@ import { useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks"; import { useGetLoginInfo } from "@multiversx/sdk-dapp/hooks/account"; import { Link as ReactRouterLink } from "react-router-dom"; import NftMediaComponent from "components/NftMediaComponent"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { Favourite } from "../../../components/Favourite/Favourite"; import { NoDataHere } from "../../../components/Sections/NoDataHere"; import { IS_DEVNET, getFavoritesFromBackendApi } from "../../../libs/MultiversX"; @@ -21,12 +22,12 @@ export const FavoriteCards: React.FC = () => { const [loadedOffers, setLoadedOffers] = useState(false); const [favouriteItems, setFavouriteItems] = React.useState>([]); const [dataNfts, setDataNfts] = React.useState>([]); - const skeletonHeight = { base: "160px", md: "190px", "2xl": "220px" }; useEffect(() => { (async () => { - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + if (tokenLogin?.nativeAuthToken) { const bearerToken = tokenLogin.nativeAuthToken; const getFavourites = await getFavoritesFromBackendApi(chainID, bearerToken); diff --git a/src/pages/Enterprise/components/DataNftCollection/ClaimRoyalties.tsx b/src/pages/Enterprise/components/DataNftCollection/ClaimRoyalties.tsx index 69d9a0f3..fea89ffe 100644 --- a/src/pages/Enterprise/components/DataNftCollection/ClaimRoyalties.tsx +++ b/src/pages/Enterprise/components/DataNftCollection/ClaimRoyalties.tsx @@ -10,7 +10,7 @@ import { useParams } from "react-router-dom"; import { IS_DEVNET } from "libs/config"; import { tokenContractAddress_Mx_Devnet, tokenContractAddress_Mx_Mainnet } from "libs/contractAddresses"; import { ImageTooltip } from "../../../../components/UtilComps/ImageTooltip"; -import { getApi } from "../../../../libs/MultiversX/api"; +import { getMvxRpcApi } from "../../../../libs/MultiversX/api"; import { ClaimsContract } from "../../../../libs/MultiversX/claims"; type ClaimRoyaltiesProps = { @@ -31,8 +31,8 @@ export const ClaimRoyalties: React.FC = (props) => { const getAddressToken = async () => { // request for minter tokens and egld balance - const urlForExternalToken = `https://${getApi(chainID)}/accounts/${minterAddress}/tokens?size=10000`; - const egldAccountBalance = `https://${getApi(chainID)}/accounts/${minterAddress}`; + const urlForExternalToken = `https://${getMvxRpcApi(chainID)}/accounts/${minterAddress}/tokens?size=10000`; + const egldAccountBalance = `https://${getMvxRpcApi(chainID)}/accounts/${minterAddress}`; const { data: externalTokenData } = await axios.get(urlForExternalToken); const { data: egldBalance } = await axios.get(egldAccountBalance); // request for claims portal diff --git a/src/pages/Enterprise/components/DataNftCollection/CurateNfts.tsx b/src/pages/Enterprise/components/DataNftCollection/CurateNfts.tsx index 4e429a17..aef1f4e2 100644 --- a/src/pages/Enterprise/components/DataNftCollection/CurateNfts.tsx +++ b/src/pages/Enterprise/components/DataNftCollection/CurateNfts.tsx @@ -11,7 +11,7 @@ import { FaArrowRightLong } from "react-icons/fa6"; import { MdInfo, MdNavigateBefore, MdOutlineNavigateNext } from "react-icons/md"; import { IS_DEVNET } from "libs/config"; import { ImageTooltip } from "../../../../components/UtilComps/ImageTooltip"; -import { getApi, getExplorer } from "../../../../libs/MultiversX/api"; +import { getMvxRpcApi, getExplorer } from "../../../../libs/MultiversX/api"; type CurateNftsProp = { nftMinter: NftMinter; @@ -36,10 +36,11 @@ export const CurateNfts: React.FC = (props) => { const pageCount = Math.ceil(nftCount / paginationSizeNft); - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + const getCreatedDataNftsFromAPI = async () => { setHasRequestLoaded(false); - const apiLink = getApi(chainID); + const apiLink = getMvxRpcApi(chainID); const url = `https://${apiLink}/collections/${tokenIdentifier}/nfts?from=${paginationFromNft}&size=${paginationSizeNft}&withOwner=true&sort=nonce`; const urlForNftCount = `https://${apiLink}/collections/${tokenIdentifier}/nfts/count`; const { data: paginatedNfts } = await axios.get(url); @@ -48,6 +49,7 @@ export const CurateNfts: React.FC = (props) => { setNftCount(nftsCount); setHasRequestLoaded(true); }; + const freezeDataNft = async (creator: string, nonce: number, owner: string | undefined) => { const tx = await nftMinter.freezeSingleNFT(new Address(address), nonce, new Address(owner ?? "")); tx.setGasLimit(100000000); @@ -55,6 +57,7 @@ export const CurateNfts: React.FC = (props) => { transactions: [tx], }); }; + const unFreezeDataNft = async (creator: string, nonce: number, owner: string | undefined) => { const tx = await nftMinter.unFreezeSingleNFT(new Address(address), nonce, new Address(owner ?? "")); tx.setGasLimit(100000000); @@ -62,6 +65,7 @@ export const CurateNfts: React.FC = (props) => { transactions: [tx], }); }; + const wipeDataNft = async (creator: string, nonce: number, owner: string | undefined) => { const tx = await nftMinter.wipeSingleNFT(new Address(address), nonce, new Address(owner ?? "")); tx.setGasLimit(100000000); diff --git a/src/pages/Home/components/TrendingData.tsx b/src/pages/Home/components/TrendingData.tsx index 3290cad8..79814a5e 100644 --- a/src/pages/Home/components/TrendingData.tsx +++ b/src/pages/Home/components/TrendingData.tsx @@ -5,6 +5,7 @@ import { useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks"; import { useGetLoginInfo } from "@multiversx/sdk-dapp/hooks/account"; import { Link as ReactRouterLink } from "react-router-dom"; import NftMediaComponent from "components/NftMediaComponent"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { NftMedia } from "libs/types"; import { Favourite } from "../../../components/Favourite/Favourite"; import { IS_DEVNET, getFavoritesFromBackendApi, getTrendingFromBackendApi } from "../../../libs/MultiversX"; @@ -47,7 +48,8 @@ export const TrendingData: React.FC = () => { useEffect(() => { (async () => { - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); + const getTrendingData = await getTrendingFromBackendApi(chainID); const _trendingData: Array = []; diff --git a/src/pages/Home/components/VolumesDataNfts.tsx b/src/pages/Home/components/VolumesDataNfts.tsx index 6d33f706..9821d104 100644 --- a/src/pages/Home/components/VolumesDataNfts.tsx +++ b/src/pages/Home/components/VolumesDataNfts.tsx @@ -5,6 +5,7 @@ import { useGetLoginInfo, useGetNetworkConfig } from "@multiversx/sdk-dapp/hooks import { Link } from "react-router-dom"; import NftMediaComponent from "components/NftMediaComponent"; import { IS_DEVNET, getTopVolumes } from "libs/MultiversX"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { NftMedia } from "libs/types"; import { convertToLocalString } from "libs/utils"; import { useMarketStore } from "store"; @@ -50,7 +51,7 @@ const VolumesDataNfts: React.FC = () => { useEffect(() => { (async () => { - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); const dataNftsVolumes: DataNftVolume[] = await getTopVolumes(chainID, tokenLogin?.nativeAuthToken ?? "", 10); const _volumesData: { nonce: number; tokenIdentifier: string }[] = []; @@ -62,6 +63,7 @@ const VolumesDataNfts: React.FC = () => { _volumesData.push({ nonce: nonce, tokenIdentifier: tokenIdentifier }); }); + console.log("Debug ABOUT TO HIT VolumesDataNfts:createManyFromApi"); const dataNfts: DataNft[] = await DataNft.createManyFromApi(_volumesData); const _volume = dataNftsVolumes.map((dataNft) => { diff --git a/src/store/StoreProvider.tsx b/src/store/StoreProvider.tsx index 012a5602..8ff0f530 100644 --- a/src/store/StoreProvider.tsx +++ b/src/store/StoreProvider.tsx @@ -14,6 +14,7 @@ import { getMarketRequirements, } from "libs/MultiversX"; import { getAccountTokenFromApi, getItheumPriceFromApi } from "libs/MultiversX/api"; +import { getMvxRpcApi } from "libs/MultiversX/api"; import { DataNftMintContract } from "libs/MultiversX/dataNftMint"; import { computeRemainingCooldown, convertWeiToEsdt, decodeNativeAuthToken, tokenDecimals } from "libs/utils"; import { useAccountStore, useMarketStore, useMintStore } from "store"; @@ -51,7 +52,8 @@ export const StoreProvider = ({ children }: PropsWithChildren) => { const bondingContract = new BondContract(import.meta.env.VITE_ENV_NETWORK); const marketContractSDK = new DataNftMarket(import.meta.env.VITE_ENV_NETWORK); const mintContract = new DataNftMintContract(chainID); - DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet"); + + DataNft.setNetworkConfig(IS_DEVNET ? "devnet" : "mainnet", `https://${getMvxRpcApi(chainID)}`); useEffect(() => { (async () => {