This tutorial explains how the Token API components in the packages/nextjs/app/token-api/_components directory were built using custom hooks and Scaffold-ETH 2 (SE-2) utilities. It covers both traditional token data and NFT data fetching.
The token-api components are built using a layered approach:
- Base API Hook:
useTokenApiserves as the foundation for all API interactions - Specialized Hooks: Custom hooks for specific endpoints (e.g.,
useTokenMetadata,useTokenBalances) - UI Components: React components that consume these hooks to display token data
useTokenApi is the core hook that handles all API communication, providing consistent error handling, loading states, and data formatting. It's designed as a generic hook that:
- Makes fetch requests to the token API proxy endpoint
- Handles loading and error states
- Supports pagination and interval-based refetching
- Normalizes API responses
export const useTokenApi = <DataType, ParamsType = Record<string, any>>(
endpoint: string,
params?: ParamsType,
options: TokenApiOptions = {}
) => {
// State management for data, loading, errors
const [data, setData] = useState<DataType | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
// Fetch implementation and other logic
// ...
return {
data,
isLoading,
error,
refetch: fetchData,
lastUpdated,
};
};Each specialized hook extends useTokenApi for specific API endpoints, adding type safety and specialized logic:
Used in GetMetadata.tsx to fetch detailed information about ERC20 tokens.
export const useTokenMetadata = (
contract: string | undefined,
params?: TokenMetadataParams,
options = { skip: contract ? false : true }
) => {
// Uses useTokenApi internally with endpoint formatting
const result = useTokenApi<any>(
normalizedContract ? `tokens/evm/${normalizedContract}` : "",
{ ...params },
options
);
// Response normalization and formatting
// ...
return {
...result,
data: formattedData,
};
};useTokenBalances: Used inGetBalances.tsxfor retrieving token balancesuseTokenHolders: Used inGetHolders.tsxfor fetching token holder informationuseTokenTransfers: Used inGetTransfers.tsxfor transaction historyuseTokenSwaps: Used inGetSwaps.tsxfor DEX swap datauseTokenPools: Used inGetPools.tsxfor liquidity pool informationuseTokenOHLCByPool: Used inGetOHLCByPool.tsxfor pool price chart datauseTokenOHLCByContract: Used inGetOHLCByContract.tsxfor token price charts
useNFTCollections: Used inGetNFTCollections.tsxfor fetching NFT collection metadatauseNFTItems: Used inGetNFTItems.tsxfor retrieving individual NFT itemsuseNFTOwnerships: Used inGetNFTOwnerships.tsxfor fetching NFT ownership datauseNFTActivities: Used inGetNFTActivities.tsxfor NFT transaction historyuseNFTHolders: Used inGetNFTHolders.tsxfor fetching NFT holder distribution datauseNFTSales: Used inGetNFTSales.tsxfor NFT marketplace sales data
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. 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). 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. 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.
Each component follows a consistent pattern:
- State Management: Using React's
useStatefor form inputs and UI state - Hook Integration: Leveraging the specialized hooks with proper parameters
- Error Handling: Displaying user-friendly error messages
- Loading States: Showing loading indicators during API requests
- Data Rendering: Presenting the fetched data in a structured format
export const GetMetadata = ({ isOpen = true }: { isOpen?: boolean }) => {
// Local state for form inputs and UI
const [contractAddress, setContractAddress] = useState<string>("");
const [selectedNetwork, setSelectedNetwork] =
useState<NetworkId>("mainnet");
const [error, setError] = useState<string | null>(null);
const [shouldFetch, setShouldFetch] = useState<boolean>(false);
// Use the specialized hook
const {
data: tokenData,
isLoading,
error: hookError,
refetch,
} = useTokenMetadata(
contractAddress,
{
network_id: selectedNetwork,
include_market_data: true,
},
{ skip: !shouldFetch } // Skip initial fetch until explicitly triggered
);
// Error handling, UI rendering, etc.
// ...
};The token-api components leverage SE-2's pre-built components for enhanced UX:
- Address: Used to display wallet and contract addresses with ENS support
- AddressInput: Used for user input of Ethereum addresses with validation
- Balance: Used to display token balances in the UI
Example from GetMetadata.tsx:
<AddressInput
value={contractAddress}
onChange={setContractAddress}
placeholder="Enter token contract address"
/>-
GetMetadata.tsx
- Uses
useTokenMetadatato fetch basic token information - Displays token name, symbol, supply, and price data
- Uses
-
GetBalances.tsx
- Uses
useTokenBalancesto fetch account token balances - Displays balance information with proper decimals handling
- Uses
-
GetHolders.tsx
- Uses
useTokenHoldersto fetch top token holders - Implements pagination through the hook's options
- Uses
-
GetTransfers.tsx
- Uses
useTokenTransfersto fetch token transfer history - Implements filtering by wallet and contract addresses
- Uses
-
GetSwaps.tsx
- Uses
useTokenSwapsto fetch DEX swap events - Displays price impact and slippage information
- Uses
-
GetPools.tsx
- Uses
useTokenPoolsto fetch liquidity pool data - Shows TVL and fee tier information
- Uses
-
GetOHLCByPool.tsx and GetOHLCByContract.tsx
- Use
useTokenOHLCByPoolanduseTokenOHLCByContractrespectively - Implement time interval selection and chart rendering
- Support different parameter formats based on API requirements
- Use
-
GetNFTCollections.tsx
- Uses
useNFTCollectionsto fetch NFT collection metadata - Displays collection statistics like total supply, owners, transfers
- Includes contract validation and network selection
- Uses
-
GetNFTItems.tsx
- Uses
useNFTItemsto fetch individual NFT items from a collection - Shows NFT metadata, images, and attributes
- Supports filtering by token ID and pagination
- Uses
-
GetNFTOwnerships.tsx
- Uses
useNFTOwnershipsto fetch NFTs owned by a wallet - Displays owned NFTs with metadata and contract information
- Implements contract filtering and pagination
- Uses
-
GetNFTActivities.tsx
- Uses
useNFTActivitiesto fetch NFT transaction history - Requires contract address - this is a mandatory parameter
- Implements advanced time filtering to prevent database timeouts
- Supports filtering by wallet addresses (from/to/any)
- Uses
-
GetNFTHolders.tsx
- Uses
useNFTHoldersto 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
- Uses
-
GetNFTSales.tsx
- Uses
useNFTSalesto fetch NFT marketplace sales data - Maps
tokenparameter tocontractfor API compatibility - Displays sales information including price and marketplace data
- Uses
The packages/nextjs/app/test/page.tsx component serves as a comprehensive testbed and interactive demonstration for all the custom data fetching hooks provided by the Token API SDK. It allows developers to see each hook in action, modify input parameters, and observe the fetched data directly in the browser's console.
The main goal of TestPage.tsx is to:
- Instantiate every specialized hook (e.g.,
useTokenMetadata,useTokenBalances, etc.). - Provide a user interface to dynamically change the common parameters for these hooks (like network, contract addresses, wallet addresses).
- Automatically fetch data when the component mounts or when these parameters change.
- Log the data, loading states, and errors for each hook to the browser's console, ensuring each piece of data is logged only once upon arrival.
The component utilizes several React useState hooks to manage user inputs and component behavior:
-
selectedNetwork:const [selectedNetwork, setSelectedNetwork] = useState<NetworkId>("mainnet");
Stores the currently selected blockchain network (e.g., "mainnet", "base"). This is used by almost all hooks.
-
contractAddress:const [contractAddress, setContractAddress] = useState<string>( "0xc944E90C64B2c07662A292be6244BDf05Cda44a7" );
Holds the token contract address, used by hooks like
useTokenMetadata,useTokenHolders,useTokenOHLCByContract, and as a filter foruseTokenTransfers. -
walletAddress:const [walletAddress, setWalletAddress] = useState<string>( "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" );
Stores the wallet address, used by
useTokenBalances,useTokenTransfers, anduseHistoricalBalances. -
poolAddress:const [poolAddress, setPoolAddress] = useState<string>( "0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801" );
Contains the liquidity pool address, used by
useTokenOHLCByPoolanduseTokenSwaps. -
historicalBalancesFromTsandhistoricalBalancesToTs:const [historicalBalancesFromTs, setHistoricalBalancesFromTs] = useState< number | undefined >(undefined); const [historicalBalancesToTs, setHistoricalBalancesToTs] = useState< number | undefined >(undefined);
These store the start and end Unix timestamps for the
useHistoricalBalanceshook. They are initialized toundefinedto prevent server-client hydration mismatches and then set in auseEffecthook to default to the last 7 days:useEffect(() => { setHistoricalBalancesFromTs( Math.floor((Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000) ); setHistoricalBalancesToTs(Math.floor(Date.now() / 1000)); }, []);
-
logged:const [logged, setLogged] = useState<Partial<Record<LoggedKeys, boolean>>>( {} );
This state, along with the
LoggedKeystype, is crucial for managing console output. It keeps track of which hooks' data has already been logged to ensure that each piece of data is only output once when it first becomes available.LoggedKeysis a union type of string literals representing each hook (e.g., "metadata", "balances").
All specialized data fetching hooks are initialized in the component. They automatically fetch data when the component mounts and whenever their relevant parameters (derived from the state variables above) change.
Here's a brief overview of how each hook is set up:
-
useTokenMetadata:const { data: metadataData, ... } = useTokenMetadata(contractAddress, { network_id: selectedNetwork });
Fetches metadata for the given
contractAddresson theselectedNetwork. -
useTokenBalances:const { data: balancesData, ... } = useTokenBalances(walletAddress, { network_id: selectedNetwork });
Fetches token balances for the
walletAddresson theselectedNetwork. -
useTokenHolders:const { data: holdersData, ... } = useTokenHolders(contractAddress, { network_id: selectedNetwork });
Fetches a list of token holders for the
contractAddress. -
useTokenTransfers:const { data: transfersData, ... } = useTokenTransfers(walletAddress, { network_id: selectedNetwork, contract: contractAddress, limit: 100 });
Fetches token transfers for the
walletAddress, with an optional filter forcontractAddresson theselectedNetwork. -
useTokenOHLCByPool:const { data: ohlcByPoolData, ... } = useTokenOHLCByPool(poolAddress, { network_id: selectedNetwork, resolution: "1d" });
Fetches OHLC (Open, High, Low, Close) price data for the specified
poolAddress. -
useTokenOHLCByContract:const { data: ohlcByContractData, ... } = useTokenOHLCByContract({ contract: contractAddress, network: selectedNetwork, timeframe: 86400, limit: 100, enabled: true });
Fetches OHLC price data for the
contractAddress. -
useTokenPools:const { data: poolsData, ... } = useTokenPools({ network_id: selectedNetwork, token: contractAddress, page_size: 10 });
Fetches a list of liquidity pools, optionally filtered by
contractAddress(token). -
useTokenSwaps:const { data: swapsData, ... } = useTokenSwaps({ network_id: selectedNetwork, pool: poolAddress, page_size: 10 });
Fetches swap events for the specified
poolAddress. -
useHistoricalBalances:const { data: historicalBalancesData, ... } = useHistoricalBalances(walletAddress, { network_id: selectedNetwork, contract_address: contractAddress, from_timestamp: historicalBalancesFromTs, to_timestamp: historicalBalancesToTs, resolution: "day", });
Fetches historical token balances for the
walletAddress, filtered bycontractAddresswithin the given timestamp range.
To prevent the console from being flooded with logs on every re-render, a series of useEffect hooks are implemented, one for each data-fetching hook. These useEffect hooks depend on the data, loading state, error state, and the logged state object.
Example for useTokenMetadata:
useEffect(() => {
if (!logged.metadata && metadataData !== undefined) {
console.log("useTokenMetadata:", {
data: metadataData,
isLoading: isLoadingMetadata,
error: errorMetadata,
});
setLogged((l) => ({ ...l, metadata: true }));
}
}, [metadataData, isLoadingMetadata, errorMetadata, logged]);Explanation:
- This effect runs when
metadataData,isLoadingMetadata,errorMetadata, orloggedchanges. !logged.metadata: Checks if the metadata has already been logged.metadataData !== undefined: Ensures there's actual data to log.- If both conditions are true, it logs the current data, loading, and error status for
useTokenMetadata. setLogged(l => ({ ...l, metadata: true })): It then updates theloggedstate to mark thatmetadatahas been logged, preventing future logs for the same data untilmetadataDataitself changes to a new defined value (after perhaps an input change and refetch) andlogged.metadatawould have been reset or handled accordingly if re-logging of new data was desired (though in the current setup, it's a one-time log per data arrival).
This pattern is repeated for all other hooks, ensuring a clean and informative console output.
The TestPage.tsx provides a user-friendly interface to modify the parameters for the hooks:
- Network Selector: A dropdown list to select the
NetworkId(e.g., Ethereum, Base, Polygon). Changing this will refetch data for all relevant hooks on the new network. - Address Inputs: Uses Scaffold-ETH's
AddressInputcomponent for:- Contract Address
- Wallet Address
- Pool Address Changing these values will trigger refetches for hooks that depend on them.
- Timestamp Inputs: Standard number input fields for "From Timestamp" and "To Timestamp" (Unix seconds) for the
useHistoricalBalanceshook.
As the page itself doesn't render the fetched data directly in complex tables or charts (to keep the test page focused on hook functionality), users are guided to:
<p className="mt-4 p-4 bg-yellow-100 text-yellow-800 rounded-md">
Check your browser's console (Developer Tools) to see the data, loading
states, and errors for each hook.
</p>The console output will show the structured data, loading status (isLoading), and any errors (error) for each hook as they resolve.
This TestPage.tsx provides a robust way to understand the behavior, request parameters, and response structures of all the Token API SDK hooks.
-
Controlled Fetching: Hooks support an
optionsobject with askipparameter to prevent automatic fetching (e.g.,{ skip: true }). This is useful when waiting for user input, as seen in components likeGetMetadata.tsx. Alternatively, theenabledflag (used byuseTokenOHLCByContract) provides fine-grained control, ensuring fetches only occur when specific conditions are met (like necessary parameters being defined), as demonstrated inTestPage.tsx. -
Error Handling: Providing user-friendly error messages
-
Loading States: Clear loading indicators during data fetching
-
Data Formatting: Consistent number and address formatting
-
Proper Component Composition: Using Scaffold-ETH components like
<Address>for consistent UI
The token-api components demonstrate a well-structured approach to building data-driven React components with:
- A reusable base hook for API communication
- Specialized hooks for different data types
- Consistent UI patterns and error handling
- Integration with Scaffold-ETH 2 components and utilities
- Comprehensive testing through a dedicated test page
This architecture allows for easy addition of new components and features while maintaining code consistency and developer experience.