/** * TRC20 USDT Monitor & Auto-Distribution Service * * Flow: * 1. Poll TRON address for incoming USDT transactions every 30s * 2. For each new confirmed tx, record in DB * 3. Calculate XIC amount at $0.02/XIC * 4. Distribute XIC from operator wallet to buyer's address (if EVM address provided) * OR mark as pending manual distribution * * Note: TRON users must provide their EVM (BSC/ETH) address in the memo field * to receive automatic XIC distribution. Otherwise, admin will distribute manually. */ import { eq, and, sql } from "drizzle-orm"; import { getDb } from "./db"; import { trc20Purchases } from "../drizzle/schema"; import { TOKEN_PRICE_USDT } from "./onchain"; const TRON_RECEIVING_ADDRESS = "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp"; const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; // Trongrid API endpoint (public, no key needed for basic queries) const TRONGRID_API = "https://api.trongrid.io"; let isMonitoring = false; let monitorInterval: ReturnType | null = null; interface TronTransaction { transaction_id: string; token_info?: { address: string; decimals: number }; from: string; to: string; value: string; block_timestamp: number; type: string; } async function fetchRecentTRC20Transfers(): Promise { try { const url = `${TRONGRID_API}/v1/accounts/${TRON_RECEIVING_ADDRESS}/transactions/trc20?limit=50&contract_address=${TRON_USDT_CONTRACT}&only_confirmed=true`; const resp = await fetch(url, { headers: { "Content-Type": "application/json" }, signal: AbortSignal.timeout(10000), }); if (!resp.ok) { console.warn(`[TRC20Monitor] API error: ${resp.status}`); return []; } const data = await resp.json() as { data?: TronTransaction[]; success?: boolean }; if (!data.success || !Array.isArray(data.data)) return []; // Only incoming transfers (to our address) return data.data.filter((tx) => tx.to === TRON_RECEIVING_ADDRESS); } catch (e) { console.warn("[TRC20Monitor] Fetch error:", e); return []; } } async function processTransaction(tx: TronTransaction): Promise { const db = await getDb(); if (!db) return; // Check if already processed const existing = await db .select() .from(trc20Purchases) .where(eq(trc20Purchases.txHash, tx.transaction_id)) .limit(1); if (existing.length > 0) return; // Already recorded // USDT has 6 decimals on TRON const usdtAmount = Number(tx.value) / 1_000_000; if (usdtAmount < 0.01) return; // Skip dust const xicAmount = usdtAmount / TOKEN_PRICE_USDT; console.log( `[TRC20Monitor] New purchase: ${tx.from} → ${usdtAmount} USDT → ${xicAmount} XIC (tx: ${tx.transaction_id})` ); // Record in DB await db.insert(trc20Purchases).values({ txHash: tx.transaction_id, fromAddress: tx.from, usdtAmount: String(usdtAmount), xicAmount: String(xicAmount), blockNumber: tx.block_timestamp, status: "confirmed", createdAt: new Date(), updatedAt: new Date(), }); // Attempt auto-distribution via BSC await attemptAutoDistribute(tx.transaction_id, tx.from, xicAmount); } async function attemptAutoDistribute( txHash: string, fromTronAddress: string, xicAmount: number ): Promise { const db = await getDb(); if (!db) return; const operatorPrivateKey = process.env.OPERATOR_PRIVATE_KEY; const xicTokenAddress = "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24"; if (!operatorPrivateKey) { console.warn("[TRC20Monitor] No OPERATOR_PRIVATE_KEY set, skipping auto-distribute"); return; } // We need the buyer's EVM address. Since TRON addresses can't directly receive BSC tokens, // we look for a mapping or use a conversion. For now, log and mark as pending. // In production, buyers should provide their EVM address in the payment memo. console.log( `[TRC20Monitor] Distribution pending for ${fromTronAddress}: ${xicAmount} XIC` ); console.log( `[TRC20Monitor] Admin must manually distribute to buyer's EVM address` ); // Mark as pending distribution (admin will handle via admin panel) // Status stays "confirmed" until admin distributes } export async function startTRC20Monitor(): Promise { if (isMonitoring) return; isMonitoring = true; console.log("[TRC20Monitor] Starting monitor for", TRON_RECEIVING_ADDRESS); const poll = async () => { try { const txs = await fetchRecentTRC20Transfers(); for (const tx of txs) { await processTransaction(tx); } } catch (e) { console.error("[TRC20Monitor] Poll error:", e); } }; // Initial poll await poll(); // Poll every 30 seconds monitorInterval = setInterval(poll, 30_000); } export function stopTRC20Monitor(): void { if (monitorInterval) { clearInterval(monitorInterval); monitorInterval = null; } isMonitoring = false; } export async function getRecentPurchases(limit = 20): Promise> { const db = await getDb(); if (!db) return []; const rows = await db .select() .from(trc20Purchases) .orderBy(sql`${trc20Purchases.createdAt} DESC`) .limit(limit); return rows.map((r) => ({ txHash: r.txHash, fromAddress: r.fromAddress, usdtAmount: Number(r.usdtAmount), xicAmount: Number(r.xicAmount), status: r.status, createdAt: r.createdAt, chain: "TRON", })); }