The decentralized finance (DeFi) ecosystem has grown exponentially, with platforms like Poocoin, DexTools, and DexScreener emerging as indispensable tools for traders. These platforms provide real-time token analytics, liquidity tracking, and price monitoring, enabling users to make informed trading decisions. But have you ever wondered how these platforms work under the hood?
In this tutorial, we’ll explore the mechanics of these tools by building a simplified version using a Next.js TypeScript application and ethers.js. We’ll implement a token service that interacts with Ethereum smart contracts to fetch token data, liquidity pool information, and price details.
Note: This tutorial focuses on a basic implementation to help you understand the core concepts. Platforms like DexTools and DexScreener use advanced backend infrastructure to deliver their services, while Poocoin largely operates directly through frontend integrations. The OpenDeFi repository at github.com/ivesfurtado/opendefi provides access to more sophisticated services, including several features that go beyond the scope of this guide.
View the live app at opendefi.cc. (it might be more updated than what you will learn in this tutorial, star and follow the github repository 🙂 ).
Understanding Poocoin, DexTools, and DexScreener
Before diving into the implementation, let’s break down the core features of these platforms:
Poocoin
- Focuses on tracking tokens on Binance Smart Chain (BSC).
- Provides real-time charts, token analytics, and wallet tracking.
- Highlights liquidity pool details and token burn statistics.
DexTools
- A multi-chain analytics platform for DEX trading.
- Offers advanced charting tools, token pair explorers, and wallet monitoring.
- Integrates with Uniswap, PancakeSwap, and other major DEXs.
DexScreener
- Specializes in tracking token prices across multiple chains.
- Displays liquidity pool data, token swaps, and price movements in real-time.
- Provides an intuitive interface for discovering trending tokens.
These platforms rely heavily on blockchain data fetched from smart contracts. By leveraging tools like ethers.js
and multicall providers, we can replicate their functionality in our own application.
Prerequisites
Ensure you have the following tools and knowledge:
Development Environment
- Node.js (v18 or later)
- pnpm (v8 or later) for package management
- Visual Studio Code
- Git for version control
Required Knowledge
- Basic understanding of blockchain concepts and Ethereum
- Familiarity with TypeScript and React
- Understanding of smart contracts and ERC-20 tokens
- Experience with Next.js and API development
Project Setup
First, create your project directory and initialize it:
pnpm create next-app opendefi --template typescript && cd opendefi
Install core dependencies:
pnpm add ethers@^6.0.0 @ethers-ext/provider-multicall
npx shadcn@latest init
npx shadcn@latest add input button card alert table skeleton
Project Structure
Your project should follow this structure:
opendefi/
├── src/
│ ├── services/
│ │ ├── token-service.ts # Uniswap token service
│ │ └── constants.ts # Contract addresses & ABIs
│ ├── types/
│ │ └── token.ts # Token interfaces
│ └── app/
│ ├── api/
│ │ └── token/
│ │ └── route.ts # Token API endpoint
│ └── page.tsx # Main page
├── package.json
└── tsconfig.json
Environment Setup
Create a .env.local
file:
NEXT_PUBLIC_RPC_URL=https://go.getblock.io/YOUR_API_KEY
You can create a free RPC at GetBlocks.
AI Integration Tips
To enhance your development process:
- Use GitHub Copilot for smart contract interactions and TypeScript types
- Leverage ChatGPT for debugging complex blockchain interactions
- Utilize AI tools for code optimization and security analysis
Or read about Boost Your Skills with AI: Become a 10x Engineer.
Blockchain Resources
Before implementing the token service, familiarize yourself with:
- Uniswap V2 documentation and smart contracts
- ERC-20 token standard and implementations
- Ethereum JSON-RPC API specifications
- Web3 security best practices
Now that we’ve explored the features of Poocoin, DexTools, DexScreener and set up the initial config, let’s dive into building our own token service. We’ll use Next.js and TypeScript to replicate core functionalities, giving you a hands-on experience in DeFi development.
Building the Token Service
To implement the core functionality of these platforms, we’ll create a UniswapTokenService class that interacts with Ethereum smart contracts to fetch token information. This service will include:
- Token Metadata: Basic token information, such as name, symbol, and decimal values.
- Liquidity Pool Details: Reserves and associated pair addresses.
- Price Information: USD valuations of tokens.
- Real supply: Burn address and zero address balances.
Key Components of the Token Service
MulticallProvider
Efficiently batches multiple blockchain calls into a single request to reduce latency and costs.
ERC20 Token Contract
Fetches token-specific data such as name, symbol, decimals, total supply, and balances.
Uniswap Factory Contract
Retrieves liquidity pools for token pairs.
Price Calculation
Determines token prices based on WETH/USD pair reserves.
Here is the constants you need to add.
export const UNISWAP_V2_CONSTANTS = {
FACTORY_ADDRESS: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
ROUTER_ADDRESS: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
WETH_ADDRESS: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
};
export const FACTORY_ABI = [
"function getPair(address tokenA, address tokenB) external view returns (address pair)",
"function allPairs(uint) external view returns (address pair)",
"function allPairsLength() external view returns (uint)",
];
export const PAIR_ABI = [
"function token0() external view returns (address)",
"function token1() external view returns (address)",
"function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
"function price0CumulativeLast() external view returns (uint)",
"function price1CumulativeLast() external view returns (uint)",
];
export const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
];
Below is the full implementation of the UniswapTokenService
class:
import { MulticallProvider } from "@ethers-ext/provider-multicall";
import { ethers } from "ethers";
import { ERC20_ABI, FACTORY_ABI, PAIR_ABI, UNISWAP_V2_CONSTANTS } from "./constants";
export interface TokenInfo {
name: string;
symbol: string;
address: string;
decimals: number;
priceUSD: number;
burnAddressBalance: string;
zeroAddressBalance: string;
totalSupply: string;
pools: LiquidityPool[];
}
export interface LiquidityPool {
pairAddress: string;
pairSymbol: string;
token0: { address: string; symbol: string; decimals: number };
token1: { address: string; symbol: string; decimals: number };
reserve0: string;
reserve1: string;
liquidityUSD: number;
}
export class UniswapTokenService {
private factory: ethers.Contract;
private multiCallProvider: MulticallProvider;
private wethPriceUSD = 0;
private lastWETHPriceUpdate = 0;
private readonly WETH_PRICE_REFRESH_INTERVAL = 300000; // 5 minutes
private tokenConstsCache = new Map<string, TokenInfo>();
constructor(private readonly provider: ethers.JsonRpcProvider) {
this.factory = new ethers.Contract(UNISWAP_V2_CONSTANTS.FACTORY_ADDRESS, FACTORY_ABI, provider);
this.multiCallProvider = new MulticallProvider(provider);
}
public async getTokenInformation(tokenAddress: string): Promise<TokenInfo> {
try {
const tokenInfo = await this.getTokenBasicInfo(tokenAddress);
if (!tokenInfo) {
throw new Error(`Token with address ${tokenAddress} not found.`);
}
await this.updateWETHPrice();
tokenInfo.pools = await this.findLiquidityPools(tokenInfo);
tokenInfo.priceUSD = await this.calculateTokenPrice(tokenInfo);
return tokenInfo;
} catch (error) {
throw new Error(`Failed to fetch token information for ${tokenAddress}: ${(error as Error).message}`);
}
}
private async getTokenBasicInfo(tokenAddress: string): Promise<TokenInfo | undefined> {
if (this.tokenConstsCache.has(tokenAddress)) {
const token = this.tokenConstsCache.get(tokenAddress);
if (!token) return undefined;
return {...token};
}
const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, this.multiCallProvider);
if (
!tokenContract.name ||
!tokenContract.symbol ||
!tokenContract.decimals ||
!tokenContract.totalSupply ||
!tokenContract.balanceOf
) {
console.warn("Token contract not initialized.");
return undefined;
}
const [name, symbol, decimals, totalSupply, zeroAddressBalance, burnAddressBalance] = await Promise.all([
tokenContract.name(),
tokenContract.symbol(),
tokenContract.decimals(),
tokenContract.totalSupply(),
tokenContract.balanceOf(ethers.ZeroAddress),
tokenContract.balanceOf("0x000000000000000000000000000000000000dEaD"),
]);
const tokenInfo = {
address: tokenAddress,
name,
symbol,
decimals: Number(decimals),
totalSupply: ethers.formatUnits(totalSupply.toString(), decimals),
zeroAddressBalance: ethers.formatUnits(zeroAddressBalance.toString(), decimals),
burnAddressBalance: ethers.formatUnits(burnAddressBalance.toString(), decimals),
priceUSD: 0,
pools: [],
};
this.tokenConstsCache.set(tokenAddress, tokenInfo);
return tokenInfo;
}
private async getPair(tokenA: string, tokenB: string): Promise<string> {
if (!this.factory.getPair) {
console.warn("Factory contract not initialized.");
return ethers.ZeroAddress;
}
return this.factory.getPair(tokenA, tokenB);
}
private async updateWETHPrice(): Promise<void> {
if (Date.now() - this.lastWETHPriceUpdate < this.WETH_PRICE_REFRESH_INTERVAL) return;
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const usdcWethPair = await this.getPair(UNISWAP_V2_CONSTANTS.WETH_ADDRESS, USDC_ADDRESS);
if (!usdcWethPair || usdcWethPair === ethers.ZeroAddress) {
console.warn("USDC-WETH pair not found.");
return;
}
const pairContract = new ethers.Contract(usdcWethPair, PAIR_ABI, this.multiCallProvider);
if (!pairContract.token0 || !pairContract.getReserves) {
console.warn("Pair contract not initialized.");
return;
}
const [token0, reserves] = await Promise.all([pairContract.token0(), pairContract.getReserves()]);
const [reserve0, reserve1] = reserves.map((reserve: string) => Number(reserve));
const isWETHToken0 = token0.toLowerCase() === UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase();
this.wethPriceUSD = isWETHToken0
? (Number(reserve1) * 1e12) / Number(reserve0)
: (Number(reserve0) * 1e12) / Number(reserve1);
this.lastWETHPriceUpdate = Date.now();
}
private async findLiquidityPools(tokenInfo: TokenInfo): Promise<LiquidityPool[]> {
const mainTokens = [
UNISWAP_V2_CONSTANTS.WETH_ADDRESS,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
"0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
];
try {
const poolPromises = mainTokens.map((mainToken) => this.getPoolInfo(tokenInfo.address, mainToken));
const pools = (await Promise.all(poolPromises)).filter((pool) => pool !== null) as LiquidityPool[];
return pools.sort((a, b) => b.liquidityUSD - a.liquidityUSD);
} catch (error) {
console.error("Error finding liquidity pools:", (error as Error).message);
return [];
}
}
private async getPoolInfo(tokenA: string, tokenB: string): Promise<LiquidityPool | null> {
try {
const pairAddress = await this.getPair(tokenA, tokenB);
if (!pairAddress || pairAddress === ethers.ZeroAddress) return null;
const pairContract = new ethers.Contract(pairAddress, PAIR_ABI, this.multiCallProvider);
if (!pairContract.token0 || !pairContract.getReserves) {
console.error(`Pair contract not initialized for pair ${pairAddress}`);
return null;
}
const [token0Addr, reserves] = await Promise.all([pairContract.token0(), pairContract.getReserves()]);
const isTokenAFirst = tokenA.toLowerCase() === token0Addr.toLowerCase();
const token0Info = await this.getTokenBasicInfo(token0Addr);
const token1Info = await this.getTokenBasicInfo(isTokenAFirst ? tokenB : tokenA);
if (!token0Info || !token1Info) {
console.error(`Token info not found for pair ${pairAddress}`);
return null;
}
const reserve0Formatted = ethers.formatUnits(reserves[0], token0Info.decimals);
const reserve1Formatted = ethers.formatUnits(reserves[1], token1Info.decimals);
const liquidityUSD = await this.calculatePoolLiquidityUSD(
reserve0Formatted,
reserve1Formatted,
isTokenAFirst ? tokenA : tokenB,
isTokenAFirst ? tokenB : tokenA,
);
return {
pairAddress,
pairSymbol: `${isTokenAFirst ? token1Info.symbol : token0Info.symbol}`,
token0: token0Info,
token1: token1Info,
reserve0: reserve0Formatted,
reserve1: reserve1Formatted,
liquidityUSD,
};
} catch (error) {
console.error(`Error fetching pool info for ${tokenA} and ${tokenB}:`, (error as Error).message);
return null;
}
}
private async calculatePoolLiquidityUSD(
reserveAFormatted: string,
reserveBFormatted: string,
referenceTokenAddr: string,
baseTokenAddr: string,
): Promise<number> {
if (referenceTokenAddr.toLowerCase() === UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase()) {
return Number.parseFloat(reserveAFormatted) * this.wethPriceUSD;
}
if (baseTokenAddr.toLowerCase() === UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase()) {
return Number.parseFloat(reserveBFormatted) * this.wethPriceUSD;
}
return Number.parseFloat(reserveBFormatted);
}
private async calculateTokenPrice(tokenInfo: TokenInfo): Promise<number> {
if (tokenInfo.address.toLowerCase() === UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase()) {
return this.wethPriceUSD;
}
const wethPairPool = tokenInfo.pools.find((pool) =>
[pool.token0.address.toLowerCase(), pool.token1.address.toLowerCase()].includes(
UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase(),
),
);
if (!wethPairPool) return 0;
const isTokenInPositionZero = wethPairPool.token0.address.toLowerCase() === tokenInfo.address.toLowerCase();
const [tokenReserveFormatted, wethReserveFormatted] = isTokenInPositionZero
? [wethPairPool.reserve0, wethPairPool.reserve1]
: [wethPairPool.reserve1, wethPairPool.reserve0];
return (Number.parseFloat(wethReserveFormatted) / Number.parseFloat(tokenReserveFormatted)) * this.wethPriceUSD;
}
}
Understanding the Uniswap Token Service Code
The Uniswap Token Service is a practical JavaScript class that helps you get detailed information about ERC-20 tokens on Uniswap. By using ethers.js
for blockchain interactions, this service pulls crucial data such as token price, supply, and liquidity pool details. Let’s walk through how it all works and why certain techniques are used to make it efficient and accurate.
Retrieving Token Information
The method that ties everything together is getTokenInformation
. It’s responsible for gathering a token’s key details, figuring out its price, and identifying related liquidity pools.
public async getTokenInformation(tokenAddress: string): Promise<TokenInfo> {
try {
const tokenInfo = await this.getTokenBasicInfo(tokenAddress);
if (!tokenInfo) {
throw new Error(`Token with address ${tokenAddress} not found.`);
}
await this.updateWETHPrice();
tokenInfo.pools = await this.findLiquidityPools(tokenInfo);
tokenInfo.priceUSD = await this.calculateTokenPrice(tokenInfo);
return tokenInfo;
} catch (error) {
throw new Error(`Failed to fetch token information for ${tokenAddress}: ${(error as Error).message}`);
}
}
This method synthesizes data from various sources:
- Basic Information:
getTokenBasicInfo
fetches essential details, including the token’s name, symbol, decimals, and total supply. - Liquidity Pools: The service identifies liquidity pools pairing the token with significant assets such as WETH or USDC.
- Token Price: The token’s USD price is calculated through its correlation with WETH, a standard reference in Uniswap.
The Efficiency of Multicall
The service uses multicall
to group multiple contract calls into one, saving time and resources. This is especially helpful when fetching data like the token’s name, symbol, and total supply all at once:
private async getTokenBasicInfo(tokenAddress: string): Promise<TokenInfo | undefined> {
if (this.tokenConstsCache.has(tokenAddress)) {
const token = this.tokenConstsCache.get(tokenAddress);
if (!token) return undefined;
return {...token};
}
const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, this.multiCallProvider);
if (
!tokenContract.name ||
!tokenContract.symbol ||
!tokenContract.decimals ||
!tokenContract.totalSupply ||
!tokenContract.balanceOf
) {
console.warn("Token contract not initialized.");
return undefined;
}
const [name, symbol, decimals, totalSupply, zeroAddressBalance, burnAddressBalance] = await Promise.all([
tokenContract.name(),
tokenContract.symbol(),
tokenContract.decimals(),
tokenContract.totalSupply(),
tokenContract.balanceOf(ethers.ZeroAddress),
tokenContract.balanceOf("0x000000000000000000000000000000000000dEaD"),
]);
const tokenInfo = {
address: tokenAddress,
name,
symbol,
decimals: Number(decimals),
totalSupply: ethers.formatUnits(totalSupply.toString(), decimals),
zeroAddressBalance: ethers.formatUnits(zeroAddressBalance.toString(), decimals),
burnAddressBalance: ethers.formatUnits(burnAddressBalance.toString(), decimals),
priceUSD: 0,
pools: [],
};
this.tokenConstsCache.set(tokenAddress, tokenInfo);
return tokenInfo;
}
With Promise.all
, this approach gathers all necessary data in parallel, drastically reducing the time required to compile basic token information. This enhances performance, particularly important when dealing with Ethereum’s latency-inducing blockchain calls.
Updating WETH Prices
Token price computation frequently revolves around WETH (Wrapped Ether), a commonly used token on Uniswap. The updateWETHPrice
method accurately determines the current WETH price in USD by examining the USDC-WETH liquidity pair on Uniswap:
private async updateWETHPrice(): Promise<void> {
if (Date.now() - this.lastWETHPriceUpdate < this.WETH_PRICE_REFRESH_INTERVAL) return;
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const usdcWethPair = await this.getPair(UNISWAP_V2_CONSTANTS.WETH_ADDRESS, USDC_ADDRESS);
if (!usdcWethPair || usdcWethPair === ethers.ZeroAddress) {
console.warn("USDC-WETH pair not found.");
return;
}
const pairContract = new ethers.Contract(usdcWethPair, PAIR_ABI, this.multiCallProvider);
if (!pairContract.token0 || !pairContract.getReserves) {
console.warn("Pair contract not initialized.");
return;
}
const [token0, reserves] = await Promise.all([pairContract.token0(), pairContract.getReserves()]);
const [reserve0, reserve1] = reserves.map((reserve: string) => Number(reserve));
const isWETHToken0 = token0.toLowerCase() === UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase();
this.wethPriceUSD = isWETHToken0
? (Number(reserve1) * 1e12) / Number(reserve0)
: (Number(reserve0) * 1e12) / Number(reserve1);
this.lastWETHPriceUpdate = Date.now();
}
This ensures you always have the latest price for accurate token valuation.
Discovering Liquidity Pools
Understanding where a token is traded is essential. Uniswap uses a Constant Product Market Maker (CPMM) model, and the findLiquidityPools
method helps discover pools where the token pairs with key assets like WETH or USDC:
private async findLiquidityPools(tokenInfo: TokenInfo): Promise<LiquidityPool[]> {
const mainTokens = [
UNISWAP_V2_CONSTANTS.WETH_ADDRESS,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
"0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
];
try {
const poolPromises = mainTokens.map((mainToken) => this.getPoolInfo(tokenInfo.address, mainToken));
const pools = (await Promise.all(poolPromises)).filter((pool) => pool !== null) as LiquidityPool[];
return pools.sort((a, b) => b.liquidityUSD - a.liquidityUSD);
} catch (error) {
console.error("Error finding liquidity pools:", (error as Error).message);
return [];
}
}
This method methodically scans for liquidity pools that pair the token with common currencies like WETH, USDC, and USDT, prioritizing those with higher USD liquidity.
Token Price Calculation with Uniswap V2’s CPMM
Utilizing the Constant Product Market Maker (CPMM) model, Uniswap V2 ensures the product of token reserves in a pool remains constant, facilitating fair pricing. The formula for deriving token prices is:
The calculateTokenPrice
method employs this formula to ascertain the token’s USD price relative to its WETH reserve:
private async calculateTokenPrice(tokenInfo: TokenInfo): Promise<number> {
if (tokenInfo.address.toLowerCase() === UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase()) {
return this.wethPriceUSD;
}
const wethPairPool = tokenInfo.pools.find((pool) =>
[pool.token0.address.toLowerCase(), pool.token1.address.toLowerCase()].includes(
UNISWAP_V2_CONSTANTS.WETH_ADDRESS.toLowerCase(),
),
);
if (!wethPairPool) return 0;
const isTokenInPositionZero = wethPairPool.token0.address.toLowerCase() === tokenInfo.address.toLowerCase();
const [tokenReserveFormatted, wethReserveFormatted] = isTokenInPositionZero
? [wethPairPool.reserve0, wethPairPool.reserve1]
: [wethPairPool.reserve1, wethPairPool.reserve0];
return (Number.parseFloat(wethReserveFormatted) / Number.parseFloat(tokenReserveFormatted)) * this.wethPriceUSD;
}
It finds the pool where the token pairs with WETH and calculates its USD price using reserve data.
The Uniswap Token Service is a powerful tool, efficiently gathering data with techniques like multicall and ensuring accurate pricing using Uniswap’s CPMM model. It keeps token prices updated in USD with respect to WETH, making it perfect for anyone who needs real-time token data from Uniswap.
Integrating the Token Service in Next.js
This guide demonstrates how to integrate the UniswapTokenService into a Next.js application using shadcn/ui components to display real-time token data with a clean and responsive UI.
API Route for Token Data
Create an API route to fetch token information (app/api/token/route.ts
):
import { NextResponse } from 'next/server'
import { ethers } from 'ethers'
import { UniswapTokenService } from '@/services/token-service'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const address = searchParams.get('address')
if (!address) {
return NextResponse.json({ error: 'Address parameter is required' }, { status: 400 })
}
try {
const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL)
const service = new UniswapTokenService(provider)
const tokenData = await service.getTokenInformation(address)
return NextResponse.json(tokenData)
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 500 })
}
}
Displaying Token Data
Use shadcn/ui components to create a responsive and modern UI (app/page.tsx
):
"use client"
import { useState } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Skeleton } from "@/components/ui/skeleton"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
interface TokenData {
name: string
symbol: string
priceUSD: number
totalSupply: string
pools: Array<{
token0: { symbol: string }
token1: { symbol: string }
liquidityUSD: number
}>
}
export default function TokenAnalytics() {
const [address, setAddress] = useState("")
const [tokenData, setTokenData] = useState<TokenData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchTokenData = async () => {
if (!address) return
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/token?address=${address}`)
if (!response.ok) throw new Error("Failed to fetch token data")
const data = await response.json()
setTokenData(data)
} catch (err) {
setError((err as Error).message)
setTokenData(null)
} finally {
setLoading(false)
}
}
return (
<main className="container mx-auto p-6 space-y-6">
<Card>
<CardHeader>
<CardTitle>Token Analytics</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-4">
<Input
placeholder="Enter token address"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="max-w-xl"
/>
<Button onClick={fetchTokenData}>Analyze</Button>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{loading ? (
<LoadingState />
) : tokenData ? (
<div className="grid gap-6 md:grid-cols-2">
<TokenInfoCard data={tokenData} />
<PoolsCard pools={tokenData.pools} />
</div>
) : null}
</main>
)
}
Subcomponents in the Token Analytics Page
To ensure modularity, the Token Analytics page is broken into reusable subcomponents for token details, liquidity pool data, and loading states. Here’s how they work:
TokenInfoCard
Displays key token information such as name, symbol, price, and total supply.
function TokenInfoCard({ data }: { data: TokenData }) {
return (
<Card>
<CardHeader>
<CardTitle>Token Information</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell className="font-medium">Name</TableCell>
<TableCell>{data.name}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Symbol</TableCell>
<TableCell>{data.symbol}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Price</TableCell>
<TableCell>${data.priceUSD.toFixed(6)}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Supply</TableCell>
<TableCell>{data.totalSupply}</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
)
}
PoolsCard
Shows details of liquidity pools associated with the token.
function PoolsCard({ pools }: { pools: TokenData['pools'] }) {
return (
<Card>
<CardHeader>
<CardTitle>Liquidity Pools</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Pair</TableHead>
<TableHead>Liquidity</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pools.map((pool, i) => (
<TableRow key={i}>
<TableCell>
{pool.token0.symbol}/{pool.token1.symbol}
</TableCell>
<TableCell>
${pool.liquidityUSD.toLocaleString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}
LoadingState
Offers a skeleton-based placeholder UI while token data is being fetched.
function LoadingState() {
return (
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<Skeleton className="h-8 w-[200px]" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-8 w-[200px]" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</CardContent>
</Card>
</div>
)
}
By leveraging these subcomponents, the Token Analytics page becomes clean, maintainable, and visually appealing, while ensuring type safety and responsiveness. This is how the page will look like if you query for WETH:
Deploying to Vercel
- Push your code to GitHub.
- Connect the repository to Vercel.
- Add the environment variable
NEXT_PUBLIC_RPC_URL
in Vercel settings. - Deploy your application.
This implementation offers:
- A clean and responsive UI with shadcn/ui components.
- Enhanced error handling and loading states.
- A scalable structure for future DeFi analytics tools.
For advanced features, explore the full codebase at github.com/ivesfurtado/opendefi.
Leave a Reply