301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
// 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<PurchaseState>({
|
||
step: "idle",
|
||
txHash: null,
|
||
error: null,
|
||
tokenAmount: 0,
|
||
});
|
||
|
||
const [presaleStats, setPresaleStats] = useState<PresaleStats>({
|
||
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<number> => {
|
||
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<number> => {
|
||
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,
|
||
};
|
||
}
|