fix: 将购买逻辑改为 USDT 直接转账到接收钱包 (2026-03-22)
This commit is contained in:
parent
8c52fc72a2
commit
06ca56d5d1
|
|
@ -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<PurchaseState>({
|
||||
step: "idle",
|
||||
txHash: null,
|
||||
error: null,
|
||||
tokenAmount: 0,
|
||||
usdtAmount: 0,
|
||||
});
|
||||
|
||||
const [presaleStats, setPresaleStats] = useState<PresaleStats>({
|
||||
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<number> => {
|
||||
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<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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue