From 330848d54e958cb4ff0323ebfd610b03333c87d1 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Thu, 12 Feb 2026 19:35:42 -0400 Subject: [PATCH] refactor: use updated eth/usd volume data --- .../leaderboard/LeaderboardPageClient.tsx | 16 ++-- src/app/(app)/leaderboard/page.tsx | 20 ++--- src/app/api/analytics/leaderboard/route.ts | 29 ++----- src/app/api/analytics/user/[address]/route.ts | 13 ++- src/app/api/analytics/volume/swap/route.ts | 5 +- src/app/claim/page.tsx | 5 +- src/components/claim/ClaimPageClient.tsx | 24 ++---- src/components/dashboard/LeaderboardTable.tsx | 18 ++-- .../dashboard/UserMetricsSection.tsx | 51 ++++------- src/hooks/use-dashboard-data.ts | 4 +- src/hooks/use-leaderboard-data.ts | 9 +- src/lib/analytics-server.ts | 86 +++++-------------- src/lib/analytics/queries.ts | 42 +++++++-- .../services/leaderboard-transform.ts | 19 ++-- .../analytics/services/leaderboard.service.ts | 29 +++++-- .../services/transactions.service.ts | 29 ++++--- src/lib/analytics/services/users.service.ts | 32 +++++-- 17 files changed, 190 insertions(+), 241 deletions(-) diff --git a/src/app/(app)/leaderboard/LeaderboardPageClient.tsx b/src/app/(app)/leaderboard/LeaderboardPageClient.tsx index b328bc4f..cd015002 100644 --- a/src/app/(app)/leaderboard/LeaderboardPageClient.tsx +++ b/src/app/(app)/leaderboard/LeaderboardPageClient.tsx @@ -19,7 +19,7 @@ import { LeaderboardTable } from "@/components/dashboard/LeaderboardTable" interface LeaderboardPageClientProps { preloadedActiveTraders: number | null preloadedSwapVolumeEth: number | null - preloadedEthPrice: number | null + preloadedSwapVolumeUsd: number | null preloadedLeaderboard: Array<{ rank: number wallet: string @@ -34,7 +34,7 @@ interface LeaderboardPageClientProps { export function LeaderboardPageClient({ preloadedActiveTraders, preloadedSwapVolumeEth, - preloadedEthPrice, + preloadedSwapVolumeUsd, preloadedLeaderboard, }: LeaderboardPageClientProps) { const [isMounted, setIsMounted] = useState(false) @@ -47,19 +47,17 @@ export function LeaderboardPageClient({ // Prepare initial data for React Query hydration const initialLeaderboardData = preloadedLeaderboard.length > 0 - ? { - success: true, - leaderboard: preloadedLeaderboard, - ethPrice: preloadedEthPrice, - } + ? { success: true, leaderboard: preloadedLeaderboard } : undefined const initialStatsData = - preloadedActiveTraders !== null || preloadedSwapVolumeEth !== null || preloadedEthPrice !== null + preloadedActiveTraders !== null || + preloadedSwapVolumeEth !== null || + preloadedSwapVolumeUsd !== null ? { activeTraders: preloadedActiveTraders, swapVolumeEth: preloadedSwapVolumeEth, - ethPrice: preloadedEthPrice, + swapVolumeUsd: preloadedSwapVolumeUsd, } : undefined diff --git a/src/app/(app)/leaderboard/page.tsx b/src/app/(app)/leaderboard/page.tsx index 80e3899b..2f1771ab 100644 --- a/src/app/(app)/leaderboard/page.tsx +++ b/src/app/(app)/leaderboard/page.tsx @@ -2,7 +2,6 @@ import { Suspense } from "react" import { getActiveTraders, getCumulativeSwapVolume, - getEthPrice, getLeaderboardTop15, } from "@/lib/analytics-server" import { LeaderboardPageClient } from "./LeaderboardPageClient" @@ -33,25 +32,16 @@ async function StatsLoader({ }> }) { // Start all stats fetches in parallel - they'll complete independently - // Don't use Promise.all - let them resolve individually for better streaming const activeTradersPromise = getActiveTraders() - const swapVolumeEthPromise = getCumulativeSwapVolume() - const ethPricePromise = getEthPrice() + const swapVolumePromise = getCumulativeSwapVolume() - // Wait for all stats - but they're already loading in parallel - // In a true streaming setup, we'd render each as it completes - // For now, we wait for all but they load in parallel - const [activeTraders, swapVolumeEth, ethPrice] = await Promise.all([ - activeTradersPromise, - swapVolumeEthPromise, - ethPricePromise, - ]) + const [activeTraders, swapVolume] = await Promise.all([activeTradersPromise, swapVolumePromise]) return ( ) @@ -88,7 +78,7 @@ export default function LeaderboardPage() { } diff --git a/src/app/api/analytics/leaderboard/route.ts b/src/app/api/analytics/leaderboard/route.ts index 863c9ede..fc50fd92 100644 --- a/src/app/api/analytics/leaderboard/route.ts +++ b/src/app/api/analytics/leaderboard/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from "next/server" -import { getEthPrice } from "@/lib/analytics-server" import { getLeaderboard, getUserLeaderboardData, @@ -56,14 +55,10 @@ export async function GET(request: NextRequest) { // Get main leaderboard (top 15) const leaderboardRows = await getLeaderboard(15) - // Get ETH price for USD conversion - const ethPrice = await getEthPrice() - - // Transform leaderboard rows using shared transformation utility - // useTotalVolume=true means we use total_swap_vol_eth instead of swap_vol_eth_24h + // Transform leaderboard rows (USD from DB columns) + // useTotalVolume=true means we use total_swap_vol_usd let leaderboard = transformLeaderboardRows( leaderboardRows, - ethPrice, currentUserAddress, true // Use total volume ) @@ -90,8 +85,7 @@ export async function GET(request: NextRequest) { } else { // Fetch user's data separately to add them to the leaderboard try { - // Fetch user data, rank, and next rank volume in parallel for speed - const [userData, actualRank, nextRankVolEth] = await Promise.all([ + const [userData, actualRank, nextRankThreshold] = await Promise.all([ getUserLeaderboardData(currentUserAddress), getUserRank(currentUserAddress), getNextRankThreshold(currentUserAddress), @@ -99,31 +93,25 @@ export async function GET(request: NextRequest) { if (actualRank !== null && userData && userData[0] > 0) { const userTotalSwapVolEth = Number(userData[0]) || 0 - const userSwapCount = Number(userData[1]) || 0 - const userChange24hPct = Number(userData[3]) || 0 - - const userTotalSwapVolUsd = - ethPrice !== null ? userTotalSwapVolEth * ethPrice : userTotalSwapVolEth + const userTotalSwapVolUsd = Number(userData[1]) || 0 + const userSwapCount = Number(userData[2]) || 0 + const userChange24hPct = Number(userData[5]) || 0 userPosition = actualRank userVolume = userTotalSwapVolUsd userChange24h = userChange24hPct - // Find the next rank user's volume (for all positions > 1) if (userPosition > 1) { if (userPosition <= 15) { - // User is in top 15, find next rank user from leaderboard const nextRankUser = leaderboard.find((entry) => entry.rank === userPosition! - 1) if (nextRankUser) { nextRankVolume = nextRankUser.swapVolume24h } - } else if (nextRankVolEth !== null) { - // User is outside top 15, use the next rank threshold from query - nextRankVolume = ethPrice !== null ? nextRankVolEth * ethPrice : nextRankVolEth + } else if (nextRankThreshold.usd !== null) { + nextRankVolume = nextRankThreshold.usd } } - // Add current user to leaderboard if not in top 15 if (userPosition > 15 && currentUserAddress) { leaderboard.push({ rank: userPosition, @@ -149,7 +137,6 @@ export async function GET(request: NextRequest) { userPosition, userVolume, nextRankVolume, - ethPrice, } // Cache the response diff --git a/src/app/api/analytics/user/[address]/route.ts b/src/app/api/analytics/user/[address]/route.ts index 6043caa5..33d93f49 100644 --- a/src/app/api/analytics/user/[address]/route.ts +++ b/src/app/api/analytics/user/[address]/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server" import { env } from "@/env/server" -import { getEthPrice } from "@/lib/analytics-server" import { getUserSwapVolume } from "@/lib/analytics/services/users.service" import { AnalyticsClientError } from "@/lib/analytics/client" @@ -56,12 +55,14 @@ export async function GET( const totalTxs = fastRpcData.txn_count || 0 const swapTxs = fastRpcData.swap_count || 0 - // Get swap volume from analytics database + // Get swap volume from analytics database (ETH and USD) let totalSwapVolEth = 0 + let totalSwapVolUsd = 0 try { - totalSwapVolEth = await getUserSwapVolume(normalizedAddress) + const vol = await getUserSwapVolume(normalizedAddress) + totalSwapVolEth = vol.eth + totalSwapVolUsd = vol.usd } catch (error) { - // Log error but don't fail the request - return partial data if (error instanceof AnalyticsClientError) { console.error("Analytics DB API error:", error.message) } else { @@ -69,13 +70,11 @@ export async function GET( } } - const ethPrice = await getEthPrice() - return NextResponse.json({ totalTxs, swapTxs, totalSwapVolEth, - ethPrice, + totalSwapVolUsd, }) } catch (error) { console.error("Error fetching user metrics:", error) diff --git a/src/app/api/analytics/volume/swap/route.ts b/src/app/api/analytics/volume/swap/route.ts index 7498ce30..7b567c00 100644 --- a/src/app/api/analytics/volume/swap/route.ts +++ b/src/app/api/analytics/volume/swap/route.ts @@ -6,13 +6,14 @@ export async function GET() { try { const cumulativeSwapVolume = await getSwapVolume() - if (cumulativeSwapVolume === null) { + if (cumulativeSwapVolume.eth === null && cumulativeSwapVolume.usd === null) { return NextResponse.json({ error: "No data returned from analytics API" }, { status: 500 }) } return NextResponse.json({ success: true, - cumulativeSwapVolEth: cumulativeSwapVolume, + cumulativeSwapVolEth: cumulativeSwapVolume.eth, + cumulativeSwapVolUsd: cumulativeSwapVolume.usd, }) } catch (error) { console.error("Error fetching transaction volume analytics:", error) diff --git a/src/app/claim/page.tsx b/src/app/claim/page.tsx index 06922cdf..d4078a4d 100644 --- a/src/app/claim/page.tsx +++ b/src/app/claim/page.tsx @@ -22,7 +22,8 @@ const ClaimPage = async () => { const totalSupplyString = totalSupply !== null ? totalSupply.toString() : null const transactionsString = cumulativeTransactions !== null ? cumulativeTransactions.toString() : null - const swapVolumeString = cumulativeSwapVolume !== null ? cumulativeSwapVolume.toString() : null + const swapVolumeUsdString = + cumulativeSwapVolume?.usd != null ? cumulativeSwapVolume.usd.toString() : null const ethPriceString = ethPrice !== null ? ethPrice.toString() : null const totalPointsString = totalPoints !== null ? totalPoints.toString() : null @@ -30,7 +31,7 @@ const ClaimPage = async () => { diff --git a/src/components/claim/ClaimPageClient.tsx b/src/components/claim/ClaimPageClient.tsx index 557778a5..b7fdfd8e 100644 --- a/src/components/claim/ClaimPageClient.tsx +++ b/src/components/claim/ClaimPageClient.tsx @@ -5,13 +5,12 @@ import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" import { Zap, TrendingUp, Users, Shield } from "lucide-react" import { useRouter } from "next/navigation" -import { DEFAULT_ETH_PRICE_USD } from "@/lib/constants" import { formatNumber } from "@/lib/utils" interface ClaimPageClientProps { initialTotalSupply: string | null initialTransactions: string | null - initialSwapVolume: string | null + initialSwapVolumeUsd: string | null initialEthPrice: string | null initialTotalPoints: string | null } @@ -19,7 +18,7 @@ interface ClaimPageClientProps { export const ClaimPageClient = ({ initialTotalSupply, initialTransactions, - initialSwapVolume, + initialSwapVolumeUsd, initialEthPrice, initialTotalPoints, }: ClaimPageClientProps) => { @@ -140,19 +139,12 @@ export const ClaimPageClient = ({

- {initialSwapVolume !== null - ? (() => { - const swapVolume = Number(initialSwapVolume) - // Use ETH price if available, otherwise fallback to default price - const price = - initialEthPrice !== null ? Number(initialEthPrice) : DEFAULT_ETH_PRICE_USD - const totalUsd = swapVolume * price - return `$${totalUsd.toLocaleString(undefined, { - maximumFractionDigits: 1, - notation: "compact", - compactDisplay: "short", - })}` - })() + {initialSwapVolumeUsd != null + ? `$${Number(initialSwapVolumeUsd).toLocaleString(undefined, { + maximumFractionDigits: 1, + notation: "compact", + compactDisplay: "short", + })}` : "$0"}

Swap Volume

diff --git a/src/components/dashboard/LeaderboardTable.tsx b/src/components/dashboard/LeaderboardTable.tsx index 421f7115..c343df52 100644 --- a/src/components/dashboard/LeaderboardTable.tsx +++ b/src/components/dashboard/LeaderboardTable.tsx @@ -31,13 +31,12 @@ interface LeaderboardData { userVolume?: number | null userPosition?: number | null nextRankVolume?: number | null - ethPrice?: number | null } interface LeaderboardStats { activeTraders: number | null swapVolumeEth: number | null - ethPrice: number | null + swapVolumeUsd: number | null } interface LeaderboardTableProps { @@ -62,20 +61,15 @@ export const LeaderboardTable = ({ // Get data from props (React Query managed) const activeTraders = statsData?.activeTraders ?? null const swapVolumeEth = statsData?.swapVolumeEth ?? null - const ethPrice = statsData?.ethPrice ?? leaderboardData?.ethPrice ?? null + const swapVolumeUsd = statsData?.swapVolumeUsd ?? null const lbData = leaderboardData?.leaderboard || [] const userVol = leaderboardData?.userVolume ?? null const userPos = leaderboardData?.userPosition ?? null const nextRankVol = leaderboardData?.nextRankVolume ?? null - // Only userSwapTxs needs separate state since it's not in leaderboardData const [userSwapTxs, setUserSwapTxs] = useState(null) - // Computed Values - const totalVol = useMemo( - () => (swapVolumeEth && ethPrice ? swapVolumeEth * ethPrice : null), - [swapVolumeEth, ethPrice] - ) + const totalVol = useMemo(() => swapVolumeUsd ?? null, [swapVolumeUsd]) // Apply testing multiplier to user volume const adjustedUserVol = useMemo( @@ -117,7 +111,7 @@ export const LeaderboardTable = ({ swapCount: userSwapTxs !== null ? userSwapTxs : undefined, change24h: 0, isCurrentUser: true, - ethValue: ethPrice && adjustedUserVol ? adjustedUserVol / ethPrice : undefined, + ethValue: undefined, }) } @@ -165,9 +159,7 @@ export const LeaderboardTable = ({ swapCount: userSwapTxs !== null ? userSwapTxs : undefined, change24h: 0, isCurrentUser: true, - ethValue: - fromLb?.ethValue ?? - (ethPrice && adjustedUserVol ? adjustedUserVol / ethPrice : undefined), + ethValue: fromLb?.ethValue, }, ] } diff --git a/src/components/dashboard/UserMetricsSection.tsx b/src/components/dashboard/UserMetricsSection.tsx index 9bd13e51..65e4aa5c 100644 --- a/src/components/dashboard/UserMetricsSection.tsx +++ b/src/components/dashboard/UserMetricsSection.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from "react" import { Card } from "@/components/ui/card" import { TrendingUp, ArrowUpRight, Coins } from "lucide-react" import { formatNumber } from "@/lib/utils" -import { DEFAULT_ETH_PRICE_USD } from "@/lib/constants" import { FEATURE_FLAGS } from "@/lib/feature-flags" import { useUserAnalyticsData } from "@/hooks/use-dashboard-data" @@ -14,7 +13,7 @@ interface UserMetricsSectionProps { totalTxs: number | null swapTxs: number | null totalSwapVolEth: number | null - ethPrice: number | null + totalSwapVolUsd: number | null } | null } @@ -22,7 +21,7 @@ interface UserMetrics { totalTxs: number swapTxs: number totalSwapVolEth: number - ethPrice: number | null + totalSwapVolUsd: number } export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsSectionProps) => { @@ -38,7 +37,7 @@ export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsS totalTxs: initialGlobalStats.totalTxs ?? 0, swapTxs: initialGlobalStats.swapTxs ?? 0, totalSwapVolEth: initialGlobalStats.totalSwapVolEth ?? 0, - ethPrice: initialGlobalStats.ethPrice ?? null, + totalSwapVolUsd: initialGlobalStats.totalSwapVolUsd ?? 0, } } // Otherwise, use user-specific data from React Query if available @@ -84,13 +83,11 @@ export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsS try { // If feature flag is enabled, fetch global stats (same endpoints as claim page) if (FEATURE_FLAGS.show_global_stats) { - const [transactionsResponse, swapVolumeResponse, swapCountResponse, ethPriceResponse] = - await Promise.all([ - fetch("/api/analytics/transactions"), - fetch("/api/analytics/volume/swap"), - fetch("/api/analytics/swap-count"), - fetch("/api/analytics/eth-price"), - ]) + const [transactionsResponse, swapVolumeResponse, swapCountResponse] = await Promise.all([ + fetch("/api/analytics/transactions"), + fetch("/api/analytics/volume/swap"), + fetch("/api/analytics/swap-count"), + ]) if (!transactionsResponse.ok || !swapVolumeResponse.ok) { throw new Error("Failed to fetch global metrics") @@ -99,16 +96,12 @@ export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsS const transactionsData = await transactionsResponse.json() const swapVolumeData = await swapVolumeResponse.json() const swapCountData = swapCountResponse.ok ? await swapCountResponse.json() : null - const ethPriceData = ethPriceResponse.ok ? await ethPriceResponse.json() : null setMetrics({ totalTxs: transactionsData.cumulativeSuccessfulTxs || 0, swapTxs: swapCountData?.swapTxCount || 0, totalSwapVolEth: swapVolumeData.cumulativeSwapVolEth || 0, - ethPrice: - ethPriceData?.ethPrice !== null && ethPriceData?.ethPrice !== undefined - ? Number(ethPriceData.ethPrice) - : null, + totalSwapVolUsd: swapVolumeData.cumulativeSwapVolUsd ?? 0, }) } else { // User-specific data is handled by React Query hook @@ -130,10 +123,7 @@ export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsS totalTxs: data.totalTxs || 0, swapTxs: data.swapTxs || 0, totalSwapVolEth: data.totalSwapVolEth || 0, - ethPrice: - data.ethPrice !== null && data.ethPrice !== undefined - ? Number(data.ethPrice) - : null, + totalSwapVolUsd: data.totalSwapVolUsd ?? 0, }) } } @@ -151,7 +141,7 @@ export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsS // Show placeholder data when not logged in (only for user-specific stats) const showPlaceholder = !FEATURE_FLAGS.show_global_stats && !address const displayMetrics = showPlaceholder - ? { totalTxs: 0, swapTxs: 0, totalSwapVolEth: 0, ethPrice: null } + ? { totalTxs: 0, swapTxs: 0, totalSwapVolEth: 0, totalSwapVolUsd: 0 } : metrics const isGlobalStats = FEATURE_FLAGS.show_global_stats @@ -219,19 +209,12 @@ export const UserMetricsSection = ({ address, initialGlobalStats }: UserMetricsS
- {(() => { - const swapVolume = displayMetrics.totalSwapVolEth - const price = - displayMetrics.ethPrice !== null - ? displayMetrics.ethPrice - : DEFAULT_ETH_PRICE_USD - const totalUsd = swapVolume * price - return `$${totalUsd.toLocaleString(undefined, { - maximumFractionDigits: 1, - notation: "compact", - compactDisplay: "short", - })}` - })()} + $ + {(displayMetrics.totalSwapVolUsd ?? 0).toLocaleString(undefined, { + maximumFractionDigits: 1, + notation: "compact", + compactDisplay: "short", + })}
diff --git a/src/hooks/use-dashboard-data.ts b/src/hooks/use-dashboard-data.ts index 358c3504..dbb2c1bf 100644 --- a/src/hooks/use-dashboard-data.ts +++ b/src/hooks/use-dashboard-data.ts @@ -21,7 +21,7 @@ interface UserAnalyticsData { totalTxs: number swapTxs: number totalSwapVolEth: number - ethPrice: number | null + totalSwapVolUsd: number } /** @@ -51,7 +51,7 @@ async function fetchUserAnalytics(address: string): Promise { totalTxs: data.totalTxs || 0, swapTxs: data.swapTxs || 0, totalSwapVolEth: data.totalSwapVolEth || 0, - ethPrice: data.ethPrice !== null && data.ethPrice !== undefined ? Number(data.ethPrice) : null, + totalSwapVolUsd: data.totalSwapVolUsd || 0, } } diff --git a/src/hooks/use-leaderboard-data.ts b/src/hooks/use-leaderboard-data.ts index 1fea41fb..5b154d81 100644 --- a/src/hooks/use-leaderboard-data.ts +++ b/src/hooks/use-leaderboard-data.ts @@ -19,13 +19,12 @@ export interface LeaderboardData { userPosition?: number | null userVolume?: number | null nextRankVolume?: number | null - ethPrice?: number | null } interface LeaderboardStats { activeTraders: number | null swapVolumeEth: number | null - ethPrice: number | null + swapVolumeUsd: number | null } /** @@ -47,20 +46,18 @@ async function fetchLeaderboard(currentUserAddress?: string | null): Promise { - const [activeTradersRes, swapVolumeRes, ethPriceRes] = await Promise.all([ + const [activeTradersRes, swapVolumeRes] = await Promise.all([ fetch("/api/analytics/active-traders"), fetch("/api/analytics/volume/swap"), - fetch("/api/analytics/eth-price"), ]) const activeTradersData = activeTradersRes.ok ? await activeTradersRes.json() : null const swapVolumeData = swapVolumeRes.ok ? await swapVolumeRes.json() : null - const ethPriceData = ethPriceRes.ok ? await ethPriceRes.json() : null return { activeTraders: activeTradersData?.activeTraders ?? null, swapVolumeEth: swapVolumeData?.cumulativeSwapVolEth ?? null, - ethPrice: ethPriceData?.ethPrice ?? null, + swapVolumeUsd: swapVolumeData?.cumulativeSwapVolUsd ?? null, } } diff --git a/src/lib/analytics-server.ts b/src/lib/analytics-server.ts index 12a15c04..681de586 100644 --- a/src/lib/analytics-server.ts +++ b/src/lib/analytics-server.ts @@ -34,61 +34,29 @@ export async function getCumulativeTransactions(): Promise { } /** - * Server-side function to fetch current ETH price from Alchemy + * Server-side function to fetch current ETH price from Alchemy (used by claim page and eth-price API) */ export async function getEthPrice(): Promise { try { const apiKey = env.ALCHEMY_API_KEY - if (!apiKey) { console.error("ALCHEMY_API_KEY not configured") return null } - const response = await fetch( `https://api.g.alchemy.com/prices/v1/${apiKey}/tokens/by-symbol?symbols=ETH`, - { - method: "GET", - headers: { - accept: "application/json", - }, - cache: "no-store", - } + { method: "GET", headers: { accept: "application/json" }, cache: "no-store" } ) - if (!response.ok) { - const errorText = await response.text() - console.error("Failed to fetch ETH price:", response.status, errorText) + console.error("Failed to fetch ETH price:", response.status, await response.text()) return null } - const data = await response.json() - - // Alchemy returns data in format: - // { - // "data": [ - // { - // "symbol": "ETH", - // "prices": [ - // { - // "currency": "usd", - // "value": "2933.1463611734", - // "lastUpdatedAt": "2025-12-29T16:38:25Z" - // } - // ] - // } - // ] - // } - if (data.data && Array.isArray(data.data) && data.data.length > 0) { + if (data.data?.length > 0) { const ethData = data.data.find((item: any) => item.symbol === "ETH") - if (ethData && ethData.prices && Array.isArray(ethData.prices) && ethData.prices.length > 0) { - const usdPrice = ethData.prices.find((price: any) => price.currency === "usd") - if (usdPrice && usdPrice.value) { - return Number(usdPrice.value) - } - } + const usdPrice = ethData?.prices?.find((p: any) => p.currency === "usd") + if (usdPrice?.value) return Number(usdPrice.value) } - return null } catch (error) { console.error("Error fetching ETH price:", error) @@ -100,9 +68,11 @@ export async function getEthPrice(): Promise { * Server-side function to fetch cumulative swap volume from analytics API * Calls the internal API route which handles the external API call */ -export async function getCumulativeSwapVolume(): Promise { +export async function getCumulativeSwapVolume(): Promise<{ + eth: number | null + usd: number | null +}> { try { - // Call the internal API route const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000" const response = await fetch(`${baseUrl}/api/analytics/volume/swap`, { cache: "no-store", @@ -110,19 +80,18 @@ export async function getCumulativeSwapVolume(): Promise { if (!response.ok) { console.error("Failed to fetch swap volume:", response.statusText) - return null + return { eth: null, usd: null } } const data = await response.json() + if (!data.success) return { eth: null, usd: null } - if (data.success && data.cumulativeSwapVolEth !== null) { - return Number(data.cumulativeSwapVolEth) - } - - return null + const eth = data.cumulativeSwapVolEth != null ? Number(data.cumulativeSwapVolEth) : null + const usd = data.cumulativeSwapVolUsd != null ? Number(data.cumulativeSwapVolUsd) : null + return { eth, usd } } catch (error) { console.error("Error fetching cumulative swap volume:", error) - return null + return { eth: null, usd: null } } } @@ -212,7 +181,7 @@ export async function getActiveTraders(): Promise { /** * Server-side function to fetch leaderboard data (top 15) without user-specific data - * This can be used for SSR to show the leaderboard immediately + * This can be used for SSR to show the leaderboard immediately (USD from DB) */ export async function getLeaderboardTop15(): Promise<{ leaderboard: Array<{ @@ -224,28 +193,13 @@ export async function getLeaderboardTop15(): Promise<{ isCurrentUser: boolean ethValue: number }> - ethPrice: number | null } | null> { try { - // Get ETH price for USD conversion - const ethPrice = await getEthPrice() - - // Use the SQL flow via leaderboard service const leaderboardRows = await getLeaderboard(15) + // useTotalVolume=false: use swap_vol_usd_24h for SSR + const leaderboard = transformLeaderboardRows(leaderboardRows, null, false) - // Transform to expected format with USD conversion using shared utility - // useTotalVolume=false means we use swap_vol_eth_24h (24h volume) for SSR - const leaderboard = transformLeaderboardRows( - leaderboardRows, - ethPrice, - null, // No current user for SSR - false // Use 24h volume for SSR - ) - - return { - leaderboard, - ethPrice, - } + return { leaderboard } } catch (error) { console.error("Error fetching leaderboard top 15:", error) return null diff --git a/src/lib/analytics/queries.ts b/src/lib/analytics/queries.ts index 542c5c91..537d9ef6 100644 --- a/src/lib/analytics/queries.ts +++ b/src/lib/analytics/queries.ts @@ -61,7 +61,8 @@ ORDER BY day_utc DESC export const GET_SWAP_COUNT = ` SELECT COUNT(*) AS swap_tx_count, - SUM(COALESCE(p.swap_vol_eth, 0)) AS total_swap_vol_eth + SUM(COALESCE(p.swap_vol_eth, 0)) AS total_swap_vol_eth, + SUM(COALESCE(p.swap_vol_usd, 0)) AS total_swap_vol_usd FROM mevcommit_57173.processed_l1_txns_v2 p WHERE p.is_swap = TRUE AND EXISTS ( @@ -76,7 +77,9 @@ WITH daily AS ( SELECT date_trunc('day', l1_timestamp) AS day, SUM(COALESCE(total_vol_eth, 0)) AS daily_total_tx_vol_eth, - SUM(COALESCE(swap_vol_eth, 0)) AS daily_total_swap_vol_eth + SUM(COALESCE(swap_vol_eth, 0)) AS daily_total_swap_vol_eth, + SUM(COALESCE(total_vol_usd, 0)) AS daily_total_tx_vol_usd, + SUM(COALESCE(swap_vol_usd, 0)) AS daily_total_swap_vol_usd FROM mevcommit_57173.processed_l1_txns_v2 GROUP BY 1 ), @@ -90,13 +93,23 @@ cumulative AS ( SUM(daily_total_swap_vol_eth) OVER ( ORDER BY day ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - ) AS cumulative_total_swap_vol_eth + ) AS cumulative_total_swap_vol_eth, + SUM(daily_total_tx_vol_usd) OVER ( + ORDER BY day ASC + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS cumulative_total_tx_vol_usd, + SUM(daily_total_swap_vol_usd) OVER ( + ORDER BY day ASC + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS cumulative_total_swap_vol_usd FROM daily ) SELECT day, cumulative_total_tx_vol_eth, - cumulative_total_swap_vol_eth + cumulative_total_swap_vol_eth, + cumulative_total_tx_vol_usd, + cumulative_total_swap_vol_usd FROM cumulative WHERE CAST(day AS DATE) >= DATE '2025-11-20' ORDER BY day DESC @@ -108,6 +121,7 @@ WITH all_time AS ( SELECT lower(from_address) AS wallet, SUM(COALESCE(swap_vol_eth, 0)) AS total_swap_vol_eth, + SUM(COALESCE(swap_vol_usd, 0)) AS total_swap_vol_usd, COUNT(*) AS swap_count FROM mevcommit_57173.processed_l1_txns_v2 WHERE is_swap = TRUE @@ -116,7 +130,8 @@ WITH all_time AS ( current_24h AS ( SELECT lower(from_address) AS wallet, - SUM(COALESCE(swap_vol_eth, 0)) AS swap_vol_eth_24h + SUM(COALESCE(swap_vol_eth, 0)) AS swap_vol_eth_24h, + SUM(COALESCE(swap_vol_usd, 0)) AS swap_vol_usd_24h FROM mevcommit_57173.processed_l1_txns_v2 WHERE is_swap = TRUE AND l1_timestamp >= date_trunc('day', CURRENT_TIMESTAMP) - INTERVAL '1' DAY @@ -135,8 +150,10 @@ previous_24h AS ( SELECT a.wallet, COALESCE(a.total_swap_vol_eth, 0) AS total_swap_vol_eth, + COALESCE(a.total_swap_vol_usd, 0) AS total_swap_vol_usd, COALESCE(a.swap_count, 0) AS swap_count, COALESCE(c.swap_vol_eth_24h, 0) AS swap_vol_eth_24h, + COALESCE(c.swap_vol_usd_24h, 0) AS swap_vol_usd_24h, CASE WHEN COALESCE(p.swap_vol_eth_prev_24h, 0) > 0 THEN ((COALESCE(c.swap_vol_eth_24h, 0) - COALESCE(p.swap_vol_eth_prev_24h, 0)) / p.swap_vol_eth_prev_24h * 100) @@ -156,6 +173,7 @@ export const LEADERBOARD_USER_DATA = ` WITH all_time_user AS ( SELECT SUM(COALESCE(swap_vol_eth, 0)) AS total_swap_vol_eth, + SUM(COALESCE(swap_vol_usd, 0)) AS total_swap_vol_usd, COUNT(*) AS swap_count FROM mevcommit_57173.processed_l1_txns_v2 WHERE is_swap = TRUE @@ -163,7 +181,8 @@ WITH all_time_user AS ( ), current_24h_user AS ( SELECT - SUM(COALESCE(swap_vol_eth, 0)) AS swap_vol_eth_24h + SUM(COALESCE(swap_vol_eth, 0)) AS swap_vol_eth_24h, + SUM(COALESCE(swap_vol_usd, 0)) AS swap_vol_usd_24h FROM mevcommit_57173.processed_l1_txns_v2 WHERE is_swap = TRUE AND lower(from_address) = lower(:addr) @@ -180,8 +199,10 @@ previous_24h_user AS ( ) SELECT COALESCE(a.total_swap_vol_eth, 0) AS total_swap_vol_eth, + COALESCE(a.total_swap_vol_usd, 0) AS total_swap_vol_usd, COALESCE(a.swap_count, 0) AS swap_count, COALESCE(c.swap_vol_eth_24h, 0) AS swap_vol_eth_24h, + COALESCE(c.swap_vol_usd_24h, 0) AS swap_vol_usd_24h, CASE WHEN COALESCE(p.swap_vol_eth_prev_24h, 0) > 0 THEN ((COALESCE(c.swap_vol_eth_24h, 0) - COALESCE(p.swap_vol_eth_prev_24h, 0)) / p.swap_vol_eth_prev_24h * 100) @@ -214,11 +235,13 @@ FROM ( export const LEADERBOARD_NEXT_RANK_THRESHOLD = ` SELECT - MIN(total_swap_vol_eth) AS total_swap_vol_eth + MIN(total_swap_vol_eth) AS total_swap_vol_eth, + MIN(total_swap_vol_usd) AS total_swap_vol_usd FROM ( SELECT lower(from_address) AS wallet, - SUM(COALESCE(swap_vol_eth, 0)) AS total_swap_vol_eth + SUM(COALESCE(swap_vol_eth, 0)) AS total_swap_vol_eth, + SUM(COALESCE(swap_vol_usd, 0)) AS total_swap_vol_usd FROM mevcommit_57173.processed_l1_txns_v2 WHERE is_swap = TRUE GROUP BY lower(from_address) @@ -234,7 +257,8 @@ FROM ( // Users domain export const GET_USER_SWAP_VOLUME = ` SELECT - SUM(COALESCE(p.swap_vol_eth, 0)) AS total_swap_vol_eth + SUM(COALESCE(p.swap_vol_eth, 0)) AS total_swap_vol_eth, + SUM(COALESCE(p.swap_vol_usd, 0)) AS total_swap_vol_usd FROM mevcommit_57173.processed_l1_txns_v2 p WHERE lower(p.from_address) = lower(:addr) AND p.is_swap = TRUE diff --git a/src/lib/analytics/services/leaderboard-transform.ts b/src/lib/analytics/services/leaderboard-transform.ts index 803abf4c..c581d42a 100644 --- a/src/lib/analytics/services/leaderboard-transform.ts +++ b/src/lib/analytics/services/leaderboard-transform.ts @@ -28,15 +28,13 @@ export interface TransformedLeaderboardEntry { } /** - * Transform raw leaderboard rows into formatted entries + * Transform raw leaderboard rows into formatted entries (USD from DB columns) * @param rows Raw leaderboard rows from database - * @param ethPrice ETH price for USD conversion * @param currentUserAddress Optional current user address for isCurrentUser flag - * @param useTotalVolume If true, uses total_swap_vol_eth; if false, uses swap_vol_eth_24h + * @param useTotalVolume If true, uses total_swap_vol_usd; if false, uses swap_vol_usd_24h */ export function transformLeaderboardRows( rows: LeaderboardRow[], - ethPrice: number | null, currentUserAddress?: string | null, useTotalVolume: boolean = true ): TransformedLeaderboardEntry[] { @@ -46,15 +44,14 @@ export function transformLeaderboardRows( const wallet = row[0] const walletLower = wallet?.toLowerCase() || wallet const totalSwapVolEth = Number(row[1]) || 0 - const swapCount = Number(row[2]) || 0 - const swapVolEth24h = Number(row[3]) || 0 - const change24hPct = Number(row[4]) || 0 + const totalSwapVolUsd = Number(row[2]) || 0 + const swapCount = Number(row[3]) || 0 + const swapVolEth24h = Number(row[4]) || 0 + const swapVolUsd24h = Number(row[5]) || 0 + const change24hPct = Number(row[6]) || 0 - // Use total volume or 24h volume based on parameter const volumeEth = useTotalVolume ? totalSwapVolEth : swapVolEth24h - - // Convert volume to USD - const volumeUsd = ethPrice !== null ? volumeEth * ethPrice : volumeEth + const volumeUsd = useTotalVolume ? totalSwapVolUsd : swapVolUsd24h return { rank: index + 1, diff --git a/src/lib/analytics/services/leaderboard.service.ts b/src/lib/analytics/services/leaderboard.service.ts index 22c68183..56fc83d3 100644 --- a/src/lib/analytics/services/leaderboard.service.ts +++ b/src/lib/analytics/services/leaderboard.service.ts @@ -9,8 +9,10 @@ import type { QueryOptions } from "../client" export type LeaderboardRow = [ wallet: string, total_swap_vol_eth: number, + total_swap_vol_usd: number, swap_count: number, swap_vol_eth_24h: number, + swap_vol_usd_24h: number, change_24h_pct: number, ] @@ -19,8 +21,10 @@ export type LeaderboardRow = [ */ export type UserLeaderboardDataRow = [ total_swap_vol_eth: number, + total_swap_vol_usd: number, swap_count: number, swap_vol_eth_24h: number, + swap_vol_usd_24h: number, change_24h_pct: number, ] @@ -30,9 +34,12 @@ export type UserLeaderboardDataRow = [ export type UserRankResult = [user_rank: number] /** - * Next rank threshold result + * Next rank threshold result (eth and usd) */ -export type NextRankThresholdResult = [total_swap_vol_eth: number | null] +export type NextRankThresholdResult = [ + total_swap_vol_eth: number | null, + total_swap_vol_usd: number | null, +] const client = getAnalyticsClient() @@ -99,22 +106,26 @@ export async function getUserRank(address: string, options?: QueryOptions): Prom } /** - * Get the volume threshold needed to reach the next rank + * Get the volume threshold needed to reach the next rank (ETH and USD) * Returns the total swap volume of the user just above the current user - * Returns null if user is already #1 or has no swap volume + * Returns { eth: null, usd: null } if user is already #1 or has no swap volume */ export async function getNextRankThreshold( address: string, options?: QueryOptions -): Promise { +): Promise<{ eth: number | null; usd: number | null }> { const addr = sanitizeAddress(address) const row = await client.executeOne("leaderboard/next-rank-threshold", { addr }, options) - if (!row || row[0] === null) { - return null + if (!row) { + return { eth: null, usd: null } } - const threshold = Number(row[0]) - return Number.isFinite(threshold) ? threshold : null + const eth = row[0] !== null && row[0] !== undefined ? Number(row[0]) : null + const usd = row[1] !== null && row[1] !== undefined ? Number(row[1]) : null + return { + eth: eth !== null && Number.isFinite(eth) ? eth : null, + usd: usd !== null && Number.isFinite(usd) ? usd : null, + } } diff --git a/src/lib/analytics/services/transactions.service.ts b/src/lib/analytics/services/transactions.service.ts index 1cb3f7de..515fba78 100644 --- a/src/lib/analytics/services/transactions.service.ts +++ b/src/lib/analytics/services/transactions.service.ts @@ -19,7 +19,11 @@ export type TransactionsAnalyticsRow = [ /** * Swap count result row */ -export type SwapCountRow = [swap_tx_count: number, total_swap_vol_eth: number] +export type SwapCountRow = [ + swap_tx_count: number, + total_swap_vol_eth: number, + total_swap_vol_usd: number, +] /** * Swap volume result row @@ -28,6 +32,8 @@ export type SwapVolumeRow = [ day: string, cumulative_total_tx_vol_eth: number, cumulative_total_swap_vol_eth: number, + cumulative_total_tx_vol_usd: number, + cumulative_total_swap_vol_usd: number, ] const client = getAnalyticsClient() @@ -116,23 +122,24 @@ export async function getSwapCount(options?: QueryOptions): Promise { +export async function getSwapVolume( + options?: QueryOptions +): Promise<{ eth: number | null; usd: number | null }> { const rows = await client.execute("transactions/get-swap-volume", undefined, options) if (rows.length === 0) { - return null + return { eth: null, usd: null } } - // Get the first row (most recent day) - the query already orders by day DESC - // Format: [day, cumulative_total_tx_vol_eth, cumulative_total_swap_vol_eth] const latestRow = rows[0] as SwapVolumeRow + const eth = latestRow[2] !== null && latestRow[2] !== undefined ? Number(latestRow[2]) : null + const usd = latestRow[4] !== null && latestRow[4] !== undefined ? Number(latestRow[4]) : null - // Extract cumulative_total_swap_vol_eth from index 2 (swap volume) - const cumulativeSwapVolume = - latestRow[2] !== null && latestRow[2] !== undefined ? Number(latestRow[2]) : null - - return cumulativeSwapVolume !== null && !isNaN(cumulativeSwapVolume) ? cumulativeSwapVolume : null + return { + eth: eth !== null && !isNaN(eth) ? eth : null, + usd: usd !== null && !isNaN(usd) ? usd : null, + } } diff --git a/src/lib/analytics/services/users.service.ts b/src/lib/analytics/services/users.service.ts index 2176bdd6..fe6b91c6 100644 --- a/src/lib/analytics/services/users.service.ts +++ b/src/lib/analytics/services/users.service.ts @@ -6,7 +6,10 @@ import type { QueryOptions } from "../client" /** * User swap volume result row */ -export type UserSwapVolumeRow = [total_swap_vol_eth: number | null] +export type UserSwapVolumeRow = [ + total_swap_vol_eth: number | null, + total_swap_vol_usd: number | null, +] const client = getAnalyticsClient() @@ -23,19 +26,32 @@ function sanitizeAddress(address: string): string { } /** - * Get total swap volume for a specific user address + * Get total swap volume for a specific user address (ETH and USD from DB) * @param address Ethereum address (0x-prefixed hex string) - * @returns Total swap volume in ETH, or 0 if user has no swaps */ -export async function getUserSwapVolume(address: string, options?: QueryOptions): Promise { +export async function getUserSwapVolume( + address: string, + options?: QueryOptions +): Promise<{ eth: number; usd: number }> { const addr = sanitizeAddress(address) const row = await client.executeOne("users/get-user-swap-volume", { addr }, options) - if (!row || row[0] === null || row[0] === undefined) { - return 0 + if (!row) { + return { eth: 0, usd: 0 } } - const volume = Number(row[0]) - return Number.isFinite(volume) ? volume : 0 + const eth = + row[0] !== null && row[0] !== undefined + ? Number.isFinite(Number(row[0])) + ? Number(row[0]) + : 0 + : 0 + const usd = + row[1] !== null && row[1] !== undefined + ? Number.isFinite(Number(row[1])) + ? Number(row[1]) + : 0 + : 0 + return { eth, usd } }