1use crate::{
2 neo_clients::{
3 APITrait, Cache, CacheConfig, CircuitBreaker, CircuitBreakerConfig, ConnectionPool,
4 HttpProvider, PoolConfig, RpcCache, RpcClient,
5 },
6 neo_error::{Neo3Error, Neo3Result},
7};
8use serde_json::Value;
9use std::{sync::Arc, time::Duration};
10use tokio::sync::RwLock;
11
12pub struct ProductionRpcClient {
14 pool: ConnectionPool,
15 cache: RpcCache,
16 circuit_breaker: CircuitBreaker,
17 config: ProductionClientConfig,
18 stats: Arc<RwLock<ProductionClientStats>>,
19}
20
21#[derive(Debug, Clone)]
23pub struct ProductionClientConfig {
24 pub pool_config: PoolConfig,
26 pub cache_config: CacheConfig,
28 pub circuit_breaker_config: CircuitBreakerConfig,
30 pub enable_logging: bool,
32 pub enable_metrics: bool,
34}
35
36impl Default for ProductionClientConfig {
37 fn default() -> Self {
38 Self {
39 pool_config: PoolConfig {
40 max_connections: 20,
41 min_idle: 5,
42 max_idle_time: Duration::from_secs(300),
43 connection_timeout: Duration::from_secs(30),
44 request_timeout: Duration::from_secs(60),
45 max_retries: 3,
46 retry_delay: Duration::from_millis(1000),
47 },
48 cache_config: CacheConfig {
49 max_entries: 10000,
50 default_ttl: Duration::from_secs(30),
51 cleanup_interval: Duration::from_secs(60),
52 enable_lru: true,
53 },
54 circuit_breaker_config: CircuitBreakerConfig {
55 failure_threshold: 5,
56 timeout: Duration::from_secs(60),
57 success_threshold: 3,
58 failure_window: Duration::from_secs(60),
59 half_open_max_requests: 3,
60 },
61 enable_logging: true,
62 enable_metrics: true,
63 }
64 }
65}
66
67#[derive(Debug, Default)]
69pub struct ProductionClientStats {
70 pub total_requests: u64,
71 pub cache_hits: u64,
72 pub cache_misses: u64,
73 pub circuit_breaker_rejections: u64,
74 pub successful_requests: u64,
75 pub failed_requests: u64,
76 pub average_response_time_ms: f64,
77}
78
79impl ProductionRpcClient {
80 pub fn new(endpoint: String, config: ProductionClientConfig) -> Self {
82 let pool = ConnectionPool::new(endpoint, config.pool_config.clone());
83 let cache = Cache::new(config.cache_config.clone());
84 let circuit_breaker = CircuitBreaker::new(config.circuit_breaker_config.clone());
85
86 Self {
87 pool,
88 cache,
89 circuit_breaker,
90 config,
91 stats: Arc::new(RwLock::new(ProductionClientStats::default())),
92 }
93 }
94
95 pub async fn call(&self, method: &str, params: Vec<Value>) -> Neo3Result<Value> {
97 let start_time = std::time::Instant::now();
98
99 {
101 let mut stats = self.stats.write().await;
102 stats.total_requests += 1;
103 }
104
105 let cache_key = self.create_cache_key(method, ¶ms);
107
108 if self.is_cacheable_method(method) {
110 if let Some(cached_result) = self.cache.get(&cache_key).await {
111 let mut stats = self.stats.write().await;
112 stats.cache_hits += 1;
113 return Ok(cached_result);
114 } else {
115 let mut stats = self.stats.write().await;
116 stats.cache_misses += 1;
117 }
118 }
119
120 let params_clone = params.clone();
122 let method_clone = method.to_string();
123
124 let result: Neo3Result<Value> = self
126 .circuit_breaker
127 .call(async move {
128 self.pool
129 .execute(move |client| {
130 let params_inner = params_clone.clone();
131 let method_inner = method_clone.clone();
132 Box::pin(async move {
133 client.request(&method_inner, params_inner).await.map_err(|e| {
134 Neo3Error::Network(crate::neo_error::NetworkError::RpcError {
135 code: -1,
136 message: e.to_string(),
137 })
138 })
139 })
140 })
141 .await
142 })
143 .await;
144
145 let elapsed = start_time.elapsed();
147 let mut stats = self.stats.write().await;
148
149 match &result {
150 Ok(value) => {
151 stats.successful_requests += 1;
152
153 if self.is_cacheable_method(method) {
155 let ttl = self.get_cache_ttl(method);
156 drop(stats);
157 self.cache.insert_with_ttl(cache_key, value.clone(), ttl).await;
158 stats = self.stats.write().await;
159 }
160 },
161 Err(_) => {
162 stats.failed_requests += 1;
163 },
164 }
165
166 let total_requests = stats.successful_requests + stats.failed_requests;
168 if total_requests > 0 {
169 stats.average_response_time_ms = (stats.average_response_time_ms
170 * (total_requests - 1) as f64
171 + elapsed.as_millis() as f64)
172 / total_requests as f64;
173 }
174
175 if self.config.enable_logging {
176 match &result {
177 Ok(_) => {
178 tracing::info!(
179 method = method,
180 duration_ms = elapsed.as_millis(),
181 "RPC call successful"
182 );
183 },
184 Err(e) => {
185 tracing::error!(
186 method = method,
187 duration_ms = elapsed.as_millis(),
188 error = %e,
189 "RPC call failed"
190 );
191 },
192 }
193 }
194
195 result
196 }
197
198 pub async fn get_block_count(&self) -> Neo3Result<u64> {
200 let result = self.call("getblockcount", vec![]).await?;
201 result.as_u64().ok_or_else(|| {
202 Neo3Error::Serialization(crate::neo_error::SerializationError::InvalidFormat(
203 "Invalid block count format".to_string(),
204 ))
205 })
206 }
207
208 pub async fn get_block(&self, identifier: Value) -> Neo3Result<Value> {
210 let result = self.call("getblock", vec![identifier.clone(), Value::Bool(true)]).await?;
211
212 let cache_key = format!("block:{}", identifier);
214 self.cache
215 .insert_with_ttl(cache_key, result.clone(), Duration::from_secs(3600))
216 .await;
217
218 Ok(result)
219 }
220
221 pub async fn get_transaction(&self, tx_hash: String) -> Neo3Result<Value> {
223 let result = self
224 .call("getrawtransaction", vec![Value::String(tx_hash.clone()), Value::Bool(true)])
225 .await?;
226
227 let cache_key = format!("tx:{}", tx_hash);
229 self.cache
230 .insert_with_ttl(cache_key, result.clone(), Duration::from_secs(3600))
231 .await;
232
233 Ok(result)
234 }
235
236 pub async fn get_contract_state(&self, contract_hash: String) -> Neo3Result<Value> {
238 let result = self
239 .call("getcontractstate", vec![Value::String(contract_hash.clone())])
240 .await?;
241
242 let cache_key = format!("contract:{}", contract_hash);
244 self.cache
245 .insert_with_ttl(cache_key, result.clone(), Duration::from_secs(60))
246 .await;
247
248 Ok(result)
249 }
250
251 pub async fn get_nep17_balances(&self, address: String) -> Neo3Result<Value> {
253 let result = self.call("getnep17balances", vec![Value::String(address.clone())]).await?;
254
255 let cache_key = format!("balance:{}", address);
257 self.cache
258 .insert_with_ttl(cache_key, result.clone(), Duration::from_secs(10))
259 .await;
260
261 Ok(result)
262 }
263
264 pub async fn send_raw_transaction(&self, transaction_hex: String) -> Neo3Result<Value> {
266 self.call("sendrawtransaction", vec![Value::String(transaction_hex)]).await
267 }
268
269 pub async fn get_stats(&self) -> ProductionClientStats {
271 let stats = self.stats.read().await;
272 ProductionClientStats {
273 total_requests: stats.total_requests,
274 cache_hits: stats.cache_hits,
275 cache_misses: stats.cache_misses,
276 circuit_breaker_rejections: stats.circuit_breaker_rejections,
277 successful_requests: stats.successful_requests,
278 failed_requests: stats.failed_requests,
279 average_response_time_ms: stats.average_response_time_ms,
280 }
281 }
282
283 pub async fn get_health(&self) -> serde_json::Value {
285 let stats = self.get_stats().await;
286 let pool_stats = self.pool.get_stats().await;
287 let cache_stats = self.cache.stats().await;
288 let cb_stats = self.circuit_breaker.get_stats().await;
289
290 serde_json::json!({
291 "status": if cb_stats.current_state == crate::neo_clients::CircuitState::Open { "unhealthy" } else { "healthy" },
292 "timestamp": chrono::Utc::now().to_rfc3339(),
293 "stats": {
294 "total_requests": stats.total_requests,
295 "success_rate": if stats.total_requests > 0 {
296 stats.successful_requests as f64 / stats.total_requests as f64
297 } else { 0.0 },
298 "cache_hit_rate": cache_stats.hit_rate(),
299 "average_response_time_ms": stats.average_response_time_ms,
300 "circuit_breaker_state": format!("{:?}", cb_stats.current_state),
301 "pool": {
302 "active_connections": pool_stats.current_active_connections,
303 "idle_connections": pool_stats.current_idle_connections,
304 "total_created": pool_stats.total_connections_created
305 }
306 }
307 })
308 }
309
310 pub async fn health_check(&self) -> Neo3Result<bool> {
312 match self.call("getversion", vec![]).await {
313 Ok(_) => Ok(true),
314 Err(_) => Ok(false),
315 }
316 }
317
318 fn create_cache_key(&self, method: &str, params: &[Value]) -> String {
320 let params_str = serde_json::to_string(params).unwrap_or_default();
321 format!("{}:{}", method, params_str)
322 }
323
324 fn is_cacheable_method(&self, method: &str) -> bool {
326 matches!(
327 method,
328 "getblock"
329 | "getrawtransaction"
330 | "getcontractstate"
331 | "getnep17balances"
332 | "getblockcount"
333 | "getversion"
334 | "getpeers" | "getconnectioncount"
335 )
336 }
337
338 fn get_cache_ttl(&self, method: &str) -> Duration {
340 match method {
341 "getblock" | "getrawtransaction" => Duration::from_secs(3600), "getcontractstate" => Duration::from_secs(60), "getnep17balances" => Duration::from_secs(10), "getblockcount" => Duration::from_secs(5), _ => self.config.cache_config.default_ttl,
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[tokio::test]
355 async fn test_production_client_creation() {
356 let config = ProductionClientConfig::default();
357 let client = ProductionRpcClient::new("https://testnet.neo.org:443".to_string(), config);
358
359 let stats = client.get_stats().await;
360 assert_eq!(stats.total_requests, 0);
361 }
362
363 #[tokio::test]
364 async fn test_cache_key_generation() {
365 let config = ProductionClientConfig::default();
366 let client = ProductionRpcClient::new("https://testnet.neo.org:443".to_string(), config);
367
368 let key1 = client.create_cache_key("getblock", &[Value::String("hash1".to_string())]);
369 let key2 = client.create_cache_key("getblock", &[Value::String("hash2".to_string())]);
370
371 assert_ne!(key1, key2);
372 assert!(key1.contains("getblock"));
373 }
374
375 #[tokio::test]
376 async fn test_cacheable_methods() {
377 let config = ProductionClientConfig::default();
378 let client = ProductionRpcClient::new("https://testnet.neo.org:443".to_string(), config);
379
380 assert!(client.is_cacheable_method("getblock"));
381 assert!(client.is_cacheable_method("getrawtransaction"));
382 assert!(!client.is_cacheable_method("sendrawtransaction"));
383 }
384}