/** * tokenDistributionService.ts * * Unified token distribution service for NAC XIC presale. * ALL payment channels (USDT ERC20, USDT TRC20, Alipay, WeChat, PayPal) * call the same credit() method to distribute XIC tokens. * * Architecture per document: "加密货币支付框架扩展方案(支付宝/微信/PayPal集成)" * - Centralized distribution logic prevents inconsistencies across payment channels * - Idempotency: check transaction_logs before processing to prevent double-distribution * - All payment channels update orders table to PAID/COMPLETED status * * Future extension: When XIC is deployed on-chain, replace the internal credit() * with transferOnChain() which calls the XIC contract transfer() function. */ import { getDb } from "./db"; import { bridgeOrders, transactionLogs } from "../drizzle/schema"; import { eq } from "drizzle-orm"; export interface CreditParams { /** Bridge order txHash (unique identifier for this payment) */ txHash: string; /** Chain type: 'ERC20' | 'TRC20' | 'ALIPAY' | 'WECHAT' | 'PAYPAL' */ chainType: string; /** Sender address on source chain */ fromAddress: string; /** Our receiving address on source chain */ toAddress: string; /** USDT amount received */ usdtAmount: number; /** XIC amount to distribute (calculated from usdtAmount / XIC_PRICE) */ xicAmount: number; /** Block number (for on-chain transactions) */ blockNumber?: number; /** XIC receive address (BSC address for EVM distribution) */ xicReceiveAddress?: string; /** Remark for logging */ remark?: string; } export interface CreditResult { success: boolean; alreadyProcessed?: boolean; orderId?: number; error?: string; } /** * Credit XIC tokens to a user after successful payment. * * This is the SINGLE entry point for all payment channels. * Implements idempotency via transaction_logs table. * * Flow: * 1. Check transaction_logs — if txHash exists, skip (already processed) * 2. Record in transaction_logs (status=1 processed) * 3. Find matching bridge order and update status to 'confirmed' * 4. Mark order as 'distributed' (Phase 2: will call on-chain transfer) * 5. Return success */ export async function creditXic(params: CreditParams): Promise { const { txHash, chainType, fromAddress, toAddress, usdtAmount, xicAmount, blockNumber, xicReceiveAddress, remark, } = params; try { const db = await getDb(); if (!db) return { success: false, error: "Database not available" }; // Step 1: Idempotency check — has this txHash been processed before? const existing = await db.select().from(transactionLogs) .where(eq(transactionLogs.txHash, txHash)) .limit(1); if (existing.length > 0) { console.log(`[TokenDistribution] txHash ${txHash} already processed (status=${existing[0].status}), skipping`); return { success: true, alreadyProcessed: true }; } // Step 2: Find matching bridge order const orders = await db.select().from(bridgeOrders) .where(eq(bridgeOrders.txHash, txHash)) .limit(1); const order = orders[0] ?? null; // Step 3: Record in transaction_logs (idempotency guard) await db.insert(transactionLogs).values({ txHash, chainType, fromAddress, toAddress, amount: usdtAmount.toString(), blockNumber: blockNumber ?? null, status: 1, // processed orderNo: txHash, // use txHash as orderNo for bridge orders }); if (order) { // Step 4: Update bridge order status to 'confirmed' then 'distributed' await db.update(bridgeOrders) .set({ status: "confirmed", confirmedAt: new Date(), blockNumber: blockNumber ?? null, }) .where(eq(bridgeOrders.id, order.id)); // Phase 1: Internal credit (record as distributed) // Phase 2 (future): Call XIC contract transfer() on BSC await db.update(bridgeOrders) .set({ status: "distributed", distributedAt: new Date(), }) .where(eq(bridgeOrders.id, order.id)); console.log( `[TokenDistribution] ✅ Credited ${xicAmount} XIC for order ${txHash} ` + `(${chainType}, ${usdtAmount} USDT from ${fromAddress}) ` + `→ ${xicReceiveAddress || order.xicReceiveAddress || "unknown"} | ${remark || ""}` ); return { success: true, orderId: order.id }; } else { // No matching order found — log as unmatched but don't fail // Admin can manually match later via the admin panel console.warn( `[TokenDistribution] ⚠️ No matching bridge order for txHash ${txHash} ` + `(${chainType}, ${usdtAmount} USDT from ${fromAddress} to ${toAddress})` ); // Update transaction log status to 2 (no_match) await db.update(transactionLogs) .set({ status: 2 }) .where(eq(transactionLogs.txHash, txHash)); return { success: false, error: "No matching bridge order found" }; } } catch (err) { console.error(`[TokenDistribution] ❌ Error processing txHash ${txHash}:`, err); return { success: false, error: String(err) }; } } /** * Calculate XIC amount from USDT amount. * XIC price: $0.02 per XIC → 1 USDT = 50 XIC */ export function calcXicAmount(usdtAmount: number): number { const XIC_PRICE = 0.02; // $0.02 per XIC return Math.floor(usdtAmount / XIC_PRICE); }