openzeppelin_monitor/services/blockchain/clients/evm/
client.rs

1//! EVM-compatible blockchain client implementation.
2//!
3//! This module provides functionality to interact with Ethereum and other EVM-compatible
4//! blockchains, supporting operations like block retrieval, transaction receipt lookup,
5//! and log filtering.
6
7use std::marker::PhantomData;
8
9use anyhow::Context;
10use async_trait::async_trait;
11use futures;
12use serde_json::json;
13use tracing::instrument;
14
15use crate::{
16	models::{BlockType, EVMBlock, EVMReceiptLog, EVMTransactionReceipt, Network},
17	services::{
18		blockchain::{
19			client::BlockChainClient,
20			transports::{BlockchainTransport, EVMTransportClient},
21			BlockFilterFactory,
22		},
23		filter::{evm_helpers::string_to_h256, EVMBlockFilter},
24	},
25};
26
27/// Client implementation for Ethereum Virtual Machine (EVM) compatible blockchains
28///
29/// Provides high-level access to EVM blockchain data and operations through HTTP transport.
30#[derive(Clone)]
31pub struct EvmClient<T: Send + Sync + Clone> {
32	/// The underlying HTTP transport client for RPC communication
33	http_client: T,
34}
35
36impl<T: Send + Sync + Clone> EvmClient<T> {
37	/// Creates a new EVM client instance with a specific transport client
38	pub fn new_with_transport(http_client: T) -> Self {
39		Self { http_client }
40	}
41}
42
43impl EvmClient<EVMTransportClient> {
44	/// Creates a new EVM client instance
45	///
46	/// # Arguments
47	/// * `network` - Network configuration containing RPC endpoints and chain details
48	///
49	/// # Returns
50	/// * `Result<Self, anyhow::Error>` - New client instance or connection error
51	pub async fn new(network: &Network) -> Result<Self, anyhow::Error> {
52		let client = EVMTransportClient::new(network).await?;
53		Ok(Self::new_with_transport(client))
54	}
55}
56
57impl<T: Send + Sync + Clone + BlockchainTransport> BlockFilterFactory<Self> for EvmClient<T> {
58	type Filter = EVMBlockFilter<Self>;
59	fn filter() -> Self::Filter {
60		EVMBlockFilter {
61			_client: PhantomData,
62		}
63	}
64}
65
66/// Extended functionality specific to EVM-compatible blockchains
67#[async_trait]
68pub trait EvmClientTrait {
69	/// Retrieves a transaction receipt by its hash
70	///
71	/// # Arguments
72	/// * `transaction_hash` - The hash of the transaction to look up
73	///
74	/// # Returns
75	/// * `Result<TransactionReceipt, anyhow::Error>` - Transaction receipt or error
76	async fn get_transaction_receipt(
77		&self,
78		transaction_hash: String,
79	) -> Result<EVMTransactionReceipt, anyhow::Error>;
80
81	/// Retrieves logs for a range of blocks
82	///
83	/// # Arguments
84	/// * `from_block` - Starting block number
85	/// * `to_block` - Ending block number
86	/// * `addresses` - Optional list of addresses to filter logs by
87	/// # Returns
88	/// * `Result<Vec<Log>, anyhow::Error>` - Collection of matching logs or error
89	async fn get_logs_for_blocks(
90		&self,
91		from_block: u64,
92		to_block: u64,
93		addresses: Option<Vec<String>>,
94	) -> Result<Vec<EVMReceiptLog>, anyhow::Error>;
95}
96
97#[async_trait]
98impl<T: Send + Sync + Clone + BlockchainTransport> EvmClientTrait for EvmClient<T> {
99	/// Retrieves a transaction receipt by hash with proper error handling
100	#[instrument(skip(self), fields(transaction_hash))]
101	async fn get_transaction_receipt(
102		&self,
103		transaction_hash: String,
104	) -> Result<EVMTransactionReceipt, anyhow::Error> {
105		let hash = string_to_h256(&transaction_hash)
106			.map_err(|e| anyhow::anyhow!("Invalid transaction hash: {}", e))?;
107
108		let params = json!([format!("0x{:x}", hash)])
109			.as_array()
110			.with_context(|| "Failed to create JSON-RPC params array")?
111			.to_vec();
112
113		let response = self
114			.http_client
115			.send_raw_request(
116				"eth_getTransactionReceipt",
117				Some(serde_json::Value::Array(params)),
118			)
119			.await
120			.with_context(|| format!("Failed to get transaction receipt: {}", transaction_hash))?;
121
122		// Extract the "result" field from the JSON-RPC response
123		let receipt_data = response
124			.get("result")
125			.with_context(|| "Missing 'result' field")?;
126
127		// Handle null response case
128		if receipt_data.is_null() {
129			return Err(anyhow::anyhow!("Transaction receipt not found"));
130		}
131
132		Ok(serde_json::from_value(receipt_data.clone())
133			.with_context(|| "Failed to parse transaction receipt")?)
134	}
135
136	/// Retrieves logs within the specified block range
137	///
138	/// # Arguments
139	/// * `from_block` - Starting block number
140	/// * `to_block` - Ending block number
141	/// * `addresses` - Optional list of addresses to filter logs by
142	/// # Returns
143	/// * `Result<Vec<EVMReceiptLog>, anyhow::Error>` - Collection of matching logs or error
144	#[instrument(skip(self), fields(from_block, to_block))]
145	async fn get_logs_for_blocks(
146		&self,
147		from_block: u64,
148		to_block: u64,
149		addresses: Option<Vec<String>>,
150	) -> Result<Vec<EVMReceiptLog>, anyhow::Error> {
151		// Convert parameters to JSON-RPC format
152		let params = json!([{
153			"fromBlock": format!("0x{:x}", from_block),
154			"toBlock": format!("0x{:x}", to_block),
155			"address": addresses
156		}])
157		.as_array()
158		.with_context(|| "Failed to create JSON-RPC params array")?
159		.to_vec();
160
161		let response = self
162			.http_client
163			.send_raw_request("eth_getLogs", Some(params))
164			.await
165			.with_context(|| {
166				format!(
167					"Failed to get logs for blocks: {} - {}",
168					from_block, to_block
169				)
170			})?;
171
172		// Extract the "result" field from the JSON-RPC response
173		let logs_data = response
174			.get("result")
175			.with_context(|| "Missing 'result' field")?;
176
177		// Parse the response into the expected type
178		Ok(serde_json::from_value(logs_data.clone()).with_context(|| "Failed to parse logs")?)
179	}
180}
181
182#[async_trait]
183impl<T: Send + Sync + Clone + BlockchainTransport> BlockChainClient for EvmClient<T> {
184	/// Retrieves the latest block number with retry functionality
185	#[instrument(skip(self))]
186	async fn get_latest_block_number(&self) -> Result<u64, anyhow::Error> {
187		let response = self
188			.http_client
189			.send_raw_request::<serde_json::Value>("eth_blockNumber", None)
190			.await
191			.with_context(|| "Failed to get latest block number")?;
192
193		// Extract the "result" field from the JSON-RPC response
194		let hex_str = response
195			.get("result")
196			.and_then(|v| v.as_str())
197			.ok_or_else(|| anyhow::anyhow!("Missing 'result' field"))?;
198
199		// Parse hex string to u64
200		u64::from_str_radix(hex_str.trim_start_matches("0x"), 16)
201			.map_err(|e| anyhow::anyhow!("Failed to parse block number: {}", e))
202	}
203
204	/// Retrieves blocks within the specified range with retry functionality
205	///
206	/// # Note
207	/// If end_block is None, only the start_block will be retrieved
208	#[instrument(skip(self), fields(start_block, end_block))]
209	async fn get_blocks(
210		&self,
211		start_block: u64,
212		end_block: Option<u64>,
213	) -> Result<Vec<BlockType>, anyhow::Error> {
214		let block_futures: Vec<_> = (start_block..=end_block.unwrap_or(start_block))
215			.map(|block_number| {
216				let params = json!([
217					format!("0x{:x}", block_number),
218					true // include full transaction objects
219				]);
220				let client = self.http_client.clone();
221
222				async move {
223					let response = client
224						.send_raw_request("eth_getBlockByNumber", Some(params))
225						.await
226						.with_context(|| format!("Failed to get block: {}", block_number))?;
227
228					let block_data = response
229						.get("result")
230						.ok_or_else(|| anyhow::anyhow!("Missing 'result' field"))?;
231
232					if block_data.is_null() {
233						return Err(anyhow::anyhow!("Block not found"));
234					}
235
236					let block: EVMBlock = serde_json::from_value(block_data.clone())
237						.map_err(|e| anyhow::anyhow!("Failed to parse block: {}", e))?;
238
239					Ok(BlockType::EVM(Box::new(block)))
240				}
241			})
242			.collect();
243
244		futures::future::join_all(block_futures)
245			.await
246			.into_iter()
247			.collect::<Result<Vec<_>, _>>()
248	}
249}