import { type Provider, type TransactionRequest, type TransactionResponse, type TypedDataDomain, type TypedDataField, AbstractSigner, } from 'ethers' import type { Account, Chain, PublicClient, Transport, WalletClient } from 'viem' import { ViemTransportProvider } from './client-adapter.ts' import { CCIPViemAdapterError } from '../../errors/index.ts' /** * Adapter that wraps viem WalletClient as ethers Signer. * * IMPORTANT: This uses a custom AbstractSigner implementation rather than * JsonRpcSigner to properly support LOCAL accounts (privateKeyToAccount). * The standard BrowserProvider approach fails for local accounts because * eth_accounts RPC call doesn't know about client-side accounts. * * @see https://github.com/wevm/viem/discussions/2066 */ class ViemWalletAdapter extends AbstractSigner { private readonly walletClient: WalletClient /** Creates a new ViemWalletAdapter wrapping the given WalletClient. */ constructor( walletClient: WalletClient, provider: ViemTransportProvider, ) { super(provider) this.walletClient = walletClient } /** Returns the address of the underlying viem account. */ override getAddress(): Promise { return Promise.resolve(this.walletClient.account.address as string) } /** Throws an error - viem wallet adapters cannot be reconnected. */ override connect(_provider: Provider | null): never { throw new CCIPViemAdapterError( 'ViemWalletAdapter cannot be reconnected to a different provider', ) } /** * Sign a transaction using viem's WalletClient. * Required by AbstractSigner but rarely used directly (sendTransaction is more common). */ override async signTransaction(tx: TransactionRequest): Promise { const signedTx = await this.walletClient.signTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: tx.value ? BigInt(tx.value.toString()) : undefined, gas: tx.gasLimit ? BigInt(tx.gasLimit.toString()) : undefined, nonce: tx.nonce ? Number(tx.nonce) : undefined, chain: this.walletClient.chain, account: this.walletClient.account, ...(tx.maxFeePerGas ? { maxFeePerGas: BigInt(tx.maxFeePerGas.toString()), maxPriorityFeePerGas: tx.maxPriorityFeePerGas ? BigInt(tx.maxPriorityFeePerGas.toString()) : undefined, } : tx.gasPrice ? { gasPrice: BigInt(tx.gasPrice.toString()) } : {}), }) return signedTx } /** * Sign a message using viem's WalletClient. */ override async signMessage(message: string | Uint8Array): Promise { const messageToSign = typeof message === 'string' ? message : { raw: message } return this.walletClient.signMessage({ account: this.walletClient.account, message: messageToSign, }) } /** * Sign typed data using viem's WalletClient. * Converts ethers.js typed data format to viem format. */ override async signTypedData( domain: TypedDataDomain, types: Record, value: Record, ): Promise { // Convert ethers domain to viem domain format const viemDomain: { chainId?: number | bigint name?: string salt?: `0x${string}` verifyingContract?: `0x${string}` version?: string } = {} if (domain.name) viemDomain.name = domain.name if (domain.version) viemDomain.version = domain.version if (domain.chainId != null) viemDomain.chainId = BigInt(domain.chainId.toString()) if (domain.verifyingContract) viemDomain.verifyingContract = domain.verifyingContract as `0x${string}` if (domain.salt) viemDomain.salt = domain.salt as `0x${string}` // Convert ethers types to viem types format const viemTypes: Record = {} for (const [key, fields] of Object.entries(types)) { viemTypes[key] = fields.map((f) => ({ name: f.name, type: f.type })) } return this.walletClient.signTypedData({ account: this.walletClient.account, domain: viemDomain, types: viemTypes, primaryType: Object.keys(types).find((k) => k !== 'EIP712Domain') || '', message: value, }) } /** * Send a transaction using viem's WalletClient. * This delegates directly to viem for signing, avoiding eth_accounts issues. */ override async sendTransaction(tx: TransactionRequest): Promise { // Build transaction params, handling EIP-1559 vs legacy gas const txParams: Parameters['sendTransaction']>[0] = { to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: tx.value ? BigInt(tx.value.toString()) : undefined, gas: tx.gasLimit ? BigInt(tx.gasLimit.toString()) : undefined, nonce: tx.nonce ? Number(tx.nonce) : undefined, chain: this.walletClient.chain, account: this.walletClient.account, } // Use EIP-1559 if available, otherwise legacy gasPrice if (tx.maxFeePerGas) { txParams.maxFeePerGas = BigInt(tx.maxFeePerGas.toString()) if (tx.maxPriorityFeePerGas) { txParams.maxPriorityFeePerGas = BigInt(tx.maxPriorityFeePerGas.toString()) } } else if (tx.gasPrice) { txParams.gasPrice = BigInt(tx.gasPrice.toString()) } const hash = await this.walletClient.sendTransaction(txParams) // Wait for transaction and return ethers-compatible response await this.provider!.waitForTransaction(hash) return this.provider!.getTransaction(hash) as Promise } } /** * Convert viem WalletClient to ethers-compatible Signer. * * Supports both: * - Local accounts (privateKeyToAccount, mnemonicToAccount) * - JSON-RPC accounts (browser wallets like MetaMask) * * Works with ALL viem transport types including: * - http() - Standard HTTP transport * - webSocket() - WebSocket transport * - custom() - Injected providers (MetaMask, WalletConnect, etc.) * * @param client - viem WalletClient instance with account and chain defined * @returns ethers AbstractSigner for use with sendMessage, manuallyExecute, etc. * * @example * ```typescript * import { createWalletClient, http } from 'viem' * import { privateKeyToAccount } from 'viem/accounts' * import { mainnet } from 'viem/chains' * import { fromViemClient, viemWallet } from '@chainlink/ccip-sdk/viem' * * const account = privateKeyToAccount('0x...') * const walletClient = createWalletClient({ * chain: mainnet, * transport: http('https://eth.llamarpc.com'), * account, * }) * * const chain = await fromViemClient(publicClient) * const request = await chain.sendMessage( * router, * destChainSelector, * message, * { wallet: viemWallet(walletClient) } * ) * ``` * * @example Browser wallet (MetaMask) * ```typescript * import { createWalletClient, custom } from 'viem' * import { mainnet } from 'viem/chains' * import { viemWallet } from '@chainlink/ccip-sdk/viem' * * const walletClient = createWalletClient({ * chain: mainnet, * transport: custom(window.ethereum), * account: connectedAccount, * }) * * // Works with injected providers! * const signer = viemWallet(walletClient) * ``` */ export function viemWallet(client: WalletClient): AbstractSigner { // Validate account is defined if (!(client.account as Account | undefined)) { throw new CCIPViemAdapterError('WalletClient must have an account defined', { recovery: 'Pass an account to createWalletClient or use .extend(walletActions)', }) } if (!(client.chain as Chain | undefined)) { throw new CCIPViemAdapterError('WalletClient must have a chain defined', { recovery: 'Pass a chain to createWalletClient: createWalletClient({ chain: mainnet, ... })', }) } // Create provider that wraps viem transport (works for ALL transport types including injected) const provider = new ViemTransportProvider(client as unknown as PublicClient) // Return adapter that delegates signing to viem return new ViemWalletAdapter(client, provider) }