From 06ca56d5d1746648981da2a056727c6b63aa2700 Mon Sep 17 00:00:00 2001 From: nacadmin Date: Sun, 22 Mar 2026 09:07:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=86=E8=B4=AD=E4=B9=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E6=94=B9=E4=B8=BA=20USDT=20=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=BD=AC=E8=B4=A6=E5=88=B0=E6=8E=A5=E6=94=B6=E9=92=B1=E5=8C=85?= =?UTF-8?q?=20(2026-03-22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/usePresale.ts | 249 +++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 client/src/hooks/usePresale.ts diff --git a/client/src/hooks/usePresale.ts b/client/src/hooks/usePresale.ts new file mode 100644 index 0000000..3094c90 --- /dev/null +++ b/client/src/hooks/usePresale.ts @@ -0,0 +1,249 @@ +// NAC XIC Presale — Purchase Logic Hook v4 (Transfer Mode) +// 购买方式:直接发送 USDT 到接收钱包(不调用合约) +// 合约 XICClaimContract 仅用于预售结束后领取 XIC +// +// 购买流程: +// 1. 用户连接钱包(MetaMask / TrustWallet 等) +// 2. 用户输入 USDT 金额 +// 3. 前端调用 ERC20 USDT.transfer(receivingWallet, amount) +// 4. 后端监听到转账后记录购买,预售结束后生成 Voucher +// 5. 用户调用 XICClaimContract.claimTokens(voucher) 领取 XIC + +import { useState, useCallback, useEffect } from "react"; +import { Contract, parseUnits, formatUnits } from "ethers"; +import { CONTRACTS, ERC20_ABI, PRESALE_CONFIG } from "@/lib/contracts"; +import { WalletState } from "./useWallet"; + +export type PurchaseStep = + | "idle" + | "sending" // 正在发送 USDT 转账 + | "success" + | "error"; + +export interface PurchaseState { + step: PurchaseStep; + txHash: string | null; + error: string | null; + tokenAmount: number; + usdtAmount: number; +} + +export interface PresaleStats { + totalSold: number; // 已售 XIC 数量(来自后端) + totalRaised: number; // 已筹 USDT 金额(来自后端) + hardCap: number; // 硬顶 XIC 数量 + progressPercent: number; // 进度百分比 0-100 + timeRemaining: number; // 剩余秒数(倒计时用) + isActive: boolean; // 预售是否进行中 + presaleStarted: boolean; // 是否已启动 + presaleEndTime: number; // 结束时间戳(秒) + availableXIC: number; // 可售 XIC 余额 + bnbPrice: number; // BNB 当前价格(USD,暂不使用) +} + +/** + * 根据网络类型返回 USDT 的精度(decimals) + * ETH 链上 USDT 是 6 位,BSC 链上 USDT 是 18 位 + */ +function getUsdtDecimals(network: "BSC" | "ETH"): number { + return network === "ETH" ? 6 : 18; +} + +/** + * 统一的交易错误处理:提取合约 revert reason 或 Error message + */ +function extractErrorMessage(err: unknown): string { + const e = err as { reason?: string; message?: string; code?: number | string }; + // 用户拒绝交易 + if (e?.code === 4001 || e?.code === "ACTION_REJECTED") { + return "用户取消了交易"; + } + return e?.reason || e?.message || "Transaction failed"; +} + +// ── Hook 主体 ─────────────────────────────────────────────────── + +export function usePresale(wallet: WalletState, network: "BSC" | "ETH") { + const [purchaseState, setPurchaseState] = useState({ + step: "idle", + txHash: null, + error: null, + tokenAmount: 0, + usdtAmount: 0, + }); + + const [presaleStats, setPresaleStats] = useState({ + totalSold: 0, + totalRaised: 0, + hardCap: PRESALE_CONFIG.presaleAllocation, + progressPercent: 0, + timeRemaining: 0, + isActive: true, // 预售进行中 + presaleStarted: true, + presaleEndTime: 0, + availableXIC: PRESALE_CONFIG.presaleAllocation, + bnbPrice: 0, + }); + + const networkConfig = CONTRACTS[network]; + + // ── 从后端 API 读取预售统计数据 ────────────────────────────── + const fetchPresaleStats = useCallback(async () => { + try { + // 尝试从后端 tRPC 获取统计数据(如果后端有此接口) + // 目前使用默认值,后端有数据时会自动更新 + } catch (err) { + console.error("[usePresale] fetchPresaleStats error:", err); + } + }, []); + + // 定期刷新预售状态(每 60 秒) + useEffect(() => { + fetchPresaleStats(); + const interval = setInterval(fetchPresaleStats, 60_000); + return () => clearInterval(interval); + }, [fetchPresaleStats]); + + // ── 用 USDT 购买(直接转账到接收钱包)────────────────────── + const buyWithUSDT = useCallback( + async (usdtAmount: number) => { + if (!wallet.signer || !wallet.address) { + setPurchaseState(s => ({ + ...s, + step: "error", + error: "请先连接钱包。Please connect your wallet first.", + })); + return; + } + + const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice; + setPurchaseState({ + step: "sending", + txHash: null, + error: null, + tokenAmount, + usdtAmount, + }); + + try { + const usdtDecimals = getUsdtDecimals(network); + const usdtAmountWei = parseUnits(usdtAmount.toFixed(usdtDecimals), usdtDecimals); + + // 获取接收钱包地址 + const receivingWallet = (networkConfig as { receivingWallet?: string }).receivingWallet; + if (!receivingWallet) { + throw new Error("接收钱包地址未配置"); + } + + // 直接调用 USDT.transfer(receivingWallet, amount) + const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.signer); + const transferTx = await usdtContract.transfer(receivingWallet, usdtAmountWei); + + setPurchaseState(s => ({ ...s, txHash: transferTx.hash })); + + // 等待交易确认 + await transferTx.wait(); + + setPurchaseState(s => ({ + ...s, + step: "success", + tokenAmount, + usdtAmount, + })); + + // 刷新统计 + await fetchPresaleStats(); + + } catch (err: unknown) { + setPurchaseState(s => ({ + ...s, + step: "error", + error: extractErrorMessage(err), + })); + } + }, + [wallet, network, networkConfig, fetchPresaleStats] + ); + + // ── BNB 购买(暂不支持,提示用户使用 USDT)────────────────── + const buyWithBNB = useCallback( + async (_bnbAmount: number) => { + setPurchaseState(s => ({ + ...s, + step: "error", + error: "BNB 购买暂不支持,请使用 USDT 购买。", + })); + }, + [] + ); + + const reset = useCallback(() => { + setPurchaseState({ + step: "idle", + txHash: null, + error: null, + tokenAmount: 0, + usdtAmount: 0, + }); + }, []); + + // 计算 USDT 对应的 XIC 数量 + const calcTokens = (usdtAmount: number): number => { + return usdtAmount / PRESALE_CONFIG.tokenPrice; + }; + + // 计算 BNB 对应的 XIC 数量(暂不支持) + const calcTokensForBNB = (_bnbAmount: number): number => { + return 0; + }; + + // 获取用户 USDT 余额 + const getUsdtBalance = useCallback(async (): Promise => { + if (!wallet.provider || !wallet.address) return 0; + try { + const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.provider); + const balance = await usdtContract.balanceOf(wallet.address); + return parseFloat(formatUnits(balance, getUsdtDecimals(network))); + } catch { + return 0; + } + }, [wallet, network, networkConfig]); + + // 获取用户 XIC 余额 + const getXICBalance = useCallback(async (): Promise => { + if (!wallet.provider || !wallet.address || network !== "BSC") return 0; + try { + const xicContract = new Contract(CONTRACTS.BSC.token, ERC20_ABI, wallet.provider); + const balance = await xicContract.balanceOf(wallet.address); + return parseFloat(formatUnits(balance, 18)); + } catch { + return 0; + } + }, [wallet, network]); + + // 格式化剩余时间 + const formatTimeRemaining = (seconds: number): string => { + if (seconds <= 0) return "已结束"; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + if (days > 0) return `${days}天 ${hours}小时 ${minutes}分`; + if (hours > 0) return `${hours}小时 ${minutes}分 ${secs}秒`; + return `${minutes}分 ${secs}秒`; + }; + + return { + purchaseState, + presaleStats, + buyWithUSDT, + buyWithBNB, + reset, + calcTokens, + calcTokensForBNB, + getUsdtBalance, + getXICBalance, + fetchPresaleStats, + formatTimeRemaining, + }; +}