253 lines
7.1 KiB
TypeScript
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(),
|
|
};
|
|
}
|