import { requestWalletNonce, verifyWalletSignature } from "./api"; export type WalletKind = "tokenPocket" | "metaMask" | "imToken"; const BNB_CHAIN_ID_HEX = "0x38"; const BNB_CHAIN_PARAMS = { chainId: BNB_CHAIN_ID_HEX, chainName: "BNB Smart Chain", nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 }, rpcUrls: ["https://bsc-dataseed.binance.org"], blockExplorerUrls: ["https://bscscan.com"], }; export type EthereumProvider = { isMetaMask?: boolean; isTokenPocket?: boolean; isImToken?: boolean; providers?: EthereumProvider[]; request: (args: { method: string; params?: unknown[]; }) => Promise; }; function isAddress(value: unknown): value is string { return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value); } function utf8ToHex(value: string): string { return `0x${Array.from(new TextEncoder().encode(value), (byte) => byte.toString(16).padStart(2, "0"), ).join("")}`; } function errorText(error: unknown): string { if (!error || typeof error !== "object") return String(error ?? ""); const parts: string[] = []; const record = error as Record; for (const key of ["shortMessage", "message", "details"]) { const value = record[key]; if (typeof value === "string") parts.push(value); } if (record.cause) parts.push(errorText(record.cause)); return parts.join("\n"); } function isNoAccountError(error: unknown): boolean { return /wallet must has at least one account|wallet must has one account|must have at least one account|no wallet account returned/i.test( errorText(error), ); } function normalizeWalletError(error: unknown): Error { if (isNoAccountError(error)) return new Error("walletNoAccount"); if (error instanceof Error) return error; const message = errorText(error); return new Error(message || "Wallet login failed"); } function shouldRetryPersonalSign(error: unknown): boolean { const text = errorText(error); return /wallet must has at least one account|wallet must has one account|must have at least one account|invalid params|invalid account|account not found/i.test( text, ); } async function ensureBnbChain(ethereum: EthereumProvider): Promise { const chainId = await ethereum .request({ method: "eth_chainId" }) .catch(() => ""); if (chainId.toLowerCase() === BNB_CHAIN_ID_HEX) return; try { await ethereum.request({ method: "wallet_switchEthereumChain", params: [{ chainId: BNB_CHAIN_ID_HEX }], }); } catch (error) { const code = (error as { code?: number | string } | null)?.code; if (code !== 4902 && code !== "4902") throw error; await ethereum.request({ method: "wallet_addEthereumChain", params: [BNB_CHAIN_PARAMS], }); } } async function requestInjectedAddress( ethereum: EthereumProvider, ): Promise { const existingAccounts: unknown[] = await ethereum .request({ method: "eth_accounts" }) .catch((): unknown[] => []); const existingAddress = existingAccounts.find(isAddress); if (existingAddress) return existingAddress; const requestedAccounts = await ethereum .request({ method: "eth_requestAccounts", }) .catch((error: unknown): never => { throw normalizeWalletError(error); }); const requestedAddress = requestedAccounts.find(isAddress); if (!requestedAddress) throw new Error("walletNoAccount"); return requestedAddress; } async function personalSign(params: { ethereum: EthereumProvider; message: string; address: string; }): Promise { const { ethereum, message, address } = params; const hexMessage = utf8ToHex(message); try { return await ethereum.request({ method: "personal_sign", params: [hexMessage, address], }); } catch (error) { if (!shouldRetryPersonalSign(error)) throw error; // Some injected wallets incorrectly expect the legacy param order. return ethereum .request({ method: "personal_sign", params: [address, hexMessage], }) .catch((retryError: unknown): never => { throw normalizeWalletError(retryError); }); } } export function getInjectedEthereum(): EthereumProvider | null { if (typeof window === "undefined") return null; const maybeWindow = window as typeof window & { ethereum?: EthereumProvider }; return maybeWindow.ethereum ?? null; } export function getInjectedWallet(kind?: WalletKind): EthereumProvider | null { const ethereum = getInjectedEthereum(); if (!ethereum || !kind) return ethereum; const providers = ethereum.providers?.length ? ethereum.providers : [ethereum]; const match = providers.find((provider) => { if (kind === "metaMask") return provider.isMetaMask; if (kind === "tokenPocket") return provider.isTokenPocket; if (kind === "imToken") return provider.isImToken; return false; }); return match ?? null; } /** Diagnostic: log what injected providers the browser exposes. */ export function logWalletProviders(): void { const ethereum = getInjectedEthereum(); const list = ( ethereum?.providers?.length ? ethereum.providers : ethereum ? [ethereum] : [] ).map((p) => ({ isMetaMask: Boolean(p.isMetaMask), isTokenPocket: Boolean(p.isTokenPocket), isImToken: Boolean(p.isImToken), })); // eslint-disable-next-line no-console console.info("[wallet-login] providers", { hasEthereum: Boolean(ethereum), count: list.length, list, }); } export async function signInWithInjectedWallet(kind?: WalletKind): Promise<{ token: string; wallet: string; }> { /* eslint-disable no-console */ console.info("[wallet-login] start injected", { kind }); logWalletProviders(); const ethereum = getInjectedWallet(kind); if (!ethereum) { console.warn("[wallet-login] no injected provider found"); throw new Error("No injected wallet found"); } // BNB Smart Chain is EVM-compatible, so browser wallets still expose the // standard EIP-1193 method names (`eth_*`) while operating on BNB chain 56. console.info("[wallet-login] requesting BNB wallet account…"); const address = await requestInjectedAddress(ethereum); console.info("[wallet-login] account", address); console.info("[wallet-login] ensuring BNB Chain (0x38)…"); await ensureBnbChain(ethereum); console.info("[wallet-login] requesting nonce for", address); const nonce = await requestWalletNonce(address); console.info("[wallet-login] got nonce, requesting personal_sign…"); const signature = await personalSign({ ethereum, message: nonce.message, address, }); console.info("[wallet-login] signed, verifying with backend…"); const result = await verifyWalletSignature({ address, message: nonce.message, signature, }); console.info("[wallet-login] verified, wallet =", result.wallet); return result; /* eslint-enable no-console */ }