openzeppelin_monitor/services/blockchain/
pool.rs

1//! Client pool for managing blockchain clients.
2//!
3//! This module provides a thread-safe client pooling system that:
4//! - Caches blockchain clients by network
5//! - Creates clients lazily on first use
6//! - Handles EVM, Stellar, and Midnight clients
7//! - Provides type-safe access to clients
8//! - Manages client lifecycles automatically
9//!
10//! The pool uses a fast path for existing clients and a slow path for
11//! creating new ones, optimizing performance while maintaining safety.
12
13use crate::utils::client_storage::ClientStorage;
14use crate::{
15	models::{BlockChainType, Network},
16	services::blockchain::{
17		BlockChainClient, BlockFilterFactory, EVMTransportClient, EvmClient, EvmClientTrait,
18		MidnightClient, MidnightClientTrait, MidnightWsTransportClient, StellarClient,
19		StellarClientTrait, StellarTransportClient,
20	},
21};
22use anyhow::Context;
23use async_trait::async_trait;
24use futures::future::BoxFuture;
25use std::{any::Any, collections::HashMap, sync::Arc};
26
27/// Trait for the client pool.
28#[async_trait]
29pub trait ClientPoolTrait: Send + Sync {
30	type EvmClient: EvmClientTrait + BlockChainClient + BlockFilterFactory<Self::EvmClient>;
31	type StellarClient: StellarClientTrait
32		+ BlockChainClient
33		+ BlockFilterFactory<Self::StellarClient>;
34	type MidnightClient: MidnightClientTrait
35		+ BlockChainClient
36		+ BlockFilterFactory<Self::MidnightClient>;
37
38	async fn get_evm_client(
39		&self,
40		network: &Network,
41	) -> Result<Arc<Self::EvmClient>, anyhow::Error>;
42	async fn get_stellar_client(
43		&self,
44		network: &Network,
45	) -> Result<Arc<Self::StellarClient>, anyhow::Error>;
46	async fn get_midnight_client(
47		&self,
48		network: &Network,
49	) -> Result<Arc<Self::MidnightClient>, anyhow::Error>;
50}
51
52/// Main client pool manager that handles multiple blockchain types.
53///
54/// Provides type-safe access to cached blockchain clients. Clients are created
55/// on demand when first requested and then cached for future use. Uses RwLock
56/// for thread-safe access and Arc for shared ownership.
57pub struct ClientPool {
58	/// Map of client storages indexed by client type
59	pub storages: HashMap<BlockChainType, Box<dyn Any + Send + Sync>>,
60}
61
62impl ClientPool {
63	/// Creates a new empty client pool.
64	///
65	/// Initializes empty hashmaps for all supported clients types.
66	pub fn new() -> Self {
67		let mut pool = Self {
68			storages: HashMap::new(),
69		};
70
71		// Register client types
72		pool.register_client_type::<EvmClient<EVMTransportClient>>(BlockChainType::EVM);
73		pool.register_client_type::<StellarClient<StellarTransportClient>>(BlockChainType::Stellar);
74		pool.register_client_type::<MidnightClient<MidnightWsTransportClient>>(
75			BlockChainType::Midnight,
76		);
77
78		pool
79	}
80
81	fn register_client_type<T: 'static + Send + Sync>(&mut self, client_type: BlockChainType) {
82		self.storages
83			.insert(client_type, Box::new(ClientStorage::<T>::new()));
84	}
85
86	/// Internal helper method to get or create a client of any type.
87	///
88	/// Uses a double-checked locking pattern:
89	/// 1. Fast path with read lock to check for existing client
90	/// 2. Slow path with write lock to create new client if needed
91	///
92	/// This ensures thread-safety while maintaining good performance
93	/// for the common case of accessing existing clients.
94	async fn get_or_create_client<T: BlockChainClient + 'static>(
95		&self,
96		client_type: BlockChainType,
97		network: &Network,
98		create_fn: impl Fn(&Network) -> BoxFuture<'static, Result<T, anyhow::Error>>,
99	) -> Result<Arc<T>, anyhow::Error> {
100		let storage = self
101			.storages
102			.get(&client_type)
103			.and_then(|s| s.downcast_ref::<ClientStorage<T>>())
104			.with_context(|| "Invalid client type")?;
105
106		// Fast path: check if client exists
107		if let Some(client) = storage.clients.read().await.get(&network.slug) {
108			return Ok(client.clone());
109		}
110
111		// Slow path: create new client
112		let mut clients = storage.clients.write().await;
113		let client = Arc::new(create_fn(network).await?);
114		clients.insert(network.slug.clone(), client.clone());
115		Ok(client)
116	}
117
118	/// Get the number of clients for a given client type.
119	pub async fn get_client_count<T: 'static>(&self, client_type: BlockChainType) -> usize {
120		match self
121			.storages
122			.get(&client_type)
123			.and_then(|s| s.downcast_ref::<ClientStorage<T>>())
124		{
125			Some(storage) => storage.clients.read().await.len(),
126			None => 0,
127		}
128	}
129}
130
131#[async_trait]
132impl ClientPoolTrait for ClientPool {
133	type EvmClient = EvmClient<EVMTransportClient>;
134	type StellarClient = StellarClient<StellarTransportClient>;
135	type MidnightClient = MidnightClient<MidnightWsTransportClient>;
136
137	/// Gets or creates an EVM client for the given network.
138	///
139	/// First checks the cache for an existing client. If none exists,
140	/// creates a new client under a write lock.
141	async fn get_evm_client(
142		&self,
143		network: &Network,
144	) -> Result<Arc<Self::EvmClient>, anyhow::Error> {
145		self.get_or_create_client(BlockChainType::EVM, network, |n| {
146			let network = n.clone();
147			Box::pin(async move { Self::EvmClient::new(&network).await })
148		})
149		.await
150		.with_context(|| "Failed to get or create EVM client")
151	}
152
153	/// Gets or creates a Stellar client for the given network.
154	///
155	/// First checks the cache for an existing client. If none exists,
156	/// creates a new client under a write lock.
157	async fn get_stellar_client(
158		&self,
159		network: &Network,
160	) -> Result<Arc<Self::StellarClient>, anyhow::Error> {
161		self.get_or_create_client(BlockChainType::Stellar, network, |n| {
162			let network = n.clone();
163			Box::pin(async move { Self::StellarClient::new(&network).await })
164		})
165		.await
166		.with_context(|| "Failed to get or create Stellar client")
167	}
168
169	/// Gets or creates a Midnight client for the given network.
170	///
171	/// First checks the cache for an existing client. If none exists,
172	/// creates a new client under a write lock.
173	async fn get_midnight_client(
174		&self,
175		network: &Network,
176	) -> Result<Arc<Self::MidnightClient>, anyhow::Error> {
177		self.get_or_create_client(BlockChainType::Midnight, network, |n| {
178			let network = n.clone();
179			Box::pin(async move { Self::MidnightClient::new(&network).await })
180		})
181		.await
182		.with_context(|| "Failed to get or create Midnight client")
183	}
184}
185
186impl Default for ClientPool {
187	fn default() -> Self {
188		Self::new()
189	}
190}