/** * On-chain data service * Reads presale stats from BSC and ETH contracts using ethers.js * Caches results in DB to avoid rate limiting */ import { ethers } from "ethers"; import { eq } from "drizzle-orm"; import { getDb } from "./db"; import { presaleStatsCache } from "../drizzle/schema"; // ─── Contract Addresses ──────────────────────────────────────────────────────── export const CONTRACTS = { BSC: { presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c", token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", rpc: "https://bsc-dataseed1.binance.org/", chainId: 56, chainName: "BNB Smart Chain", explorerUrl: "https://bscscan.com", usdt: "0x55d398326f99059fF775485246999027B3197955", }, ETH: { presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3", token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", rpc: "https://eth.llamarpc.com", chainId: 1, chainName: "Ethereum", explorerUrl: "https://etherscan.io", usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7", }, TRON: { receivingWallet: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp", evmReceivingWallet: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", usdtContract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", }, }; // Minimal ABI for reading presale stats const PRESALE_ABI = [ "function totalUSDTRaised() view returns (uint256)", "function totalTokensSold() view returns (uint256)", "function weiRaised() view returns (uint256)", "function tokensSold() view returns (uint256)", "function usdtRaised() view returns (uint256)", ]; // Token price: $0.02 per XIC export const TOKEN_PRICE_USDT = 0.02; export const HARD_CAP_USDT = 5_000_000; export const TOTAL_SUPPLY = 100_000_000_000; export const MAX_PURCHASE_USDT = 50_000; export interface PresaleStats { chain: string; usdtRaised: number; tokensSold: number; lastUpdated: Date; fromCache: boolean; } export interface CombinedStats { totalUsdtRaised: number; totalTokensSold: number; hardCap: number; progressPct: number; bsc: PresaleStats | null; eth: PresaleStats | null; trc20UsdtRaised: number; trc20TokensSold: number; lastUpdated: Date; } // Cache TTL: 60 seconds const CACHE_TTL_MS = 60_000; async function fetchChainStats(chain: "BSC" | "ETH"): Promise<{ usdtRaised: number; tokensSold: number }> { const cfg = CONTRACTS[chain]; const provider = new ethers.JsonRpcProvider(cfg.rpc); const contract = new ethers.Contract(cfg.presale, PRESALE_ABI, provider); let usdtRaised = 0; let tokensSold = 0; // Try different function names that might exist in the contract try { const raw = await contract.totalUSDTRaised(); usdtRaised = Number(ethers.formatUnits(raw, 6)); // USDT has 6 decimals } catch { try { const raw = await contract.usdtRaised(); usdtRaised = Number(ethers.formatUnits(raw, 6)); } catch { try { const raw = await contract.weiRaised(); usdtRaised = Number(ethers.formatUnits(raw, 6)); } catch { console.warn(`[OnChain] Could not read usdtRaised from ${chain}`); } } } try { const raw = await contract.totalTokensSold(); tokensSold = Number(ethers.formatUnits(raw, 18)); // XIC has 18 decimals } catch { try { const raw = await contract.tokensSold(); tokensSold = Number(ethers.formatUnits(raw, 18)); } catch { console.warn(`[OnChain] Could not read tokensSold from ${chain}`); } } return { usdtRaised, tokensSold }; } export async function getPresaleStats(chain: "BSC" | "ETH"): Promise { const db = await getDb(); // Check cache first if (db) { try { const cached = await db .select() .from(presaleStatsCache) .where(eq(presaleStatsCache.chain, chain)) .limit(1); if (cached.length > 0) { const row = cached[0]; const age = Date.now() - new Date(row.lastUpdated).getTime(); if (age < CACHE_TTL_MS) { return { chain, usdtRaised: Number(row.usdtRaised || 0), tokensSold: Number(row.tokensSold || 0), lastUpdated: new Date(row.lastUpdated), fromCache: true, }; } } } catch (e) { console.warn("[OnChain] Cache read error:", e); } } // Fetch fresh from chain let usdtRaised = 0; let tokensSold = 0; try { const data = await fetchChainStats(chain); usdtRaised = data.usdtRaised; tokensSold = data.tokensSold; } catch (e) { console.error(`[OnChain] Failed to fetch ${chain} stats:`, e); } // Update cache if (db) { try { const existing = await db .select() .from(presaleStatsCache) .where(eq(presaleStatsCache.chain, chain)) .limit(1); if (existing.length > 0) { await db .update(presaleStatsCache) .set({ usdtRaised: String(usdtRaised), tokensSold: String(tokensSold), lastUpdated: new Date(), }) .where(eq(presaleStatsCache.chain, chain)); } else { await db.insert(presaleStatsCache).values({ chain, usdtRaised: String(usdtRaised), tokensSold: String(tokensSold), lastUpdated: new Date(), }); } } catch (e) { console.warn("[OnChain] Cache write error:", e); } } return { chain, usdtRaised, tokensSold, lastUpdated: new Date(), fromCache: false, }; } export async function getCombinedStats(): Promise { const [bsc, eth] = await Promise.allSettled([ getPresaleStats("BSC"), getPresaleStats("ETH"), ]); const bscStats = bsc.status === "fulfilled" ? bsc.value : null; const ethStats = eth.status === "fulfilled" ? eth.value : null; // Get TRC20 stats from DB let trc20UsdtRaised = 0; let trc20TokensSold = 0; try { const db = await getDb(); if (db) { const { trc20Purchases } = await import("../drizzle/schema"); const { sql, inArray } = await import("drizzle-orm"); const result = await db .select({ totalUsdt: sql`SUM(CAST(${trc20Purchases.usdtAmount} AS DECIMAL(30,6)))`, totalXic: sql`SUM(CAST(${trc20Purchases.xicAmount} AS DECIMAL(30,6)))`, }) .from(trc20Purchases) .where(inArray(trc20Purchases.status, ["confirmed", "distributed"])); if (result[0]) { trc20UsdtRaised = Number(result[0].totalUsdt || 0); trc20TokensSold = Number(result[0].totalXic || 0); } } } catch (e) { console.warn("[OnChain] TRC20 stats error:", e); } const totalUsdtRaised = (bscStats?.usdtRaised || 0) + (ethStats?.usdtRaised || 0) + trc20UsdtRaised; const totalTokensSold = (bscStats?.tokensSold || 0) + (ethStats?.tokensSold || 0) + trc20TokensSold; return { totalUsdtRaised, totalTokensSold, hardCap: HARD_CAP_USDT, progressPct: Math.min((totalUsdtRaised / HARD_CAP_USDT) * 100, 100), bsc: bscStats, eth: ethStats, trc20UsdtRaised, trc20TokensSold, lastUpdated: new Date(), }; }