use-token.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { PricePoint, SwapEvent, TransferEvent } from "@/types/token";
  2. import { ethers, BigNumberish } from "ethers";
  3. import { ERC20_ABI } from "@/utils/abi/erc20";
  4. import { useQuery } from "@tanstack/react-query";
  5. import {
  6. getMainContract,
  7. getProvider,
  8. getUniswapPairContract,
  9. } from "@/lib/provider";
  10. /**
  11. * React hook to fetch and return token statistics and historical pricing data
  12. * @param tokenId - ID of the token to fetch data for
  13. */
  14. export function useTokens(tokenId: number) {
  15. const CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
  16. const { data, isLoading, error } = useQuery({
  17. queryKey: ["token-stats", tokenId],
  18. queryFn: () => fetchTokenStats(tokenId),
  19. staleTime: CACHE_TTL, // Use the same value as in your hooks
  20. gcTime: CACHE_TTL, // Use the same time for garbage collection
  21. });
  22. // Check if error message contains "Pair not created"
  23. const processedError =
  24. error && error instanceof Error && error.message.includes("Pair not created")
  25. ? "Pair not created"
  26. : (error as Error | null);
  27. return {
  28. stats: data?.stats || null,
  29. priceHistory: data?.priceHistory || [],
  30. tokenAddress: data?.tokenAddress || null,
  31. loading: isLoading,
  32. error: processedError,
  33. };
  34. }
  35. /**
  36. * Fetches token data from blockchain and computes market metrics
  37. */
  38. async function fetchTokenStats(tokenId: number) {
  39. const provider = getProvider();
  40. const nftContract = getMainContract();
  41. const [erc20Token, totalSupply, reserve0, reserve1, pairAddress] =
  42. await nftContract.getTokenData(tokenId);
  43. if (!erc20Token || !pairAddress)
  44. throw "Pair not created";
  45. const pairContract = getUniswapPairContract(pairAddress);
  46. const tokenContract = new ethers.Contract(erc20Token, ERC20_ABI, provider);
  47. // Fetch required on-chain logs in parallel
  48. const [token0, swapLogs, transferLogs] = await Promise.all([
  49. pairContract.token0(),
  50. pairContract.queryFilter(pairContract.filters.Swap(), 0, "latest"),
  51. tokenContract.queryFilter(tokenContract.filters.Transfer(), 0, "latest"),
  52. ]);
  53. const { aiusReserveParsed, tokenReserveParsed } = parseReserves({
  54. reserve0,
  55. reserve1,
  56. token0,
  57. erc20Token,
  58. });
  59. const totalSupplyParsed = parseFloat(ethers.formatUnits(totalSupply, 18));
  60. const price =
  61. tokenReserveParsed === 0 ? 0 : aiusReserveParsed / tokenReserveParsed;
  62. const marketCap = price * totalSupplyParsed;
  63. const tvl = 2 * aiusReserveParsed; // Total Value Locked in liquidity pool
  64. const swapEvents = await enrichSwapEvents(swapLogs, provider);
  65. const pastPrice = computePastPrice(swapEvents, price, erc20Token, token0);
  66. const change24h =
  67. pastPrice !== 0 ? ((price - pastPrice) / pastPrice) * 100 : 0;
  68. const volume = compute24hVolume(swapEvents);
  69. // Unique holders based on transfer event recipients
  70. const holders = new Set(
  71. transferLogs
  72. .filter((e) => "args" in e)
  73. .map((e) => (e as unknown as TransferEvent).args.to),
  74. ).size;
  75. const priceHistory = generatePriceHistory(
  76. swapEvents,
  77. erc20Token,
  78. token0,
  79. price,
  80. );
  81. return {
  82. stats: { marketCap, tvl, price, volume, holders, change24h },
  83. priceHistory,
  84. tokenAddress: erc20Token,
  85. };
  86. }
  87. /**
  88. * Helper to determine which reserve belongs to the token vs AIUS
  89. */
  90. function parseReserves({
  91. reserve0,
  92. reserve1,
  93. token0,
  94. erc20Token,
  95. }: {
  96. reserve0: BigNumberish;
  97. reserve1: BigNumberish;
  98. token0: string;
  99. erc20Token: string;
  100. }) {
  101. const [tokenReserve, aiusReserve] =
  102. token0 === erc20Token ? [reserve0, reserve1] : [reserve1, reserve0];
  103. return {
  104. tokenReserveParsed: parseFloat(ethers.formatUnits(tokenReserve, 18)),
  105. aiusReserveParsed: parseFloat(ethers.formatUnits(aiusReserve, 18)),
  106. };
  107. }
  108. /**
  109. * Enhances swap events by attaching block timestamps for time-based analytics
  110. */
  111. async function enrichSwapEvents(
  112. events: any[],
  113. provider: ethers.Provider,
  114. ): Promise<SwapEvent[]> {
  115. const blockCache = new Map<number, number>();
  116. const enrichedEvents = await Promise.all(
  117. events.map(async (event) => {
  118. if (!("args" in event)) return null;
  119. if (!blockCache.has(event.blockNumber)) {
  120. const block = await provider.getBlock(event.blockNumber);
  121. if (block) blockCache.set(event.blockNumber, block.timestamp);
  122. }
  123. return {
  124. args: {
  125. amount0In: event.args.amount0In,
  126. amount0Out: event.args.amount0Out,
  127. amount1In: event.args.amount1In,
  128. amount1Out: event.args.amount1Out,
  129. },
  130. blockTimestamp: blockCache.get(event.blockNumber)!,
  131. };
  132. }),
  133. );
  134. return enrichedEvents.filter((e): e is SwapEvent => e !== null);
  135. }
  136. /**
  137. * Computes the token price 24 hours ago using historical swap events
  138. */
  139. function computePastPrice(
  140. events: SwapEvent[],
  141. currentPrice: number,
  142. erc20Token: string,
  143. token0: string,
  144. ): number {
  145. const timestamp24hAgo = Math.floor(Date.now() / 1000) - 86400;
  146. for (let i = events.length - 1; i >= 0; i--) {
  147. const e = events[i];
  148. if (e.blockTimestamp <= timestamp24hAgo) {
  149. const [in0, out0, in1, out1] = [
  150. getFloat(e.args.amount0In),
  151. getFloat(e.args.amount0Out),
  152. getFloat(e.args.amount1In),
  153. getFloat(e.args.amount1Out),
  154. ];
  155. const amount0 = in0 - out0;
  156. const amount1 = out1 - in1;
  157. if (amount1 !== 0) return amount0 / amount1;
  158. }
  159. }
  160. return currentPrice;
  161. }
  162. /**
  163. * Calculates total 24-hour trading volume from swap events
  164. */
  165. function compute24hVolume(events: SwapEvent[]): number {
  166. const now = Math.floor(Date.now() / 1000);
  167. return events
  168. .filter((e) => now - e.blockTimestamp <= 86400)
  169. .reduce((sum, e) => {
  170. return (
  171. sum +
  172. getFloat(e.args.amount0In) +
  173. getFloat(e.args.amount0Out) +
  174. getFloat(e.args.amount1In) +
  175. getFloat(e.args.amount1Out)
  176. );
  177. }, 0);
  178. }
  179. /**
  180. * Generates a time-series price history from swap events
  181. */
  182. function generatePriceHistory(
  183. events: SwapEvent[],
  184. erc20Token: string,
  185. token0: string,
  186. currentPrice: number,
  187. ): PricePoint[] {
  188. const history: PricePoint[] = events
  189. .map((e) => {
  190. const [in0, out0, in1, out1] = [
  191. getFloat(e.args.amount0In),
  192. getFloat(e.args.amount0Out),
  193. getFloat(e.args.amount1In),
  194. getFloat(e.args.amount1Out),
  195. ];
  196. let price = 0;
  197. if (token0 === erc20Token) {
  198. if (in0 > 0 && out1 > 0) price = out1 / in0;
  199. else if (in1 > 0 && out0 > 0) price = in1 / out0;
  200. } else {
  201. if (in1 > 0 && out0 > 0) price = out0 / in1;
  202. else if (in0 > 0 && out1 > 0) price = in0 / out1;
  203. }
  204. return {
  205. x: new Date(e.blockTimestamp * 1000).toISOString(),
  206. y: price,
  207. };
  208. })
  209. // Filter out unrealistic values
  210. .filter((d) => isFinite(d.y) && d.y > 0.01 && d.y < 1_000_000)
  211. .sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime());
  212. // Append current price to the history
  213. history.push({ x: new Date().toISOString(), y: currentPrice });
  214. return history;
  215. }
  216. /**
  217. * Utility to parse a BigNumberish value into a float
  218. */
  219. function getFloat(val: BigNumberish | undefined): number {
  220. return parseFloat(ethers.formatUnits(val ?? 0, 18));
  221. }