/** * 浏览器注入钱包(EIP-1193)接入:EIP-6963 多钱包发现 + personal_sign 封装。 * 不引钱包 SDK;WalletConnect 等远程签名方案不在支持范围。 */ /** EIP-1193 provider 最小接口 */ export interface Eip1193Provider { request(args: { method: string; params?: unknown[] }): Promise } /** EIP-6963 announce 事件携带的钱包信息 */ export interface WalletDetail { info: { uuid: string; name: string; icon: string; rdns: string } provider: Eip1193Provider } interface Eip6963AnnounceEvent extends Event { detail: WalletDetail } /** 兼容旧式单一注入的 window.ethereum */ declare global { interface Window { ethereum?: Eip1193Provider } } /** * 发现已安装的注入钱包:广播 EIP-6963 请求并收集 announce; * 无响应时回退 window.ethereum(旧式单钱包注入)。 */ export function discoverWallets(collectMs = 300): Promise { return new Promise((resolve) => { const found: WalletDetail[] = [] const onAnnounce = (e: Event) => { const detail = (e as Eip6963AnnounceEvent).detail if (detail?.provider && !found.some((w) => w.info.uuid === detail.info.uuid)) found.push(detail) } window.addEventListener('eip6963:announceProvider', onAnnounce) window.dispatchEvent(new Event('eip6963:requestProvider')) setTimeout(() => { window.removeEventListener('eip6963:announceProvider', onAnnounce) if (!found.length && window.ethereum) { found.push({ info: { uuid: 'legacy', name: '浏览器钱包', icon: '', rdns: 'legacy.injected' }, provider: window.ethereum, }) } resolve(found) }, collectMs) }) } /** 连接钱包并返回首个授权账户地址;用户拒绝时抛出钱包侧错误 */ export async function requestAccount(provider: Eip1193Provider): Promise { const accounts = (await provider.request({ method: 'eth_requestAccounts' })) as string[] const addr = accounts?.[0] if (!addr) throw new Error('钱包未返回账户地址') return addr } /** 对消息做 personal_sign(EIP-191);返回 0x 前缀的 65 字节签名 hex */ export async function personalSign( provider: Eip1193Provider, message: string, address: string, ): Promise { const sig = (await provider.request({ method: 'personal_sign', params: [message, address], })) as string if (!sig || typeof sig !== 'string') throw new Error('钱包未返回签名') return sig } /** 钱包侧用户拒绝(EIP-1193 code 4001);拒绝不作为错误提示 */ export function isUserRejected(e: unknown): boolean { return typeof e === 'object' && e !== null && (e as { code?: number }).code === 4001 }