nac-presale/server/trc20Monitor.ts

188 lines
5.4 KiB
TypeScript

/**
* 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<typeof setInterval> | 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<TronTransaction[]> {
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<void> {
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<void> {
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<void> {
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<Array<{
txHash: string;
fromAddress: string;
usdtAmount: number;
xicAmount: number;
status: string;
createdAt: Date;
chain: string;
}>> {
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",
}));
}