[Update] Refactor useWalletNFTs hook and NFT handling (#4374)
### TL;DR Refactored wallet NFT handling and improved font loading across the dashboard. ### What changed? - Updated `useWalletNFTs` hook to accept `chainId` as a parameter - Modified `NftsOwned` component to use contract chain ID - Refactored font loading in `layout.tsx` and `MiniPlayground.tsx` - Updated NFT data structures to use `id` instead of `tokenId` - Adjusted NFT card rendering to include chain ID and contract address - Modified Alchemy, Moralis, and SimpleHash NFT transformations ### How to test? 1. Test the wallet NFT fetching functionality with different chain IDs 2. Verify that NFT cards display correctly in the dashboard 3. Check font loading performance in various components 4. Ensure that NFT details are correctly displayed in listings and overview pages ### Why make this change? This change improves the consistency of NFT handling across the dashboard, enhances font loading performance, and provides better support for multi-chain NFT display. It also aligns the NFT data structure with the latest ThirdWeb SDK conventions, using `id` instead of `tokenId`. ---
This commit is contained in:
@@ -1,24 +1,17 @@
|
||||
import type { WalletNFTApiReturn } from "pages/api/wallet/nfts/[chainId]";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { useQueryWithNetwork } from "./query/useQueryWithNetwork";
|
||||
import { useDashboardEVMChainId } from "./useActiveChainId";
|
||||
|
||||
export function useWalletNFTs(walletAddress?: string) {
|
||||
const activeChainId = useDashboardEVMChainId();
|
||||
const connectedAddress = useActiveAccount()?.address;
|
||||
|
||||
const address = walletAddress || connectedAddress;
|
||||
|
||||
export function useWalletNFTs(walletAddress?: string, chainId?: number) {
|
||||
return useQueryWithNetwork(
|
||||
["walletNfts", address],
|
||||
["walletNfts", walletAddress],
|
||||
async () => {
|
||||
const response = await fetch(
|
||||
`/api/wallet/nfts/${activeChainId}?owner=${address}`,
|
||||
`/api/wallet/nfts/${chainId}?owner=${walletAddress}`,
|
||||
);
|
||||
return (await response.json()) as WalletNFTApiReturn;
|
||||
},
|
||||
{
|
||||
enabled: !!address && !!activeChainId,
|
||||
enabled: !!walletAddress && !!chainId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Inter as interFont } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { PostHogProvider } from "./components/root-providers";
|
||||
import { AppRouterProviders } from "./providers";
|
||||
|
||||
const fontSans = interFont({
|
||||
const fontSans = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
+5
-6
@@ -11,10 +11,7 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import {
|
||||
Londrina_Solid as londrinaSolidConstructor,
|
||||
Source_Serif_4 as sourceSerif4Constructor,
|
||||
} from "next/font/google";
|
||||
import { Londrina_Solid, Source_Serif_4 } from "next/font/google";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FiChevronRight } from "react-icons/fi";
|
||||
import {
|
||||
@@ -41,14 +38,16 @@ import { ThemeButton } from "../ConnectWalletPlayground/ThemeButton";
|
||||
import { usePlaygroundTheme } from "../ConnectWalletPlayground/usePlaygroundTheme";
|
||||
|
||||
// If loading a variable font, you don't need to specify the font weight
|
||||
const nounsDaoFont = londrinaSolidConstructor({
|
||||
const nounsDaoFont = Londrina_Solid({
|
||||
subsets: ["latin"],
|
||||
weight: ["900", "400"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const web3WarriorsFont = sourceSerif4Constructor({
|
||||
const web3WarriorsFont = Source_Serif_4({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
type WalletIdSubset =
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { useWalletNFTs } from "@3rdweb-sdk/react";
|
||||
import { NFTCards } from "contract-ui/tabs/overview/components/NFTCards";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Text } from "tw-components";
|
||||
|
||||
interface NftsOwnedProps {
|
||||
address: string;
|
||||
contract: ThirdwebContract;
|
||||
}
|
||||
|
||||
export const NftsOwned: React.FC<NftsOwnedProps> = ({ address }) => {
|
||||
const { data: walletNFTs, isLoading: isWalletNFTsLoading } =
|
||||
useWalletNFTs(address);
|
||||
export const NftsOwned: React.FC<NftsOwnedProps> = ({ contract }) => {
|
||||
const { data: walletNFTs, isLoading: isWalletNFTsLoading } = useWalletNFTs(
|
||||
contract.address,
|
||||
contract.chain.id,
|
||||
);
|
||||
|
||||
const nfts = walletNFTs?.result || [];
|
||||
const error = walletNFTs?.error;
|
||||
|
||||
return nfts.length !== 0 ? (
|
||||
<NFTCards
|
||||
nfts={nfts}
|
||||
nfts={nfts.map((nft) => ({
|
||||
...nft,
|
||||
contractAddress: nft.contractAddress,
|
||||
chainId: contract.chain.id,
|
||||
}))}
|
||||
allNfts
|
||||
isLoading={isWalletNFTsLoading}
|
||||
trackingCategory="account_nfts_owned"
|
||||
|
||||
@@ -61,7 +61,7 @@ export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
<Flex direction="row" justify="space-between" align="center">
|
||||
<Heading size="title.sm">NFTs owned</Heading>
|
||||
</Flex>
|
||||
<NftsOwned address={contract.address} />
|
||||
<NftsOwned contract={contract} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,7 +100,12 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
isAlchemySupported(chainId) ||
|
||||
isMoralisSupported(chainId));
|
||||
|
||||
const { data: walletNFTs, isLoading: isWalletNFTsLoading } = useWalletNFTs();
|
||||
const account = useActiveAccount();
|
||||
|
||||
const { data: walletNFTs, isLoading: isWalletNFTsLoading } = useWalletNFTs(
|
||||
account?.address,
|
||||
chainId,
|
||||
);
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
const form = useForm<ListForm>({
|
||||
@@ -117,8 +122,6 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
},
|
||||
});
|
||||
|
||||
const account = useActiveAccount();
|
||||
|
||||
const selectedContract = form.watch("selected.contractAddress")
|
||||
? getContract({
|
||||
address: form.watch("selected.contractAddress"),
|
||||
@@ -141,7 +144,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
|
||||
const isSelected = (nft: WalletNFT) => {
|
||||
return (
|
||||
form.watch("selected")?.tokenId === nft.tokenId &&
|
||||
form.watch("selected")?.id === nft.id &&
|
||||
form.watch("selected")?.contractAddress === nft.contractAddress
|
||||
);
|
||||
};
|
||||
@@ -229,7 +232,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
const transaction = createListing({
|
||||
contract,
|
||||
assetContractAddress: formData.selected.contractAddress,
|
||||
tokenId: BigInt(formData.selected.tokenId),
|
||||
tokenId: BigInt(formData.selected.id),
|
||||
currencyContractAddress: formData.currencyContractAddress,
|
||||
quantity: BigInt(formData.quantity),
|
||||
startTimestamp: formData.startTimestamp,
|
||||
@@ -276,7 +279,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
const transaction = createAuction({
|
||||
contract,
|
||||
assetContractAddress: formData.selected.contractAddress,
|
||||
tokenId: BigInt(formData.selected.tokenId),
|
||||
tokenId: BigInt(formData.selected.id),
|
||||
startTimestamp: formData.startTimestamp,
|
||||
currencyContractAddress: formData.currencyContractAddress,
|
||||
endTimestamp: new Date(
|
||||
@@ -392,7 +395,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
boxShadow="none"
|
||||
shouldWrapChildren
|
||||
placement="left-end"
|
||||
key={nft.contractAddress + nft.tokenId}
|
||||
key={nft.contractAddress + nft.id}
|
||||
label={<ListLabel nft={nft} />}
|
||||
>
|
||||
<Box
|
||||
@@ -517,7 +520,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{!form.watch("selected.tokenId") && (
|
||||
{!form.watch("selected.id") && (
|
||||
<Alert>
|
||||
<CircleAlertIcon className="size-4" />
|
||||
<AlertTitle>No NFT selected</AlertTitle>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ListLabel: React.FC<ListLabelProps> = ({ nft }) => {
|
||||
{shortenIfAddress(nft.contractAddress)}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<strong>Token ID: </strong> {nft.tokenId}
|
||||
<strong>Token ID: </strong> {nft.id.toString()}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
AspectRatio,
|
||||
Flex,
|
||||
@@ -8,10 +7,8 @@ import {
|
||||
SkeletonText,
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { useMemo } from "react";
|
||||
import type { NFT } from "thirdweb";
|
||||
import { type NFT, ZERO_ADDRESS } from "thirdweb";
|
||||
import {
|
||||
Card,
|
||||
Heading,
|
||||
@@ -21,7 +18,11 @@ import {
|
||||
} from "tw-components";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
|
||||
const dummyMetadata: (idx: number) => NFT = (idx) => ({
|
||||
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
|
||||
|
||||
const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
|
||||
chainId: 1,
|
||||
contractAddress: ZERO_ADDRESS,
|
||||
id: BigInt(idx || 0),
|
||||
tokenURI: `1-0x123-${idx}`,
|
||||
metadata: {
|
||||
@@ -35,27 +36,20 @@ const dummyMetadata: (idx: number) => NFT = (idx) => ({
|
||||
supply: 1n,
|
||||
});
|
||||
|
||||
function isOnlyNumbers(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
interface NFTCardsProps {
|
||||
nfts: NFT[] | WalletNFT[];
|
||||
nfts: Array<NFTWithContract>;
|
||||
trackingCategory: TrackedLinkProps["category"];
|
||||
isLoading: boolean;
|
||||
contractAddress?: string;
|
||||
allNfts?: boolean;
|
||||
}
|
||||
|
||||
export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
nfts,
|
||||
contractAddress,
|
||||
trackingCategory,
|
||||
isLoading,
|
||||
allNfts,
|
||||
}) => {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
const chainId = useDashboardEVMChainId();
|
||||
|
||||
const dummyData = useMemo(() => {
|
||||
return Array.from({
|
||||
@@ -63,31 +57,20 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
}).map((_, idx) => dummyMetadata(idx));
|
||||
}, [nfts.length, isMobile, allNfts]);
|
||||
|
||||
const chainSlug = useChainSlug(chainId || 1);
|
||||
|
||||
return (
|
||||
<SimpleGrid
|
||||
gap={{ base: 3, md: 6 }}
|
||||
columns={allNfts ? { base: 2, md: 4 } : { base: 2, md: 3 }}
|
||||
>
|
||||
{(isLoading ? dummyData : nfts).map((token) => {
|
||||
const tokenId = (token as WalletNFT)?.tokenId || (token as NFT).id;
|
||||
const ctrAddress =
|
||||
(token as WalletNFT)?.contractAddress || contractAddress;
|
||||
|
||||
if (
|
||||
(!tokenId && tokenId !== 0n) ||
|
||||
!isOnlyNumbers(tokenId.toString())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const tokenId = token.id.toString();
|
||||
|
||||
return (
|
||||
<GridItem
|
||||
key={`${chainId}-${ctrAddress}-${tokenId}`}
|
||||
key={`${token.chainId}_${token.contractAddress}_${tokenId}`}
|
||||
as={TrackedLink}
|
||||
category={trackingCategory}
|
||||
href={`/${chainSlug}/${ctrAddress}/nfts/${tokenId.toString()}`}
|
||||
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
|
||||
_hover={{ opacity: 0.75, textDecoration: "none" }}
|
||||
>
|
||||
<Card p={0} h="full">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Flex, useBreakpointValue } from "@chakra-ui/react";
|
||||
import { useTabHref } from "contract-ui/utils";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getNFTs } from "thirdweb/extensions/erc721";
|
||||
import * as ERC721 from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155 from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Heading, TrackedLink, type TrackedLinkProps } from "tw-components";
|
||||
import { NFTCards } from "./NFTCards";
|
||||
@@ -9,20 +10,25 @@ import { NFTCards } from "./NFTCards";
|
||||
interface NFTDetailsProps {
|
||||
contract: ThirdwebContract;
|
||||
trackingCategory: TrackedLinkProps["category"];
|
||||
isErc721: boolean;
|
||||
}
|
||||
|
||||
export const NFTDetails: React.FC<NFTDetailsProps> = ({
|
||||
contract,
|
||||
trackingCategory,
|
||||
isErc721,
|
||||
}) => {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
const nftsHref = useTabHref("nfts");
|
||||
|
||||
const nftQuery = useReadContract(getNFTs, {
|
||||
contract,
|
||||
count: 5,
|
||||
includeOwners: true,
|
||||
});
|
||||
const nftQuery = useReadContract(
|
||||
isErc721 ? ERC721.getNFTs : ERC1155.getNFTs,
|
||||
{
|
||||
contract,
|
||||
count: 5,
|
||||
includeOwners: false,
|
||||
},
|
||||
);
|
||||
|
||||
const displayableNFTs =
|
||||
nftQuery.data
|
||||
@@ -47,8 +53,11 @@ export const NFTDetails: React.FC<NFTDetailsProps> = ({
|
||||
</TrackedLink>
|
||||
</Flex>
|
||||
<NFTCards
|
||||
contractAddress={contract.address}
|
||||
nfts={displayableNFTs}
|
||||
nfts={displayableNFTs.map((t) => ({
|
||||
...t,
|
||||
contractAddress: contract.address,
|
||||
chainId: contract.chain.id,
|
||||
}))}
|
||||
trackingCategory={trackingCategory}
|
||||
isLoading={nftQuery.isLoading}
|
||||
/>
|
||||
|
||||
@@ -55,6 +55,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
<NFTDetails
|
||||
contract={contract}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
isErc721={isErc721}
|
||||
/>
|
||||
)}
|
||||
{isErc20 && <TokenDetails contract={contract} />}
|
||||
|
||||
@@ -36,8 +36,8 @@ export async function transformAlchemyResponseToNFT(
|
||||
|
||||
try {
|
||||
return {
|
||||
id: BigInt(alchemyNFT.id.tokenId),
|
||||
contractAddress: alchemyNFT.contract.address,
|
||||
tokenId: alchemyNFT.id.tokenId,
|
||||
metadata: shouldDownloadURI(rawUri)
|
||||
? await StorageSingleton.downloadJSON(
|
||||
handleArbitraryTokenURI(rawUri),
|
||||
@@ -46,6 +46,7 @@ export async function transformAlchemyResponseToNFT(
|
||||
owner,
|
||||
supply: alchemyNFT.balance || "1",
|
||||
type: alchemyNFT.id.tokenMetadata.tokenType,
|
||||
tokenURI: rawUri,
|
||||
} as WalletNFT;
|
||||
} catch {
|
||||
return undefined as unknown as WalletNFT;
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function transformMoralisResponseToNFT(
|
||||
moralisResponse.result.map(async (moralisNft) => {
|
||||
try {
|
||||
return {
|
||||
id: BigInt(moralisNft.token_id),
|
||||
contractAddress: moralisNft.token_address,
|
||||
tokenId: moralisNft.token_id,
|
||||
metadata: shouldDownloadURI(moralisNft.token_uri)
|
||||
@@ -39,6 +40,7 @@ export async function transformMoralisResponseToNFT(
|
||||
)
|
||||
: moralisNft.token_uri,
|
||||
owner,
|
||||
tokenURI: moralisNft.token_uri,
|
||||
supply: moralisNft.amount || "1",
|
||||
type: moralisNft.contract_type,
|
||||
} as WalletNFT;
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function transformSimpleHashResponseToNFT(
|
||||
simpleHashResponse.nfts.map(async (simpleHashNft) => {
|
||||
try {
|
||||
return {
|
||||
id: BigInt(simpleHashNft.token_id),
|
||||
contractAddress: simpleHashNft.contract_address,
|
||||
tokenId: simpleHashNft.token_id,
|
||||
metadata: {
|
||||
@@ -49,6 +50,7 @@ export async function transformSimpleHashResponseToNFT(
|
||||
owner: walletAddress,
|
||||
supply: simpleHashNft.token_count.toString(),
|
||||
type: simpleHashNft.contract.type,
|
||||
tokenURI: simpleHashNft.extra_metadata.metadata_original_url,
|
||||
} as WalletNFT;
|
||||
} catch {
|
||||
return undefined as unknown as WalletNFT;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NFT } from "thirdweb";
|
||||
import {
|
||||
arbitrum,
|
||||
arbitrumNova,
|
||||
@@ -50,25 +51,8 @@ import {
|
||||
zoraSepolia,
|
||||
} from "thirdweb/chains";
|
||||
|
||||
type NFT = {
|
||||
metadata: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
image?: string | null;
|
||||
animation_url?: string | null;
|
||||
external_url?: string | null;
|
||||
background_color?: string;
|
||||
properties?: Record<string, unknown> | Array<Record<string, unknown>>;
|
||||
} & Record<string, unknown>;
|
||||
owner: string;
|
||||
type: "ERC1155" | "ERC721";
|
||||
supply: string;
|
||||
quantityOwned?: bigint;
|
||||
};
|
||||
|
||||
export type WalletNFT = NFT & {
|
||||
contractAddress: string;
|
||||
tokenId: string;
|
||||
};
|
||||
|
||||
// List: https://docs.alchemy.com/reference/nft-api-faq
|
||||
|
||||
Reference in New Issue
Block a user