diff --git a/.changeset/thin-days-add.md b/.changeset/thin-days-add.md new file mode 100644 index 00000000000..3a8fc4818e8 --- /dev/null +++ b/.changeset/thin-days-add.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Fix `thirdweb/insight` import typescript error diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 438abb63e56..ecb488aa74b 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -53,14 +53,6 @@ DASHBOARD_SECRET_KEY="" # Client id for api routes API_ROUTES_CLIENT_ID= -# -MORALIS_API_KEY= - -# alchemy.com API key (used for wallet NFTS) -# - cannot be restricted to IP/domain because vercel has no stable IPs and it happens during build & runtime api route call -# - not required to build (unless testing wallet NFTs)> -SSR_ALCHEMY_KEY= # Hubspot Access Token (used for contact us form) diff --git a/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts b/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts deleted file mode 100644 index 56414cb8d08..00000000000 --- a/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts +++ /dev/null @@ -1,89 +0,0 @@ -"use server"; -import { isAddress, toTokens, ZERO_ADDRESS } from "thirdweb"; -import { getWalletBalance } from "thirdweb/wallets"; -import { MORALIS_API_KEY } from "@/constants/server-envs"; -import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; -import { defineDashboardChain } from "@/lib/defineDashboardChain"; - -type BalanceQueryResponse = Array<{ - balance: string; - decimals: number; - name?: string; - symbol: string; - token_address: string; - display_balance: string; -}>; - -export async function getTokenBalancesFromMoralis(params: { - contractAddress: string; - chainId: number; -}): Promise< - | { data: BalanceQueryResponse; error: undefined } - | { - data: undefined; - error: string; - } -> { - const { contractAddress, chainId } = params; - - if (!isAddress(contractAddress)) { - return { - data: undefined, - error: "invalid address", - }; - } - - const getNativeBalance = async (): Promise => { - // eslint-disable-next-line no-restricted-syntax - const chain = defineDashboardChain(chainId, undefined); - const balance = await getWalletBalance({ - address: contractAddress, - chain, - client: serverThirdwebClient, - }); - return [ - { - balance: balance.value.toString(), - decimals: balance.decimals, - display_balance: toTokens(balance.value, balance.decimals), - name: "Native Token", - symbol: balance.symbol, - token_address: ZERO_ADDRESS, - }, - ]; - }; - - const getTokenBalances = async (): Promise => { - const _chain = encodeURIComponent(`0x${chainId?.toString(16)}`); - const _address = encodeURIComponent(contractAddress); - const tokenBalanceEndpoint = `https://deep-index.moralis.io/api/v2/${_address}/erc20?chain=${_chain}`; - - const resp = await fetch(tokenBalanceEndpoint, { - headers: { - "x-api-key": MORALIS_API_KEY, - }, - method: "GET", - }); - - if (!resp.ok) { - resp.body?.cancel(); - return []; - } - const json = await resp.json(); - // biome-ignore lint/suspicious/noExplicitAny: FIXME - return json.map((balance: any) => ({ - ...balance, - display_balance: toTokens(BigInt(balance.balance), balance.decimals), - })); - }; - - const [nativeBalance, tokenBalances] = await Promise.all([ - getNativeBalance(), - getTokenBalances(), - ]); - - return { - data: [...nativeBalance, ...tokenBalances], - error: undefined, - }; -} diff --git a/apps/dashboard/src/@/actions/getWalletNFTs.ts b/apps/dashboard/src/@/actions/getWalletNFTs.ts deleted file mode 100644 index a8f061f61af..00000000000 --- a/apps/dashboard/src/@/actions/getWalletNFTs.ts +++ /dev/null @@ -1,210 +0,0 @@ -"use server"; - -import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs"; -import { MORALIS_API_KEY } from "@/constants/server-envs"; -import { - generateAlchemyUrl, - transformAlchemyResponseToNFT, -} from "@/lib/wallet/nfts/alchemy"; -import { - generateMoralisUrl, - transformMoralisResponseToNFT, -} from "@/lib/wallet/nfts/moralis"; -import type { WalletNFT } from "@/lib/wallet/nfts/types"; -import { getVercelEnv } from "@/utils/vercel"; -import { isAlchemySupported } from "../lib/wallet/nfts/isAlchemySupported"; -import { isMoralisSupported } from "../lib/wallet/nfts/isMoralisSupported"; - -type WalletNFTApiReturn = - | { result: WalletNFT[]; error?: undefined } - | { result?: undefined; error: string }; - -export async function getWalletNFTs(params: { - chainId: number; - owner: string; - isInsightSupported: boolean; -}): Promise { - const { chainId, owner } = params; - - if (params.isInsightSupported) { - const response = await getWalletNFTsFromInsight({ chainId, owner }); - - if (!response.ok) { - return { - error: response.error, - }; - } - - return { result: response.data }; - } - - if (isAlchemySupported(chainId)) { - const url = generateAlchemyUrl({ chainId, owner }); - - const response = await fetch(url, { - next: { - revalidate: 10, // cache for 10 seconds - }, - }); - if (response.status >= 400) { - return { error: response.statusText }; - } - try { - const parsedResponse = await response.json(); - const result = await transformAlchemyResponseToNFT(parsedResponse, owner); - - return { error: undefined, result }; - } catch (err) { - console.error("Error fetching NFTs", err); - return { error: "error parsing response" }; - } - } - - if (isMoralisSupported(chainId) && MORALIS_API_KEY) { - const url = generateMoralisUrl({ chainId, owner }); - - const response = await fetch(url, { - headers: { - "X-API-Key": MORALIS_API_KEY, - }, - method: "GET", - next: { - revalidate: 10, // cache for 10 seconds - }, - }); - - if (response.status >= 400) { - return { error: response.statusText }; - } - - try { - const parsedResponse = await response.json(); - const result = await transformMoralisResponseToNFT( - await parsedResponse, - owner, - chainId, - ); - - return { result }; - } catch (err) { - console.error("Error fetching NFTs", err); - return { error: "error parsing response" }; - } - } - - return { error: "unsupported chain" }; -} - -type OwnedNFTInsightResponse = { - name: string; - description: string; - image_url?: string; - background_color: string; - external_url: string; - metadata_url: string; - extra_metadata: { - customImage?: string; - customAnimationUrl?: string; - animation_original_url?: string; - image_original_url?: string; - }; - collection: { - name: string; - description: string; - extra_metadata: Record; - }; - contract: { - chain_id: number; - address: string; - type: "erc1155" | "erc721"; - name: string; - }; - owner_addresses: string[]; - token_id: string; - balance: string; - token_type: "erc1155" | "erc721"; -}; - -async function getWalletNFTsFromInsight(params: { - chainId: number; - owner: string; -}): Promise< - | { - data: WalletNFT[]; - ok: true; - } - | { - ok: false; - error: string; - } -> { - const { chainId, owner } = params; - - const thirdwebDomain = - getVercelEnv() === "production" ? "thirdweb" : "thirdweb-dev"; - const url = new URL(`https://insight.${thirdwebDomain}.com/v1/nfts`); - url.searchParams.append("chain", chainId.toString()); - url.searchParams.append("limit", "10"); - url.searchParams.append("owner_address", owner); - - const response = await fetch(url, { - headers: { - "x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID, - }, - }); - - if (!response.ok) { - const errorMessage = await response.text(); - return { - error: errorMessage, - ok: false, - }; - } - - const nftsResponse = (await response.json()) as { - data: OwnedNFTInsightResponse[]; - }; - - const isDev = getVercelEnv() !== "production"; - - // NOTE: ipfscdn.io/ to thirdwebstorage-dev.com/ replacement is temporary - // This should be fixed in the insight dev endpoint - - const walletNFTs = nftsResponse.data.map((nft) => { - const walletNFT: WalletNFT = { - chainId: nft.contract.chain_id, - contractAddress: nft.contract.address, - id: nft.token_id, - metadata: { - animation_url: isDev - ? nft.extra_metadata.animation_original_url?.replace( - "ipfscdn.io/", - "thirdwebstorage-dev.com/", - ) - : nft.extra_metadata.animation_original_url, - background_color: nft.background_color, - description: nft.description, - external_url: nft.external_url, - image: isDev - ? nft.image_url?.replace("ipfscdn.io/", "thirdwebstorage-dev.com/") - : nft.image_url, - name: nft.name, - uri: isDev - ? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/") - : nft.metadata_url, - }, - owner: params.owner, - supply: nft.balance, - tokenAddress: nft.contract.address, - tokenURI: nft.metadata_url, - type: nft.token_type === "erc721" ? "ERC721" : "ERC1155", - }; - - return walletNFT; - }); - - return { - data: walletNFTs, - ok: true, - }; -} diff --git a/apps/dashboard/src/@/constants/server-envs.ts b/apps/dashboard/src/@/constants/server-envs.ts index ee0e4e5f9fa..991001ec199 100644 --- a/apps/dashboard/src/@/constants/server-envs.ts +++ b/apps/dashboard/src/@/constants/server-envs.ts @@ -58,16 +58,6 @@ if (isProd && INSIGHT_SERVICE_API_KEY) { ); } -export const MORALIS_API_KEY = process.env.MORALIS_API_KEY || ""; - -if (MORALIS_API_KEY) { - experimental_taintUniqueValue( - "Do not pass MORALIS_API_KEY to the client", - process, - MORALIS_API_KEY, - ); -} - export const ANALYTICS_SERVICE_URL = process.env.ANALYTICS_SERVICE_URL || ""; export const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || ""; diff --git a/apps/dashboard/src/@/hooks/useSplit.ts b/apps/dashboard/src/@/hooks/useSplit.ts index 219e9b971c6..bbf823f0dc1 100644 --- a/apps/dashboard/src/@/hooks/useSplit.ts +++ b/apps/dashboard/src/@/hooks/useSplit.ts @@ -5,62 +5,85 @@ import { useQueryClient, } from "@tanstack/react-query"; import { toast } from "sonner"; -import { sendAndConfirmTransaction, type ThirdwebContract } from "thirdweb"; +import { + type Chain, + sendAndConfirmTransaction, + type ThirdwebClient, + type ThirdwebContract, +} from "thirdweb"; import { distribute, distributeByToken } from "thirdweb/extensions/split"; +import { getOwnedTokens } from "thirdweb/insight"; import { useActiveAccount } from "thirdweb/react"; import invariant from "tiny-invariant"; -import { getTokenBalancesFromMoralis } from "@/actions/getBalancesFromMoralis"; +import { parseError } from "../utils/errorParser"; -function getTokenBalancesQuery(contract: ThirdwebContract) { +function getTokenBalancesQuery(params: { + ownerAddress: string; + client: ThirdwebClient; + chain: Chain; +}) { return queryOptions({ queryFn: async () => { - const res = await getTokenBalancesFromMoralis({ - chainId: contract.chain.id, - contractAddress: contract.address, + return getOwnedTokens({ + client: params.client, + chains: [params.chain], + ownerAddress: params.ownerAddress, + queryOptions: { + include_native: "true", + }, }); - - if (!res.data) { - throw new Error(res.error); - } - return res.data; }, - queryKey: ["split-balances", contract.chain.id, contract.address], + queryKey: ["getOwnedTokens", params.chain.id, params.ownerAddress], retry: false, }); } -export function useSplitBalances(contract: ThirdwebContract) { - return useQuery(getTokenBalancesQuery(contract)); +export function useOwnedTokenBalances(params: { + ownerAddress: string; + client: ThirdwebClient; + chain: Chain; +}) { + return useQuery(getTokenBalancesQuery(params)); } export function useSplitDistributeFunds(contract: ThirdwebContract) { const account = useActiveAccount(); const queryClient = useQueryClient(); + const params = { + ownerAddress: contract.address, // because we want to fetch the balance of split contract + client: contract.client, + chain: contract.chain, + }; + return useMutation({ mutationFn: async () => { invariant(account, "No active account"); + const balances = // get the cached data if it exists, otherwise fetch it - queryClient.getQueryData(getTokenBalancesQuery(contract).queryKey) || - (await queryClient.fetchQuery(getTokenBalancesQuery(contract))); + queryClient.getQueryData(getTokenBalancesQuery(params).queryKey) || + (await queryClient.fetchQuery(getTokenBalancesQuery(params))); const distributions = balances - .filter((token) => token.display_balance !== "0.0") + .filter((token) => token.value !== 0n) .map(async (currency) => { const transaction = currency.name === "Native Token" ? distribute({ contract }) : distributeByToken({ contract, - tokenAddress: currency.token_address, + tokenAddress: currency.tokenAddress, }); const promise = sendAndConfirmTransaction({ account, transaction, }); toast.promise(promise, { - error: `Error distributing ${currency.name}`, + error: (err) => ({ + message: `Error distributing ${currency.name}`, + description: parseError(err), + }), loading: `Distributing ${currency.name}`, success: `Successfully distributed ${currency.name}`, }); @@ -69,7 +92,7 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) { return await Promise.all(distributions); }, onSettled: () => { - queryClient.invalidateQueries(getTokenBalancesQuery(contract)); + queryClient.invalidateQueries(getTokenBalancesQuery(params)); }, }); } diff --git a/apps/dashboard/src/@/hooks/useWalletNFTs.ts b/apps/dashboard/src/@/hooks/useWalletNFTs.ts index 36cdd2276f0..c73b201f253 100644 --- a/apps/dashboard/src/@/hooks/useWalletNFTs.ts +++ b/apps/dashboard/src/@/hooks/useWalletNFTs.ts @@ -1,21 +1,31 @@ import { useQuery } from "@tanstack/react-query"; +import type { Chain, ThirdwebClient } from "thirdweb"; +import { getOwnedNFTs } from "thirdweb/insight"; import invariant from "tiny-invariant"; -import { getWalletNFTs } from "@/actions/getWalletNFTs"; -export function useWalletNFTs(params: { +export function useOwnedNFTsInsight(params: { chainId: number; walletAddress?: string; isInsightSupported: boolean; + client: ThirdwebClient; + chain: Chain; }) { return useQuery({ enabled: !!params.walletAddress, queryFn: async () => { invariant(params.walletAddress, "walletAddress is required"); - return getWalletNFTs({ - chainId: params.chainId, - isInsightSupported: params.isInsightSupported, - owner: params.walletAddress, + + if (!params.isInsightSupported) { + throw new Error("Unsupported chain"); + } + + const res = await getOwnedNFTs({ + client: params.client, + chains: [params.chain], + ownerAddress: params.walletAddress, }); + + return res; }, queryKey: ["walletNfts", params.chainId, params.walletAddress], }); diff --git a/apps/dashboard/src/@/lib/wallet/nfts/alchemy.ts b/apps/dashboard/src/@/lib/wallet/nfts/alchemy.ts deleted file mode 100644 index 0b22eeeb838..00000000000 --- a/apps/dashboard/src/@/lib/wallet/nfts/alchemy.ts +++ /dev/null @@ -1,92 +0,0 @@ -import "server-only"; - -import { download } from "thirdweb/storage"; -import type { NFTMetadata } from "thirdweb/utils"; -import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; -import { handleArbitraryTokenURI, shouldDownloadURI } from "./tokenUri"; -import { - type AlchemySupportedChainId, - alchemySupportedChainIdsMap, - type GenerateURLParams, - type WalletNFT, -} from "./types"; - -export function generateAlchemyUrl({ chainId, owner }: GenerateURLParams) { - const url = new URL( - `https://${ - alchemySupportedChainIdsMap[chainId as AlchemySupportedChainId] - }.g.alchemy.com/nft/v2/${process.env.SSR_ALCHEMY_KEY}/getNFTs`, - ); - url.searchParams.set("owner", owner); - return url.toString(); -} - -export async function transformAlchemyResponseToNFT( - alchemyResponse: AlchemyResponse, - owner: string, -): Promise { - return ( - await Promise.all( - alchemyResponse.ownedNfts.map(async (alchemyNFT) => { - const rawUri = alchemyNFT.tokenUri.raw; - - try { - return { - contractAddress: alchemyNFT.contract.address, - id: alchemyNFT.id.tokenId, - metadata: shouldDownloadURI(rawUri) - ? await download({ - client: serverThirdwebClient, - uri: handleArbitraryTokenURI(rawUri), - }) - .then((res) => res.json()) - .catch(() => ({})) - : rawUri, - owner, - supply: alchemyNFT.balance || "1", - tokenURI: rawUri, - type: alchemyNFT.id.tokenMetadata.tokenType, - } as WalletNFT; - } catch { - return undefined as unknown as WalletNFT; - } - }), - ) - ).filter(Boolean); -} - -type AlchemyResponse = { - ownedNfts: Array<{ - contract: { - address: string; - }; - id: { - tokenId: string; - tokenMetadata: { - tokenType: "ERC721" | "ERC1155"; - }; - }; - balance: string; - title: string; - description: string; - tokenUri: { - raw: string; - gateway: string; - }; - media: Array<{ - raw: string; - gateway: string; - }>; - metadata: NFTMetadata; - timeLastUpdated: string; - contractMetadata: { - name: string; - symbol: string; - totalSupply: string; - tokenType: "ERC721" | "ERC1155"; - }; - }>; - pageKey: string; - totalCount: number; - blockHash: string; -}; diff --git a/apps/dashboard/src/@/lib/wallet/nfts/isAlchemySupported.ts b/apps/dashboard/src/@/lib/wallet/nfts/isAlchemySupported.ts deleted file mode 100644 index 8f3629a6377..00000000000 --- a/apps/dashboard/src/@/lib/wallet/nfts/isAlchemySupported.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - type AlchemySupportedChainId, - alchemySupportedChainIds, -} from "./types"; - -export function isAlchemySupported( - chainId: number, -): chainId is AlchemySupportedChainId { - return alchemySupportedChainIds.includes(chainId.toString()); -} diff --git a/apps/dashboard/src/@/lib/wallet/nfts/isMoralisSupported.ts b/apps/dashboard/src/@/lib/wallet/nfts/isMoralisSupported.ts deleted file mode 100644 index d319202e245..00000000000 --- a/apps/dashboard/src/@/lib/wallet/nfts/isMoralisSupported.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - type MoralisSupportedChainId, - moralisSupportedChainIds, -} from "./types"; - -export function isMoralisSupported( - chainId: number, -): chainId is MoralisSupportedChainId { - return moralisSupportedChainIds.includes(chainId.toString()); -} diff --git a/apps/dashboard/src/@/lib/wallet/nfts/moralis.ts b/apps/dashboard/src/@/lib/wallet/nfts/moralis.ts deleted file mode 100644 index eec1b6bdf32..00000000000 --- a/apps/dashboard/src/@/lib/wallet/nfts/moralis.ts +++ /dev/null @@ -1,74 +0,0 @@ -import "server-only"; - -import { download } from "thirdweb/storage"; -import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; -import { handleArbitraryTokenURI, shouldDownloadURI } from "./tokenUri"; -import type { GenerateURLParams, WalletNFT } from "./types"; - -export function generateMoralisUrl({ chainId, owner }: GenerateURLParams) { - const url = new URL(`https://deep-index.moralis.io/api/v2/${owner}/nft`); - - url.searchParams.append("chain", `0x${chainId.toString(16)}`); - url.searchParams.append("format", "hex"); - - return url.toString(); -} - -export async function transformMoralisResponseToNFT( - moralisResponse: MoralisResponse, - owner: string, - chainId: number, -): Promise { - return ( - await Promise.all( - moralisResponse.result.map(async (moralisNft) => { - try { - return { - chainId, - contractAddress: moralisNft.token_address, - id: moralisNft.token_id, - metadata: shouldDownloadURI(moralisNft.token_uri) - ? await download({ - client: serverThirdwebClient, - uri: handleArbitraryTokenURI(moralisNft.token_uri), - }) - .then((res) => res.json()) - .catch(() => ({})) - : moralisNft.token_uri, - owner, - supply: moralisNft.amount || "1", - tokenAddress: moralisNft.token_address, - tokenId: moralisNft.token_id, - tokenURI: moralisNft.token_uri, - type: moralisNft.contract_type, - } as WalletNFT; - } catch { - return undefined as unknown as WalletNFT; - } - }), - ) - ).filter(Boolean); -} - -type MoralisResponse = { - total: number; - page: number; - page_size: number; - cursor: string | null; - result: Array<{ - token_address: string; - token_id: string; - owner_of: string; - block_number: string; - block_number_minted: string; - token_hash: string; - amount: string; - contract_type: string; - name: string; - symbol: string; - token_uri: `https://ipfs.moralis.io:2053/ipfs/${string}`; - metadata: string; - last_token_uri_sync: string; - last_metadata_sync: string; - }>; -}; diff --git a/apps/dashboard/src/@/lib/wallet/nfts/tokenUri.ts b/apps/dashboard/src/@/lib/wallet/nfts/tokenUri.ts deleted file mode 100644 index 6ecb56ec464..00000000000 --- a/apps/dashboard/src/@/lib/wallet/nfts/tokenUri.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function handleArbitraryTokenURI(rawUri: string): string { - if (rawUri.startsWith("ipfs://")) { - return rawUri; - } - if (!rawUri.includes("ipfs")) { - return rawUri; - } - const uriSplit = `ipfs://${rawUri.split("/ipfs/")}`; - - return uriSplit[uriSplit.length - 1] || ""; -} - -export function shouldDownloadURI(rawUri: string): boolean { - return handleArbitraryTokenURI(rawUri).startsWith("ipfs://"); -} diff --git a/apps/dashboard/src/@/lib/wallet/nfts/types.ts b/apps/dashboard/src/@/lib/wallet/nfts/types.ts index a55531741c9..4bbcf0f57b1 100644 --- a/apps/dashboard/src/@/lib/wallet/nfts/types.ts +++ b/apps/dashboard/src/@/lib/wallet/nfts/types.ts @@ -1,123 +1,8 @@ import type { NFT } from "thirdweb"; -import { - arbitrum, - avalanche, - base, - blast, - blastSepolia, - bsc, - bscTestnet, - cronos, - ethereum, - fantom, - gnosis, - gnosisChiadoTestnet, - linea, - moonbeam, - optimism, - palm, - polygon, - polygonAmoy, - polygonMumbai, - polygonZkEvm, - polygonZkEvmTestnet, - sepolia, - zkSync, - zkSyncSepolia, -} from "thirdweb/chains"; -// Cannot use BigInt for the values here because it will result in error: "fail to serialize bigint" -// when the data is being sent from server to client (when we fetch the owned NFTs from insight/alchemy/moralis) -export type WalletNFT = Omit & { +export type OwnedNFT = { id: string; contractAddress: string; - supply: string; + type: NFT["type"]; + metadata: NFT["metadata"]; }; - -// List: https://docs.alchemy.com/reference/nft-api-faq -export const alchemySupportedChainIdsMap: Record = { - [ethereum.id]: "eth-mainnet", - [sepolia.id]: "eth-sepolia", - [polygon.id]: "polygon-mainnet", - [polygonMumbai.id]: "polygon-mumbai", - [optimism.id]: "opt-mainnet", - [arbitrum.id]: "arb-mainnet", -}; - -// List: https://docs.moralis.io/supported-chains -const moralisSupportedChainIdsMap: Record = { - [moonbeam.id]: "", - // Flow testnet - 545: "", - // Flow - 747: "", - [bscTestnet.id]: "", - [avalanche.id]: "", - [fantom.id]: "", - [cronos.id]: "", - [palm.id]: "", - [arbitrum.id]: "", - [gnosis.id]: "", - [gnosisChiadoTestnet.id]: "", - [base.id]: "", - [polygonAmoy.id]: "", - [optimism.id]: "", - [linea.id]: "", - [sepolia.id]: "", - // opBNB - 204: "", - // Pulse chain - 369: "", - [blast.id]: "", - [blastSepolia.id]: "", - [zkSync.id]: "", - [zkSyncSepolia.id]: "", - [polygonMumbai.id]: "", - [polygonZkEvm.id]: "", - [polygonZkEvmTestnet.id]: "", - [ethereum.id]: "", - [bsc.id]: "", - // Lisk - 1135: "", - [polygon.id]: "", - // Moonriver - 1285: "", - // Moonbase Alpha - 1287: "", - // Ronin - 2020: "", - // Ronin Saigon testnet - 2021: "", - // Lisk Sepolia testnet - 4202: "", - // Mantle - 5000: "", - // Mantle Sepolia - 5003: "", - // Zeta chain - 7000: "", - // Zeta chain testnet - 7001: "", - // Holesky - 17000: "", - // Chiliz testnet - 88882: "", - // Chiliz - 88888: "", -}; - -export type AlchemySupportedChainId = keyof typeof alchemySupportedChainIdsMap; -export type MoralisSupportedChainId = keyof typeof moralisSupportedChainIdsMap; - -export const alchemySupportedChainIds = Object.keys( - alchemySupportedChainIdsMap, -); - -export const moralisSupportedChainIds = Object.keys( - moralisSupportedChainIdsMap, -); - -export interface GenerateURLParams { - chainId: number; - owner: string; -} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx index f7d6c9113dd..a5ad8addc29 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx @@ -14,8 +14,6 @@ import { SheetTrigger, } from "@/components/ui/sheet"; import { TabButtons } from "@/components/ui/tabs"; -import { isAlchemySupported } from "@/lib/wallet/nfts/isAlchemySupported"; -import { isMoralisSupported } from "@/lib/wallet/nfts/isMoralisSupported"; import { CreateListingsForm } from "./list-form"; interface CreateListingButtonProps { @@ -41,12 +39,6 @@ export const CreateListingButton: React.FC = ({ const [listingMode, setListingMode] = useState<(typeof LISTING_MODES)[number]>("Select NFT"); - const isSupportedChain = - contract.chain.id && - (isInsightSupported || - isAlchemySupported(contract.chain.id) || - isMoralisSupported(contract.chain.id)); - return ( @@ -69,7 +61,7 @@ export const CreateListingButton: React.FC = ({ If the chain is not supported by the indexer providers we don't show the tabs, we only show the Manual input form. Otherwise we show both */} - {isSupportedChain ? ( + {isInsightSupported ? ( <> & { currencyContractAddress: string; - selected?: WalletNFT; + selected?: OwnedNFT; listingType: "direct"; listingDurationInSeconds: string; quantity: string; @@ -73,7 +71,7 @@ type ListForm = }) | (Omit & { currencyContractAddress: string; - selected?: WalletNFT; + selected?: OwnedNFT; listingType: "auction"; listingDurationInSeconds: string; quantity: string; @@ -114,18 +112,14 @@ export const CreateListingsForm: React.FC = ({ const [isFormLoading, setIsFormLoading] = useState(false); - const isSupportedChain = - chainId && - (isInsightSupported || - isAlchemySupported(chainId) || - isMoralisSupported(chainId)); - const account = useActiveAccount(); - const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({ + const ownedNFTsInsightQuery = useOwnedNFTsInsight({ chainId, isInsightSupported, walletAddress: account?.address, + client: contract.client, + chain: contract.chain, }); const sendAndConfirmTx = useSendAndConfirmTransaction(); @@ -173,58 +167,52 @@ export const CreateListingsForm: React.FC = ({ }) : undefined; - const { data: ownedNFTs, isPending: isOwnedNFTsLoading } = - useDashboardOwnedNFTs({ - contract: selectedContract, - // Only run this hook as the last resort if this chain is not supported by the API services we are using - disabled: - !selectedContract || - isSupportedChain || - isWalletNFTsLoading || - (walletNFTs?.result || []).length > 0 || - mode === "manual", - owner: account?.address, - }); + const ownedNFTsRPCQuery = useDashboardOwnedNFTs({ + contract: selectedContract, + // Only run this hook as the last resort if this chain is not supported by the API services we are using + disabled: + !selectedContract || + isInsightSupported || + ownedNFTsInsightQuery.isPending || + (ownedNFTsInsightQuery.data && ownedNFTsInsightQuery.data.length > 0) || + mode === "manual", + owner: account?.address, + }); - const isSelected = (nft: WalletNFT) => { + const isSelected = (nft: OwnedNFT) => { return ( form.watch("selected")?.id === nft.id && form.watch("selected")?.contractAddress === nft.contractAddress ); }; - const ownedWalletNFTs: WalletNFT[] = useMemo(() => { - return ownedNFTs?.map((nft) => { - if (nft.type === "ERC721") { - return { - chainId: nft.chainId, - contractAddress: form.watch("selected.contractAddress"), - id: String(nft.id), - metadata: nft.metadata, - owner: nft.owner, - supply: "1", - tokenAddress: nft.tokenAddress, - tokenId: nft.id.toString(), - tokenURI: nft.tokenURI, - type: "ERC721", - }; - } - return { - chainId: nft.chainId, - contractAddress: form.watch("selected.contractAddress"), + const ownedWalletNFTsRPC: OwnedNFT[] = useMemo(() => { + return ownedNFTsRPCQuery.data?.map((nft) => { + const nftObj: OwnedNFT = { + contractAddress: nft.tokenAddress, id: String(nft.id), metadata: nft.metadata, - owner: nft.owner, - supply: String(nft.supply), - tokenAddress: nft.tokenAddress, - tokenId: nft.id.toString(), - tokenURI: nft.tokenURI, - type: "ERC1155", + type: nft.type, }; - }) as WalletNFT[]; - }, [ownedNFTs, form]); - const nfts = ownedWalletNFTs || walletNFTs?.result; + return nftObj; + }) as OwnedNFT[]; + }, [ownedNFTsRPCQuery.data]); + + const ownedWalletNFTsInsight: OwnedNFT[] | undefined = useMemo(() => { + return ownedNFTsInsightQuery.data?.map((nft) => { + const nftObj: OwnedNFT = { + id: nft.id.toString(), + contractAddress: nft.tokenAddress, + metadata: nft.metadata, + type: nft.type, + }; + + return nftObj; + }); + }, [ownedNFTsInsightQuery.data]); + + const nfts = ownedWalletNFTsRPC || ownedWalletNFTsInsight; return (
@@ -472,7 +460,7 @@ export const CreateListingsForm: React.FC = ({ Select the NFT you want to list for sale - {!isSupportedChain ? ( + {!isInsightSupported ? (
@@ -504,9 +492,9 @@ export const CreateListingsForm: React.FC = ({
) : null} - {isWalletNFTsLoading || - (isOwnedNFTsLoading && - !isSupportedChain && + {ownedNFTsInsightQuery.isPending || + (ownedNFTsRPCQuery.isPending && + !isInsightSupported && form.watch("selected.contractAddress")) ? (
{new Array(8).fill(0).map((_, index) => ( @@ -527,7 +515,7 @@ export const CreateListingsForm: React.FC = ({ return (
  • diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/NFTCards.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/NFTCards.tsx index b8e081b557d..8f54003fe34 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/NFTCards.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/NFTCards.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { type NFT, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; import { NFTMediaWithEmptyState } from "@/components/blocks/nft-media"; import { SkeletonContainer } from "@/components/ui/skeleton"; +import type { OwnedNFT } from "@/lib/wallet/nfts/types"; import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types"; import { buildContractPagePath } from "../_utils/contract-page-path"; @@ -27,7 +28,7 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({ }); interface NFTCardsProps { - nfts: Array; + nfts: Array; isPending: boolean; allNfts?: boolean; projectMeta: ProjectMeta | undefined; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/account-balance.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/account-balance.tsx index b8779bdc506..d80f09ca5cd 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/account-balance.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/account-balance.tsx @@ -1,37 +1,26 @@ "use client"; import type { ThirdwebContract } from "thirdweb"; -import { useActiveWalletChain, useWalletBalance } from "thirdweb/react"; -import { useSplitBalances } from "@/hooks/useSplit"; +import { useOwnedTokenBalances } from "@/hooks/useSplit"; import { StatCard } from "../../overview/components/stat-card"; export function AccountBalance(props: { contract: ThirdwebContract }) { - const activeChain = useActiveWalletChain(); - const { data: balance } = useWalletBalance({ - address: props.contract.address, - chain: activeChain, + const balanceQuery = useOwnedTokenBalances({ + ownerAddress: props.contract.address, client: props.contract.client, + chain: props.contract.chain, }); - const balanceQuery = useSplitBalances(props.contract); - return (
    - - {balanceQuery?.data - ?.filter((bl) => bl.name !== "Native Token") - .map((bl) => ( - - ))} + {balanceQuery?.data?.map((bl) => ( + + ))}
    ); } diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx index b88f9e40dc6..535d2f468c0 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx @@ -1,7 +1,7 @@ "use client"; import type { ThirdwebContract } from "thirdweb"; -import { useWalletNFTs } from "@/hooks/useWalletNFTs"; +import { useOwnedNFTsInsight } from "@/hooks/useWalletNFTs"; import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types"; import { NFTCards } from "../../_components/NFTCards"; @@ -16,36 +16,34 @@ export const NftsOwned: React.FC = ({ isInsightSupported, projectMeta, }) => { - const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({ + const ownedNFTInsightQuery = useOwnedNFTsInsight({ chainId: contract.chain.id, isInsightSupported: isInsightSupported, walletAddress: contract.address, + client: contract.client, + chain: contract.chain, }); - const nfts = walletNFTs?.result || []; - const error = walletNFTs?.error; + const nfts = ownedNFTInsightQuery.data || []; + const error = ownedNFTInsightQuery.error; return nfts.length !== 0 ? ( ({ + isPending={ownedNFTInsightQuery.isPending} + nfts={nfts.map((x) => ({ chainId: contract.chain.id, - contractAddress: nft.contractAddress, - id: BigInt(nft.id), - metadata: nft.metadata, - owner: nft.owner, - supply: BigInt(nft.supply), - tokenAddress: nft.tokenAddress, - tokenURI: nft.tokenURI, - type: nft.type, + contractAddress: x.tokenAddress, + id: x.id.toString(), + metadata: x.metadata, + type: x.type, }))} projectMeta={projectMeta} /> - ) : isWalletNFTsLoading ? null : error ? ( + ) : ownedNFTInsightQuery.isPending ? null : error ? (

    - Failed to fetch NFTs for this account: {error} + Failed to fetch NFTs for this account: {error.message}

    ) : (

    diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx index 5e8e381d7b4..a0df7812b99 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx @@ -63,9 +63,11 @@ export function NFTDetails({ client={contract.client} isPending={nftQuery.isPending} nfts={displayableNFTs.map((t) => ({ - ...t, chainId: contract.chain.id, contractAddress: contract.address, + id: t.id.toString(), + metadata: t.metadata, + type: t.type, }))} projectMeta={projectMeta} /> diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.tsx index b10e6486c8d..cca5fbaa4ff 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.tsx @@ -1,19 +1,12 @@ "use client"; -import { useMemo } from "react"; -import { - type ThirdwebContract, - toEther, - toTokens, - ZERO_ADDRESS, -} from "thirdweb"; +import { ExternalLinkIcon } from "lucide-react"; +import Link from "next/link"; +import { NATIVE_TOKEN_ADDRESS, type ThirdwebContract } from "thirdweb"; import { getAllRecipientsPercentages } from "thirdweb/extensions/split"; -import { - useActiveAccount, - useReadContract, - useWalletBalance, -} from "thirdweb/react"; +import { useActiveAccount, useReadContract } from "thirdweb/react"; import { WalletAddress } from "@/components/blocks/wallet-address"; +import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, @@ -24,160 +17,154 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useAllChainsData } from "@/hooks/chains/allChains"; -import { useSplitBalances } from "@/hooks/useSplit"; -import { shortenIfAddress } from "@/utils/usedapp-external"; +import { useOwnedTokenBalances } from "@/hooks/useSplit"; import { DistributeButton } from "./components/distribute-button"; -export type Balance = { - name: string; - token_address: string; - balance: string; - display_balance: string; - decimals: number; -}; - -interface SplitPageProps { +export function ContractSplitPage({ + contract, + isLoggedIn, +}: { contract: ThirdwebContract; isLoggedIn: boolean; -} - -export function ContractSplitPage({ contract, isLoggedIn }: SplitPageProps) { +}) { const address = useActiveAccount()?.address; - const { idToChain } = useAllChainsData(); - const chainId = contract.chain.id; - const v4Chain = idToChain.get(chainId); - const contractAddress = contract.address; - const nativeBalanceQuery = useWalletBalance({ - address: contractAddress, - chain: contract.chain, - client: contract.client, - }); + const { data: allRecipientsPercentages } = useReadContract( getAllRecipientsPercentages, { contract }, ); - const balanceQuery = useSplitBalances(contract); - const balances = useMemo(() => { - if (!balanceQuery.data && !nativeBalanceQuery.data) { - return []; - } - - return [ - { - balance: nativeBalanceQuery?.data?.value?.toString() || "0", - decimals: nativeBalanceQuery?.data?.decimals || 18, - display_balance: nativeBalanceQuery?.data?.displayValue || "0.0", - name: "Native Token", - token_address: ZERO_ADDRESS, - }, - ...(balanceQuery.data || []).filter((bl) => bl.name !== "Native Token"), - ]; - }, [balanceQuery.data, nativeBalanceQuery.data]); - - const shareOfBalancesForConnectedWallet = useMemo(() => { - const activeRecipient = (allRecipientsPercentages || []).find( - (r) => r.address.toLowerCase() === address?.toLowerCase(), - ); - if (!activeRecipient || !balances) { - return {}; - } - - return balances.reduce( - (acc, curr) => { - // For native token balance, Moralis returns the zero address - // this logic will potentially have to change if we decide to replace the service - const isNativeToken = curr.token_address === ZERO_ADDRESS; - const displayBalance = isNativeToken - ? toEther(BigInt(curr.balance)) - : toTokens(BigInt(curr.balance), curr.decimals); - return { - // biome-ignore lint/performance/noAccumulatingSpread: FIXME - ...acc, - [curr.token_address]: displayBalance, - }; - }, - {} as { [address: string]: string }, - ); - }, [allRecipientsPercentages, balances, address]); + const balanceQuery = useOwnedTokenBalances({ + ownerAddress: contract.address, // fetch the balance of split contract + client: contract.client, + chain: contract.chain, + }); - const isPending = balanceQuery.isPending || nativeBalanceQuery.isPending; + const activeRecipient = (allRecipientsPercentages || []).find( + (r) => r.address.toLowerCase() === address?.toLowerCase(), + ); return (

    {/* header */} -
    -

    Balances

    - +
    +
    +

    + Balances +

    +

    + The Split can receive funds in the native token or in any ERC20 +

    +
    - {/* balances */} -
    -
    - {isPending ? ( - new Array(4).fill(null).map((_, index) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: ok - - )) - ) : ( - <> - + {/* balances table */} +
    +
    + + + + + Token + Balance + {activeRecipient && Your Share} + + + + {balanceQuery.isPending + ? new Array(3).fill(null).map((_, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: ok + + + + + + + + {activeRecipient && ( + + + + )} + + )) + : balanceQuery.data?.map((tokenBalance) => ( + + {/* token */} + + + - {(balanceQuery?.data || []) - ?.filter((bl) => bl.name !== "Native Token") - ?.map((balance) => ( - - ))} - - )} - + {/* balance */} + + {tokenBalance.displayValue} {tokenBalance.symbol} + - {balanceQuery.isError && ( -

    - {(balanceQuery?.error as Error).message === "Invalid chain!" - ? "Showing ERC20 balances for this network is not currently supported. You can distribute ERC20 funds from the Explorer tab." - : "Error loading balances"} -

    - )} + {/* your share percent */} + {activeRecipient && ( + + {activeRecipient.splitPercentage}% + + )} +
    + ))} +
    +
    + + {balanceQuery.isError && ( +
    + {balanceQuery.error.message} +
    + )} -

    - The Split can receive funds in the native token or in any ERC20. - Balances may take a couple of minutes to display after being received. -
    - {/* We currently use Moralis and high chances are they don't recognize all ERC20 tokens in the contract */} - If you are looking to distribute an ERC20 token and it's not being - recognized on this page, you can manually call the `distribute` method - in the Explorer page -

    + {!balanceQuery.isPending && balanceQuery.data?.length === 0 && ( +
    + No funds received yet +
    + )} +
    + + {balanceQuery.data && balanceQuery.data.length > 0 && ( +
    + +
    + )} +
    - {/* recipients e */} + {/* recipients table */}
    -

    Split Recipients

    +
    +

    + Split Recipients +

    +

    + List of addresses that can receive funds from the Split and their + percentage share. +

    +
    @@ -205,23 +192,3 @@ export function ContractSplitPage({ contract, isLoggedIn }: SplitPageProps) { ); } - -function BalanceCard(props: { - symbol: string; - displayValue: string; - userShare: string | undefined; -}) { - return ( -
    -
    - {props.displayValue} {props.symbol} -
    - {props.userShare && ( -
    - Your Share:{" "} - {props.userShare} -
    - )} -
    - ); -} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/components/distribute-button.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/components/distribute-button.tsx index 3b4842fecad..50bb1153d53 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/components/distribute-button.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/components/distribute-button.tsx @@ -4,12 +4,12 @@ import { SplitIcon } from "lucide-react"; import { useMemo } from "react"; import { toast } from "sonner"; import type { ThirdwebContract } from "thirdweb"; +import type { GetBalanceResult } from "thirdweb/extensions/erc20"; import { TransactionButton } from "@/components/tx-button"; import { Button } from "@/components/ui/button"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { useSplitDistributeFunds } from "@/hooks/useSplit"; import { parseError } from "@/utils/errorParser"; -import type { Balance } from "../ContractSplitPage"; export const DistributeButton = ({ contract, @@ -19,14 +19,12 @@ export const DistributeButton = ({ isLoggedIn, }: { contract: ThirdwebContract; - balances: Balance[]; + balances: GetBalanceResult[]; balancesIsPending: boolean; balancesIsError: boolean; isLoggedIn: boolean; }) => { - const validBalances = balances.filter( - (item) => item.balance !== "0" && item.balance !== "0.0", - ); + const validBalances = balances.filter((item) => item.value !== 0n); const numTransactions = useMemo(() => { if ( validBalances.length === 1 && @@ -37,9 +35,7 @@ export const DistributeButton = ({ if (!validBalances || balancesIsPending) { return 0; } - return validBalances?.filter( - (b) => b.display_balance !== "0.0" && b.display_balance !== "0", - ).length; + return validBalances?.filter((b) => b.value !== 0n).length; }, [validBalances, balancesIsPending]); const mutation = useSplitDistributeFunds(contract); diff --git a/packages/thirdweb/package.json b/packages/thirdweb/package.json index 5441af2efe5..ca2e15c85a4 100644 --- a/packages/thirdweb/package.json +++ b/packages/thirdweb/package.json @@ -402,6 +402,9 @@ ], "wallets/*": [ "./dist/types/exports/wallets/*.d.ts" + ], + "insight": [ + "./dist/types/exports/insight.d.ts" ] } },