import { createWalletClient, custom } from 'viem' import type { EthereumProvider } from '../../eip6963-discovery' import type { EVMNativeTransferRequest, EVMContractCallRequest, EVMBatchTransactionRequest, BatchTransactionResult, EthereumTransactionRequest, EVMDataTransferRequest } from '@meshconnect/uwc-types' import { parseError } from '../../utils/error-utils' import { EthereumTransactionBuilder } from './ethereum-transaction-builder' /** * Service for handling Ethereum/EVM transaction operations */ export class EthereumTransactionService { private builder: EthereumTransactionBuilder constructor() { this.builder = new EthereumTransactionBuilder() } /** * Send a transaction for Ethereum */ async sendTransaction( request: EthereumTransactionRequest, provider: EthereumProvider ): Promise { try { // Handle different types of EVM transactions if ('contractAddress' in request && 'abi' in request) { // Contract call return await this.sendContractCall( provider, request as EVMContractCallRequest ) } else if ('calls' in request) { // Batch transaction return await this.sendBatch( provider, request as EVMBatchTransactionRequest ) } else if ('data' in request && 'chainId' in request) { // Data transfer (raw transaction data) return await this.sendDataTransfer( provider, request as EVMDataTransferRequest ) } else if ('amount' in request && typeof request.amount === 'bigint') { // Native transfer return await this.sendNativeTransaction( provider, request as EVMNativeTransferRequest ) } else { throw new Error('Invalid transaction request type') } } catch (error) { parseError(error) } } /** * Send native token transfer (ETH, BNB, MATIC, etc.) */ private async sendNativeTransaction( provider: EthereumProvider, request: EVMNativeTransferRequest ): Promise { try { // Build transaction const tx = await this.builder.getNativeTransaction(request) // Create viem wallet client const client = createWalletClient({ account: request.from as `0x${string}`, transport: custom(provider) }) // Send transaction and wait for confirmation const hash = await client.sendTransaction({ account: request.from as `0x${string}`, chain: null, to: tx.to as `0x${string}`, value: tx.value, ...(tx.gasLimit && { gas: tx.gasLimit }), ...(tx.maxFeePerGas && { maxFeePerGas: tx.maxFeePerGas }), ...(tx.maxPriorityFeePerGas && { maxPriorityFeePerGas: tx.maxPriorityFeePerGas }) }) return hash } catch (error) { parseError(error) } } /** * Send smart contract transaction (including ERC-20 transfers) */ private async sendContractCall( provider: EthereumProvider, request: EVMContractCallRequest ): Promise { try { // Build transaction const tx = await this.builder.getContractTransaction(request) // Create viem wallet client const client = createWalletClient({ account: request.from as `0x${string}`, transport: custom(provider) }) // Send transaction const hash = await client.sendTransaction({ account: request.from as `0x${string}`, chain: null, to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: tx.value, ...(tx.gasLimit && { gas: tx.gasLimit }), ...(tx.maxFeePerGas && { maxFeePerGas: tx.maxFeePerGas }), ...(tx.maxPriorityFeePerGas && { maxPriorityFeePerGas: tx.maxPriorityFeePerGas }) }) return hash } catch (error) { parseError(error) } } /** * Send transaction with raw data */ private async sendDataTransfer( provider: EthereumProvider, request: EVMDataTransferRequest ): Promise { try { // Build transaction const tx = await this.builder.getDataTransaction(request) // Create viem wallet client const client = createWalletClient({ account: request.from as `0x${string}`, transport: custom(provider) }) // Send transaction const hash = await client.sendTransaction({ account: request.from as `0x${string}`, chain: null, to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: tx.value, ...(tx.gasLimit && { gas: tx.gasLimit }), ...(tx.maxFeePerGas && { maxFeePerGas: tx.maxFeePerGas }), ...(tx.maxPriorityFeePerGas && { maxPriorityFeePerGas: tx.maxPriorityFeePerGas }) }) return hash } catch (error) { parseError(error) } } /** * Send batch of transactions (EIP-5792) */ private async sendBatch( provider: EthereumProvider, request: EVMBatchTransactionRequest ): Promise { try { // Prepare calls const calls = await Promise.all( request.calls.map(async tx => { if ('contractAddress' in tx && 'abi' in tx) { // Contract call const result = await this.builder.getContractTransaction( tx as EVMContractCallRequest ) return { to: result.to ?? '', value: '0x' + (result.value ?? 0n).toString(16), data: result.data } } else if ('amount' in tx && typeof tx.amount === 'bigint') { // Native transfer const result = await this.builder.getNativeTransaction( tx as EVMNativeTransferRequest ) return { to: result.to ?? '', value: '0x' + (result.value ?? 0n).toString(16) } } else if ('data' in tx && 'chainId' in tx) { // Data transfer const result = await this.builder.getDataTransaction( tx as EVMDataTransferRequest ) return { to: result.to ?? '', data: result.data, value: '0x' + (result.value ?? 0n).toString(16) } } else { throw new Error('Invalid transaction request type') } }) ) // Create viem wallet client const client = createWalletClient({ account: request.from as `0x${string}`, transport: custom(provider) }) // Send batch transaction using EIP-5792 const response: { id: string } = await ( client.request as (args: { method: string params: unknown[] }) => Promise<{ id: string }> )({ method: 'wallet_sendCalls', params: [ { version: request.version, from: request.from, chainId: request.chainId, atomicRequired: request.atomicRequired, calls: calls } ] }) // Poll for completion with retry logic for specific errors const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) let result: BatchTransactionResult | undefined while (true) { try { result = await ( client.request as (args: { method: string params: unknown[] }) => Promise )({ method: 'wallet_getCallsStatus', params: [response.id] }) // Wait 1 second if still pending if (result?.status === 100) { await sleep(1000) continue } // Exit loop if we have a result (success or failure) break // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { const errorMessage = e?.message ?? String(e) // Retry only for specific errors; throw others immediately if (errorMessage.includes('No matching bundle found')) { await sleep(1000) continue } throw new Error(errorMessage) } } // Check if successful if (result.status !== 200) { throw new Error('Batch transaction failed') } // Return first transaction hash const firstReceipt = result.receipts.find(r => r) if (!firstReceipt) { throw new Error('No transaction receipt found') } return firstReceipt.transactionHash } catch (error) { parseError(error) } } }