diff --git a/token-api/token-api-scaffold-eth/README.md b/token-api/token-api-scaffold-eth/README.md index 84dc031..a612442 100644 --- a/token-api/token-api-scaffold-eth/README.md +++ b/token-api/token-api-scaffold-eth/README.md @@ -29,6 +29,7 @@ A comprehensive SDK for interacting with The Graph Token API built on Scaffold-E - [useNFTItems](#usenftitems) - [useNFTOwnerships](#usenftownerships) - [useNFTActivities](#usenftactivities) + - [useNFTHolders](#usenftholders) - [useNFTSales](#usenftSales) - [UI Components](#ui-components) - [Common Patterns](#common-patterns) @@ -255,6 +256,7 @@ export const useTokenApi = >( - **Auto-Refetching**: Supports auto-refreshing with `refetchInterval` - **Error Handling**: Comprehensive error handling and state management - **Manual Refetch**: Provides a function to manually trigger refetches +- **Authentication Error Detection**: Clear guidance for API setup and troubleshooting ### Token API Hooks @@ -973,6 +975,46 @@ interface NFTActivity { } ``` +#### useNFTHolders + +Fetches NFT holder information for a specific contract. + +**Location**: `packages/nextjs/app/token-api/_hooks/useNFTHolders.ts` + +```typescript +export function useNFTHolders(options: UseNFTHoldersOptions) { + const { contractAddress, network = "mainnet", enabled = true } = options; + + const normalizedContractAddress = normalizeContractAddress(contractAddress); + const endpoint = `nft/holders/evm/${normalizedContractAddress}`; + + return useTokenApi( + endpoint, + { network_id: network }, + { skip: !normalizedContractAddress || !enabled } + ); +} +``` + +**Parameters**: + +- `contractAddress`: NFT contract address (required) +- `network`: Network identifier (default: "mainnet") +- `enabled`: Whether to enable the query (default: true) + +**Response Type**: + +```typescript +interface NFTHolder { + token_standard: string; // ERC721, ERC1155, etc. + address: string; // Holder's wallet address + quantity: number; // Number of tokens held + unique_tokens: number; // Number of unique tokens held + percentage: number; // Percentage of total supply held + network_id: NetworkId; +} +``` + #### useNFTSales Fetches NFT sales/marketplace data. @@ -1067,6 +1109,7 @@ The SDK includes UI components for each data type: - **GetNFTItems**: Shows individual NFTs from a collection with metadata - **GetNFTOwnerships**: Lists NFTs owned by a wallet address - **GetNFTActivities**: Displays NFT transaction history with advanced filtering +- **GetNFTHolders**: Shows NFT holder information and distribution statistics - **GetNFTSales**: Shows NFT marketplace sales data Example usage: @@ -1076,6 +1119,7 @@ import { GetMetadata } from "~~/app/token-api/_components/GetMetadata"; import { GetBalances } from "~~/app/token-api/_components/GetBalances"; import { GetNFTCollections } from "~~/app/token-api/_components/GetNFTCollections"; import { GetNFTOwnerships } from "~~/app/token-api/_components/GetNFTOwnerships"; +import { GetNFTHolders } from "~~/app/token-api/_components/GetNFTHolders"; export default function YourPage() { return ( @@ -1095,6 +1139,7 @@ export default function YourPage() { {/* NFT Components */} + ); } @@ -1360,8 +1405,26 @@ The SDK covers the following Token API endpoints: - `/nft/items/evm/{contract}` - NFT items - `/nft/ownerships/evm/{address}` - NFT ownerships - `/nft/activities/evm` - NFT activities +- `/nft/holders/evm/{contract}` - NFT holders - `/nft/sales/evm` - NFT sales +## Recent Updates & Fixes + +**Major Improvements in Latest Version:** + +- **Authentication Fix**: All APIs now properly handle authentication with comprehensive error messages +- **Data Structure Normalization**: Hooks return arrays directly (`NFTCollection[]`) instead of nested response objects +- **NFT Activities Requirements**: Contract address is now a required parameter with proper validation +- **Time Filtering**: Automatic 30-day time ranges prevent database timeouts on popular contracts +- **Interface Completeness**: Added missing fields like `token_standard` and `total_unique_supply` to NFT interfaces +- **Error Handling**: Enhanced error detection for authentication, validation, and timeout issues + +**Breaking Changes:** + +- Hook return types changed from `{data: T[]}` to `T[]` directly +- NFT Activities API requires `contract_address` parameter (no longer optional) +- Time filtering now uses default ranges to prevent timeouts + ## Troubleshooting ### Authentication Issues @@ -1385,6 +1448,7 @@ The SDK covers the following Token API endpoints: - This was resolved in recent updates where hooks now return data arrays directly - Ensure you're using the latest version of the hooks - Check that components use `Array.isArray(data)` instead of `data?.data` +- All hooks now return proper array types (e.g., `NFTCollection[]`, `NFTItem[]`) instead of wrapper objects ### NFT Activities API Issues @@ -1394,6 +1458,7 @@ The SDK covers the following Token API endpoints: - The NFT Activities API requires a contract address parameter - Ensure you provide a valid NFT contract address (not just a wallet address) - Use time filters to prevent database timeouts on popular contracts +- Contract address is now a required parameter with proper validation ### Database Timeout Issues @@ -1403,6 +1468,7 @@ The SDK covers the following Token API endpoints: - Add time filters (startTime and endTime) to your queries - Use the provided time range buttons (Last 24h, Last 7 days, etc.) - Popular NFT contracts like BAYC require time filtering to avoid timeouts +- Default 30-day time ranges are now automatically applied to prevent timeouts ### Network Connection Issues diff --git a/token-api/token-api-scaffold-eth/TUTORIAL.MD b/token-api/token-api-scaffold-eth/TUTORIAL.MD index 7299687..bdb211b 100644 --- a/token-api/token-api-scaffold-eth/TUTORIAL.MD +++ b/token-api/token-api-scaffold-eth/TUTORIAL.MD @@ -88,19 +88,24 @@ export const useTokenMetadata = ( - `useNFTCollections`: Used in `GetNFTCollections.tsx` for fetching NFT collection metadata - `useNFTItems`: Used in `GetNFTItems.tsx` for retrieving individual NFT items -- `useNFTOwnerships`: Used in `GetNFTOwnerships.tsx` for fetching NFT ownership data -- `useNFTActivities`: Used in `GetNFTActivities.tsx` for NFT transaction history -- `useNFTSales`: Used in `GetNFTSales.tsx` for NFT marketplace sales data + - `useNFTOwnerships`: Used in `GetNFTOwnerships.tsx` for fetching NFT ownership data + - `useNFTActivities`: Used in `GetNFTActivities.tsx` for NFT transaction history + - `useNFTHolders`: Used in `GetNFTHolders.tsx` for fetching NFT holder distribution data + - `useNFTSales`: Used in `GetNFTSales.tsx` for NFT marketplace sales data ### Key Improvements Made **Data Structure Fix**: All hooks now return arrays directly (e.g., `NFTCollection[]`) instead of response wrapper objects, eliminating the need for components to access `data?.data`. -**Authentication Error Handling**: Enhanced error detection and user guidance for API authentication issues. +**Authentication Error Handling**: Enhanced error detection and user guidance for API authentication issues. The Graph Token API requires proper `NEXT_PUBLIC_GRAPH_TOKEN` configuration. -**Parameter Validation**: Added proper validation for required parameters (e.g., contract addresses for NFT Activities). +**Parameter Validation**: Added proper validation for required parameters (e.g., contract addresses for NFT Activities). The NFT Activities API specifically requires a contract address and cannot work with just wallet addresses. -**Time Filtering**: Implemented advanced time filtering with quick-select buttons to prevent database timeouts on popular contracts. +**Time Filtering**: Implemented advanced time filtering with quick-select buttons to prevent database timeouts on popular contracts. Default 30-day ranges are now automatically applied. + +**Interface Completeness**: Added missing fields like `token_standard`, `total_unique_supply` to NFT collection interfaces, ensuring complete data structure coverage. + +**Contract Address Normalization**: Implemented proper address cleaning and normalization functions to handle various input formats. ## Component Implementation Pattern @@ -227,7 +232,14 @@ Example from GetMetadata.tsx: - Implements advanced time filtering to prevent database timeouts - Supports filtering by wallet addresses (from/to/any) -12. **GetNFTSales.tsx** +12. **GetNFTHolders.tsx** + + - Uses `useNFTHolders` to fetch NFT holder information for a contract + - Displays holder addresses, quantities, and percentage distribution + - Shows token standards (ERC721, ERC1155) and unique token counts + - Includes proper address normalization and contract validation + +13. **GetNFTSales.tsx** - Uses `useNFTSales` to fetch NFT marketplace sales data - Maps `token` parameter to `contract` for API compatibility - Displays sales information including price and marketplace data diff --git a/token-api/token-api-scaffold-eth/WORKSHOP.MD b/token-api/token-api-scaffold-eth/WORKSHOP.MD index ff38151..811d3f5 100644 --- a/token-api/token-api-scaffold-eth/WORKSHOP.MD +++ b/token-api/token-api-scaffold-eth/WORKSHOP.MD @@ -39,6 +39,7 @@ This workshop guides you through recreating a `WorkshopPage`. This component ser useNFTItems, useNFTOwnerships, useNFTActivities, + useNFTHolders, useNFTSales, } from "~~/app/token-api/_hooks"; import type { NetworkId } from "~~/app/token-api/_types"; @@ -121,6 +122,7 @@ type LoggedKeys = | "nftItems" | "nftOwnerships" | "nftActivities" + | "nftHolders" | "nftSales"; const [logged, setLogged] = useState>>({}); ``` @@ -277,6 +279,17 @@ const { : null ); +// --- NFTHolders Hook --- +const { + data: nftHoldersData, + isLoading: isLoadingNFTHolders, + error: errorNFTHolders, +} = useNFTHolders({ + contractAddress: contractAddress, + network: selectedNetwork, + enabled: timestampsReady, +}); + // --- NFTSales Hook --- const { data: nftSalesData, @@ -387,6 +400,17 @@ useEffect(() => { } }, [nftActivitiesData, isLoadingNFTActivities, errorNFTActivities, logged]); +useEffect(() => { + if (!logged.nftHolders && nftHoldersData !== undefined) { + console.log("useNFTHolders:", { + data: nftHoldersData, + isLoading: isLoadingNFTHolders, + error: errorNFTHolders, + }); + setLogged((l) => ({ ...l, nftHolders: true })); + } +}, [nftHoldersData, isLoadingNFTHolders, errorNFTHolders, logged]); + useEffect(() => { if (!logged.nftSales && nftSalesData !== undefined) { console.log("useNFTSales:", { @@ -563,10 +587,20 @@ This workshop provides a solid foundation for integrating both token and NFT hoo **Key Takeaways:** -- Token hooks work with traditional ERC20 data (balances, transfers, prices) -- NFT hooks provide comprehensive NFT functionality (collections, items, ownerships, activities, sales) -- Some NFT hooks (like NFT Activities) require specific parameters (contract addresses) -- Time filtering is crucial for preventing database timeouts on popular contracts -- All hooks now return arrays directly, eliminating data structure confusion +- **Token hooks** work with traditional ERC20 data (balances, transfers, prices) +- **NFT hooks** provide comprehensive NFT functionality (collections, items, ownerships, activities, sales) +- **Authentication is required**: Ensure `NEXT_PUBLIC_GRAPH_TOKEN` is set in your `.env.local` file +- **NFT Activities API requirements**: Contract address is mandatory - cannot work with just wallet addresses +- **Time filtering prevents timeouts**: Default 30-day ranges are automatically applied for popular contracts +- **Data structure simplified**: All hooks now return arrays directly (`NFTCollection[]`), eliminating confusion +- **Error handling improved**: Clear messages for authentication, validation, and timeout issues + +**Common Issues Resolved:** + +- Authentication 401 errors → Set proper Graph API token +- "No NFT collections found" → Fixed authentication and data processing +- Database timeouts → Implemented automatic time filtering +- Validation errors → Added required parameter validation +- Data structure confusion → Hooks return arrays directly Remember to consult the hook definitions and `_types` directory for detailed parameter options and response structures. Check the main README.md for troubleshooting common issues like authentication errors and database timeouts. diff --git a/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_components/GetNFTHolders.tsx b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_components/GetNFTHolders.tsx new file mode 100644 index 0000000..26453ea --- /dev/null +++ b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_components/GetNFTHolders.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { EVM_NETWORKS, getNetworkName } from "~~/app/token-api/_config/networks"; +import { NFTHolder, useNFTHolders } from "~~/app/token-api/_hooks/useNFTHolders"; +import { NetworkId } from "~~/app/token-api/_hooks/useTokenApi"; +import { AddressInput } from "~~/components/scaffold-eth"; + +export const GetNFTHolders = ({ isOpen = true }: { isOpen?: boolean }) => { + const [contractAddress, setContractAddress] = useState(""); + const [selectedNetwork, setSelectedNetwork] = useState("mainnet"); + const [holders, setHolders] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [shouldFetch, setShouldFetch] = useState(false); + const processingData = useRef(false); + + const { + data, + isLoading: apiLoading, + error: apiError, + refetch, + } = useNFTHolders({ + contractAddress: contractAddress, + network: selectedNetwork, + enabled: shouldFetch, + }); + + useEffect(() => { + if (apiLoading) { + setIsLoading(true); + setError(null); + } else { + setIsLoading(false); + } + }, [apiLoading]); + + useEffect(() => { + if (!data || processingData.current) return; + processingData.current = true; + + try { + console.log("👥 Received NFT holders data from hook:", data); + // The useTokenApi hook already extracts the data array from the API response + if (Array.isArray(data) && data.length > 0) { + setHolders(data); + console.log("✅ Successfully processed NFT holders:", data.length, "holders found"); + } else { + console.log("No valid holders data found in response. Data structure:", typeof data, data); + setHolders([]); + } + } catch (err) { + console.error("Error processing NFT holders data:", err); + setHolders([]); + } finally { + setTimeout(() => { + processingData.current = false; + }, 100); + } + }, [data]); + + useEffect(() => { + if (!apiError) { + setError(null); + return; + } + console.error("❌ API error from hook (NFT Holders):", apiError); + setError(typeof apiError === "string" ? apiError : (apiError as Error)?.message || "Failed to fetch NFT holders"); + }, [apiError]); + + const handleNetworkChange = (newNetwork: string) => { + setSelectedNetwork(newNetwork as NetworkId); + setHolders([]); + setError(null); + setShouldFetch(false); + }; + + const fetchHolders = async () => { + if (!contractAddress) { + setError("Please enter a contract address"); + setIsLoading(false); + return; + } + setError(null); + setHolders([]); + setIsLoading(true); + processingData.current = false; + setShouldFetch(true); + }; + + const formatPercentage = (percentage: number): string => { + return `${(percentage * 100).toFixed(4)}%`; + }; + + const formatQuantity = (quantity: number): string => { + return quantity.toLocaleString(); + }; + + return ( +
+ +
+ 👥 NFT Holders - Get holders for a contract +
+
+
+
+
+
+
+
+ + +

+ e.g., 0xBd3531dA5CF5857e7CfAA92426877b022e612cf8 (Pudgy Penguins) +

+
+
+ + +
+
+
+ +
+
+
+ + {isLoading && ( +
+ + + Loading NFT holders for {contractAddress} on {getNetworkName(selectedNetwork)}... + +
+ )} + + {error && ( +
+ Error: {error} +
+ )} + + {!isLoading && !error && holders.length > 0 && ( +
+
+

NFT Holders ({holders.length} found):

+
+ + + + + + + + + + + + + + {holders.map((holder, index) => ( + + + + + + + + + + ))} + +
#Holder AddressToken StandardQuantityUnique TokensPercentageNetwork
{index + 1} + {}} placeholder="Holder Address" /> + +
{holder.token_standard}
+
+
{formatQuantity(holder.quantity)}
+
+
{formatQuantity(holder.unique_tokens)}
+
+
{formatPercentage(holder.percentage)}
+
+
{getNetworkName(holder.network_id)}
+
+
+
+
+ )} + + {!isLoading && !error && holders.length === 0 && shouldFetch && ( +
+ No holders found for the specified contract on {getNetworkName(selectedNetwork)}. +
+ )} +
+
+
+ ); +}; diff --git a/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/index.ts b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/index.ts index 8a9067c..bcde750 100644 --- a/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/index.ts +++ b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/index.ts @@ -9,6 +9,7 @@ export * from "./useTokenSwaps"; export { useTokenTransfers } from "./useTokenTransfers"; export { useHistoricalBalances } from "./useHistoricalBalances"; export * from "./useNFTCollections"; +export * from "./useNFTHolders"; export * from "./useNFTItems"; export * from "./useNFTActivities"; export * from "./useNFTOwnerships"; diff --git a/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/useNFTHolders.ts b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/useNFTHolders.ts new file mode 100644 index 0000000..f868130 --- /dev/null +++ b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/_hooks/useNFTHolders.ts @@ -0,0 +1,119 @@ +"use client"; + +/** + * Hook for fetching NFT holders data from The Graph Token API + * + * IMPORTANT: This hook requires authentication to work properly. + * + * Setup Instructions: + * 1. Get your API token from https://thegraph.com/market/ + * 2. Create a .env.local file in packages/nextjs/ directory + * 3. Add: NEXT_PUBLIC_GRAPH_TOKEN=your_token_here + * + * Alternative: Use API Key instead of token: + * - Add: NEXT_PUBLIC_GRAPH_API_KEY=your_api_key_here + * + * Without proper authentication, you'll get 401 unauthorized errors. + */ +import type { NetworkId } from "./useTokenApi"; +import { useTokenApi } from "./useTokenApi"; + +export interface NFTHolder { + token_standard: string; // ERC721, ERC1155, etc. + address: string; // Holder's wallet address + quantity: number; // Number of tokens held + unique_tokens: number; // Number of unique tokens held + percentage: number; // Percentage of total supply held + network_id: NetworkId; +} + +export interface NFTHoldersResponse { + data: NFTHolder[]; + statistics?: { + elapsed?: number; + rows_read?: number; + bytes_read?: number; + }; +} + +export interface UseNFTHoldersOptions { + contractAddress: string; // NFT contract address + network?: NetworkId; + enabled?: boolean; +} + +/** + * Normalize contract address to ensure proper format + */ +function normalizeContractAddress(address: string): string { + if (!address) return address; + + // Remove any whitespace + const trimmed = address.trim(); + + // Ensure it starts with 0x and is lowercase + if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) { + return trimmed.toLowerCase(); + } else { + return `0x${trimmed.toLowerCase()}`; + } +} + +/** + * React hook to get NFT holders for a specific contract + */ +export function useNFTHolders(options: UseNFTHoldersOptions) { + const { contractAddress, network = "mainnet", enabled = true } = options; + + const normalizedContractAddress = normalizeContractAddress(contractAddress); + + const endpoint = `nft/holders/evm/${normalizedContractAddress}`; + + const result = useTokenApi( + endpoint, + { + network_id: network, + }, + { + // Skip if contractAddress is not provided, or if the hook is disabled + skip: !normalizedContractAddress || !enabled, + }, + ); + + // Enhanced debugging and error handling + if (normalizedContractAddress && !result.isLoading) { + const isWellKnownContract = + normalizedContractAddress.toLowerCase() === "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb"; // CryptoPunks + + if (result.error) { + console.error(`🔴 Error fetching NFT holders for ${normalizedContractAddress}:`, result.error); + + // Specific error handling for authentication issues + if (result.error.includes("401") || result.error.includes("unauthorized")) { + console.error(`🔑 Authentication error detected. Please check your Graph API credentials: + - Set NEXT_PUBLIC_GRAPH_TOKEN or NEXT_PUBLIC_GRAPH_API_KEY in your .env.local file + - Get your API token from https://thegraph.com/market/`); + } + } else if (!result.data || (Array.isArray(result.data) && result.data.length === 0)) { + console.log(`🔍 No data returned for contract ${normalizedContractAddress}:`, { + endpoint, + network, + enabled, + isWellKnownContract, + skip: !normalizedContractAddress || !enabled, + }); + + if (isWellKnownContract) { + console.warn(`⚠️ This is a well-known contract (CryptoPunks) that should return data. + This might indicate an authentication or network issue.`); + } + } else if (result.data && Array.isArray(result.data) && result.data.length > 0) { + console.log( + `✅ Successfully fetched NFT holders data for ${normalizedContractAddress}:`, + `${result.data.length} holders found`, + ); + } + } + + return result; +} diff --git a/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/page.tsx b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/page.tsx index 37f430d..2f7fcf6 100644 --- a/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/page.tsx +++ b/token-api/token-api-scaffold-eth/packages/nextjs/app/token-api/page.tsx @@ -6,6 +6,7 @@ import { GetHolders } from "./_components/GetHolders"; import { GetMetadata } from "./_components/GetMetadata"; import { GetNFTActivities } from "./_components/GetNFTActivities"; import { GetNFTCollections } from "./_components/GetNFTCollections"; +import { GetNFTHolders } from "./_components/GetNFTHolders"; import { GetNFTItems } from "./_components/GetNFTItems"; import { GetNFTOwnerships } from "./_components/GetNFTOwnerships"; import { GetNFTSales } from "./_components/GetNFTSales"; @@ -58,6 +59,7 @@ export default function TokenAPI() {

NFT API Showcase

+