188 lines
5.4 KiB
TypeScript
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",
|
|
}));
|
|
}
|