MCP Ethers Wallet

import { ethers } from "ethers"; import { z } from "zod"; import { DefaultProvider, DEFAULT_PROVIDERS } from "../config/networks.js"; import { networkList, NetworkName, NetworkInfo } from "../config/networkList.js"; // Move addressSchema to class level to avoid duplication const addressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/); const networkToEthersMap: Record<string, string> = { "Ethereum": "mainnet", "Polygon PoS": "matic", "Arbitrum": "arbitrum", "Arbitrum Nova": "arbitrum-nova", "Optimism": "optimism", "Avalanche C-Chain": "avalanche", "Base": "base", "BNB Smart Chain": "bnb", "Linea": "linea", "Polygon zkEVM": "polygon-zkevm" }; export class EthersService { private _provider: ethers.Provider; private _signer?: ethers.Signer; constructor(provider?: ethers.Provider, signer?: ethers.Signer) { this._provider = provider || new ethers.JsonRpcProvider('http://localhost:8545'); this._signer = signer; } get provider() { return this._provider; } setProvider(provider: ethers.Provider): void { this._provider = provider; } setSigner(signer: ethers.Signer): void { this._signer = signer; } private getAlchemyApiKey(): string { const alchemyApiKey = process.env.ALCHEMY_API_KEY; if (!alchemyApiKey) { throw new Error("Missing ALCHEMY_API_KEY in environment variables."); } return alchemyApiKey; } private createAlchemyProvider(network: DefaultProvider): ethers.Provider { try { return new ethers.AlchemyProvider(network as ethers.Networkish, this.getAlchemyApiKey()); } catch (error) { this.handleProviderError(error, `create Alchemy provider for network ${network}`); } } private validateRpcUrl(url: string): void { if (!url.match(/^https?:\/\/.+$/)) { throw new Error(`Invalid RPC URL format: ${url}. URL must start with http:// or https:// and include a valid domain.`); } } private handleProviderError(error: unknown, context: string, details?: Record<string, any>): never { if (error instanceof z.ZodError) { const firstError = error.errors[0]; const message = firstError?.message || 'Invalid input format'; throw new Error(`Invalid input format: ${message}. Expected a valid Ethereum address (0x followed by 40 hexadecimal characters)`); } // Handle provider errors if (error instanceof Error && 'code' in error) { throw new Error(`Failed to ${context}: Provider error: ${error.message}`); } // Generic error with context const err = error as Error; const errorMessage = err.message || String(error); const detailsStr = details ? ` Details: ${Object.entries(details).map(([k, v]) => `${k}=${this.serializeValue(v)}`).join(', ')}` : ''; throw new Error(`Failed to ${context}: ${errorMessage}${detailsStr}`); } private serializeValue(value: any): string { if (value === undefined) return 'undefined'; if (value === null) return 'null'; if (typeof value === 'bigint') return value.toString(); if (Array.isArray(value)) { return `[${value.map(v => this.serializeValue(v)).join(', ')}]`; } if (typeof value === 'object') { if ('toJSON' in value && typeof value.toJSON === 'function') { return value.toJSON(); } return JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v ); } return String(value); } private getEthersNetworkName(network: string): string { return networkToEthersMap[network] || network.toLowerCase(); } private getProvider(provider?: string, chainId?: number): ethers.Provider { if (!provider) { return this._provider; } // Check if it's a default provider if (DEFAULT_PROVIDERS.includes(provider as DefaultProvider)) { try { const networkName = this.getEthersNetworkName(provider); const newProvider = new ethers.AlchemyProvider(networkName, process.env.ALCHEMY_API_KEY); if (chainId) { const providerChainId = (newProvider as any)._network?.chainId; if (providerChainId && providerChainId !== chainId) { console.warn(`Chain ID mismatch: specified ${chainId} but provider network is ${providerChainId}, using provider's chain ID`); } } return newProvider; } catch (error) { throw this.handleProviderError(error, `create Alchemy provider for network ${provider}`); } } // Otherwise treat it as an RPC URL if (provider.startsWith("http")) { try { this.validateRpcUrl(provider); const newProvider = new ethers.JsonRpcProvider(provider); if (chainId) { const providerChainId = (newProvider as any)._network?.chainId; if (providerChainId && providerChainId !== chainId) { console.warn(`Chain ID mismatch: specified ${chainId} but provider network is ${providerChainId}, using provider's chain ID`); } } return newProvider; } catch (error) { throw this.handleProviderError(error, `create provider with RPC URL ${provider}`); } } throw new Error( `Invalid provider: ${provider}. Must be either:\n` + `1. A supported network name (${DEFAULT_PROVIDERS.join(", ")})\n` + `2. A valid RPC URL starting with http:// or https://` ); } async getBalance(address: string, provider?: string, chainId?: number): Promise<string> { try { addressSchema.parse(address); const selectedProvider = this.getProvider(provider, chainId); const balance = await selectedProvider.getBalance(address); return ethers.formatEther(balance); } catch (error) { this.handleProviderError(error, "fetch balance", { address }); } } async getERC20Balance(address: string, tokenAddress: string, provider?: string, chainId?: number): Promise<string> { try { addressSchema.parse(address); addressSchema.parse(tokenAddress); const selectedProvider = this.getProvider(provider, chainId); const contract = new ethers.Contract( tokenAddress, [ "function balanceOf(address) view returns (uint)", "function decimals() view returns (uint8)" ], selectedProvider ); const decimals = await contract.decimals(); const balance = await contract.balanceOf(address); return ethers.formatUnits(balance, decimals); } catch (error) { this.handleProviderError(error, "fetch ERC20 balance", { address, tokenAddress }); } } async getTransactionCount(address: string, provider?: string, chainId?: number): Promise<number> { try { addressSchema.parse(address); const selectedProvider = this.getProvider(provider, chainId); const count = await selectedProvider.getTransactionCount(address); return count; } catch (error) { this.handleProviderError(error, "fetch transaction count", { address }); } } async getBlockNumber(provider?: string, chainId?: number): Promise<number> { try { const selectedProvider = this.getProvider(provider, chainId); return await selectedProvider.getBlockNumber(); } catch (error) { this.handleProviderError(error, "fetch latest block number"); } } async getBlockDetails(blockTag: string | number, provider?: string, chainId?: number): Promise<ethers.Block | null> { try { const selectedProvider = this.getProvider(provider, chainId); const block = await selectedProvider.getBlock(blockTag); return block; } catch (error) { this.handleProviderError(error, "fetch block details", { blockTag: String(blockTag) }); } } async getTransactionDetails(txHash: string, provider?: string, chainId?: number): Promise<ethers.TransactionResponse | null> { try { const txSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/); txSchema.parse(txHash); let selectedProvider = this.getProvider(provider, chainId); if (!provider && !chainId) { try { const derivedChainId = await this.getChainIdFromTransaction(txHash); selectedProvider = this.getProvider(provider, derivedChainId); } catch (error) { // If we can't get the chainId, continue with the default provider console.warn("Could not derive chainId from transaction, using default provider"); } } return await selectedProvider.getTransaction(txHash); } catch (error) { this.handleProviderError(error, "fetch transaction details", { txHash }); } } async getGasPrice(provider?: string, chainId?: number): Promise<string> { try { const selectedProvider = this.getProvider(provider, chainId); const feeData = await selectedProvider.getFeeData(); return ethers.formatUnits(feeData.gasPrice || 0n, "gwei"); } catch (error) { this.handleProviderError(error, "get gas price"); } } async getFeeData(provider?: string, chainId?: number): Promise<ethers.FeeData> { try { const selectedProvider = this.getProvider(provider, chainId); return await selectedProvider.getFeeData(); } catch (error) { this.handleProviderError(error, "get fee data"); } } async getContractCode(address: string, provider?: string, chainId?: number): Promise<string | null> { try { addressSchema.parse(address); const selectedProvider = this.getProvider(provider, chainId); return await selectedProvider.getCode(address); } catch (error) { this.handleProviderError(error, "get contract bytecode", { address }); } } async lookupAddress(address: string, provider?: string, chainId?: number): Promise<string | null> { try { addressSchema.parse(address); const selectedProvider = this.getProvider(provider, chainId); return await selectedProvider.lookupAddress(address); } catch (error) { this.handleProviderError(error, "look up ENS name for address", { address }); } } async resolveName(name: string, provider?: string, chainId?: number): Promise<string | null> { try { const selectedProvider = this.getProvider(provider, chainId); return await selectedProvider.resolveName(name); } catch (error) { this.handleProviderError(error, "resolve ENS name", { name }); } } formatEther(wei: string | number | bigint): string { try { return ethers.formatEther(wei); } catch (error) { this.handleProviderError(error, "format Ether value", { wei: String(wei) }); } } parseEther(ether: string): bigint { try { return ethers.parseEther(ether); } catch (error) { this.handleProviderError(error, "parse Ether string", { ether }); } } formatUnits(value: string | number | bigint, unit: string | number): string { try { return ethers.formatUnits(value, unit); } catch (error) { this.handleProviderError(error, "format units", { value: String(value), unit: String(unit) }); } } parseUnits(value: string, unit: string | number): bigint { try { return ethers.parseUnits(value, unit); } catch (error) { this.handleProviderError(error, "parse units", { value, unit: String(unit) }); } } private getSigner(provider?: string, chainId?: number, signerOverride?: ethers.Signer): ethers.Signer { if (signerOverride) { return signerOverride; } if (this._signer) { return this._signer; } const privateKey = process.env.PRIVATE_KEY; if (!privateKey) { throw new Error("Missing PRIVATE_KEY in environment variables. Either provide a signer in the constructor or set PRIVATE_KEY in environment variables."); } const selectedProvider = this.getProvider(provider, chainId); return new ethers.Wallet(privateKey, selectedProvider); } async createTransaction(to: string, value: string, data?: string, provider?: string): Promise<ethers.TransactionRequest> { try { addressSchema.parse(to); const parsedValue = ethers.parseEther(value); const transaction: ethers.TransactionRequest = { to, value: parsedValue, data: data || "0x", }; const signer = this.getSigner(provider); const populatedTx = await signer.populateTransaction(transaction); return populatedTx; } catch (error) { this.handleProviderError(error, "create transaction", { to, value }); } } async estimateGas(tx: ethers.TransactionRequest, provider?: string): Promise<bigint> { try { const signer = this.getSigner(provider); const result = await signer.estimateGas(tx); return result; } catch (error) { this.handleProviderError(error, "estimate gas", { tx: JSON.stringify(tx) }); } } async sendTransaction( toOrTx: string | ethers.TransactionRequest, value?: string, data?: string, provider?: string ): Promise<ethers.TransactionResponse> { try { let tx: ethers.TransactionRequest; if (typeof toOrTx === 'string') { // Handle old-style parameter based call addressSchema.parse(toOrTx); tx = { to: toOrTx, value: value ? ethers.parseEther(value) : undefined, data: data || "0x" }; } else { // Handle object-style call if (toOrTx.to) { addressSchema.parse(toOrTx.to); } tx = toOrTx; } const signer = this.getSigner(provider); return await signer.sendTransaction(tx); } catch (error) { this.handleProviderError(error, "send transaction", { tx: toOrTx }); } } async signMessage(message: string, provider?: string): Promise<string> { try { const signer = this.getSigner(provider); return await signer.signMessage(message); } catch (error) { this.handleProviderError(error, "sign message", { message }); } } async contractCall( contractAddress: string, abi: string | Array<string>, method: string, args: any[] = [], provider?: string, chainId?: number ): Promise<any> { try { addressSchema.parse(contractAddress); const selectedProvider = this.getProvider(provider, chainId); // Parse ABI if it's a string let parsedAbi: any = abi; if (typeof abi === 'string') { try { parsedAbi = JSON.parse(abi); } catch (e) { throw new Error(`Invalid ABI: ${abi}. The ABI must be a valid JSON string or array of strings`); } } // Create contract instance with provider const contract = new ethers.Contract( contractAddress, parsedAbi, selectedProvider ); // Get function fragment to check if it's view/pure const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // For view/pure functions, use provider directly if (fragment.constant || fragment.stateMutability === 'view' || fragment.stateMutability === 'pure') { const result = await contract.getFunction(method).staticCall(...args); return this.serializeEventArgs(result); // Use our serializer for the result } throw new Error(`Use contractSendTransaction for state-changing function: ${method}`); } catch (error) { this.handleProviderError(error, `call contract method: ${method}`, { contractAddress, abi: typeof abi === 'string' ? abi : JSON.stringify(abi), args: this.serializeValue(args), }); } } async contractCallView( contractAddress: string, abi: string | Array<string>, method: string, args: any[] = [], provider?: string, chainId?: number ): Promise<any> { try { addressSchema.parse(contractAddress); const selectedProvider = this.getProvider(provider, chainId); // Parse ABI if it's a string let parsedAbi: any = abi; if (typeof abi === 'string') { try { parsedAbi = JSON.parse(abi); } catch (e) { throw new Error(`Invalid ABI: ${abi}. The ABI must be a valid JSON string or array of strings`); } } // Create contract instance with provider const contract = new ethers.Contract( contractAddress, parsedAbi, selectedProvider ); // Get function fragment to check if it's view/pure const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // For view/pure functions, use provider directly if (!fragment.constant && fragment.stateMutability !== 'view' && fragment.stateMutability !== 'pure') { throw new Error(`Use contractSendTransaction for state-changing function: ${method}`); } const result = await contract.getFunction(method).staticCall(...args); return this.serializeEventArgs(result); // Use our serializer for the result } catch (error) { this.handleProviderError(error, `call contract view method: ${method}`, { contractAddress, abi: typeof abi === 'string' ? abi : JSON.stringify(abi), args: this.serializeValue(args), }); } } async contractCallWithEstimate( contractAddress: string, abi: string, method: string, args: any[] = [], value: string = "0", provider?: string ): Promise<any> { try { addressSchema.parse(contractAddress); const signer = this.getSigner(provider); const contract = new ethers.Contract( contractAddress, abi, signer ); const parsedValue = ethers.parseEther(value); // Get the function fragment for the method const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // Encode the function data const data = contract.interface.encodeFunctionData(fragment, args); // Create the transaction request const tx = { to: contractAddress, data, value: parsedValue }; // Estimate the gas const estimatedGas = await signer.estimateGas(tx); // Add the estimated gas and send the transaction return await this.contractSendTransaction( contractAddress, abi, method, args, value, provider, { gasLimit: estimatedGas } ); } catch (error) { this.handleProviderError(error, `call contract method with estimate: ${method}`, { contractAddress, abi: JSON.stringify(abi), args: JSON.stringify(args), value }); } } async contractCallWithOverrides( contractAddress: string, abi: string, method: string, args: any[] = [], value: string = "0", provider?: string, overrides?: ethers.Overrides ): Promise<any> { try { addressSchema.parse(contractAddress); const signer = this.getSigner(provider); const contract = new ethers.Contract( contractAddress, abi, signer ); const parsedValue = ethers.parseEther(value); // Get the function fragment for the method const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // Merge value with other overrides const txOverrides = { ...overrides, value: parsedValue }; // Call the contract method with overrides const tx = await contract[method](...args, txOverrides); return tx; } catch (error) { this.handleProviderError(error, `call contract method with overrides: ${method}`, { contractAddress, abi: JSON.stringify(abi), args: this.serializeValue(args), value, overrides: this.serializeValue(overrides) }); } } async contractSendTransaction( contractAddress: string, abi: string, method: string, args: any[] = [], value: string = "0", provider?: string, overrides?: ethers.Overrides ): Promise<ethers.TransactionResponse> { try { addressSchema.parse(contractAddress); const signer = this.getSigner(provider); const contract = new ethers.Contract( contractAddress, abi, signer ); const parsedValue = ethers.parseEther(value); // Get the function fragment for the method const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // Encode the function data const data = contract.interface.encodeFunctionData(fragment, args); // Create the transaction request with overrides const tx = { to: contractAddress, data, value: parsedValue, ...overrides }; // Send the transaction return await signer.sendTransaction(tx); } catch (error) { this.handleProviderError(error, `send transaction to contract method: ${method}`, { contractAddress, abi: JSON.stringify(abi), args: JSON.stringify(args), value }); } } async contractSendTransactionWithEstimate( contractAddress: string, abi: string, method: string, args: any[], value: string = "0", provider?: string ): Promise<ethers.TransactionResponse> { try { const parsedAddress = addressSchema.parse(contractAddress); const contract = new ethers.Contract(parsedAddress, abi, await this.getSigner(provider)); const parsedValue = ethers.parseEther(value); // Get the function fragment for the method const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // Encode the function data with value const data = contract.interface.encodeFunctionData(fragment, args); const tx = { to: parsedAddress, data, value: parsedValue }; // Estimate gas const gasEstimate = await contract.getFunction(method).estimateGas(...args, { value: parsedValue }); // Send transaction with estimated gas return await contract.getFunction(method)(...args, { value: parsedValue, gasLimit: gasEstimate }); } catch (error) { throw this.handleProviderError(error, `send transaction to contract method with estimate: ${method}`, { contractAddress, abi: JSON.stringify(abi), args: JSON.stringify(args), value }); } } async contractSendTransactionWithOverrides( contractAddress: string, abi: string, method: string, args: any[], value: string = "0", provider?: string, overrides: ethers.Overrides = {} ): Promise<ethers.TransactionResponse> { try { const parsedAddress = addressSchema.parse(contractAddress); const contract = new ethers.Contract(parsedAddress, abi, await this.getSigner(provider)); const parsedValue = ethers.parseEther(value); // Get the function fragment for the method const fragment = contract.interface.getFunction(method); if (!fragment) { throw new Error(`Method ${method} not found in contract ABI`); } // Merge value with other overrides const txOverrides = { ...overrides, value: parsedValue }; // Encode the function data const data = contract.interface.encodeFunctionData(fragment, args); // Send transaction with overrides return await contract.getFunction(method)(...args, txOverrides); } catch (error) { throw this.handleProviderError(error, `send transaction to contract method with overrides: ${method}`, { contractAddress, abi: JSON.stringify(abi), args: this.serializeValue(args), value, overrides: this.serializeValue(overrides) }); } } async sendRawTransaction( signedTransaction: string, provider?: string ): Promise<ethers.TransactionResponse> { try { const selectedProvider = this.getProvider(provider); return await selectedProvider.broadcastTransaction(signedTransaction); } catch (error) { this.handleProviderError(error, "send raw transaction", { signedTransaction }); } } private formatEvent(log: ethers.EventLog | ethers.Log): any { const formattedEvent = { address: log.address, blockNumber: log.blockNumber?.toString(), transactionHash: log.transactionHash, logIndex: log.index, name: 'eventName' in log ? log.eventName : undefined, args: 'args' in log ? this.serializeEventArgs(log.args) : undefined, data: log.data, topics: log.topics }; return formattedEvent; } private serializeEventArgs(args: any): any { if (args === null || args === undefined) return args; if (typeof args === 'bigint') return args.toString(); if (Array.isArray(args)) { return args.map(arg => this.serializeEventArgs(arg)); } if (typeof args === 'object') { const serialized: any = {}; for (const [key, value] of Object.entries(args)) { if (key === 'length' && Array.isArray(args)) continue; if (key === '_isBigNumber' || key === 'type' || key === 'hash') continue; // Skip internal ethers properties serialized[key] = this.serializeEventArgs(value); } return serialized; } return args; } async queryLogs( address?: string, topics?: Array<string | null | Array<string>>, fromBlock?: string | number, toBlock?: string | number, provider?: string, chainId?: number ): Promise<any> { try { let checksummedAddress: string | undefined; if (address) { checksummedAddress = ethers.getAddress(address); } const selectedProvider = this.getProvider(provider, chainId); const filter: ethers.Filter = { address: checksummedAddress, topics: topics }; const logs = await selectedProvider.getLogs({ ...filter, fromBlock: fromBlock, toBlock: toBlock }); return logs.map((log) => this.formatEvent(log)); } catch (error) { this.handleProviderError(error, "query logs", { address: address || "any", topics: topics ? JSON.stringify(topics) : "any", fromBlock: String(fromBlock || "any"), toBlock: String(toBlock || "any") }); } } async contractEvents( contractAddress: string, abi: string | Array<string>, eventName?: string, topics?: Array<string | null | Array<string>>, fromBlock?: string | number, toBlock?: string | number, provider?: string, chainId?: number ): Promise<any> { try { // Use queryLogs under the hood as it's more reliable const checksummedAddress = ethers.getAddress(contractAddress); const selectedProvider = this.getProvider(provider, chainId); const contract = new ethers.Contract(checksummedAddress, abi, selectedProvider); // If no event name specified, get all events if (!eventName) { return this.queryLogs( checksummedAddress, topics, fromBlock, toBlock, provider, chainId ); } // Get the event fragment to encode topics const fragment = contract.interface.getEvent(eventName); if (!fragment) { throw new Error(`Event ${eventName} not found in contract ABI`); } // Get the topic hash for this event const topicHash = fragment.topicHash; const eventTopics: (string | null | Array<string>)[] = [topicHash]; if (topics && topics.length > 0) { eventTopics.push(...topics); } // Use queryLogs with the event-specific topic const logs = await this.queryLogs( checksummedAddress, eventTopics, fromBlock, toBlock, provider, chainId ); // Parse the logs with the contract interface return logs.map((log: ethers.Log) => { try { const parsedLog = contract.interface.parseLog({ topics: log.topics, data: log.data }); return { ...log, name: parsedLog?.name, args: this.serializeEventArgs(parsedLog?.args) }; } catch (e) { // If parsing fails, return the raw log return log; } }); } catch (error) { this.handleProviderError(error, "query contract events", { contractAddress, abi: typeof abi === 'string' ? abi : JSON.stringify(abi), eventName: eventName || "any", topics: topics ? this.serializeValue(topics) : "any", fromBlock: String(fromBlock || "any"), toBlock: String(toBlock || "any") }); } } async sendTransactionWithOptions( toOrTx: string | ethers.TransactionRequest, value?: string, data?: string, gasLimit?: string, gasPrice?: string, nonce?: number, provider?: string, chainId?: number, signerOverride?: ethers.Signer ): Promise<ethers.TransactionResponse> { try { let tx: ethers.TransactionRequest; if (typeof toOrTx === 'string') { addressSchema.parse(toOrTx); tx = { to: toOrTx, value: value ? ethers.parseEther(value) : undefined, data: data || "0x", gasLimit: gasLimit ? ethers.getBigInt(gasLimit) : undefined, gasPrice: gasPrice ? ethers.parseUnits(gasPrice, "gwei") : undefined, nonce, }; } else { if(toOrTx.to) { addressSchema.parse(toOrTx.to); } tx = { ...toOrTx, gasLimit: gasLimit ? ethers.getBigInt(gasLimit) : undefined, gasPrice: gasPrice ? ethers.parseUnits(gasPrice, "gwei") : undefined, nonce, } } const signer = this.getSigner(provider, chainId, signerOverride); return await signer.sendTransaction(tx); } catch (error) { this.handleProviderError(error, "send transaction with options", { tx: toOrTx, value, data, gasLimit, gasPrice, nonce }); } } getSupportedNetworks(): Array<{ name: string; chainId?: number; isTestnet?: boolean; nativeCurrency?: { name: string; symbol: string; decimals: number; }; isDefault?: boolean; }> { try { const defaultNetwork = process.env.DEFAULT_NETWORK || "mainnet"; return DEFAULT_PROVIDERS.map((network) => { const networkInfo = networkList[network as NetworkName]; return { name: network, chainId: networkInfo?.chainId, isTestnet: network.toLowerCase().includes('testnet') || network.toLowerCase().includes('goerli') || network.toLowerCase().includes('sepolia'), nativeCurrency: { name: networkInfo?.currency || 'Native Token', symbol: networkInfo?.currency || 'NATIVE', decimals: 18 }, isDefault: network === defaultNetwork }; }); } catch (error) { throw this.handleProviderError(error, "get supported networks"); } } async getWalletInfo(provider?: string): Promise<{ address: string } | null> { try { if (!this._signer) { return null; } const selectedProvider = provider ? this.getProvider(provider) : this._provider; const signer = this._signer.connect(selectedProvider); const address = await signer.getAddress(); return { address }; } catch (error) { this.handleProviderError(error, "get wallet info"); } } async getChainIdFromTransaction(txHash: string, provider?: string): Promise<number> { try { const txSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/); txSchema.parse(txHash); const selectedProvider = this.getProvider(provider); const tx = await selectedProvider.getTransaction(txHash); if (!tx) { throw new Error("Transaction not found"); } return Number(tx.chainId); } catch (error) { this.handleProviderError(error, "fetch transaction details", { txHash }); } } async getTransactionsByBlock(blockTag: string | number, provider?: string, chainId?: number): Promise<ethers.TransactionResponse[]> { try { const selectedProvider = this.getProvider(provider, chainId); const block = await selectedProvider.getBlock(blockTag, true); if (!block || !block.transactions) { return []; } const transactionRequests = await Promise.all(block.transactions.map(tx => selectedProvider.getTransaction(tx))); return transactionRequests.filter((tx): tx is ethers.TransactionResponse => tx != null); } catch (error) { this.handleProviderError(error, "get transactions by block", { blockTag: String(blockTag) }); } } }