/** * 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", // XICPresale v2 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 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; 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.totalUSDTRaised(); return Number(ethers.formatUnits(raw, 6)); } catch { try { const raw = await contract.usdtRaised(); return Number(ethers.formatUnits(raw, 6)); } catch { try { const raw = await contract.weiRaised(); return Number(ethers.formatUnits(raw, 6)); } 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(), }; }