// NAC XIC Presale — Wallet Connection Hook // Supports MetaMask, Trust Wallet, OKX Wallet, Coinbase Wallet, and all EVM-compatible wallets // v4: added forceConnect(address) for WalletSelector callback sync import { useState, useEffect, useCallback, useRef } from "react"; import { BrowserProvider, JsonRpcSigner, Eip1193Provider } from "ethers"; import { shortenAddress, switchToNetwork } from "@/lib/contracts"; export type NetworkType = "BSC" | "ETH" | "TRON"; export interface WalletState { address: string | null; shortAddress: string; isConnected: boolean; chainId: number | null; provider: BrowserProvider | null; signer: JsonRpcSigner | null; isConnecting: boolean; error: string | null; } const INITIAL_STATE: WalletState = { address: null, shortAddress: "", isConnected: false, chainId: null, provider: null, signer: null, isConnecting: false, error: null, }; // Detect the best available EVM provider across all major wallets export function detectProvider(): Eip1193Provider | null { if (typeof window === "undefined") return null; const w = window as unknown as Record; const eth = w.ethereum as (Eip1193Provider & { providers?: Eip1193Provider[]; isMetaMask?: boolean; isTrust?: boolean; isOKExWallet?: boolean; isCoinbaseWallet?: boolean; }) | undefined; if (!eth) { // Fallback: check wallet-specific globals if (w.okxwallet) return w.okxwallet as Eip1193Provider; if (w.coinbaseWalletExtension) return w.coinbaseWalletExtension as Eip1193Provider; return null; } // If multiple providers are injected (common when multiple extensions installed) if (eth.providers && Array.isArray(eth.providers) && eth.providers.length > 0) { const metamask = eth.providers.find((p: Eip1193Provider & { isMetaMask?: boolean }) => p.isMetaMask); return metamask ?? eth.providers[0]; } return eth; } // Check if MetaMask is installed but not yet initialized (no wallet created/imported) 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 }).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" }; } } // Build wallet state from a provider and accounts async function buildWalletState( rawProvider: Eip1193Provider, address: string ): Promise> { const provider = new BrowserProvider(rawProvider); let chainId: number | null = null; let signer: JsonRpcSigner | null = null; try { const network = await provider.getNetwork(); chainId = Number(network.chainId); } catch { try { const chainHex = await (rawProvider as { request: (args: { method: string }) => Promise }).request({ method: "eth_chainId" }); chainId = parseInt(chainHex, 16); } catch { chainId = null; } } try { signer = await provider.getSigner(); } catch { signer = null; } return { address, shortAddress: shortenAddress(address), isConnected: true, chainId, provider, signer, isConnecting: false, error: null, }; } export function useWallet() { const [state, setState] = useState(INITIAL_STATE); const retryRef = useRef | null>(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; if (retryRef.current) clearTimeout(retryRef.current); }; }, []); // ── Connect (explicit user action) ───────────────────────────────────────── const connect = useCallback(async (): Promise<{ success: boolean; error?: string }> => { const rawProvider = detectProvider(); if (!rawProvider) { const msg = "未检测到钱包插件。请安装 MetaMask 或其他 EVM 兼容钱包后刷新页面。"; if (mountedRef.current) setState(s => ({ ...s, error: msg })); return { success: false, error: msg }; } setState(s => ({ ...s, isConnecting: true, error: null })); try { // Request accounts — this triggers the wallet popup const accounts = await (rawProvider as { request: (args: { method: string; params?: unknown[] }) => Promise }).request({ method: "eth_requestAccounts", params: [], }); if (!accounts || accounts.length === 0) { throw new Error("no_accounts"); } const partial = await buildWalletState(rawProvider, accounts[0]); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); return { success: true }; } catch (err: unknown) { const error = err as { code?: number; message?: string }; let msg: string; if (error?.code === 4001) { // User rejected msg = "已取消连接 / Connection cancelled"; } else if (error?.code === -32002) { // MetaMask has a pending request — usually means it's not initialized or popup is already open msg = "钱包请求处理中,请检查 MetaMask 弹窗。如未弹出,请先完成 MetaMask 初始化设置(创建或导入钱包),然后刷新页面重试。"; } else if (error?.message === "no_accounts") { msg = "未获取到账户,请确认钱包已解锁并授权此网站。"; } else if ( error?.message?.toLowerCase().includes("not initialized") || error?.message?.toLowerCase().includes("setup") || error?.message?.toLowerCase().includes("onboarding") ) { msg = "MetaMask 尚未完成初始化。请先打开 MetaMask 扩展,创建或导入钱包,然后刷新页面重试。"; } else { msg = `连接失败: ${error?.message || "未知错误"}。请刷新页面重试。`; } if (mountedRef.current) setState(s => ({ ...s, isConnecting: false, error: msg })); return { success: false, error: msg }; } }, []); // ── Force connect with known address (from WalletSelector callback) ───────── // Use this when WalletSelector has already called eth_requestAccounts and got the address. // Directly builds wallet state without triggering another popup. const forceConnect = useCallback(async (address: string): Promise => { if (!address) return; const rawProvider = detectProvider(); if (!rawProvider) { // No provider available — set minimal connected state with just the address if (mountedRef.current) { setState({ ...INITIAL_STATE, address, shortAddress: shortenAddress(address), isConnected: true, }); } return; } try { const partial = await buildWalletState(rawProvider, address); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); } catch { // Fallback: set minimal state if (mountedRef.current) { setState({ ...INITIAL_STATE, address, shortAddress: shortenAddress(address), isConnected: true, }); } } }, []); // ── Disconnect ────────────────────────────────────────────────────────────── const disconnect = useCallback(() => { setState(INITIAL_STATE); }, []); // ── Switch Network ────────────────────────────────────────────────────────── const switchNetwork = useCallback(async (chainId: number) => { try { await switchToNetwork(chainId); const rawProvider = detectProvider(); if (rawProvider) { const provider = new BrowserProvider(rawProvider); const network = await provider.getNetwork(); let signer: JsonRpcSigner | null = null; try { signer = await provider.getSigner(); } catch { /* ignore */ } if (mountedRef.current) { setState(s => ({ ...s, chainId: Number(network.chainId), provider, signer, error: null, })); } } } catch (err: unknown) { if (mountedRef.current) setState(s => ({ ...s, error: (err as Error).message })); } }, []); // ── Auto-detect on page load (silent, no popup) ───────────────────────────── useEffect(() => { let cancelled = false; const tryAutoDetect = async (attempt: number) => { if (cancelled) return; const rawProvider = detectProvider(); if (!rawProvider) { if (attempt < 3) { retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 800 * attempt); } return; } try { const accounts = await (rawProvider as { request: (args: { method: string }) => Promise }).request({ method: "eth_accounts", // Silent — no popup }); if (cancelled) return; if (accounts && accounts.length > 0) { const partial = await buildWalletState(rawProvider, accounts[0]); if (!cancelled && mountedRef.current) { setState({ ...INITIAL_STATE, ...partial }); } } else if (attempt < 3) { retryRef.current = setTimeout(() => tryAutoDetect(attempt + 1), 1000 * attempt); } } catch { // Silently ignore — user hasn't connected yet } }; retryRef.current = setTimeout(() => tryAutoDetect(1), 300); return () => { cancelled = true; if (retryRef.current) clearTimeout(retryRef.current); }; }, []); // ── Listen for account / chain changes ───────────────────────────────────── useEffect(() => { const rawProvider = detectProvider(); if (!rawProvider) return; const eth = rawProvider as { on?: (event: string, handler: (data: unknown) => void) => void; removeListener?: (event: string, handler: (data: unknown) => void) => void; }; if (!eth.on) return; const handleAccountsChanged = async (accounts: unknown) => { const accs = accounts as string[]; if (!mountedRef.current) return; if (!accs || accs.length === 0) { setState(INITIAL_STATE); } else { try { const partial = await buildWalletState(rawProvider, accs[0]); if (mountedRef.current) setState({ ...INITIAL_STATE, ...partial }); } catch { if (mountedRef.current) { setState(s => ({ ...s, address: accs[0], shortAddress: shortenAddress(accs[0]), isConnected: true, })); } } } }; const handleChainChanged = async () => { if (!mountedRef.current) return; try { const provider = new BrowserProvider(rawProvider); const network = await provider.getNetwork(); let signer: JsonRpcSigner | null = null; try { signer = await provider.getSigner(); } catch { /* ignore */ } if (mountedRef.current) { setState(s => ({ ...s, chainId: Number(network.chainId), provider, signer, })); } } catch { window.location.reload(); } }; eth.on("accountsChanged", handleAccountsChanged); eth.on("chainChanged", handleChainChanged); return () => { if (eth.removeListener) { eth.removeListener("accountsChanged", handleAccountsChanged); eth.removeListener("chainChanged", handleChainChanged); } }; }, []); return { ...state, connect, forceConnect, disconnect, switchNetwork }; }