Riy777 commited on
Commit
7f7d2d2
·
verified ·
1 Parent(s): 0929789

Update whale_monitor/rpc_manager.py

Browse files
Files changed (1) hide show
  1. whale_monitor/rpc_manager.py +44 -106
whale_monitor/rpc_manager.py CHANGED
@@ -1,5 +1,5 @@
1
  # whale_monitor/rpc_manager.py
2
- # (V3.2 - GEM-Architect: Full Enterprise Edition)
3
  # يتضمن: Web3 Engine + Raw RPC Fallback + CSV Injection + Strict API Handling
4
 
5
  import asyncio
@@ -13,16 +13,22 @@ import traceback
13
  from collections import defaultdict, deque
14
  from typing import Dict, Any, Optional, List, Union
15
 
16
- # Web3 Imports
17
  from web3 import AsyncWeb3
18
  from web3.providers import AsyncHTTPProvider
19
- from web3.middleware import async_geth_poa_middleware
 
 
 
 
 
 
 
20
 
21
  # Local Imports
22
  from .config import DEFAULT_NETWORK_CONFIGS, COINGECKO_BASE_URL
23
 
24
  # --- Constants & Limits ---
25
- # التأكد من عدم الحظر من قبل API العامة
26
  RPC_HEALTH_CHECK_WINDOW = 15
27
  RPC_ERROR_THRESHOLD = 5
28
  RPC_CIRCUIT_BREAKER_DURATION = 300 # 5 Minutes
@@ -45,7 +51,7 @@ class AdaptiveRpcManager:
45
  """
46
 
47
  def __init__(self, http_client: httpx.AsyncClient):
48
- print("🔄 [RPCManager V3.2] Initializing Enterprise Connection Engine...")
49
  self.http_client = http_client
50
 
51
  # 1. Load Secure Keys
@@ -86,7 +92,7 @@ class AdaptiveRpcManager:
86
  # 6. Web3 Engine Cache
87
  self.web3_instances = {}
88
 
89
- print(f"✅ [RPCManager V3.2] System Ready. Tracking {len(self.network_configs)} networks.")
90
 
91
  # ==============================================================================
92
  # 🛠️ Initialization & Config Loading (Full Logic)
@@ -104,7 +110,6 @@ class AdaptiveRpcManager:
104
  reader = csv.DictReader(f)
105
  count = 0
106
  for row in reader:
107
- # التنظيف والتحقق
108
  net = row.get('network', '').strip().lower()
109
  url = row.get('url', '').strip()
110
 
@@ -127,7 +132,6 @@ class AdaptiveRpcManager:
127
  final_configs = {}
128
 
129
  for network, config in configs.items():
130
- # Deep Copy لتجنب تعديل الافتراضيات
131
  new_config = config.copy()
132
  current_endpoints = new_config.get('rpc_endpoints', [])
133
 
@@ -137,17 +141,16 @@ class AdaptiveRpcManager:
137
  if "<INFURA_KEY>" in ep:
138
  if infura_key:
139
  processed_endpoints.append(ep.replace("<INFURA_KEY>", infura_key))
140
- # else: skip infura endpoint if no key
141
  else:
142
  processed_endpoints.append(ep)
143
 
144
- # 2. إضافة نقاط CSV (في المقدمة للأولوية)
145
  if network in custom_rpcs:
146
  csv_points = custom_rpcs[network]
147
  print(f" ➕ [Config] Injecting {len(csv_points)} custom RPCs for {network}")
148
  processed_endpoints = csv_points + processed_endpoints
149
 
150
- # 3. إزالة التكرار والحفاظ على الترتيب
151
  unique_endpoints = []
152
  seen = set()
153
  for ep in processed_endpoints:
@@ -155,8 +158,7 @@ class AdaptiveRpcManager:
155
  unique_endpoints.append(ep)
156
  seen.add(ep)
157
 
158
- # 4. خلط عشوائي جزئي (للنقاط العامة فقط) لضمان توزيع الحمل
159
- # ملاحظة: نحافظ على أول نقطة إذا كانت من CSV غالباً
160
  if len(unique_endpoints) > 1:
161
  first = unique_endpoints[0]
162
  rest = unique_endpoints[1:]
@@ -165,7 +167,6 @@ class AdaptiveRpcManager:
165
 
166
  new_config['rpc_endpoints'] = unique_endpoints
167
 
168
- # 5. حقن مفاتيح المستكشفات (Explorers)
169
  if config.get('explorer'):
170
  key_name = config['explorer'].get('api_key_name')
171
  if key_name and key_name in self.api_keys:
@@ -176,21 +177,18 @@ class AdaptiveRpcManager:
176
  return final_configs
177
 
178
  # ==============================================================================
179
- # ⚡ Web3 Engine (High Performance)
180
  # ==============================================================================
181
 
182
  def get_web3(self, network: str) -> Optional[AsyncWeb3]:
183
  """
184
- إرجاع مثيل Web3 حيوي للشبكة.
185
- يستخدم Cache، وإذا فشل يقوم بتدوير النقاط.
186
  """
187
  config = self.network_configs.get(network)
188
  if not config or config.get('type') != 'evm':
189
  return None
190
 
191
- # التحقق من الكاش
192
  if network in self.web3_instances:
193
- # هنا يمكن إضافة فحص صحة الاتصال (isConnected) إذا لزم الأمر
194
  return self.web3_instances[network]
195
 
196
  endpoints = config['rpc_endpoints']
@@ -198,17 +196,21 @@ class AdaptiveRpcManager:
198
  print(f"❌ [Web3] No endpoints available for {network}")
199
  return None
200
 
201
- # محاولة الاتصال (نختار أفضل نقطة متاحة)
202
- # في بيئة الإنتاج، نفضل النقطة الأولى (التي قد تكون Private Node من CSV)
203
  endpoint = endpoints[0]
204
 
205
  try:
206
  provider = AsyncHTTPProvider(endpoint, request_kwargs={'timeout': 20.0})
207
  w3 = AsyncWeb3(provider)
208
 
209
- # إضافة Middleware للشبكات التي تحتاج POA (مثل BSC, Polygon)
 
210
  if network in ['bsc', 'polygon', 'avalanche', 'fantom']:
211
- w3.middleware_onion.inject(async_geth_poa_middleware, layer=0)
 
 
 
 
 
212
 
213
  self.web3_instances[network] = w3
214
  print(f" 🔌 [Web3] Connected to {network} via {endpoint.split('//')[-1][:20]}...")
@@ -222,11 +224,6 @@ class AdaptiveRpcManager:
222
  # ==============================================================================
223
 
224
  async def post_rpc(self, network: str, payload: dict, timeout: float = 20.0) -> Optional[Dict]:
225
- """
226
- إرسال طلب JSON-RPC خام عبر HTTP.
227
- ضروري للوظائف التي لا تغطيها Web3 أو عند فشل Web3.
228
- يدعم نظام Circuit Breaker والصحة.
229
- """
230
  config = self.network_configs.get(network)
231
  if not config: return None
232
 
@@ -234,14 +231,10 @@ class AdaptiveRpcManager:
234
  valid_endpoints = self._get_healthy_endpoints(network, endpoints)
235
 
236
  if not valid_endpoints:
237
- print(f"❌ [Raw RPC] No healthy endpoints for {network}")
238
  return None
239
 
240
- # المحاولة مع أفضل 3 نقاط
241
  for endpoint in valid_endpoints[:3]:
242
- ep_name = endpoint.split('//')[-1]
243
-
244
- # تحديد Semaphore المناسب
245
  semaphore = self.semaphores['infura'] if 'infura.io' in endpoint else self.semaphores['public_rpc']
246
 
247
  async with semaphore:
@@ -251,42 +244,30 @@ class AdaptiveRpcManager:
251
  response.raise_for_status()
252
  data = response.json()
253
 
254
- # تحديث الصحة
255
- latency = time.time() - start_time
256
- self._update_health(network, endpoint, True, latency)
257
  self.session_stats['rpc_raw_success'] += 1
258
-
259
  return data
260
 
261
- except Exception as e:
262
  self._update_health(network, endpoint, False, 0.0)
263
  self.session_stats['rpc_raw_fail'] += 1
264
- # print(f"⚠️ [Raw RPC] Fail {ep_name}: {str(e)}") # سجل صامت لتفادي الإزعاج
265
  continue
266
 
267
  return None
268
 
269
  def _get_healthy_endpoints(self, network: str, endpoints: List[str]) -> List[str]:
270
- """تصفية وترتيب النقاط بناءً على الصحة والأخطاء السابقة"""
271
  now = time.time()
272
  healthy = []
273
-
274
  for ep in endpoints:
275
  stats = self.endpoint_health[network][ep]
276
-
277
- # التحقق من قاطع الدائرة (Circuit Breaker)
278
  if stats['circuit_open']:
279
  if now - stats['last_error'] > RPC_CIRCUIT_BREAKER_DURATION:
280
  stats['circuit_open'] = False
281
  stats['errors'] = 0
282
  else:
283
- continue # تجاوز هذه النقطة
284
-
285
- # حساب متوسط التأخير (Latency Score)
286
  avg_lat = sum(stats['latency']) / len(stats['latency']) if stats['latency'] else 1.0
287
  healthy.append((ep, avg_lat))
288
-
289
- # الترتيب حسب الأسرع
290
  healthy.sort(key=lambda x: x[1])
291
  return [h[0] for h in healthy]
292
 
@@ -308,18 +289,13 @@ class AdaptiveRpcManager:
308
  # ==============================================================================
309
 
310
  async def get_coingecko_api(self, path: str, params: dict, retries: int = 1) -> Optional[Dict]:
311
- """
312
- التعامل الدقيق مع CoinGecko لتفادي 404 و 429.
313
- """
314
  full_url = f"{COINGECKO_BASE_URL}{path}"
315
 
316
  async with self.semaphores['coingecko']:
317
- # Throttling
318
  now = time.time()
319
  diff = now - self.last_calls['coingecko']
320
  if diff < COINGECKO_REQUEST_DELAY:
321
- wait_time = COINGECKO_REQUEST_DELAY - diff
322
- await asyncio.sleep(wait_time)
323
 
324
  for attempt in range(retries + 1):
325
  try:
@@ -330,122 +306,84 @@ class AdaptiveRpcManager:
330
  if response.status_code == 200:
331
  self.session_stats['coingecko_success'] += 1
332
  return response.json()
333
-
334
- elif response.status_code == 429: # Rate Limit
335
- wait = 10.0 * (attempt + 1)
336
- print(f"⚠️ [CoinGecko] Rate Limit (429). Waiting {wait}s...")
337
- await asyncio.sleep(wait)
338
  continue
339
-
340
  elif response.status_code == 404:
341
- # 404 تعني العملة غير موجودة أو المسار خطأ
342
- # لا نعتبرها خطأ اتصال، بل استجابة فارغة
343
  return None
344
-
345
  else:
346
  response.raise_for_status()
347
-
348
- except Exception as e:
349
  self.session_stats['coingecko_fail'] += 1
350
- print(f"❌ [CoinGecko] Error: {e}")
351
  return None
352
  return None
353
 
354
  async def get_solscan_api(self, path: str, params: dict) -> Optional[Dict]:
355
- """جلب بيانات Solscan مع التحقق من المفتاح"""
356
  key = self.api_keys.get('solscan')
357
- if not key:
358
- # print("⚠️ [Solscan] Key missing. Request skipped.")
359
- return None
360
-
361
  base_url = "https://pro-api.solscan.io"
362
  full_url = f"{base_url}{path}"
363
  headers = {"token": key, "accept": "application/json"}
364
 
365
  async with self.semaphores['solscan']:
366
- # Solscan Throttling
367
  now = time.time()
368
  if now - self.last_calls['solscan'] < SOLSCAN_RATE_LIMIT:
369
  await asyncio.sleep(SOLSCAN_RATE_LIMIT)
370
-
371
  try:
372
  self.session_stats['solscan_total'] += 1
373
  response = await self.http_client.get(full_url, params=params, headers=headers)
374
  self.last_calls['solscan'] = time.time()
375
-
376
  if response.status_code == 200:
377
  self.session_stats['solscan_success'] += 1
378
  return response.json()
379
- else:
380
- print(f"⚠️ [Solscan] Error {response.status_code}: {response.text}")
381
- return None
382
- except Exception as e:
383
  self.session_stats['solscan_fail'] += 1
384
- print(f"❌ [Solscan] Exception: {e}")
385
  return None
386
 
387
  async def get_moralis_api(self, params: dict) -> Optional[Dict]:
388
- """جلب بيانات Moralis"""
389
  key = self.api_keys.get('moralis')
390
  if not key: return None
391
-
392
  url = "https://deep-index.moralis.io/api/v2.2/erc20/transfers"
393
  headers = {"X-API-Key": key, "accept": "application/json"}
394
-
395
  async with self.semaphores['moralis']:
396
  now = time.time()
397
  if now - self.last_calls['moralis'] < MORALIS_RATE_LIMIT:
398
  await asyncio.sleep(MORALIS_RATE_LIMIT)
399
-
400
  try:
401
  self.session_stats['moralis_total'] += 1
402
  response = await self.http_client.get(url, params=params, headers=headers)
403
  self.last_calls['moralis'] = time.time()
404
-
405
  if response.status_code == 200:
406
  self.session_stats['moralis_success'] += 1
407
  return response.json()
408
  return None
409
- except Exception as e:
410
  self.session_stats['moralis_fail'] += 1
411
  return None
412
 
413
  async def get_scanner_api(self, base_url: str, params: dict) -> Optional[Dict]:
414
- """Etherscan/BscScan compatible API calls"""
415
  async with self.semaphores['scanners']:
416
  try:
417
  self.session_stats['scanner_total'] += 1
418
  response = await self.http_client.get(base_url, params=params, timeout=15.0)
419
-
420
  if response.status_code == 200:
421
  self.session_stats['scanner_success'] += 1
422
  return response.json()
423
  return None
424
- except Exception as e:
425
  self.session_stats['scanner_fail'] += 1
426
  return None
427
 
428
  # ==============================================================================
429
  # 📊 Utilities
430
  # ==============================================================================
431
-
432
- def reset_session_stats(self):
433
- self.session_stats = defaultdict(int)
434
-
435
- def get_session_stats(self):
436
- return self.session_stats.copy()
437
-
438
- def get_api_key(self, name: str) -> Optional[str]:
439
- return self.api_keys.get(name)
440
-
441
- def get_explorer_config(self, network: str) -> Optional[Dict]:
442
- return self.network_configs.get(network, {}).get('explorer')
443
-
444
- def get_network_configs(self) -> Dict:
445
- return self.network_configs
446
-
447
  async def close(self):
448
- """Cleanup resources if needed"""
449
- # Usually http_client is managed externally, but we can clear caches
450
  self.web3_instances.clear()
451
  print("🛑 [RPCManager] Shutting down connection engine.")
 
1
  # whale_monitor/rpc_manager.py
2
+ # (V3.3 - GEM-Architect: Full Enterprise Edition + V7 Crash Fix)
3
  # يتضمن: Web3 Engine + Raw RPC Fallback + CSV Injection + Strict API Handling
4
 
5
  import asyncio
 
13
  from collections import defaultdict, deque
14
  from typing import Dict, Any, Optional, List, Union
15
 
16
+ # Web3 Imports with Crash-Safety Logic
17
  from web3 import AsyncWeb3
18
  from web3.providers import AsyncHTTPProvider
19
+
20
+ # [CRITICAL FIX] معالجة التغييرات في إصدارات Web3 المختلفة
21
+ try:
22
+ from web3.middleware import async_geth_poa_middleware
23
+ except ImportError:
24
+ # في حالة web3 v7+ أو إصدارات مختلفة، نحدد المتغير كـ None لتجنب الكسر
25
+ async_geth_poa_middleware = None
26
+ print("⚠️ [RPCManager] Web3 Middleware 'async_geth_poa_middleware' not found (running v7?). PoA injection disabled.")
27
 
28
  # Local Imports
29
  from .config import DEFAULT_NETWORK_CONFIGS, COINGECKO_BASE_URL
30
 
31
  # --- Constants & Limits ---
 
32
  RPC_HEALTH_CHECK_WINDOW = 15
33
  RPC_ERROR_THRESHOLD = 5
34
  RPC_CIRCUIT_BREAKER_DURATION = 300 # 5 Minutes
 
51
  """
52
 
53
  def __init__(self, http_client: httpx.AsyncClient):
54
+ print("🔄 [RPCManager V3.3] Initializing Enterprise Connection Engine...")
55
  self.http_client = http_client
56
 
57
  # 1. Load Secure Keys
 
92
  # 6. Web3 Engine Cache
93
  self.web3_instances = {}
94
 
95
+ print(f"✅ [RPCManager V3.3] System Ready. Tracking {len(self.network_configs)} networks.")
96
 
97
  # ==============================================================================
98
  # 🛠️ Initialization & Config Loading (Full Logic)
 
110
  reader = csv.DictReader(f)
111
  count = 0
112
  for row in reader:
 
113
  net = row.get('network', '').strip().lower()
114
  url = row.get('url', '').strip()
115
 
 
132
  final_configs = {}
133
 
134
  for network, config in configs.items():
 
135
  new_config = config.copy()
136
  current_endpoints = new_config.get('rpc_endpoints', [])
137
 
 
141
  if "<INFURA_KEY>" in ep:
142
  if infura_key:
143
  processed_endpoints.append(ep.replace("<INFURA_KEY>", infura_key))
 
144
  else:
145
  processed_endpoints.append(ep)
146
 
147
+ # 2. إضافة نقاط CSV
148
  if network in custom_rpcs:
149
  csv_points = custom_rpcs[network]
150
  print(f" ➕ [Config] Injecting {len(csv_points)} custom RPCs for {network}")
151
  processed_endpoints = csv_points + processed_endpoints
152
 
153
+ # 3. إزالة التكرار
154
  unique_endpoints = []
155
  seen = set()
156
  for ep in processed_endpoints:
 
158
  unique_endpoints.append(ep)
159
  seen.add(ep)
160
 
161
+ # 4. خلط عشوائي (مع الحفاظ على الأولوية)
 
162
  if len(unique_endpoints) > 1:
163
  first = unique_endpoints[0]
164
  rest = unique_endpoints[1:]
 
167
 
168
  new_config['rpc_endpoints'] = unique_endpoints
169
 
 
170
  if config.get('explorer'):
171
  key_name = config['explorer'].get('api_key_name')
172
  if key_name and key_name in self.api_keys:
 
177
  return final_configs
178
 
179
  # ==============================================================================
180
+ # ⚡ Web3 Engine (High Performance & V7 Safe)
181
  # ==============================================================================
182
 
183
  def get_web3(self, network: str) -> Optional[AsyncWeb3]:
184
  """
185
+ إرجاع مثيل Web3 حيوي.
 
186
  """
187
  config = self.network_configs.get(network)
188
  if not config or config.get('type') != 'evm':
189
  return None
190
 
 
191
  if network in self.web3_instances:
 
192
  return self.web3_instances[network]
193
 
194
  endpoints = config['rpc_endpoints']
 
196
  print(f"❌ [Web3] No endpoints available for {network}")
197
  return None
198
 
 
 
199
  endpoint = endpoints[0]
200
 
201
  try:
202
  provider = AsyncHTTPProvider(endpoint, request_kwargs={'timeout': 20.0})
203
  w3 = AsyncWeb3(provider)
204
 
205
+ # [CRITICAL FIX] حقن Middleware فقط إذا كان مدعوماً ومتاحاً
206
+ # هذا يمنع الانهيار في web3 v7
207
  if network in ['bsc', 'polygon', 'avalanche', 'fantom']:
208
+ if async_geth_poa_middleware and hasattr(w3, 'middleware_onion'):
209
+ w3.middleware_onion.inject(async_geth_poa_middleware, layer=0)
210
+ else:
211
+ # في الإصدارات الجديدة، قد لا نحتاج للحقن أو يتم بطريقة أخرى
212
+ # نتخطى بصمت للحفاظ على عمل النظام
213
+ pass
214
 
215
  self.web3_instances[network] = w3
216
  print(f" 🔌 [Web3] Connected to {network} via {endpoint.split('//')[-1][:20]}...")
 
224
  # ==============================================================================
225
 
226
  async def post_rpc(self, network: str, payload: dict, timeout: float = 20.0) -> Optional[Dict]:
 
 
 
 
 
227
  config = self.network_configs.get(network)
228
  if not config: return None
229
 
 
231
  valid_endpoints = self._get_healthy_endpoints(network, endpoints)
232
 
233
  if not valid_endpoints:
234
+ # print(f"❌ [Raw RPC] No healthy endpoints for {network}")
235
  return None
236
 
 
237
  for endpoint in valid_endpoints[:3]:
 
 
 
238
  semaphore = self.semaphores['infura'] if 'infura.io' in endpoint else self.semaphores['public_rpc']
239
 
240
  async with semaphore:
 
244
  response.raise_for_status()
245
  data = response.json()
246
 
247
+ self._update_health(network, endpoint, True, time.time() - start_time)
 
 
248
  self.session_stats['rpc_raw_success'] += 1
 
249
  return data
250
 
251
+ except Exception:
252
  self._update_health(network, endpoint, False, 0.0)
253
  self.session_stats['rpc_raw_fail'] += 1
 
254
  continue
255
 
256
  return None
257
 
258
  def _get_healthy_endpoints(self, network: str, endpoints: List[str]) -> List[str]:
 
259
  now = time.time()
260
  healthy = []
 
261
  for ep in endpoints:
262
  stats = self.endpoint_health[network][ep]
 
 
263
  if stats['circuit_open']:
264
  if now - stats['last_error'] > RPC_CIRCUIT_BREAKER_DURATION:
265
  stats['circuit_open'] = False
266
  stats['errors'] = 0
267
  else:
268
+ continue
 
 
269
  avg_lat = sum(stats['latency']) / len(stats['latency']) if stats['latency'] else 1.0
270
  healthy.append((ep, avg_lat))
 
 
271
  healthy.sort(key=lambda x: x[1])
272
  return [h[0] for h in healthy]
273
 
 
289
  # ==============================================================================
290
 
291
  async def get_coingecko_api(self, path: str, params: dict, retries: int = 1) -> Optional[Dict]:
 
 
 
292
  full_url = f"{COINGECKO_BASE_URL}{path}"
293
 
294
  async with self.semaphores['coingecko']:
 
295
  now = time.time()
296
  diff = now - self.last_calls['coingecko']
297
  if diff < COINGECKO_REQUEST_DELAY:
298
+ await asyncio.sleep(COINGECKO_REQUEST_DELAY - diff)
 
299
 
300
  for attempt in range(retries + 1):
301
  try:
 
306
  if response.status_code == 200:
307
  self.session_stats['coingecko_success'] += 1
308
  return response.json()
309
+ elif response.status_code == 429:
310
+ await asyncio.sleep(10.0 * (attempt + 1))
 
 
 
311
  continue
 
312
  elif response.status_code == 404:
 
 
313
  return None
 
314
  else:
315
  response.raise_for_status()
316
+ except Exception:
 
317
  self.session_stats['coingecko_fail'] += 1
 
318
  return None
319
  return None
320
 
321
  async def get_solscan_api(self, path: str, params: dict) -> Optional[Dict]:
 
322
  key = self.api_keys.get('solscan')
323
+ if not key: return None
324
+
 
 
325
  base_url = "https://pro-api.solscan.io"
326
  full_url = f"{base_url}{path}"
327
  headers = {"token": key, "accept": "application/json"}
328
 
329
  async with self.semaphores['solscan']:
 
330
  now = time.time()
331
  if now - self.last_calls['solscan'] < SOLSCAN_RATE_LIMIT:
332
  await asyncio.sleep(SOLSCAN_RATE_LIMIT)
 
333
  try:
334
  self.session_stats['solscan_total'] += 1
335
  response = await self.http_client.get(full_url, params=params, headers=headers)
336
  self.last_calls['solscan'] = time.time()
 
337
  if response.status_code == 200:
338
  self.session_stats['solscan_success'] += 1
339
  return response.json()
340
+ return None
341
+ except Exception:
 
 
342
  self.session_stats['solscan_fail'] += 1
 
343
  return None
344
 
345
  async def get_moralis_api(self, params: dict) -> Optional[Dict]:
 
346
  key = self.api_keys.get('moralis')
347
  if not key: return None
 
348
  url = "https://deep-index.moralis.io/api/v2.2/erc20/transfers"
349
  headers = {"X-API-Key": key, "accept": "application/json"}
 
350
  async with self.semaphores['moralis']:
351
  now = time.time()
352
  if now - self.last_calls['moralis'] < MORALIS_RATE_LIMIT:
353
  await asyncio.sleep(MORALIS_RATE_LIMIT)
 
354
  try:
355
  self.session_stats['moralis_total'] += 1
356
  response = await self.http_client.get(url, params=params, headers=headers)
357
  self.last_calls['moralis'] = time.time()
 
358
  if response.status_code == 200:
359
  self.session_stats['moralis_success'] += 1
360
  return response.json()
361
  return None
362
+ except Exception:
363
  self.session_stats['moralis_fail'] += 1
364
  return None
365
 
366
  async def get_scanner_api(self, base_url: str, params: dict) -> Optional[Dict]:
 
367
  async with self.semaphores['scanners']:
368
  try:
369
  self.session_stats['scanner_total'] += 1
370
  response = await self.http_client.get(base_url, params=params, timeout=15.0)
 
371
  if response.status_code == 200:
372
  self.session_stats['scanner_success'] += 1
373
  return response.json()
374
  return None
375
+ except Exception:
376
  self.session_stats['scanner_fail'] += 1
377
  return None
378
 
379
  # ==============================================================================
380
  # 📊 Utilities
381
  # ==============================================================================
382
+ def reset_session_stats(self): self.session_stats = defaultdict(int)
383
+ def get_session_stats(self): return self.session_stats.copy()
384
+ def get_api_key(self, name: str): return self.api_keys.get(name)
385
+ def get_explorer_config(self, network: str): return self.network_configs.get(network, {}).get('explorer')
386
+ def get_network_configs(self): return self.network_configs
 
 
 
 
 
 
 
 
 
 
 
387
  async def close(self):
 
 
388
  self.web3_instances.clear()
389
  print("🛑 [RPCManager] Shutting down connection engine.")