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 multiple providers are injected (common when multiple extensions installed)
if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) { if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) {
const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask); // Priority order for Chinese users
return metamask ?? eth.providers[0]; const tp = eth.providers.find((p: EthProvider) => p.isTokenPocket);
if (tp) return tp;
const okx = eth.providers.find((p: EthProvider) => p.isOKExWallet || p.isOkxWallet);
if (okx) return okx;
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);
if (trust) return trust;
const metamask = eth.providers.find((p: EthProvider) => p.isMetaMask);
if (metamask) return metamask;
return eth.providers[0];
} }
// Single provider — return it directly
return eth; return eth;
} }
// Check if MetaMask is installed but not yet initialized (no wallet created/imported) // 2. OKX Wallet — sometimes injects window.okxwallet separately
export async function checkWalletReady(rawProvider: Eip1193Provider): Promise<{ ready: boolean; reason?: string }> { if (w.okxwallet) return w.okxwallet as Eip1193Provider;
try {
// eth_accounts is silent — if it returns empty array, wallet is installed but locked or not initialized // 3. Bitget Wallet — sometimes injects window.bitkeep.ethereum
const accounts = await (rawProvider as { request: (args: { method: string }) => Promise<string[]> }).request({ const bitkeep = w.bitkeep as { ethereum?: Eip1193Provider } | undefined;
method: "eth_accounts", if (bitkeep?.ethereum) return bitkeep.ethereum;
});
// If we get here, the wallet is at least initialized (even if locked / no accounts) // 4. Coinbase Wallet
return { ready: true }; if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider;
} catch (err: unknown) {
const error = err as { code?: number; message?: string }; return null;
// -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" };
}
} }
// 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=== 测试完成 ===");