Checkpoint: 修复预售网站三个关键问题:1) 购买按钮永远禁用(maxPurchaseUSDT=0导致),2) 新增Add XIC to Wallet按钮,3) 完整重写useWallet.ts支持TokenPocket/OKX/Bitget等中国钱包

This commit is contained in:
Manus 2026-03-09 23:14:34 -04:00
parent ca5883ace8
commit dd24e6ba13
5 changed files with 230 additions and 60 deletions

View File

@ -1,6 +1,6 @@
// NAC XIC Presale — Wallet Connection Hook // NAC XIC Presale — Wallet Connection Hook
// Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets // Supports MetaMask, TokenPocket, OKX, Bitget, Trust Wallet, imToken, SafePal, and all EVM wallets
// v3: improved error handling, MetaMask initialization detection, toast notifications // v4: improved Chinese wallet support (TokenPocket, OKX, Bitget first priority)
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers"; import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers";
@ -30,53 +30,60 @@ const INITIAL_STATE: WalletState = {
error: null, error: null,
}; };
type EthProvider = Eip1193Provider & {
isMetaMask?: boolean;
isTrust?: boolean;
isTrustWallet?: boolean;
isOKExWallet?: boolean;
isOkxWallet?: boolean;
isCoinbaseWallet?: boolean;
isTokenPocket?: boolean;
isBitkeep?: boolean;
isBitgetWallet?: boolean;
providers?: EthProvider[];
};
// Detect the best available EVM provider across all major wallets // Detect the best available EVM provider across all major wallets
// Priority: TokenPocket > OKX > Bitget > Trust Wallet > MetaMask > others
export function detectProvider(): Eip1193Provider | null { export function detectProvider(): Eip1193Provider | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
const w = window as unknown as Record<string, unknown>; const w = window as unknown as Record<string, unknown>;
const eth = w.ethereum as (Eip1193Provider & {
providers?: Eip1193Provider[];
isMetaMask?: boolean;
isTrust?: boolean;
isOKExWallet?: boolean;
isCoinbaseWallet?: boolean;
}) | undefined;
if (!eth) { // 1. TokenPocket — injects window.ethereum with isTokenPocket flag
// Fallback: check wallet-specific globals const eth = w.ethereum as EthProvider | undefined;
if (w.okxwallet) return w.okxwallet as Eip1193Provider; if (eth) {
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider; // Check providers array first (multiple extensions installed)
return null; if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
} // Priority order for Chinese users
const tp = eth.providers.find((p: EthProvider) => p.isTokenPocket);
// If multiple providers are injected (common when multiple extensions installed) if (tp) return tp;
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) { const okx = eth.providers.find((p: EthProvider) => p.isOKExWallet || p.isOkxWallet);
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask); if (okx) return okx;
return metamask ?? eth.providers[0]; const bitget = eth.providers.find((p: EthProvider) => p.isBitkeep || p.isBitgetWallet);
} if (bitget) return bitget;
const trust = eth.providers.find((p: EthProvider) => p.isTrust || p.isTrustWallet);
return eth; if (trust) return trust;
} const metamask = eth.providers.find((p: EthProvider) => p.isMetaMask);
if (metamask) return metamask;
// Check if MetaMask is installed but not yet initialized (no wallet created/imported) return eth.providers[0];
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> {
try {
// eth_accounts is silent — if it returns empty array, wallet is installed but locked or not initialized
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({
method: "eth_accounts",
});
// If we get here, the wallet is at least initialized (even if locked / no accounts)
return { ready: true };
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
// -32002: Request already pending (MetaMask not initialized or another request pending)
if (error?.code === -32002) {
return { ready: false, reason: "pending" };
} }
// Any other error — treat as not ready
return { ready: false, reason: error?.message || "unknown" }; // Single provider — return it directly
return eth;
} }
// 2. OKX Wallet — sometimes injects window.okxwallet separately
if (w.okxwallet) return w.okxwallet as Eip1193Provider;
// 3. Bitget Wallet — sometimes injects window.bitkeep.ethereum
const bitkeep = w.bitkeep as { ethereum?: Eip1193Provider } | undefined;
if (bitkeep?.ethereum) return bitkeep.ethereum;
// 4. Coinbase Wallet
if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
return null;
} }
// Build wallet state from a provider and accounts // Build wallet state from a provider and accounts
@ -136,7 +143,7 @@ export function useWallet() {
const rawProvider = detectProvider(); const rawProvider = detectProvider();
if (!rawProvider) { if (!rawProvider) {
const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。"; const msg = "未检测到钱包插件。请安装 TokenPocket、MetaMask 或其他 EVM 兼容钱包后刷新页面。";
if (mountedRef.current) setState(s => ({ ...s, error: msg })); if (mountedRef.current) setState(s => ({ ...s, error: msg }));
return { success: false, error: msg }; return { success: false, error: msg };
} }
@ -168,16 +175,10 @@ export function useWallet() {
// User rejected // User rejected
msg = "已取消连接 / Connection cancelled"; msg = "已取消连接 / Connection cancelled";
} else if (error?.code === -32002) { } else if (error?.code === -32002) {
// MetaMask has a pending request — usually means it's not initialized or popup is already open // Wallet has a pending request
msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。"; msg = "钱包请求处理中,请检查钱包弹窗。如未弹出,请先完成钱包初始化设置,然后刷新页面重试。";
} else if (error?.message === "no_accounts") { } else if (error?.message === "no_accounts") {
msg = "未获取到账户,请确认钱包已解锁并授权此网站。"; msg = "未获取到账户,请确认钱包已解锁并授权此网站。";
} else if (
error?.message?.toLowerCase().includes("not initialized") ||
error?.message?.toLowerCase().includes("setup") ||
error?.message?.toLowerCase().includes("onboarding")
) {
msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。";
} else { } else {
msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`; msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`;
} }
@ -226,8 +227,9 @@ export function useWallet() {
const rawProvider = detectProvider(); const rawProvider = detectProvider();
if (!rawProvider) { if (!rawProvider) {
if (attempt < 3) { if (attempt < 5) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt); // Retry more times — some wallets inject later (especially mobile in-app browsers)
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 600 * attempt);
} }
return; return;
} }
@ -242,11 +244,14 @@ export function useWallet() {
if (!cancelled && mountedRef.current) { if (!cancelled && mountedRef.current) {
setState({ ...INITIAL_STATE, ...partial }); setState({ ...INITIAL_STATE, ...partial });
} }
} else if (attempt < 3) { } else if (attempt < 5) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt); retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt);
} }
} catch { } catch {
// Silently ignore — user hasn't connected yet // Silently ignore — user hasn't connected yet
if (attempt < 3) {
retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000);
}
} }
}; };

View File

@ -13,7 +13,7 @@ export const CONTRACTS = {
rpcUrl: "https://bsc-dataseed1.binance.org/", rpcUrl: "https://bsc-dataseed1.binance.org/",
explorerUrl: "https://bscscan.com", explorerUrl: "https://bscscan.com",
nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 }, nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c", presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", // XICPresale v2 — 购买即时发放
token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24", token: "0x59FF34dD59680a7125782b1f6df2A86ed46F5A24",
usdt: "0x55d398326f99059fF775485246999027B3197955", usdt: "0x55d398326f99059fF775485246999027B3197955",
}, },
@ -50,9 +50,9 @@ export const PRESALE_CONFIG = {
tokenName: "New AssetChain Token", tokenName: "New AssetChain Token",
tokenDecimals: 18, tokenDecimals: 18,
minPurchaseUSDT: 0, // No minimum purchase limit minPurchaseUSDT: 0, // No minimum purchase limit
maxPurchaseUSDT: 50000, // Maximum $50,000 USDT maxPurchaseUSDT: 50000, // Max $50,000 USDT per purchase
totalSupply: 100_000_000_000, // 100 billion XIC totalSupply: 100_000_000_000, // 100 billion XIC
presaleAllocation: 30_000_000_000, // 30 billion for presale presaleAllocation: 2_500_000_000, // 2.5 billion for presale (25亿)
// TRC20 memo format // TRC20 memo format
trc20Memo: "XIC_PRESALE", trc20Memo: "XIC_PRESALE",
}; };

View File

@ -334,7 +334,8 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
const usdtAmount = parseFloat(usdtInput) || 0; const usdtAmount = parseFloat(usdtInput) || 0;
const tokenAmount = calcTokens(usdtAmount); const tokenAmount = calcTokens(usdtAmount);
const isValidAmount = usdtAmount > 0 && usdtAmount <= PRESALE_CONFIG.maxPurchaseUSDT; // maxPurchaseUSDT=0 means no limit; otherwise check against the limit
const isValidAmount = usdtAmount > 0 && (PRESALE_CONFIG.maxPurchaseUSDT === 0 || usdtAmount <= PRESALE_CONFIG.maxPurchaseUSDT);
const handleBuy = async () => { const handleBuy = async () => {
if (!isValidAmount) { if (!isValidAmount) {
@ -516,8 +517,59 @@ function EVMPurchasePanel({ network, lang, wallet }: { network: "BSC" | "ETH"; l
</button> </button>
<p className="text-xs text-center text-white/30"> <p className="text-xs text-center text-white/30">
{t("buy_no_min_max")} ${PRESALE_CONFIG.maxPurchaseUSDT.toLocaleString()} USDT {PRESALE_CONFIG.maxPurchaseUSDT > 0
? `${t("buy_no_min_max")} $${PRESALE_CONFIG.maxPurchaseUSDT.toLocaleString()} USDT`
: (lang === "zh" ? "无最低/最高购买限制" : "No minimum or maximum purchase limit")}
</p> </p>
{/* Add XIC to Wallet button — only show on BSC where token address is known */}
{network === "BSC" && CONTRACTS.BSC.token && (
<button
onClick={async () => {
try {
// Use the raw provider to call wallet_watchAsset
const rawProvider = (window as unknown as { ethereum?: { request: (args: { method: string; params?: unknown }) => Promise<unknown> } }).ethereum;
if (!rawProvider) {
toast.error(lang === "zh" ? "未检测到钱包,请先安装 MetaMask 或其他 EVM 钱包" : "No wallet detected. Please install MetaMask or another EVM wallet.");
return;
}
await rawProvider.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: CONTRACTS.BSC.token,
symbol: PRESALE_CONFIG.tokenSymbol,
decimals: PRESALE_CONFIG.tokenDecimals,
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
},
},
});
toast.success(lang === "zh" ? "XIC 代币已添加到钱包!" : "XIC token added to wallet!");
} catch (err: unknown) {
const error = err as { code?: number; message?: string };
if (error?.code === 4001) {
// User rejected — not an error
return;
}
toast.error(lang === "zh" ? "添加失败,请手动添加代币" : "Failed to add token. Please add manually.");
}
}}
className="w-full py-2.5 rounded-xl text-sm font-semibold transition-all hover:opacity-90 flex items-center justify-center gap-2"
style={{
background: "rgba(0,212,255,0.08)",
border: "1px solid rgba(0,212,255,0.25)",
color: "rgba(0,212,255,0.9)",
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>
</svg>
{lang === "zh" ? "添加 XIC 到钱包" : "Add XIC to Wallet"}
</button>
)}
</div> </div>
); );
} }

View File

@ -38,7 +38,7 @@ const RPC_POOLS = {
// ─── Contract Addresses ──────────────────────────────────────────────────────── // ─── Contract Addresses ────────────────────────────────────────────────────────
export const CONTRACTS = { export const CONTRACTS = {
BSC: { BSC: {
presale: "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c", presale: "0x5953c025dA734e710886916F2d739A3A78f8bbc4", // XICPresale v2
token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", token: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24",
rpc: RPC_POOLS.BSC[0], rpc: RPC_POOLS.BSC[0],
chainId: 56, chainId: 56,

113
test-onchain.mjs Normal file
View File

@ -0,0 +1,113 @@
/**
* 测试链上数据读取
* 直接调用BSC和ETH合约查看能读到哪些数据
*/
import { ethers } from "ethers";
const BSC_PRESALE = "0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c";
const ETH_PRESALE = "0x85AB2F2d9f7ca7ecB272b5E8726c70f3fd45D1E3";
// 尝试多种可能的函数名
const TEST_ABI = [
"function totalUSDTRaised() view returns (uint256)",
"function totalTokensSold() view returns (uint256)",
"function weiRaised() view returns (uint256)",
"function tokensSold() view returns (uint256)",
"function usdtRaised() view returns (uint256)",
"function totalRaised() view returns (uint256)",
"function amountRaised() view returns (uint256)",
"function hardCap() view returns (uint256)",
"function cap() view returns (uint256)",
"function owner() view returns (address)",
"function paused() view returns (bool)",
];
async function testContract(name, address, rpcUrl) {
console.log(`\n=== 测试 ${name} 合约 ===`);
console.log(`地址: ${address}`);
console.log(`RPC: ${rpcUrl}`);
try {
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
staticNetwork: true,
polling: false,
});
// 先检查合约是否存在
const code = await provider.getCode(address);
if (code === "0x") {
console.log("❌ 该地址没有合约代码!合约地址可能错误。");
return;
}
console.log(`✅ 合约存在,字节码长度: ${code.length} 字符`);
const contract = new ethers.Contract(address, TEST_ABI, provider);
// 逐个测试函数
const functions = [
"totalUSDTRaised",
"totalTokensSold",
"weiRaised",
"tokensSold",
"usdtRaised",
"totalRaised",
"amountRaised",
"hardCap",
"cap",
"owner",
"paused",
];
for (const fn of functions) {
try {
const result = await contract[fn]();
console.log(`${fn}() = ${result}`);
} catch (e) {
console.log(`${fn}() 不存在或调用失败`);
}
}
// 获取合约事件日志最近100个块
const latestBlock = await provider.getBlockNumber();
console.log(`\n当前区块高度: ${latestBlock}`);
// 查找Transfer事件USDT转入
const usdtAddress = name === "BSC"
? "0x55d398326f99059fF775485246999027B3197955"
: "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const usdtAbi = ["event Transfer(address indexed from, address indexed to, uint256 value)"];
const usdtContract = new ethers.Contract(usdtAddress, usdtAbi, provider);
console.log(`\n查询最近1000个块内转入预售合约的USDT...`);
const fromBlock = latestBlock - 1000;
const filter = usdtContract.filters.Transfer(null, address);
try {
const events = await usdtContract.queryFilter(filter, fromBlock, latestBlock);
console.log(`找到 ${events.length} 笔USDT转入记录`);
let totalUsdt = 0n;
for (const event of events.slice(-5)) {
const args = event.args;
const amount = args[2];
totalUsdt += amount;
const decimals = name === "BSC" ? 18 : 6;
console.log(` ${args[0]}${ethers.formatUnits(amount, decimals)} USDT`);
}
} catch (e) {
console.log(`查询事件失败: ${e}`);
}
} catch (e) {
console.error(`测试失败: ${e}`);
}
}
// 测试BSC
await testContract("BSC", BSC_PRESALE, "https://bsc-dataseed1.binance.org/");
// 测试ETH
await testContract("ETH", ETH_PRESALE, "https://eth.llamarpc.com");
console.log("\n=== 测试完成 ===");