/** * On-chain data service * Reads presale stats from BSC and ETH contracts using ethers.js * Caches results in DB to avoid rate limiting * * RPC Strategy: Multi-node failover pool — tries each node in order until one succeeds */ import { ethers } from "ethers"; import { eq } from "drizzle-orm"; import { getDb } from "./db"; import { presaleStatsCache } from "../drizzle/schema"; // ─── Multi-node RPC Pool ──────────────────────────────────────────────────────── // Multiple public RPC endpoints for each chain — tried in order, first success wins const RPC_POOLS = { BSC: [ "https://bsc-dataseed1.binance.org/", "https://bsc-dataseed2.binance.org/", "https://bsc-dataseed3.binance.org/", "https://bsc-dataseed4.binance.org/", "https://bsc-dataseed1.defibit.io/", "https://bsc-dataseed2.defibit.io/", "https://bsc.publicnode.com", "https://binance.llamarpc.com", "https://rpc.ankr.com/bsc", ], ETH: [ "https://eth.llamarpc.com", "https://ethereum.publicnode.com", "https://rpc.ankr.com/eth", "https://1rpc.io/eth", "https://eth.drpc.org", "https://cloudflare-eth.com", "https://rpc.payload.de", ], }; // ─── Contract Addresses ──────────────────────────────────────────────────────── export const CONTRACTS = { BSC: { presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", rpc: RPC_POOLS.BSC[0], chainId: 56, chainName: "BNB Smart Chain", explorerUrl: "https://bscscan.com", usdt: "0x55d398326f99059fF775485246999027B3197955", }, ETH: { presale: "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3", token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", rpc: RPC_POOLS.ETH[0], chainId: 1, chainName: "Ethereum", explorerUrl: "https://etherscan.io", usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7", }, TRON: { receivingWallet: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp", evmReceivingWallet: "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3", usdtContract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", }, }; // Minimal ABI for reading presale stats (supports both old and new contract) const PRESALE_ABI = [ // New contract (XICPresale v2) "function totalRaised() view returns (uint256)", "function totalTokensSold() view returns (uint256)", "function hardCap() view returns (uint256)", "function isPresaleActive() view returns (bool)", "function presaleProgress() view returns (uint256 sold, uint256 cap, uint256 progressBps)", "function timeRemaining() view returns (uint256)", "function availableXIC() view returns (uint256)", // Old contract fallbacks "function totalUSDTRaised() 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 = 50_000_000; // 25亿 XIC × $0.02 = $5000万 USDT 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; rpcUsed?: string; } 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; // RPC timeout: 8 seconds per node const RPC_TIMEOUT_MS = 8_000; /** * Try each RPC node in the pool until one succeeds. * Returns { usdtRaised, tokensSold, rpcUsed } or throws if all fail. */ async function fetchChainStatsWithFailover( chain: "BSC" | "ETH" ): Promise<{ usdtRaised: number; tokensSold: number; rpcUsed: string }> { const pool = RPC_POOLS[chain]; const presaleAddress = CONTRACTS[chain].presale; const errors: string[] = []; for (const rpcUrl of pool) { try { const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true, polling: false, }); // Set a timeout for the provider const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`RPC timeout: ${rpcUrl}`)), RPC_TIMEOUT_MS) ); const contract = new ethers.Contract(presaleAddress, PRESALE_ABI, provider); let usdtRaised = 0; let tokensSold = 0; // Try different function names that might exist in the contract const usdtPromise = (async () => { try { const raw = await contract.totalRaised(); return Number(ethers.formatUnits(raw, 18)); // BSC USDT 18 decimals } catch { try { const raw = await contract.usdtRaised(); return Number(ethers.formatUnits(raw, 18)); // BSC USDT 18 decimals } catch { try { const raw = await contract.weiRaised(); return Number(ethers.formatUnits(raw, 18)); // BSC USDT 18 decimals } catch { return 0; } } } })(); const tokensPromise = (async () => { try { const raw = await contract.totalTokensSold(); return Number(ethers.formatUnits(raw, 18)); } catch { try { const raw = await contract.tokensSold(); return Number(ethers.formatUnits(raw, 18)); } catch { return 0; } } })(); const [usdtResult, tokensResult] = await Promise.race([ Promise.all([usdtPromise, tokensPromise]), timeoutPromise, ]); usdtRaised = usdtResult; tokensSold = tokensResult; console.log(`[OnChain] ${chain} stats fetched via ${rpcUrl}: $${usdtRaised} USDT, ${tokensSold} XIC`); return { usdtRaised, tokensSold, rpcUsed: rpcUrl }; } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); errors.push(`${rpcUrl}: ${errMsg}`); console.warn(`[OnChain] ${chain} RPC failed (${rpcUrl}): ${errMsg}`); } } throw new Error(`All ${chain} RPC nodes failed:\n${errors.join("\n")}`); } 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 with failover let usdtRaised = 0; let tokensSold = 0; let rpcUsed = ""; try { const data = await fetchChainStatsWithFailover(chain); usdtRaised = data.usdtRaised; tokensSold = data.tokensSold; rpcUsed = data.rpcUsed; } catch (e) { console.error(`[OnChain] All ${chain} RPC nodes exhausted:`, 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, rpcUsed, }; } 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(), }; }