162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
/**
|
||
* 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);
|
||
}
|