nac-presale/contracts/usePresale_new.ts

301 lines
11 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.

// 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,
};
}