// NAC XIC Presale — Purchase Logic Hook v2 // 适配新合约 XICPresale(购买即时发放版本) // 关键变更: // - 函数名: buyTokensWithUSDT → buyWithUSDT // - 函数名: buyTokens (BNB) → buyWithBNB // - BSC USDT 精度: 18 decimals(保持不变,BSC USDT 是 18d) // - 新增: 从链上读取实时预售状态(剩余时间、进度等) // - 新增: BNB 购买支持 import { useState, useCallback, useEffect } from "react"; import { Contract, parseUnits, formatUnits, parseEther } from "ethers"; import { CONTRACTS, PRESALE_ABI, ERC20_ABI, PRESALE_CONFIG, formatNumber } from "@/lib/contracts"; import { WalletState } from "./useWallet"; export type PurchaseStep = | "idle" | "approving" | "approved" | "purchasing" | "success" | "error"; export interface PurchaseState { step: PurchaseStep; txHash: string | null; error: string | null; tokenAmount: 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) } export function usePresale(wallet: WalletState, network: "BSC" | "ETH") { const [purchaseState, setPurchaseState] = useState({ step: "idle", txHash: null, error: null, tokenAmount: 0, }); const [presaleStats, setPresaleStats] = useState({ totalSold: 0, totalRaised: 0, hardCap: PRESALE_CONFIG.presaleAllocation, progressPercent: 0, timeRemaining: 0, isActive: false, presaleStarted: false, presaleEndTime: 0, availableXIC: 0, bnbPrice: 0, }); const networkConfig = CONTRACTS[network]; // ── 从链上读取预售状态 ────────────────────────────────────── const fetchPresaleStats = useCallback(async () => { if (network !== "BSC") return; // 新合约只在 BSC try { const provider = wallet.provider; if (!provider) return; const presaleContract = new Contract(networkConfig.presale, PRESALE_ABI, provider); const [ totalSoldRaw, totalRaisedRaw, hardCapRaw, progressResult, timeRemainingRaw, isActive, presaleStarted, presaleEndTimeRaw, availableXICRaw, bnbPriceRaw, ] = await Promise.all([ presaleContract.totalTokensSold(), presaleContract.totalRaised(), presaleContract.hardCap(), presaleContract.presaleProgress(), presaleContract.timeRemaining(), presaleContract.isPresaleActive(), presaleContract.presaleStarted(), presaleContract.presaleEndTime(), presaleContract.availableXIC(), presaleContract.getBNBPrice().catch(() => BigInt(0)), ]); setPresaleStats({ totalSold: parseFloat(formatUnits(totalSoldRaw, 18)), totalRaised: parseFloat(formatUnits(totalRaisedRaw, 18)), // BSC USDT 18d hardCap: parseFloat(formatUnits(hardCapRaw, 18)), progressPercent: Number(progressResult.progressBps) / 100, timeRemaining: Number(timeRemainingRaw), isActive: Boolean(isActive), presaleStarted: Boolean(presaleStarted), presaleEndTime: Number(presaleEndTimeRaw), availableXIC: parseFloat(formatUnits(availableXICRaw, 18)), bnbPrice: parseFloat(formatUnits(bnbPriceRaw, 18)), }); } catch (err) { console.error("[usePresale] fetchPresaleStats error:", err); } }, [wallet.provider, network, networkConfig]); // 定期刷新预售状态(每 30 秒) useEffect(() => { fetchPresaleStats(); const interval = setInterval(fetchPresaleStats, 30_000); return () => clearInterval(interval); }, [fetchPresaleStats]); // ── 用 USDT 购买(新合约函数名: buyWithUSDT)────────────────── const buyWithUSDT = useCallback( async (usdtAmount: number) => { if (!wallet.signer || !wallet.address) { setPurchaseState(s => ({ ...s, step: "error", error: "请先连接钱包。" })); return; } const tokenAmount = usdtAmount / PRESALE_CONFIG.tokenPrice; setPurchaseState({ step: "approving", txHash: null, error: null, tokenAmount }); try { // BSC USDT 是 18 decimals const usdtDecimals = network === "ETH" ? 6 : 18; const usdtAmountWei = parseUnits(usdtAmount.toString(), usdtDecimals); const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.signer); const presaleAddress = networkConfig.presale; // Step 1: 检查并授权 USDT const currentAllowance = await usdtContract.allowance(wallet.address, presaleAddress); if (currentAllowance < usdtAmountWei) { const approveTx = await usdtContract.approve(presaleAddress, usdtAmountWei); await approveTx.wait(); } setPurchaseState(s => ({ ...s, step: "approved" })); // Step 2: 调用新合约的 buyWithUSDT(不是 buyTokensWithUSDT) const presaleContract = new Contract(presaleAddress, PRESALE_ABI, wallet.signer); const buyTx = await presaleContract.buyWithUSDT(usdtAmountWei); setPurchaseState(s => ({ ...s, step: "purchasing", txHash: buyTx.hash })); const receipt = await buyTx.wait(); // 从事件中读取实际收到的 XIC 数量 let actualTokenAmount = tokenAmount; if (receipt?.logs) { for (const log of receipt.logs) { try { const parsed = presaleContract.interface.parseLog(log); if (parsed?.name === "TokensPurchased") { actualTokenAmount = parseFloat(formatUnits(parsed.args.tokenAmount, 18)); } } catch { /* ignore */ } } } setPurchaseState(s => ({ ...s, step: "success", tokenAmount: actualTokenAmount })); // 刷新预售状态 await fetchPresaleStats(); } catch (err: unknown) { const errMsg = (err as { reason?: string; message?: string }).reason || (err as Error).message || "Transaction failed"; setPurchaseState(s => ({ ...s, step: "error", error: errMsg })); } }, [wallet, network, networkConfig, fetchPresaleStats] ); // ── 用 BNB 购买(新合约函数名: buyWithBNB)────────────────── const buyWithBNB = useCallback( async (bnbAmount: number) => { if (!wallet.signer || !wallet.address) { setPurchaseState(s => ({ ...s, step: "error", error: "请先连接钱包。" })); return; } const bnbAmountWei = parseEther(bnbAmount.toString()); const estimatedTokens = presaleStats.bnbPrice > 0 ? (bnbAmount * presaleStats.bnbPrice) / PRESALE_CONFIG.tokenPrice : 0; setPurchaseState({ step: "purchasing", txHash: null, error: null, tokenAmount: estimatedTokens }); try { const presaleContract = new Contract(networkConfig.presale, PRESALE_ABI, wallet.signer); const buyTx = await presaleContract.buyWithBNB({ value: bnbAmountWei }); setPurchaseState(s => ({ ...s, txHash: buyTx.hash })); const receipt = await buyTx.wait(); let actualTokenAmount = estimatedTokens; if (receipt?.logs) { for (const log of receipt.logs) { try { const parsed = presaleContract.interface.parseLog(log); if (parsed?.name === "TokensPurchased") { actualTokenAmount = parseFloat(formatUnits(parsed.args.tokenAmount, 18)); } } catch { /* ignore */ } } } setPurchaseState(s => ({ ...s, step: "success", tokenAmount: actualTokenAmount })); await fetchPresaleStats(); } catch (err: unknown) { const errMsg = (err as { reason?: string; message?: string }).reason || (err as Error).message || "Transaction failed"; setPurchaseState(s => ({ ...s, step: "error", error: errMsg })); } }, [wallet, networkConfig, presaleStats.bnbPrice, fetchPresaleStats] ); const reset = useCallback(() => { setPurchaseState({ step: "idle", txHash: null, error: null, tokenAmount: 0 }); }, []); // 计算 USDT 对应的 XIC 数量 const calcTokens = (usdtAmount: number): number => { return usdtAmount / PRESALE_CONFIG.tokenPrice; }; // 计算 BNB 对应的 XIC 数量 const calcTokensForBNB = (bnbAmount: number): number => { if (presaleStats.bnbPrice <= 0) return 0; return (bnbAmount * presaleStats.bnbPrice) / PRESALE_CONFIG.tokenPrice; }; // 获取用户 USDT 余额 const getUsdtBalance = useCallback(async (): Promise => { if (!wallet.provider || !wallet.address) return 0; try { const usdtDecimals = network === "ETH" ? 6 : 18; const usdtContract = new Contract(networkConfig.usdt, ERC20_ABI, wallet.provider); const balance = await usdtContract.balanceOf(wallet.address); return parseFloat(formatUnits(balance, usdtDecimals)); } 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, // 兼容旧接口 calcTokens: calcTokens, }; }