| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- import { PricePoint, SwapEvent, TransferEvent } from "@/types/token";
- import { ethers, BigNumberish } from "ethers";
- import { ERC20_ABI } from "@/utils/abi/erc20";
- import { useQuery } from "@tanstack/react-query";
- import {
- getMainContract,
- getProvider,
- getUniswapPairContract,
- } from "@/lib/provider";
- /**
- * React hook to fetch and return token statistics and historical pricing data
- * @param tokenId - ID of the token to fetch data for
- */
- export function useTokens(tokenId: number) {
- const CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
-
- const { data, isLoading, error } = useQuery({
- queryKey: ["token-stats", tokenId],
- queryFn: () => fetchTokenStats(tokenId),
- staleTime: CACHE_TTL, // Use the same value as in your hooks
- gcTime: CACHE_TTL, // Use the same time for garbage collection
- });
- // Check if error message contains "Pair not created"
- const processedError =
- error && error instanceof Error && error.message.includes("Pair not created")
- ? "Pair not created"
- : (error as Error | null);
- return {
- stats: data?.stats || null,
- priceHistory: data?.priceHistory || [],
- tokenAddress: data?.tokenAddress || null,
- loading: isLoading,
- error: processedError,
- };
- }
- /**
- * Fetches token data from blockchain and computes market metrics
- */
- async function fetchTokenStats(tokenId: number) {
- const provider = getProvider();
- const nftContract = getMainContract();
- const [erc20Token, totalSupply, reserve0, reserve1, pairAddress] =
- await nftContract.getTokenData(tokenId);
- if (!erc20Token || !pairAddress)
- throw "Pair not created";
- const pairContract = getUniswapPairContract(pairAddress);
- const tokenContract = new ethers.Contract(erc20Token, ERC20_ABI, provider);
- // Fetch required on-chain logs in parallel
- const [token0, swapLogs, transferLogs] = await Promise.all([
- pairContract.token0(),
- pairContract.queryFilter(pairContract.filters.Swap(), 0, "latest"),
- tokenContract.queryFilter(tokenContract.filters.Transfer(), 0, "latest"),
- ]);
- const { aiusReserveParsed, tokenReserveParsed } = parseReserves({
- reserve0,
- reserve1,
- token0,
- erc20Token,
- });
- const totalSupplyParsed = parseFloat(ethers.formatUnits(totalSupply, 18));
- const price =
- tokenReserveParsed === 0 ? 0 : aiusReserveParsed / tokenReserveParsed;
- const marketCap = price * totalSupplyParsed;
- const tvl = 2 * aiusReserveParsed; // Total Value Locked in liquidity pool
- const swapEvents = await enrichSwapEvents(swapLogs, provider);
- const pastPrice = computePastPrice(swapEvents, price, erc20Token, token0);
- const change24h =
- pastPrice !== 0 ? ((price - pastPrice) / pastPrice) * 100 : 0;
- const volume = compute24hVolume(swapEvents);
- // Unique holders based on transfer event recipients
- const holders = new Set(
- transferLogs
- .filter((e) => "args" in e)
- .map((e) => (e as unknown as TransferEvent).args.to),
- ).size;
- const priceHistory = generatePriceHistory(
- swapEvents,
- erc20Token,
- token0,
- price,
- );
- return {
- stats: { marketCap, tvl, price, volume, holders, change24h },
- priceHistory,
- tokenAddress: erc20Token,
- };
- }
- /**
- * Helper to determine which reserve belongs to the token vs AIUS
- */
- function parseReserves({
- reserve0,
- reserve1,
- token0,
- erc20Token,
- }: {
- reserve0: BigNumberish;
- reserve1: BigNumberish;
- token0: string;
- erc20Token: string;
- }) {
- const [tokenReserve, aiusReserve] =
- token0 === erc20Token ? [reserve0, reserve1] : [reserve1, reserve0];
- return {
- tokenReserveParsed: parseFloat(ethers.formatUnits(tokenReserve, 18)),
- aiusReserveParsed: parseFloat(ethers.formatUnits(aiusReserve, 18)),
- };
- }
- /**
- * Enhances swap events by attaching block timestamps for time-based analytics
- */
- async function enrichSwapEvents(
- events: any[],
- provider: ethers.Provider,
- ): Promise<SwapEvent[]> {
- const blockCache = new Map<number, number>();
- const enrichedEvents = await Promise.all(
- events.map(async (event) => {
- if (!("args" in event)) return null;
- if (!blockCache.has(event.blockNumber)) {
- const block = await provider.getBlock(event.blockNumber);
- if (block) blockCache.set(event.blockNumber, block.timestamp);
- }
- return {
- args: {
- amount0In: event.args.amount0In,
- amount0Out: event.args.amount0Out,
- amount1In: event.args.amount1In,
- amount1Out: event.args.amount1Out,
- },
- blockTimestamp: blockCache.get(event.blockNumber)!,
- };
- }),
- );
- return enrichedEvents.filter((e): e is SwapEvent => e !== null);
- }
- /**
- * Computes the token price 24 hours ago using historical swap events
- */
- function computePastPrice(
- events: SwapEvent[],
- currentPrice: number,
- erc20Token: string,
- token0: string,
- ): number {
- const timestamp24hAgo = Math.floor(Date.now() / 1000) - 86400;
- for (let i = events.length - 1; i >= 0; i--) {
- const e = events[i];
- if (e.blockTimestamp <= timestamp24hAgo) {
- const [in0, out0, in1, out1] = [
- getFloat(e.args.amount0In),
- getFloat(e.args.amount0Out),
- getFloat(e.args.amount1In),
- getFloat(e.args.amount1Out),
- ];
- const amount0 = in0 - out0;
- const amount1 = out1 - in1;
- if (amount1 !== 0) return amount0 / amount1;
- }
- }
- return currentPrice;
- }
- /**
- * Calculates total 24-hour trading volume from swap events
- */
- function compute24hVolume(events: SwapEvent[]): number {
- const now = Math.floor(Date.now() / 1000);
- return events
- .filter((e) => now - e.blockTimestamp <= 86400)
- .reduce((sum, e) => {
- return (
- sum +
- getFloat(e.args.amount0In) +
- getFloat(e.args.amount0Out) +
- getFloat(e.args.amount1In) +
- getFloat(e.args.amount1Out)
- );
- }, 0);
- }
- /**
- * Generates a time-series price history from swap events
- */
- function generatePriceHistory(
- events: SwapEvent[],
- erc20Token: string,
- token0: string,
- currentPrice: number,
- ): PricePoint[] {
- const history: PricePoint[] = events
- .map((e) => {
- const [in0, out0, in1, out1] = [
- getFloat(e.args.amount0In),
- getFloat(e.args.amount0Out),
- getFloat(e.args.amount1In),
- getFloat(e.args.amount1Out),
- ];
- let price = 0;
- if (token0 === erc20Token) {
- if (in0 > 0 && out1 > 0) price = out1 / in0;
- else if (in1 > 0 && out0 > 0) price = in1 / out0;
- } else {
- if (in1 > 0 && out0 > 0) price = out0 / in1;
- else if (in0 > 0 && out1 > 0) price = in0 / out1;
- }
- return {
- x: new Date(e.blockTimestamp * 1000).toISOString(),
- y: price,
- };
- })
- // Filter out unrealistic values
- .filter((d) => isFinite(d.y) && d.y > 0.01 && d.y < 1_000_000)
- .sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime());
- // Append current price to the history
- history.push({ x: new Date().toISOString(), y: currentPrice });
- return history;
- }
- /**
- * Utility to parse a BigNumberish value into a float
- */
- function getFloat(val: BigNumberish | undefined): number {
- return parseFloat(ethers.formatUnits(val ?? 0, 18));
- }
|