diff --git a/pages/api/token-stats.ts b/pages/api/token-stats.ts new file mode 100644 index 0000000..a8e2400 --- /dev/null +++ b/pages/api/token-stats.ts @@ -0,0 +1,34 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { fetchTokenList } from "services/tokens"; +import { Network } from "types/network"; +import { TokenType } from "types/tokens"; +import { buildPoolsInfo } from "utils/info/pools"; +import { buildTokenStats } from "utils/info/tokens"; +import { getMercuryPools } from "zephyr/helpers"; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const queryParams = req.query; + + let network = queryParams?.network as string; + network = network?.toUpperCase() as Network; + + if (network !== "MAINNET" && network !== "TESTNET") { + return res.status(400).json({ error: "Invalid network" }); + } + + try { + const tokenList: TokenType[] = await fetchTokenList({ network }); + + const data = await getMercuryPools(network); + + const result = await buildPoolsInfo(data, tokenList, network); + + const tokensInfo = await buildTokenStats(tokenList, result); + + return res.json(tokensInfo); + } catch (error) { + return res.status(500).json({ error: "Failed to fetch token list" }); + } +} + +export default handler; diff --git a/pages/index.tsx b/pages/index.tsx index 0cc1fb0..730e595 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -5,7 +5,7 @@ import PoolsTable from "../src/components/pools-table/pools-table"; import TokensTable from "../src/components/tokens-table/tokens-table"; import TVLChart from "../src/components/tvl-chart"; import { useQueryPools } from "../src/hooks/pools"; -import { useQueryTokens } from "../src/hooks/tokens"; +import { useQueryTokens, useQueryTokenStats } from "../src/hooks/tokens"; import TransactionsTable from "../src/components/transaction-table/transactions-table"; import { useQueryAllEvents } from "../src/hooks/events"; import useEventTopicFilter from "../src/hooks/use-event-topic-filter"; @@ -24,10 +24,12 @@ import { useRouter } from "next/router"; import { StyledCard } from "components/styled/card"; import LoadingSkeleton from "components/loading-skeleton"; import { formatNumberToMoney } from "utils/utils"; +import { Text } from "components/styled/text"; export default function Home() { const pools = useQueryPools(); const tokens = useQueryTokens(); + const tokenStats = useQueryTokenStats(); const router = useRouter(); @@ -82,6 +84,60 @@ export default function Home() { Soroswap Info + + + + Total Volume 24h + + + {formatNumberToMoney(tokenStats.data?.volume24h)} + + + + + Total Volume 7d + + + {formatNumberToMoney(tokenStats.data?.volume7d)} + + + + + Total Volume All Time + + + {formatNumberToMoney(tokenStats.data?.volumeAllTime)} + + + + + + + Total Fees Generated 24h + + + {formatNumberToMoney(tokenStats.data?.fees24h)} + + + + + Total Fees Generated 7d + + + {formatNumberToMoney(tokenStats.data?.fees7d)} + + + + + Total Fees Generated All Time + + + {formatNumberToMoney(tokenStats.data?.feesAllTime)} + + + + + diff --git a/pages/pools/[id]/index.tsx b/pages/pools/[id]/index.tsx index 2ccaf39..8ef5c07 100644 --- a/pages/pools/[id]/index.tsx +++ b/pages/pools/[id]/index.tsx @@ -314,6 +314,35 @@ const PoolPage = () => { + + + + + Volume 7d + + + {formatNumberToMoney(pool.data?.volume7d)} + + + + + Fees 7d + + + {formatNumberToMoney(pool.data?.fees7d)} + + + + + APY + + + {formatNumberToToken(pool.data?.apy)} + + + + + {formatNumberToMoney(row.fees24h)} + + {formatNumberToMoney(row.fees7d)} + {formatNumberToMoney(row.feesYearly)} + + {formatNumberToMoney(row.apy)} + ); })} diff --git a/src/hooks/tokens.ts b/src/hooks/tokens.ts index d784117..986fa63 100644 --- a/src/hooks/tokens.ts +++ b/src/hooks/tokens.ts @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { fetchToken, fetchTokenPriceChart, + fetchTokenStats, fetchTokenTVLChart, fetchTokenVolumeChart, fetchTokens, @@ -71,3 +72,13 @@ export const useQueryTokenVolumeChart = ({ enabled: !!tokenAddress && isValidQuery, }); }; + +export const useQueryTokenStats = () => { + const { network, isValidQuery } = useQueryNetwork(); + + return useQuery({ + queryKey: [key, network, "token-stats"], + queryFn: () => fetchTokenStats({ network: network! }), + enabled: isValidQuery, + }); +}; diff --git a/src/services/tokens.ts b/src/services/tokens.ts index 817974d..d734488 100644 --- a/src/services/tokens.ts +++ b/src/services/tokens.ts @@ -1,5 +1,5 @@ import { ApiNetwork, Network } from "types/network"; -import { Token } from "../types/tokens"; +import { Token, TokenStats } from "../types/tokens"; import { fillDatesAndSort } from "../utils/complete-chart"; import axiosInstance from "./axios"; import { xlmToken } from "constants/constants"; @@ -105,3 +105,11 @@ export const fetchTokenVolumeChart = async ({ return filledData; }; + +export const fetchTokenStats = async ({ network }: ApiNetwork) => { + const { data } = await axiosInstance.get("/api/token-stats", { + params: { network }, + }); + + return data; +}; diff --git a/src/types/pools.ts b/src/types/pools.ts index 7dfd2e9..461db0d 100644 --- a/src/types/pools.ts +++ b/src/types/pools.ts @@ -44,6 +44,8 @@ export interface Pool { volume24h?: number; volume7d?: number; fees24h?: number; + fees7d?: number; + apy?:number; feesYearly?: number; tvlChartData?: TvlChartData[]; volumeChartData?: VolumeChartData[]; diff --git a/src/types/tokens.ts b/src/types/tokens.ts index 3504d04..d1c53f8 100644 --- a/src/types/tokens.ts +++ b/src/types/tokens.ts @@ -18,6 +18,21 @@ export interface Token { issuer: string; } +export interface TokenFeesChartData { + date: string; + fees: number; + timestamp: number; +} + +export interface TokenStats { + volume24h: number; + volume7d: number; + volumeAllTime: number; + fees24h: number; + fees7d: number; + feesAllTime: number; +} + export interface TokenType { code: string; issuer?: string; diff --git a/src/utils/info/pools/index.ts b/src/utils/info/pools/index.ts index f6e4ed6..c9223a0 100644 --- a/src/utils/info/pools/index.ts +++ b/src/utils/info/pools/index.ts @@ -144,6 +144,9 @@ export const buildPoolsInfo = async ( const feesYearly = fees7d * 52; + const weeklyYield = fees7d / tvl; + const apy = weeklyYield * 52 * 100; + return { ...poolData, tvlChartData, @@ -152,6 +155,8 @@ export const buildPoolsInfo = async ( volume7d, volume24h, fees24h, + fees7d, + apy, feesYearly, }; }) diff --git a/src/utils/info/tokens/index.ts b/src/utils/info/tokens/index.ts index a85e752..762ec3c 100644 --- a/src/utils/info/tokens/index.ts +++ b/src/utils/info/tokens/index.ts @@ -6,7 +6,7 @@ import { TvlChartData, VolumeChartData, } from "types/pools"; -import { Token, TokenType } from "types/tokens"; +import { Token, TokenType, TokenFeesChartData, TokenStats } from "types/tokens"; import { MercuryRsvCh, getMercuryRsvCh } from "zephyr/helpers"; import { getDate } from "../pools"; import { getExpectedAmountOfOne } from "utils/utils"; @@ -216,3 +216,138 @@ export const getTokenPriceChart = ( return filledPriceChartData as PriceChartData[]; }; + +export const buildTokenStats = async ( + tokenList: TokenType[], + pools: Pool[] +) => { + const tokens: Token[] = tokenList.map((t) => ({ + asset: t, + fees24h: 0, + price: 0, + priceChange24h: 0, + tvl: 0, + tvlSlippage24h: 0, + tvlSlippage7d: 0, + volume24h: 0, + volume24hChange: 0, + volume7d: 0, + volume7dChange: 0, + issuer: "", + })); + + const USDC = tokens.find((token) => token.asset.code === "USDC"); + + if (!USDC) return null; + + const totalStats = { + volume24h: 0, + volume7d: 0, + volumeAllTime: 0, + fees24h: 0, + fees7d: 0, + feesAllTime: 0, + }; + + tokens.forEach((token) => { + const tokenPools = pools.filter( + (pool) => + pool.tokenA.contract === token.asset.contract || + pool.tokenB.contract === token.asset.contract + ); + + const volumeChartData = getTokenVolumeChartData(token, tokenPools); + + const nowTimestamp = new Date().getTime() / 1000; + + const volume24h = volumeChartData.reduce((acc, item) => { + const itemTimestamp = new Date(item.date).getTime() / 1000; + + if (nowTimestamp - itemTimestamp <= 24 * 3600) { + return acc + item.volume; + } + return acc; + }, 0); + + const volume7d = volumeChartData.reduce((acc, item) => { + const itemTimestamp = new Date(item.date).getTime() / 1000; + + if (nowTimestamp - itemTimestamp <= 7 * 24 * 3600) { + return acc + item.volume; + } + return acc; + }, 0); + + const volumeAllTime = volumeChartData.reduce((acc, item) => { + return acc + item.volume; + }, 0); + + const feesChartData = getTokenFeesChartData(tokenPools); + + const fees24h = feesChartData.reduce((acc, item) => { + const itemTimestamp = new Date(item.date).getTime() / 1000; + + if (nowTimestamp - itemTimestamp <= 24 * 3600) { + return acc + item.fees; + } + return acc; + }, 0); + + const fees7d = feesChartData.reduce((acc, item) => { + const itemTimestamp = new Date(item.date).getTime() / 1000; + + if (nowTimestamp - itemTimestamp <= 7 * 24 * 3600) { + return acc + item.fees; + } + return acc; + }, 0); + + const feesAllTime = feesChartData.reduce((acc, item) => { + return acc + item.fees; + }, 0); + + // Accumulate totals + totalStats.volume24h += volume24h; + totalStats.volume7d += volume7d; + totalStats.volumeAllTime += volumeAllTime; + totalStats.fees24h += fees24h; + totalStats.fees7d += fees7d; + totalStats.feesAllTime += feesAllTime; + }); + + return totalStats as TokenStats; +}; + +export const getTokenFeesChartData = ( + tokenPools: Pool[] +): TokenFeesChartData[] => { + let feesChartData: { [x: string]: any } = {}; + + tokenPools.forEach((pool) => { + pool.feesChartData?.forEach((data) => { + const fees = data.fees || 0; + + if (!feesChartData[data.date]) { + feesChartData[data.date] = { + date: data.date, + fees: fees, + timestamp: data.timestamp, + }; + } else { + feesChartData[data.date] = { + ...feesChartData[data.date], + fees: feesChartData[data.date].fees + fees, + }; + } + }); + }); + + feesChartData = Object.values(feesChartData); + + feesChartData.sort( + (a: TokenFeesChartData, b: TokenFeesChartData) => + new Date(a.date).getTime() - new Date(b.date).getTime() + ); + + return feesChartData as TokenFeesChartData[]; +};