/** * External Wallet Signer * * Wraps an EIP-1193 provider (from EIP-6963 discovery) for transaction * operations. The browser wallet (MetaMask, Rainbow, etc.) handles all * signing — we just construct and forward requests. */ import type { EIP1193Provider } from './eip6963'; // ============================================================================ // TYPES // ============================================================================ export interface TransactionRequest { from: string; to: string; value?: string; data?: string; gas?: string; gasPrice?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; nonce?: string; chainId?: string; } export interface TypedDataDomain { name?: string; version?: string; chainId?: number; verifyingContract?: string; } // ============================================================================ // EXTERNAL SIGNER // ============================================================================ export class ExternalSigner { private provider: EIP1193Provider; constructor(provider: EIP1193Provider) { this.provider = provider; } async getAccounts(): Promise { return this.provider.request({ method: 'eth_requestAccounts' }); } async getChainId(): Promise { return this.provider.request({ method: 'eth_chainId' }); } async switchChain(chainId: number): Promise { const hexChainId = '0x' + chainId.toString(16); try { await this.provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: hexChainId }], }); } catch (err: any) { // 4902 = chain not added if (err?.code === 4902) { throw new Error(`Chain ${chainId} not configured in wallet. Please add it manually.`); } throw err; } } async sendTransaction(tx: TransactionRequest): Promise { // Ensure correct chain if (tx.chainId) { const currentChain = await this.getChainId(); const targetHex = '0x' + parseInt(tx.chainId).toString(16); if (currentChain.toLowerCase() !== targetHex.toLowerCase()) { await this.switchChain(parseInt(tx.chainId)); } } return this.provider.request({ method: 'eth_sendTransaction', params: [tx], }); } async personalSign(message: string, account: string): Promise { return this.provider.request({ method: 'personal_sign', params: [message, account], }); } async signTypedData( account: string, domain: TypedDataDomain, types: Record>, value: Record, ): Promise { // Build EIP712Domain type array dynamically from domain fields const domainType: Array<{ name: string; type: string }> = []; if (domain.name !== undefined) domainType.push({ name: 'name', type: 'string' }); if (domain.version !== undefined) domainType.push({ name: 'version', type: 'string' }); if (domain.chainId !== undefined) domainType.push({ name: 'chainId', type: 'uint256' }); if (domain.verifyingContract !== undefined) domainType.push({ name: 'verifyingContract', type: 'address' }); const data = { types: { EIP712Domain: domainType, ...types }, domain, primaryType: Object.keys(types).find(k => k !== 'EIP712Domain') || '', message: value, }; return this.provider.request({ method: 'eth_signTypedData_v4', params: [account, JSON.stringify(data)], }); } }