+>(({ className, ...props }, ref) => {
+ const { nftData } = useNFT();
+
+ return (
+
+ Owned by
+
+ {nftData?.owner?.ens_domain_name ??
+ formatAddress(nftData?.owner?.hash ?? "")}
+
+
+ );
+});
+
+NFTOwner.displayName = "NFTOwner";
diff --git a/packages/ui-react/src/components/nft/components/nft-title.tsx b/packages/ui-react/src/components/nft/components/nft-title.tsx
new file mode 100644
index 00000000..1e917b9b
--- /dev/null
+++ b/packages/ui-react/src/components/nft/components/nft-title.tsx
@@ -0,0 +1,19 @@
+import { CardTitle } from "#components/shadcn/card.js";
+import { cn } from "#lib/shadcn/utils.js";
+import React from "react";
+import { useNFT } from "./provider";
+
+export const NFTTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { nftData } = useNFT();
+
+ return (
+
+ {nftData?.metadata?.name}
+
+ );
+});
+
+NFTTitle.displayName = "NFTTitle";
diff --git a/packages/ui-react/src/components/nft/components/provider.tsx b/packages/ui-react/src/components/nft/components/provider.tsx
new file mode 100644
index 00000000..e7dca6e2
--- /dev/null
+++ b/packages/ui-react/src/components/nft/components/provider.tsx
@@ -0,0 +1,45 @@
+import {
+ createContext,
+ useContext,
+ useMemo,
+ type PropsWithChildren,
+} from "react";
+import type { Address } from "viem";
+import type { NFTCardType } from "../types";
+import { useGetSingleNFTMetadata } from "#hooks/data/use-blockscout.js";
+import type { NFTResponse } from "#lib/blockscout/types.js";
+
+export type NFTContextType = {
+ nftData: NFTResponse | undefined;
+ loading: boolean;
+ error: Error | null;
+};
+
+export const NFTContext = createContext(undefined as never);
+
+export const NFTProvider = ({
+ children,
+ contractAddress,
+ tokenId,
+}: PropsWithChildren) => {
+ const {
+ data: nftData,
+ isLoading: loading,
+ error,
+ } = useGetSingleNFTMetadata(contractAddress, tokenId);
+
+ const value = useMemo(
+ () => ({ nftData: nftData, loading, error }),
+ [nftData, loading, error],
+ );
+
+ return {children};
+};
+
+export const useNFT = () => {
+ const context = useContext(NFTContext);
+ if (!context) {
+ throw new Error("useNFT must be used within a NFTProvider");
+ }
+ return context;
+};
diff --git a/packages/ui-react/src/components/nft/nft-card.tsx b/packages/ui-react/src/components/nft/nft-card.tsx
new file mode 100644
index 00000000..adf2e6f3
--- /dev/null
+++ b/packages/ui-react/src/components/nft/nft-card.tsx
@@ -0,0 +1,62 @@
+import { Card } from "#components/shadcn/card.js";
+import { cn } from "#lib/shadcn/utils.js";
+import React, { type HTMLAttributes, type PropsWithChildren } from "react";
+import { NFTProvider } from "./components/provider";
+import type { Address } from "viem";
+import { NFTMedia } from "./components/nft-media";
+import { NFTTitle } from "./components/nft-title";
+import { NFTContent } from "./components/nft-content";
+import { NFTOwner } from "./components/nft-owner";
+
+function NFTCardDefaultContent() {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
+
+interface NFTCardProps extends HTMLAttributes {
+ tokenId: string;
+ contractAddress: string;
+}
+
+export const NFTCard = React.forwardRef<
+ HTMLDivElement,
+ PropsWithChildren
+>(
+ (
+ {
+ tokenId,
+ contractAddress,
+ children = ,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+NFTCard.displayName = "NFTCard";
diff --git a/packages/ui-react/src/components/nft/types.ts b/packages/ui-react/src/components/nft/types.ts
new file mode 100644
index 00000000..c8e212be
--- /dev/null
+++ b/packages/ui-react/src/components/nft/types.ts
@@ -0,0 +1,6 @@
+import type { Address } from "viem";
+
+export type NFTCardType = {
+ contractAddress: Address;
+ tokenId: string;
+};
diff --git a/packages/ui-react/src/components/nft/utils.ts b/packages/ui-react/src/components/nft/utils.ts
new file mode 100644
index 00000000..638dfdb7
--- /dev/null
+++ b/packages/ui-react/src/components/nft/utils.ts
@@ -0,0 +1,3 @@
+export const formatAddress = (address: string) => {
+ return address.slice(0, 6) + "..." + address.slice(-4);
+};
diff --git a/packages/ui-react/src/hooks/data/use-blockscout.ts b/packages/ui-react/src/hooks/data/use-blockscout.ts
index fe80d0cf..a3061e75 100644
--- a/packages/ui-react/src/hooks/data/use-blockscout.ts
+++ b/packages/ui-react/src/hooks/data/use-blockscout.ts
@@ -3,9 +3,11 @@ import { useQuery } from "@tanstack/react-query";
import {
type GetTxnByFilterQuery,
asTransactionMeta,
+ getSingleNFTMetadata,
getTransaction,
getTxnsByFilter,
} from "#lib/blockscout/api";
+import type { Address } from "viem";
export const CACHE_KEY = "blockscout";
@@ -30,3 +32,16 @@ export const useGetTransactions = (query: GetTxnByFilterQuery) => {
},
});
};
+
+export const useGetSingleNFTMetadata = (
+ contractAddress: Address,
+ tokenId: string,
+) => {
+ return useQuery({
+ queryKey: [`${CACHE_KEY}.transactions`, contractAddress, tokenId],
+ queryFn: async () => {
+ const results = await getSingleNFTMetadata(contractAddress, tokenId);
+ return results;
+ },
+ });
+};
diff --git a/packages/ui-react/src/lib/blockscout/api.ts b/packages/ui-react/src/lib/blockscout/api.ts
index 4cf7a618..33618338 100644
--- a/packages/ui-react/src/lib/blockscout/api.ts
+++ b/packages/ui-react/src/lib/blockscout/api.ts
@@ -4,6 +4,7 @@ import type {
} from "@geist/domain/transaction/transaction";
import { type Address, parseUnits } from "viem";
import * as chains from "viem/chains";
+import type { NFTResponse } from "./types";
const chainIdToApiRoot: any = {
[chains.mainnet.id]: "https://eth.blockscout.com/api/",
@@ -82,6 +83,16 @@ export const getTransaction = async (txnHash: string, chainId?: number) => {
return invokeApi(endpoint);
};
+export const getSingleNFTMetadata = async (
+ contractAddress: Address,
+ tokenId: string,
+ chainId?: number,
+) => {
+ return invokeApi(
+ `${chainIdToApiRoot[chainId || chains.mainnet.id]}v2/tokens/${contractAddress}/instances/${tokenId}`,
+ ) as Promise;
+};
+
export interface GetTxnByFilterQuery {
filter?: string;
diff --git a/packages/ui-react/src/lib/blockscout/types.ts b/packages/ui-react/src/lib/blockscout/types.ts
new file mode 100644
index 00000000..93e811bd
--- /dev/null
+++ b/packages/ui-react/src/lib/blockscout/types.ts
@@ -0,0 +1,74 @@
+export interface NFTResponse {
+ is_unique: boolean;
+ id: string;
+ holder_address_hash?: string;
+ image_url?: string;
+ animation_url?: string;
+ external_app_url?: string;
+ metadata?: NFTResponseMetadata;
+ owner: Owner;
+ token: Token;
+}
+
+export interface NFTResponseMetadata {
+ year?: number;
+ tags?: string[];
+ name?: string;
+ image_url?: string;
+ home_url?: string;
+ external_url?: string;
+ description?: string;
+ attributes?: Attribute[];
+}
+
+export interface Attribute {
+ value: string;
+ trait_type: string;
+}
+
+export interface Owner {
+ hash: string;
+ implementation_name: string;
+ name: string;
+ ens_domain_name?: string;
+ metadata?: OwnerMetadata;
+ is_contract: boolean;
+ private_tags: Tag[];
+ watchlist_names: WatchlistName[];
+ public_tags: Tag[];
+ is_verified: boolean;
+}
+
+export interface OwnerMetadata {
+ slug: string;
+ name: string;
+ tagType: string;
+ ordinal: number;
+ meta: Meta;
+}
+
+export interface Meta {}
+
+export interface Tag {
+ address_hash: string;
+ display_name: string;
+ label: string;
+}
+
+export interface WatchlistName {
+ display_name: string;
+ label: string;
+}
+
+export interface Token {
+ circulating_market_cap: string;
+ icon_url: string;
+ name: string;
+ decimals: string;
+ symbol: string;
+ address: string;
+ type: string;
+ holders: string;
+ exchange_rate: string;
+ total_supply: string;
+}