xic-presale/server/onchain.ts

253 lines
7.1 KiB
TypeScript

/**
* 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<PresaleStats> {
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<CombinedStats> {
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<string>`SUM(CAST(${trc20Purchases.usdtAmount} AS DECIMAL(30,6)))`,
totalXic: sql<string>`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(),
};
}