From c1019cbc01850f9ec11e1b43ade591729cff33fc Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Thu, 20 Nov 2025 20:50:24 +0530 Subject: [PATCH 1/5] add: token stats page --- app/(home)/stats/avax-token/layout.tsx | 33 + app/(home)/stats/avax-token/page.tsx | 873 +++++++++++++++++++++++ app/api/avax-supply/route.ts | 68 ++ app/api/icm-contract-fees/route.ts | 127 ++++ components/stats/stats-bubble.config.tsx | 21 +- 5 files changed, 1112 insertions(+), 10 deletions(-) create mode 100644 app/(home)/stats/avax-token/layout.tsx create mode 100644 app/(home)/stats/avax-token/page.tsx create mode 100644 app/api/avax-supply/route.ts create mode 100644 app/api/icm-contract-fees/route.ts diff --git a/app/(home)/stats/avax-token/layout.tsx b/app/(home)/stats/avax-token/layout.tsx new file mode 100644 index 00000000000..51176f84c76 --- /dev/null +++ b/app/(home)/stats/avax-token/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import { createMetadata } from "@/utils/metadata"; + +export const metadata: Metadata = createMetadata({ + title: "AVAX Token Stats", + description: + "Track AVAX token supply, staking, and burn metrics including total supply, circulating supply and fees burned across all chains.", + openGraph: { + url: "/stats/avax-token", + images: { + alt: "AVAX Token Stats", + url: "/api/og/stats/c-chain?title=AVAX Token Stats&description=Track AVAX token supply, staking, and burn metrics", + width: 1280, + height: 720, + }, + }, + twitter: { + images: { + alt: "AVAX Token Stats", + url: "/api/og/stats/c-chain?title=AVAX Token Stats&description=Track AVAX token supply, staking, and burn metrics", + width: 1280, + height: 720, + }, + }, +}); + +export default function AvaxTokenLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/(home)/stats/avax-token/page.tsx b/app/(home)/stats/avax-token/page.tsx new file mode 100644 index 00000000000..53f4241b314 --- /dev/null +++ b/app/(home)/stats/avax-token/page.tsx @@ -0,0 +1,873 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Coins, Users, Lock, DollarSign, RefreshCw, Flame, Award, MessageSquareIcon, Server } from "lucide-react"; +import { useEffect, useState, useMemo } from "react"; +import Image from "next/image"; +import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, Brush, LineChart, Line } from "recharts"; +import { StatsBubbleNav } from "@/components/stats/stats-bubble.config"; +import { AvalancheLogo } from "@/components/navigation/avalanche-logo"; + +interface AvaxSupplyData { + totalSupply: string; + circulatingSupply: string; + totalPBurned: string; + totalCBurned: string; + totalXBurned: string; + totalStaked: string; + totalLocked: string; + totalRewards: string; + lastUpdated: string; + genesisUnlock: string; + l1ValidatorFees: string; + price: number; + priceChange24h: number; +} + +interface FeeDataPoint { + date: string; + timestamp: number; + value: number; +} + +interface CChainFeesResponse { + feesPaid: { + data: Array<{ date: string; timestamp: number; value: string | number }>; + }; +} + +interface ICMFeesResponse { + data: Array<{ + date: string; + timestamp: number; + feesPaid: number; + txCount: number; + }>; + totalFees: number; + lastUpdated: string; +} + +type Period = "D" | "W" | "M"; + +export default function AvaxTokenPage() { + const [data, setData] = useState(null); + const [cChainFees, setCChainFees] = useState([]); + const [icmFees, setICMFees] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [period, setPeriod] = useState("D"); + const [brushIndexes, setBrushIndexes] = useState<{ + startIndex: number; + endIndex: number; + } | null>(null); + + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const [supplyRes, cChainRes, icmRes] = await Promise.all([ + fetch("/api/avax-supply"), + fetch("/api/chain-stats/43114?timeRange=all"), + fetch("/api/icm-contract-fees?timeRange=all"), + ]); + + if (!supplyRes.ok || !cChainRes.ok) { + throw new Error("Failed to fetch required data"); + } + + const supplyData = await supplyRes.json(); + const cChainData: CChainFeesResponse = await cChainRes.json(); + + setData(supplyData); + + const cChainFeesData: FeeDataPoint[] = cChainData.feesPaid.data + .map((item) => ({ + date: item.date, + timestamp: item.timestamp, + value: + typeof item.value === "string" + ? parseFloat(item.value) + : item.value, + })) + .reverse(); + + setCChainFees(cChainFeesData); + + if (icmRes.ok) { + const icmData: ICMFeesResponse = await icmRes.json(); + if (icmData.data && Array.isArray(icmData.data)) { + const icmFeesData: FeeDataPoint[] = icmData.data + .map((item) => ({ + date: item.date, + timestamp: item.timestamp, + value: item.feesPaid, + })) + .reverse(); + setICMFees(icmFeesData); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const formatNumber = (value: string | number): string => { + const num = typeof value === "string" ? parseFloat(value) : value; + if (isNaN(num)) return "N/A"; + + if (num >= 1e9) { + return `${(num / 1e9).toFixed(2)}B`; + } else if (num >= 1e6) { + return `${(num / 1e6).toFixed(2)}M`; + } else if (num >= 1e3) { + return `${(num / 1e3).toFixed(2)}K`; + } + return num.toLocaleString(undefined, { maximumFractionDigits: 2 }); + }; + + const formatFullNumber = (value: string | number): string => { + const num = typeof value === "string" ? parseFloat(value) : value; + if (isNaN(num)) return "N/A"; + return num.toLocaleString(undefined, { maximumFractionDigits: 2 }); + }; + + const formatUSD = (avaxAmount: string | number): string => { + const amount = + typeof avaxAmount === "string" ? parseFloat(avaxAmount) : avaxAmount; + const price = data?.price || 0; + if (isNaN(amount) || price === 0) return ""; + const usdValue = amount * price; + + if (usdValue >= 1e9) { + return `$${(usdValue / 1e9).toFixed(1)} Billion USD`; + } else if (usdValue >= 1e6) { + return `$${(usdValue / 1e6).toFixed(1)} Million USD`; + } else if (usdValue >= 1e3) { + return `$${(usdValue / 1e3).toFixed(1)}K USD`; + } + return `$${usdValue.toLocaleString(undefined, { + maximumFractionDigits: 2, + })} USD`; + }; + + const calculatePercentage = (part: string, total: string): string => { + const partNum = parseFloat(part); + const totalNum = parseFloat(total); + if (isNaN(partNum) || isNaN(totalNum) || totalNum === 0) return "0"; + return ((partNum / totalNum) * 100).toFixed(2); + }; + + const aggregatedFeeData = useMemo(() => { + if (cChainFees.length === 0 && icmFees.length === 0) return []; + + const allDates = new Set([...cChainFees.map((d) => d.date), ...icmFees.map((d) => d.date)]); + const cChainMap = new Map(cChainFees.map((d) => [d.date, d.value])); + const icmMap = new Map(icmFees.map((d) => [d.date, d.value])); + + let mergedData = Array.from(allDates) + .map((date) => ({ + date, + cChainFees: cChainMap.get(date) || 0, + icmFees: icmMap.get(date) || 0, + })) + .sort((a, b) => a.date.localeCompare(b.date)); + + if (period === "D") return mergedData; + + const grouped = new Map< + string, + { cChainSum: number; icmSum: number; date: string } + >(); + + mergedData.forEach((point) => { + const date = new Date(point.date); + let key: string; + + if (period === "W") { + const weekStart = new Date(date); + weekStart.setDate(date.getDate() - date.getDay()); + key = weekStart.toISOString().split("T")[0]; + } else { + key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart( + 2, + "0" + )}`; + } + + if (!grouped.has(key)) { + grouped.set(key, { cChainSum: 0, icmSum: 0, date: key }); + } + + const group = grouped.get(key)!; + group.cChainSum += point.cChainFees; + group.icmSum += point.icmFees; + }); + + return Array.from(grouped.values()) + .map((group) => ({ + date: group.date, + cChainFees: group.cChainSum, + icmFees: group.icmSum, + })) + .sort((a, b) => a.date.localeCompare(b.date)); + }, [cChainFees, icmFees, period]); + + useEffect(() => { + if (aggregatedFeeData.length === 0) return; + + if (period === "D") { + const daysToShow = 90; + setBrushIndexes({ + startIndex: Math.max(0, aggregatedFeeData.length - daysToShow), + endIndex: aggregatedFeeData.length - 1, + }); + } else { + setBrushIndexes({ + startIndex: 0, + endIndex: aggregatedFeeData.length - 1, + }); + } + }, [period, aggregatedFeeData.length]); + + const displayData = brushIndexes + ? aggregatedFeeData.slice( + brushIndexes.startIndex, + brushIndexes.endIndex + 1 + ) + : aggregatedFeeData; + + const formatXAxis = (value: string) => { + const date = new Date(value); + if (period === "M") { + return date.toLocaleDateString("en-US", { + month: "short", + year: "2-digit", + }); + } + return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); + }; + + const formatTooltipDate = (value: string) => { + const date = new Date(value); + + if (period === "M") { + return date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + } + + if (period === "W") { + const endDate = new Date(date); + endDate.setDate(date.getDate() + 6); + + const startMonth = date.toLocaleDateString("en-US", { month: "long" }); + const endMonth = endDate.toLocaleDateString("en-US", { month: "long" }); + const startDay = date.getDate(); + const endDay = endDate.getDate(); + const year = endDate.getFullYear(); + + if (startMonth === endMonth) { + return `${startMonth} ${startDay}-${endDay}, ${year}`; + } else { + return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${year}`; + } + } + + return date.toLocaleDateString("en-US", { + day: "numeric", + month: "long", + year: "numeric", + }); + }; + + const totalICMFees = useMemo( + () => icmFees.reduce((sum, item) => sum + item.value, 0), + [icmFees] + ); + + // show the actual total supply minus the total burned + const actualTotalSupply = data ? 720000000 - (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)) : 0; + + const metrics = data + ? [ + { + label: "AVAX Price", + value: data.price > 0 ? `$${data.price.toFixed(2)}` : "N/A", + fullValue: data.price > 0 ? `$${data.price.toFixed(4)}` : "N/A", + icon: DollarSign, + subtext:data.priceChange24h !== 0 ? `${data.priceChange24h > 0 ? "+" : ""}${data.priceChange24h.toFixed(2)}% (24h)` : "USD", + color: data.priceChange24h >= 0 ? "#10B981" : "#EF4444", + }, + { + label: "Total Supply", + value: formatNumber(actualTotalSupply), + fullValue: formatFullNumber(actualTotalSupply), + icon: Coins, + subtext: data.price > 0 ? formatUSD(actualTotalSupply) : "AVAX", + color: "#E84142", + }, + { + label: "Circulating Supply", + value: formatNumber(data.circulatingSupply), + fullValue: formatFullNumber(data.circulatingSupply), + icon: Users, + subtext: data.price > 0 ? formatUSD(data.circulatingSupply) : `${calculatePercentage(data.circulatingSupply, data.totalSupply)}% of total`, + color: "#3752AC", + }, + { + label: "Total Burned", + value: formatNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)), + fullValue: formatFullNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)), + icon: Flame, + subtext: + data.price > 0 ? formatUSD(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)) : `${calculatePercentage( + (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString(),data.totalSupply)}% of genesis supply`, + color: "#F59E0B", + }, + { + label: "Total Staked", + value: formatNumber(data.totalStaked), + fullValue: formatFullNumber(data.totalStaked), + icon: Lock, + subtext: data.price > 0 ? formatUSD(data.totalStaked) : `${calculatePercentage(data.totalStaked, data.totalSupply)}% of total supply`, + color: "#8B5CF6", + }, + { + label: "Total Locked", + value: formatNumber(data.totalLocked), + fullValue: formatFullNumber(data.totalLocked), + icon: Lock, + subtext: data.price > 0 ? formatUSD(data.totalLocked) : "AVAX", + color: "#10B981", + }, + { + label: "Total Rewards", + value: formatNumber(data.totalRewards), + fullValue: formatFullNumber(data.totalRewards), + icon: Award, + subtext: data.price > 0 ? formatUSD(data.totalRewards) : "AVAX", + color: "#F59E0B", + }, + { + label: "Genesis Unlock", + value: formatNumber(data.genesisUnlock), + fullValue: formatFullNumber(data.genesisUnlock), + icon: Coins, + subtext: data.price > 0 ? formatUSD(data.genesisUnlock) : "AVAX", + color: "#E84142", + }, + ] + : []; + + const chainData = data + ? [ + { + chain: "C-Chain", + burned: formatFullNumber(data.totalCBurned), + percentage: parseFloat( + calculatePercentage( + data.totalCBurned, + (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString() + ) + ), + color: "bg-[#E84142]", + logo: "https://images.ctfassets.net/gcj8jwzm6086/5VHupNKwnDYJvqMENeV7iJ/3e4b8ff10b69bfa31e70080a4b142cd0/avalanche-avax-logo.svg", + }, + { + chain: "P-Chain", + burned: formatFullNumber(data.totalPBurned), + percentage: parseFloat( + calculatePercentage( + data.totalPBurned, + (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString() + ) + ), + color: "bg-[#3752AC]", + logo: "https://images.ctfassets.net/gcj8jwzm6086/42aMwoCLblHOklt6Msi6tm/1e64aa637a8cead39b2db96fe3225c18/pchain-square.svg", + }, + { + chain: "X-Chain", + burned: formatFullNumber(data.totalXBurned), + percentage: parseFloat( + calculatePercentage( + data.totalXBurned, + (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString() + ) + ), + color: "bg-[#10B981]", + logo: "https://images.ctfassets.net/gcj8jwzm6086/5xiGm7IBR6G44eeVlaWrxi/1b253c4744a3ad21a278091e3119feba/xchain-square.svg", + }, + ] + : []; + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+
+ +
+ {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( + + +
+
+
+
+
+ + + ))} +
+
+ +
+ ); + } + + if (error) { + return ( +
+
+ + +

{error}

+ +
+
+
+ +
+ ); + } + + return ( +
+
+
+
+
+
+ +
+
+

+ Avalanche (AVAX) +

+
+ + Native Token + + + + FvwEAhm...DGCgxN5Z + + +
+
+
+ +
+ +
+
+
+ +
+
+ {metrics.map((metric) => { + const Icon = metric.icon; + + return ( + + +
+ +

+ {metric.label} +

+
+

+ {metric.value} +

+

+ {metric.subtext} +

+
+
+ ); + })} +
+ +
+
+ +
+
+
+

+ Network Fees Paid +

+

+ C-Chain and ICM contract fees over time +

+
+
+ {(["D", "W", "M"] as const).map((p) => ( + + ))} +
+
+
+ +
+ + + + + formatNumber(value)} + className="text-xs text-neutral-600 dark:text-neutral-400" + tick={{ + className: "fill-neutral-600 dark:fill-neutral-400", + }} + /> + { + if (!active || !payload?.[0]) return null; + const formattedDate = formatTooltipDate( + payload[0].payload.date + ); + return ( +
+
+
+ {formattedDate} +
+
+
+ + C-Chain:{" "} + + + {formatNumber( + payload[0].payload.cChainFees + )}{" "} + AVAX + +
+
+
+ + ICM:{" "} + + + {formatNumber(payload[0].payload.icmFees)}{" "} + AVAX + +
+
+
+ ); + }} + /> + + + + +
+ +
+ + + { + if ( + e.startIndex !== undefined && + e.endIndex !== undefined + ) { + setBrushIndexes({ + startIndex: e.startIndex, + endIndex: e.endIndex, + }); + } + }} + travellerWidth={8} + tickFormatter={formatXAxis} + > + + + + + + +
+ + +
+ +
+ +
+

+ Fees Burned by Chain +

+
+ +
+ {chainData.map((chain) => ( +
+
+
+
+ {`${chain.chain} +
+
+

+ {chain.chain} +

+

+ {chain.burned} AVAX +

+
+
+ + {chain.percentage.toFixed(2)}% + +
+ +
+
+
+
+ ))} + +
+
+ + Total Burned + + + {data && + formatFullNumber( + parseFloat(data.totalPBurned) + + parseFloat(data.totalCBurned) + + parseFloat(data.totalXBurned) + )}{" "} + AVAX + +
+
+
+ + + + {data && ( + + +
+
+
+ +
+
+

+ L1 Validator Fees +

+

+ All-time fees paid by L1 validators +

+
+
+
+

+ {formatNumber(data.l1ValidatorFees)} AVAX +

+ {data.price > 0 && ( +

+ {formatUSD(data.l1ValidatorFees)} +

+ )} +
+
+
+
+ )} + + + +
+
+
+ +
+
+

+ Total ICM Fees +

+

+ All-time fees from Interchain Messages +

+
+
+
+

+ {formatNumber(totalICMFees)} AVAX +

+ {data && data.price > 0 && ( +

+ {formatUSD(totalICMFees)} +

+ )} +
+
+
+
+
+
+
+
+ + +
+ ); +} diff --git a/app/api/avax-supply/route.ts b/app/api/avax-supply/route.ts new file mode 100644 index 00000000000..afa4fef4c21 --- /dev/null +++ b/app/api/avax-supply/route.ts @@ -0,0 +1,68 @@ +import { NextResponse } from "next/server"; + +interface CoinGeckoResponse { + "avalanche-2": { + usd: number; + usd_24h_change: number; + }; +} + +export async function GET() { + try { + const [supplyResponse, priceResponse] = await Promise.all([ + fetch("https://data-api.avax.network/v1/avax/supply", { + headers: { + accept: "application/json", + }, + next: { revalidate: 300 }, + }), + fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=avalanche-2&vs_currencies=usd&include_24hr_change=true", + { + headers: { + Accept: "application/json", + }, + next: { revalidate: 60 }, + } + ), + ]); + + if (!supplyResponse.ok) { + throw new Error(`Failed to fetch AVAX supply data: ${supplyResponse.status}`); + } + + const supplyData = await supplyResponse.json(); + + let priceData = { + price: 0, + change24h: 0, + }; + + if (priceResponse.ok) { + try { + const priceJson: CoinGeckoResponse = await priceResponse.json(); + priceData = { + price: priceJson["avalanche-2"]?.usd || 0, + change24h: priceJson["avalanche-2"]?.usd_24h_change || 0, + }; + } catch (priceError) { + console.warn("Failed to parse price data:", priceError); + } + } else { + console.warn("Price API returned non-ok response"); + } + + return NextResponse.json({ + ...supplyData, + price: priceData.price, + priceChange24h: priceData.change24h, + }); + } catch (error) { + console.error("Error fetching AVAX supply:", error); + return NextResponse.json( + { error: "Failed to fetch AVAX supply data" }, + { status: 500 } + ); + } +} + diff --git a/app/api/icm-contract-fees/route.ts b/app/api/icm-contract-fees/route.ts new file mode 100644 index 00000000000..afb2bd9b72b --- /dev/null +++ b/app/api/icm-contract-fees/route.ts @@ -0,0 +1,127 @@ +import { NextResponse } from "next/server"; + +interface ContractStatsResponse { + contracts: string[]; + timeRange: { + from: number; + to: number; + }; + transactions: { + total: number; + totalGasCost: number; + }; + icmMessages: { + count: number; + totalGasCost: number; + }; + interactions: { + uniqueAddresses: number; + avgDailyAddresses: number; + }; + concentration: { + top5AccountsPercentage: number; + top20AccountsPercentage: number; + }; +} + +interface DailyFeeData { + date: string; + timestamp: number; + feesPaid: number; + txCount: number; +} + +let cachedDailyData: { + data: DailyFeeData[]; + totalFees: number; + lastUpdated: string; +} | null = null; + +let lastCacheTime = 0; +const CACHE_DURATION = 5 * 60 * 1000; + +export async function GET(_request: Request) { + try { + const icmContract = "0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf"; + const deploymentTimestamp = 1709586720; + const now = Math.floor(Date.now() / 1000); + + if ( + cachedDailyData && + Date.now() - lastCacheTime < CACHE_DURATION + ) { + return NextResponse.json(cachedDailyData); + } + + const dailyData: DailyFeeData[] = []; + const oneDaySeconds = 24 * 60 * 60; + const oneWeekSeconds = 7 * oneDaySeconds; + let currentTimestamp = deploymentTimestamp; + + while (currentTimestamp < now) { + const nextTimestamp = Math.min(currentTimestamp + oneWeekSeconds, now); + + try { + const response = await fetch( + `https://idx6.solokhin.com/api/43114/contract-stats?contracts=${icmContract}&tsFrom=${currentTimestamp}&tsTo=${nextTimestamp}`, + { + headers: { + Accept: "application/json", + }, + } + ); + + if (response.ok) { + const data: ContractStatsResponse = await response.json(); + const weeklyFees = data.transactions?.totalGasCost || 0; + const weeklyTxCount = data.transactions?.total || 0; + const daysInThisWeek = Math.ceil((nextTimestamp - currentTimestamp) / oneDaySeconds); + const dailyFees = weeklyFees / daysInThisWeek; + const dailyTxCount = Math.floor(weeklyTxCount / daysInThisWeek); + + for (let i = 0; i < daysInThisWeek; i++) { + const dayTimestamp = currentTimestamp + (i * oneDaySeconds); + const date = new Date(dayTimestamp * 1000).toISOString().split('T')[0]; + + dailyData.push({ + date, + timestamp: dayTimestamp, + feesPaid: dailyFees, + txCount: dailyTxCount, + }); + } + } + } catch (error) { + console.warn(`Failed to fetch ICM data for week starting ${currentTimestamp}:`, error); + } + + currentTimestamp = nextTimestamp; + await new Promise(resolve => setTimeout(resolve, 50)); + } + + const totalFees = dailyData.reduce((sum, item) => sum + item.feesPaid, 0); + + const result = { + data: dailyData, + totalFees, + lastUpdated: new Date().toISOString(), + }; + + cachedDailyData = result; + lastCacheTime = Date.now(); + + return NextResponse.json(result); + } catch (error) { + console.error("Error fetching ICM contract fees:", error); + + if (cachedDailyData) { + return NextResponse.json(cachedDailyData); + } + + return NextResponse.json( + { error: "Failed to fetch ICM contract fees data" }, + { status: 500 } + ); + } +} + diff --git a/components/stats/stats-bubble.config.tsx b/components/stats/stats-bubble.config.tsx index 914dba569fa..7eccb97c50e 100644 --- a/components/stats/stats-bubble.config.tsx +++ b/components/stats/stats-bubble.config.tsx @@ -4,16 +4,17 @@ import BubbleNavigation from '@/components/navigation/BubbleNavigation'; import type { BubbleNavigationConfig } from '@/components/navigation/bubble-navigation.types'; export const statsBubbleConfig: BubbleNavigationConfig = { - items: [ - { id: "avalanche-l1s", label: "Avalanche L1s", href: "/stats/overview" }, - { id: "c-chain", label: "C-Chain", href: "/stats/primary-network/c-chain" }, - { id: "validators", label: "Validators", href: "/stats/validators" }, - ], - activeColor: "bg-blue-600", - darkActiveColor: "dark:bg-blue-500", - focusRingColor: "focus:ring-blue-500", - pulseColor: "bg-blue-200/40", - darkPulseColor: "dark:bg-blue-400/40", + items: [ + { id: "avalanche-l1s", label: "Avalanche L1s", href: "/stats/overview" }, + { id: "c-chain", label: "C-Chain", href: "/stats/primary-network/c-chain" }, + { id: "validators", label: "Validators", href: "/stats/validators" }, + { id: "avax-token", label: "AVAX", href: "/stats/avax-token" }, + ], + activeColor: "bg-blue-600", + darkActiveColor: "dark:bg-blue-500", + focusRingColor: "focus:ring-blue-500", + pulseColor: "bg-blue-200/40", + darkPulseColor: "dark:bg-blue-400/40", }; export function StatsBubbleNav() { From 31520abda927e4f8edadcdf1c91a33693420a8cb Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Mon, 24 Nov 2025 13:24:35 +0530 Subject: [PATCH 2/5] update caching schedule for token stats --- app/api/avax-supply/route.ts | 4 ++-- app/api/icm-contract-fees/route.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/avax-supply/route.ts b/app/api/avax-supply/route.ts index afa4fef4c21..f1d310cb604 100644 --- a/app/api/avax-supply/route.ts +++ b/app/api/avax-supply/route.ts @@ -14,7 +14,7 @@ export async function GET() { headers: { accept: "application/json", }, - next: { revalidate: 300 }, + next: { revalidate: 86400 }, // 24 hours - supply data changes slowly }), fetch( "https://api.coingecko.com/api/v3/simple/price?ids=avalanche-2&vs_currencies=usd&include_24hr_change=true", @@ -22,7 +22,7 @@ export async function GET() { headers: { Accept: "application/json", }, - next: { revalidate: 60 }, + next: { revalidate: 60 }, // 1 minute - price data changes frequently } ), ]); diff --git a/app/api/icm-contract-fees/route.ts b/app/api/icm-contract-fees/route.ts index afb2bd9b72b..a9dc8f242f3 100644 --- a/app/api/icm-contract-fees/route.ts +++ b/app/api/icm-contract-fees/route.ts @@ -38,7 +38,7 @@ let cachedDailyData: { } | null = null; let lastCacheTime = 0; -const CACHE_DURATION = 5 * 60 * 1000; +const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - historical fee data changes slowly export async function GET(_request: Request) { try { From a9586e94c75b95e76c634b4325cf9e317e709f77 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Mon, 24 Nov 2025 17:39:34 +0530 Subject: [PATCH 3/5] push changes from Sarp's suggestions --- app/(home)/stats/avax-token/page.tsx | 364 ++++++++++++--------------- 1 file changed, 164 insertions(+), 200 deletions(-) diff --git a/app/(home)/stats/avax-token/page.tsx b/app/(home)/stats/avax-token/page.tsx index 53f4241b314..bf499f0e8cd 100644 --- a/app/(home)/stats/avax-token/page.tsx +++ b/app/(home)/stats/avax-token/page.tsx @@ -3,7 +3,8 @@ import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Coins, Users, Lock, DollarSign, RefreshCw, Flame, Award, MessageSquareIcon, Server } from "lucide-react"; +import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { CircleDotDashed, CircleFadingPlus, Lock, BadgeDollarSign, RefreshCw, Flame, Award, MessageSquareIcon, Server, Unlock, HandCoins, Info, ArrowUpRight } from "lucide-react"; import { useEffect, useState, useMemo } from "react"; import Image from "next/image"; import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, Brush, LineChart, Line } from "recharts"; @@ -87,10 +88,7 @@ export default function AvaxTokenPage() { .map((item) => ({ date: item.date, timestamp: item.timestamp, - value: - typeof item.value === "string" - ? parseFloat(item.value) - : item.value, + value: typeof item.value === "string" ? parseFloat(item.value) : item.value, })) .reverse(); @@ -141,8 +139,7 @@ export default function AvaxTokenPage() { }; const formatUSD = (avaxAmount: string | number): string => { - const amount = - typeof avaxAmount === "string" ? parseFloat(avaxAmount) : avaxAmount; + const amount = typeof avaxAmount === "string" ? parseFloat(avaxAmount) : avaxAmount; const price = data?.price || 0; if (isNaN(amount) || price === 0) return ""; const usdValue = amount * price; @@ -238,12 +235,7 @@ export default function AvaxTokenPage() { } }, [period, aggregatedFeeData.length]); - const displayData = brushIndexes - ? aggregatedFeeData.slice( - brushIndexes.startIndex, - brushIndexes.endIndex + 1 - ) - : aggregatedFeeData; + const displayData = brushIndexes ? aggregatedFeeData.slice(brushIndexes.startIndex, brushIndexes.endIndex + 1) : aggregatedFeeData; const formatXAxis = (value: string) => { const date = new Date(value); @@ -304,51 +296,60 @@ export default function AvaxTokenPage() { label: "AVAX Price", value: data.price > 0 ? `$${data.price.toFixed(2)}` : "N/A", fullValue: data.price > 0 ? `$${data.price.toFixed(4)}` : "N/A", - icon: DollarSign, + icon: BadgeDollarSign, subtext:data.priceChange24h !== 0 ? `${data.priceChange24h > 0 ? "+" : ""}${data.priceChange24h.toFixed(2)}% (24h)` : "USD", color: data.priceChange24h >= 0 ? "#10B981" : "#EF4444", + tooltip: "Current AVAX price in USD from CoinGecko", }, { label: "Total Supply", value: formatNumber(actualTotalSupply), fullValue: formatFullNumber(actualTotalSupply), - icon: Coins, + icon: CircleDotDashed, subtext: data.price > 0 ? formatUSD(actualTotalSupply) : "AVAX", + subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#E84142", + tooltip: "Total supply minus the burned tokens from P-Chain, C-Chain, and X-Chain", }, { label: "Circulating Supply", value: formatNumber(data.circulatingSupply), fullValue: formatFullNumber(data.circulatingSupply), - icon: Users, + icon: CircleFadingPlus, subtext: data.price > 0 ? formatUSD(data.circulatingSupply) : `${calculatePercentage(data.circulatingSupply, data.totalSupply)}% of total`, + subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#3752AC", + tooltip: "AVAX tokens actively circulating in the market", }, { - label: "Total Burned", - value: formatNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)), - fullValue: formatFullNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)), - icon: Flame, - subtext: - data.price > 0 ? formatUSD(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)) : `${calculatePercentage( - (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString(),data.totalSupply)}% of genesis supply`, - color: "#F59E0B", + label: "Genesis Unlock", + value: formatNumber(data.genesisUnlock), + fullValue: formatFullNumber(data.genesisUnlock), + icon: Unlock, + subtext: data.price > 0 ? formatUSD(data.genesisUnlock) : "AVAX", + subtextTooltip: data.price > 0 ? "at current prices" : undefined, + color: "#E84142", + tooltip: "Amount of AVAX unlocked during the genesis event", }, { label: "Total Staked", value: formatNumber(data.totalStaked), fullValue: formatFullNumber(data.totalStaked), - icon: Lock, - subtext: data.price > 0 ? formatUSD(data.totalStaked) : `${calculatePercentage(data.totalStaked, data.totalSupply)}% of total supply`, + icon: HandCoins, + subtext:data.price > 0 ? formatUSD(data.totalStaked) : `${calculatePercentage(data.totalStaked, data.circulatingSupply)}% of circulating`, + subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#8B5CF6", + tooltip: "Total AVAX staked and delegated to validators on the Primary Network", }, { label: "Total Locked", value: formatNumber(data.totalLocked), fullValue: formatFullNumber(data.totalLocked), icon: Lock, - subtext: data.price > 0 ? formatUSD(data.totalLocked) : "AVAX", + subtext: data.price > 0 ? formatUSD(data.totalLocked) : `${calculatePercentage(data.totalLocked, data.circulatingSupply)}% of circulating`, + subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#10B981", + tooltip: "AVAX locked in time-locked stakeable outputs on the P-Chain", }, { label: "Total Rewards", @@ -356,15 +357,22 @@ export default function AvaxTokenPage() { fullValue: formatFullNumber(data.totalRewards), icon: Award, subtext: data.price > 0 ? formatUSD(data.totalRewards) : "AVAX", + subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#F59E0B", + tooltip: "Cumulative staking rewards issued to validators and delegators", }, { - label: "Genesis Unlock", - value: formatNumber(data.genesisUnlock), - fullValue: formatFullNumber(data.genesisUnlock), - icon: Coins, - subtext: data.price > 0 ? formatUSD(data.genesisUnlock) : "AVAX", - color: "#E84142", + label: "Total Burned", + value: formatNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)), + fullValue: formatFullNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)), + icon: Flame, + subtext: + data.price > 0 + ? formatUSD(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)) + : `${calculatePercentage((parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString(), data.totalSupply)}% of genesis supply`, + subtextTooltip: data.price > 0 ? "at current prices" : undefined, + color: "#F59E0B", + tooltip: "Total AVAX burned across P-Chain, C-Chain, and X-Chain", }, ] : []; @@ -374,36 +382,21 @@ export default function AvaxTokenPage() { { chain: "C-Chain", burned: formatFullNumber(data.totalCBurned), - percentage: parseFloat( - calculatePercentage( - data.totalCBurned, - (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString() - ) - ), + percentage: parseFloat(calculatePercentage(data.totalCBurned,(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString())), color: "bg-[#E84142]", logo: "https://images.ctfassets.net/gcj8jwzm6086/5VHupNKwnDYJvqMENeV7iJ/3e4b8ff10b69bfa31e70080a4b142cd0/avalanche-avax-logo.svg", }, { chain: "P-Chain", burned: formatFullNumber(data.totalPBurned), - percentage: parseFloat( - calculatePercentage( - data.totalPBurned, - (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString() - ) - ), + percentage: parseFloat(calculatePercentage(data.totalPBurned,(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString())), color: "bg-[#3752AC]", logo: "https://images.ctfassets.net/gcj8jwzm6086/42aMwoCLblHOklt6Msi6tm/1e64aa637a8cead39b2db96fe3225c18/pchain-square.svg", }, { chain: "X-Chain", burned: formatFullNumber(data.totalXBurned), - percentage: parseFloat( - calculatePercentage( - data.totalXBurned, - (parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString() - ) - ), + percentage: parseFloat(calculatePercentage(data.totalXBurned,(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned)).toString())), color: "bg-[#10B981]", logo: "https://images.ctfassets.net/gcj8jwzm6086/5xiGm7IBR6G44eeVlaWrxi/1b253c4744a3ad21a278091e3119feba/xchain-square.svg", }, @@ -413,31 +406,26 @@ export default function AvaxTokenPage() { if (loading) { return (
-
-
+
+
-
+
-
-
+
+
-
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( - - -
-
-
-
-
- - +
+
+
+
+
+
+
))}
@@ -449,8 +437,8 @@ export default function AvaxTokenPage() { if (error) { return (
-
- +
+

{error}

@@ -464,35 +452,37 @@ export default function AvaxTokenPage() { return (
-
-
+
+
-
- +
+
-

- Avalanche (AVAX) -

-
- - Native Token - +

Avalanche (AVAX)

+ @@ -500,12 +490,7 @@ export default function AvaxTokenPage() {
- @@ -513,58 +498,75 @@ export default function AvaxTokenPage() {
-
-
- {metrics.map((metric) => { - const Icon = metric.icon; - - return ( - - -
- -

- {metric.label} -

-
-

+

+ +
+ {metrics.map((metric) => { + const Icon = metric.icon; + return ( +
+ + +
+ +

+ {metric.label} +

+ +
+
+ +

{metric.tooltip}

+
+
+

{metric.value}

-

- {metric.subtext} -

- - - ); - })} -
+ {metric.subtextTooltip ? ( + + +

+ {metric.subtext} +

+
+ +

{metric.subtextTooltip}

+
+
+ ) : ( +

+ {metric.subtext} +

+ )} +
+ ); + })} +
+
- -
+ +
-

- Network Fees Paid -

+

Network Fees Paid

C-Chain and ICM contract fees over time

@@ -589,13 +591,10 @@ export default function AvaxTokenPage() {
- + { if (!active || !payload?.[0]) return null; - const formattedDate = formatTooltipDate( - payload[0].payload.date - ); + const formattedDate = formatTooltipDate(payload[0].payload.date); return ( -
+
{formattedDate} @@ -634,10 +631,7 @@ export default function AvaxTokenPage() { C-Chain:{" "} - {formatNumber( - payload[0].payload.cChainFees - )}{" "} - AVAX + {formatNumber(payload[0].payload.cChainFees)}{" "}AVAX
@@ -646,8 +640,7 @@ export default function AvaxTokenPage() { ICM:{" "} - {formatNumber(payload[0].payload.icmFees)}{" "} - AVAX + {formatNumber(payload[0].payload.icmFees)}{" "}AVAX
@@ -675,10 +668,7 @@ export default function AvaxTokenPage() {
- +
- -
-

- Fees Burned by Chain -

+ +
+

Fees Burned by Chain

@@ -752,15 +740,12 @@ export default function AvaxTokenPage() {

- + {chain.percentage.toFixed(2)}%
-
+
))} -
+
- - Total Burned - + Total Burned - {data && - formatFullNumber( - parseFloat(data.totalPBurned) + - parseFloat(data.totalCBurned) + - parseFloat(data.totalXBurned) - )}{" "} - AVAX + {data && formatFullNumber(parseFloat(data.totalPBurned) + parseFloat(data.totalCBurned) + parseFloat(data.totalXBurned))}{" "}AVAX
@@ -790,23 +767,15 @@ export default function AvaxTokenPage() { {data && ( - +
-
- +
+
-

- L1 Validator Fees -

+

L1 Validator Fees

All-time fees paid by L1 validators

@@ -827,7 +796,7 @@ export default function AvaxTokenPage() { )} - +
@@ -835,15 +804,10 @@ export default function AvaxTokenPage() { className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ backgroundColor: "#8B5CF620" }} > - +
-

- Total ICM Fees -

+

Total ICM Fees

All-time fees from Interchain Messages

From 19f8f1a1c91f35e5134a2814ccf92f8650a66dc8 Mon Sep 17 00:00:00 2001 From: Ashutosh <39340292+ashucoder9@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:57:06 +0530 Subject: [PATCH 4/5] nit Signed-off-by: Ashutosh <39340292+ashucoder9@users.noreply.github.com> --- app/(home)/stats/avax-token/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(home)/stats/avax-token/page.tsx b/app/(home)/stats/avax-token/page.tsx index bf499f0e8cd..1dbb0953e80 100644 --- a/app/(home)/stats/avax-token/page.tsx +++ b/app/(home)/stats/avax-token/page.tsx @@ -329,7 +329,7 @@ export default function AvaxTokenPage() { subtext: data.price > 0 ? formatUSD(data.genesisUnlock) : "AVAX", subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#E84142", - tooltip: "Amount of AVAX unlocked during the genesis event", + tooltip: "Amount of AVAX un during the genesis event", }, { label: "Total Staked", @@ -349,7 +349,7 @@ export default function AvaxTokenPage() { subtext: data.price > 0 ? formatUSD(data.totalLocked) : `${calculatePercentage(data.totalLocked, data.circulatingSupply)}% of circulating`, subtextTooltip: data.price > 0 ? "at current prices" : undefined, color: "#10B981", - tooltip: "AVAX locked in time-locked stakeable outputs on the P-Chain", + tooltip: "Total AVAX locked in UTXOs on P-Chain and X-Chain", }, { label: "Total Rewards", From 0e9fc35bbd0f4c3fa6b698f5518bf880d48dc71b Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Mon, 24 Nov 2025 20:55:50 +0530 Subject: [PATCH 5/5] nits --- app/api/icm-contract-fees/route.ts | 41 +++++++++++------------------- next-env.d.ts | 2 +- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/app/api/icm-contract-fees/route.ts b/app/api/icm-contract-fees/route.ts index a9dc8f242f3..bea8d3ccbaf 100644 --- a/app/api/icm-contract-fees/route.ts +++ b/app/api/icm-contract-fees/route.ts @@ -46,20 +46,16 @@ export async function GET(_request: Request) { const deploymentTimestamp = 1709586720; const now = Math.floor(Date.now() / 1000); - if ( - cachedDailyData && - Date.now() - lastCacheTime < CACHE_DURATION - ) { + if (cachedDailyData && Date.now() - lastCacheTime < CACHE_DURATION) { return NextResponse.json(cachedDailyData); } const dailyData: DailyFeeData[] = []; - const oneDaySeconds = 24 * 60 * 60; - const oneWeekSeconds = 7 * oneDaySeconds; + const oneDaySeconds = 86400; let currentTimestamp = deploymentTimestamp; while (currentTimestamp < now) { - const nextTimestamp = Math.min(currentTimestamp + oneWeekSeconds, now); + const nextTimestamp = Math.min(currentTimestamp + oneDaySeconds, now); try { const response = await fetch( @@ -73,30 +69,23 @@ export async function GET(_request: Request) { if (response.ok) { const data: ContractStatsResponse = await response.json(); - const weeklyFees = data.transactions?.totalGasCost || 0; - const weeklyTxCount = data.transactions?.total || 0; - const daysInThisWeek = Math.ceil((nextTimestamp - currentTimestamp) / oneDaySeconds); - const dailyFees = weeklyFees / daysInThisWeek; - const dailyTxCount = Math.floor(weeklyTxCount / daysInThisWeek); - - for (let i = 0; i < daysInThisWeek; i++) { - const dayTimestamp = currentTimestamp + (i * oneDaySeconds); - const date = new Date(dayTimestamp * 1000).toISOString().split('T')[0]; - - dailyData.push({ - date, - timestamp: dayTimestamp, - feesPaid: dailyFees, - txCount: dailyTxCount, - }); - } + const dailyFees = data.transactions?.totalGasCost || 0; + const dailyTxCount = data.transactions?.total || 0; + const date = new Date(currentTimestamp * 1000).toISOString().split('T')[0]; + + dailyData.push({ + date, + timestamp: currentTimestamp, + feesPaid: dailyFees, + txCount: dailyTxCount, + }); } } catch (error) { - console.warn(`Failed to fetch ICM data for week starting ${currentTimestamp}:`, error); + console.warn(`Failed to fetch ICM data for day ${currentTimestamp}:`, error); } currentTimestamp = nextTimestamp; - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 5)); } const totalFees = dailyData.reduce((sum, item) => sum + item.feesPaid, 0); diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7cac..c4b7818fbb2 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.