nac-presale/server/tokenDistributionService.ts

162 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<CreditResult> {
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);
}