Riy777 commited on
Commit
d10d6ba
·
1 Parent(s): 4155930

Update whale_monitor/rpc_manager.py

Browse files
Files changed (1) hide show
  1. whale_monitor/rpc_manager.py +231 -109
whale_monitor/rpc_manager.py CHANGED
@@ -1,5 +1,7 @@
1
  # whale_monitor/rpc_manager.py
2
- # هذا هو "الوكيل الذكي" لإدارة اتصالات RPC والمستكشفات
 
 
3
 
4
  import asyncio
5
  import httpx
@@ -7,8 +9,10 @@ import time
7
  import ssl
8
  import json
9
  import os
 
10
  from collections import deque, defaultdict
11
  import random
 
12
 
13
  # استيراد الإعدادات الثابتة
14
  from .config import DEFAULT_NETWORK_CONFIGS, COINGECKO_BASE_URL
@@ -21,51 +25,122 @@ RPC_CIRCUIT_BREAKER_DURATION = 300 # 5 دقائق إيقاف مؤقت
21
  # (إضافة ثابت لفرض تأخير بين طلبات CoinGecko)
22
  COINGECKO_REQUEST_DELAY = 2.0 # 2.0 ثانية (يساوي 30 طلب/دقيقة كحد أقصى)
23
 
 
 
 
 
 
24
  class AdaptiveRpcManager:
25
  """
26
- مدير RPC ذكي يتتبع صحة النقاط، ويدير حدود الطلبات،
27
- ويختار أفضل نقطة متاحة تلقائياً.
 
 
 
28
  """
29
- def __init__(self, http_client: httpx.AsyncClient, api_keys: dict):
30
  self.http_client = http_client
31
- self.api_keys = api_keys
32
 
33
- # منظمات الطلبات (Semaphores)
34
- self.rpc_semaphore = asyncio.Semaphore(5) # حد عام لـ RPC
35
- self.coingecko_semaphore = asyncio.Semaphore(1) # حد خاص لـ CoinGecko
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- # (متغير لتتبع وقت آخر طلب لـ CoinGecko)
 
 
 
 
 
38
  self.last_coingecko_call = 0.0
 
 
 
 
39
 
40
- # تهيئة إعدادات الشبكة ونقاط RPC
41
  self.network_configs = self._initialize_network_configs(DEFAULT_NETWORK_CONFIGS)
42
 
43
- # نظام تتبع الصحة (التعلم)
44
  self.endpoint_health = defaultdict(lambda: defaultdict(lambda: {
45
  "latency": deque(maxlen=RPC_HEALTH_CHECK_WINDOW),
46
- "consecutive_errors": 0,
47
- "total_errors": 0,
48
- "last_error_time": None,
49
  "circuit_open": False,
50
  }))
51
 
52
- print("✅ مدير RPC الذكي (الوكيل) مهيأ.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  def _initialize_network_configs(self, configs):
55
  """
56
- يقوم بحقن مفاتيح API في إعدادات الشبكة عند بدء التشغيل.
 
57
  """
58
  initialized_configs = {}
 
 
 
 
59
  for network, config in configs.items():
60
  new_config = config.copy()
61
 
62
- # حقن مفاتيح RPC
63
  new_config['rpc_endpoints'] = self._inject_api_keys(
64
  config['rpc_endpoints'],
65
  self.api_keys.get('infura')
66
  )
67
 
68
- # حقن مفاتيح المستكشف (Explorer)
 
 
 
 
 
 
 
 
69
  if config.get('explorer'):
70
  explorer_key_name = config['explorer'].get('api_key_name')
71
  if explorer_key_name and explorer_key_name in self.api_keys:
@@ -86,175 +161,222 @@ class AdaptiveRpcManager:
86
 
87
  return [ep.replace("<INFURA_KEY>", infura_key) for ep in endpoints]
88
 
 
 
89
  def get_network_configs(self):
90
- """
91
- إرجاع إعدادات الشبكة المهيأة (للاستخدام في core.py)
92
- """
93
  return self.network_configs
94
 
95
  def get_explorer_config(self, network: str):
96
- """
97
- إرجاع إعدادات المستكشف (URL والمفتاح) لشبكة معينة.
98
- """
99
  config = self.network_configs.get(network, {})
100
  return config.get('explorer')
101
 
102
- def _get_healthy_endpoints(self, network: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  """
104
- "الذكاء" - يرتب نقاط RPC بناءً على الصحة.
105
- يستبعد النقاط التي في وضع "قاطع الدائرة" (Circuit Breaker).
106
  """
107
- if network not in self.network_configs:
108
- return []
109
 
110
  endpoints = self.network_configs[network]['rpc_endpoints']
111
  healthy_endpoints = []
112
-
113
  current_time = time.time()
114
 
115
  for ep in endpoints:
 
 
 
 
116
  health = self.endpoint_health[network][ep]
117
-
118
- # التحقق من قاطع الدائرة
119
  if health['circuit_open']:
120
  if current_time - health['last_error_time'] > RPC_CIRCUIT_BREAKER_DURATION:
121
- # إعادة فتح الدائرة للمحاولة
122
- health['circuit_open'] = False
123
- health['consecutive_errors'] = 0
124
- print(f"ℹ️ [RPC Manager] إعادة تفعيل نقطة RPC: {ep.split('//')[-1]}")
125
  else:
126
  continue # النقطة لا تزال معطلة مؤقتاً
127
 
128
- # حساب متوسط زمن الاستجابة
129
  avg_latency = sum(health['latency']) / len(health['latency']) if health['latency'] else float('inf')
130
-
131
- # (يمكن إضافة منطق ترتيب أكثر تعقيداً هنا مستقبلاً)
132
  healthy_endpoints.append((ep, avg_latency, health['consecutive_errors']))
133
 
134
- # الترتيب: الأقل أخطاء متتالية، ثم الأقل زمن استجابة
135
  healthy_endpoints.sort(key=lambda x: (x[2], x[1]))
136
 
137
- # خلط بسيط للنقاط السليمة لمنع التحميل الزائد على نقطة واحدة
138
  healthy_list = [ep[0] for ep in healthy_endpoints if ep[2] == 0]
139
- random.shuffle(healthy_list)
140
-
141
  unhealthy_list = [ep[0] for ep in healthy_endpoints if ep[2] > 0]
142
 
143
  return healthy_list + unhealthy_list
144
 
145
  def _update_health(self, network: str, endpoint: str, success: bool, latency: float):
146
- """
147
- "التعلم" - تحديث إحصائيات صحة نقطة RPC.
148
- """
149
  health = self.endpoint_health[network][endpoint]
150
-
151
  if success:
152
  health['latency'].append(latency)
153
  health['consecutive_errors'] = 0
154
  health['circuit_open'] = False
155
  else:
156
- health['consecutive_errors'] += 1
157
- health['total_errors'] += 1
158
  health['last_error_time'] = time.time()
159
-
160
- # 🔴 --- START OF CHANGE --- 🔴
161
- # (تصحيح الخطأ الطباعي: استخدام RPC_ERROR_THRESHOLD)
162
  if health['consecutive_errors'] >= RPC_ERROR_THRESHOLD:
163
- # 🔴 --- END OF CHANGE --- 🔴
164
  health['circuit_open'] = True
165
- print(f"🚨 [RPC Manager] قاطع الدائرة مفعل! إيقاف مؤقت لـ: {endpoint.split('//')[-1]}")
 
 
166
 
167
- async def post(self, network: str, payload: dict, timeout: float = 20.0):
168
  """
169
- إرسال طلب POST إلى أفضل نقطة RPC متاحة للشبكة.
170
- يعيد المحاولة تلقائياً عند الفشل.
 
171
  """
172
- endpoints = self._get_healthy_endpoints(network)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  if not endpoints:
174
- print(f"❌ [RPC Manager] لا توجد نقاط RPC متاحة أو سليمة لشبكة {network}")
175
  return None
176
 
177
- # محاولة أفضل 3 نقاط متاحة
178
- for endpoint in endpoints[:3]:
179
  start_time = time.time()
180
- ep_name = endpoint.split('//')[-1].split('/')[0] # للاختصار
181
-
182
  try:
183
- async with self.rpc_semaphore:
184
  response = await self.http_client.post(endpoint, json=payload, timeout=timeout)
185
- response.raise_for_status() # يطلق استثناء لأخطاء 4xx/5xx
186
 
187
  latency = time.time() - start_time
188
  self._update_health(network, endpoint, success=True, latency=latency)
189
-
190
- print(f"✅ [RPC] {network} ({ep_name}) - {latency:.2f}s")
191
  return response.json()
192
 
193
- except (httpx.HTTPStatusError, httpx.RequestError, ssl.SSLError, asyncio.TimeoutError, json.JSONDecodeError) as e:
194
- latency = time.time() - start_time
195
- self._update_health(network, endpoint, success=False, latency=latency)
196
- print(f"⚠️ [RPC] فشل {network} ({ep_name}): {type(e).__name__}")
197
- continue # انتقل إلى النقطة التالية
198
  except Exception as e:
199
  latency = time.time() - start_time
200
  self._update_health(network, endpoint, success=False, latency=latency)
201
- print(f"❌ [RPC] فشل فادح {network} ({ep_name}): {e}")
202
- continue # انتقل إلى النقطة التالية
 
203
 
204
- print(f"❌ [RPC Manager] فشلت جميع المحاولات لشبكة {network} للطلب: {payload.get('method', 'N/A')}")
205
  return None
206
 
207
- async def get(self, base_url: str, params: dict, headers: dict = None, timeout: float = 15.0, use_coingecko_semaphore: bool = False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  """
209
- (محدث) إرسال طلب GET (للمستكشفات أو CoinGecko).
210
- يدير حدود الطلبات بذكاء (Throttling).
 
211
  """
212
- semaphore = self.coingecko_semaphore if use_coingecko_semaphore else self.rpc_semaphore
 
 
 
 
 
 
213
 
214
  try:
215
- async with semaphore:
 
 
 
 
 
 
 
 
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  # (تطبيق "الخنق" لـ CoinGecko)
218
- if use_coingecko_semaphore:
219
- current_time = time.time()
220
- time_since_last = current_time - self.last_coingecko_call
221
-
222
- if time_since_last < COINGECKO_REQUEST_DELAY:
223
- wait_time = COINGECKO_REQUEST_DELAY - time_since_last
224
- # print(f" [CoinGecko Throttler] الانتظار {wait_time:.2f} ثانية...")
225
- await asyncio.sleep(wait_time)
226
-
227
- # (تحديث وقت آخر استدعاء *قبل* الطلب)
228
- self.last_coingecko_call = time.time()
229
 
230
  response = await self.http_client.get(base_url, params=params, headers=headers, timeout=timeout)
231
 
232
  if response.status_code == 429: # Too Many Requests
233
- # (زيادة وقت الانتظار)
234
  wait_duration = 15.0
235
- print(f"⚠️ [GET] خطأ 429 (Rate Limit) من {base_url}. الانتظار {wait_duration} ثوان...")
236
  await asyncio.sleep(wait_duration)
237
-
238
- # (تحديث وقت آخر استدعاء مرة أخرى بعد الانتظار)
239
- if use_coingecko_semaphore:
240
- self.last_coingecko_call = time.time()
241
-
242
- # إعادة المحاولة مرة واحدة
243
  response = await self.http_client.get(base_url, params=params, headers=headers, timeout=timeout)
244
 
245
  response.raise_for_status()
246
 
 
247
  return response.json()
248
 
249
- except httpx.HTTPStatusError as e:
250
- print(f"❌ [GET] خطأ HTTP من {base_url}: {e.response.status_code}")
251
- return None
252
- except (httpx.RequestError, asyncio.TimeoutError) as e:
253
- print(f"❌ [GET] خطأ اتصال/مهلة من {base_url}: {e}")
254
- return None
255
- except json.JSONDecodeError as e:
256
- print(f"❌ [GET] خطأ JSON من {base_url}: {e}")
257
- return None
258
  except Exception as e:
259
- print(f"❌ [GET] خطأ غير متوقع من {base_url}: {e}")
 
260
  return None
 
1
  # whale_monitor/rpc_manager.py
2
+ # (V2 - الموجه الذكي)
3
+ # هذا هو "الوكيل الذكي" لإدارة اتصالات RPC والمستكشفات (APIs)
4
+ # يدير منظمات الطلبات (Rate Limiters) والإحصائيات
5
 
6
  import asyncio
7
  import httpx
 
9
  import ssl
10
  import json
11
  import os
12
+ import csv
13
  from collections import deque, defaultdict
14
  import random
15
+ from typing import Dict, Any, Optional, List
16
 
17
  # استيراد الإعدادات الثابتة
18
  from .config import DEFAULT_NETWORK_CONFIGS, COINGECKO_BASE_URL
 
25
  # (إضافة ثابت لفرض تأخير بين طلبات CoinGecko)
26
  COINGECKO_REQUEST_DELAY = 2.0 # 2.0 ثانية (يساوي 30 طلب/دقيقة كحد أقصى)
27
 
28
+ # (تحديد المسار الحالي لملف rpc_manager.py)
29
+ _CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
30
+ # (تحديد مسار ملف CSV داخل نفس المجلد)
31
+ CUSTOM_RPC_CSV_FILE = os.path.join(_CURRENT_DIR, 'rpc_endpoints (1).csv')
32
+
33
  class AdaptiveRpcManager:
34
  """
35
+ (محدث V2)
36
+ مدير RPC و API ذكي:
37
+ 1. يدير صحة نقاط RPC العامة (Public).
38
+ 2. يدير منظمات الطلبات (Rate Limiters) للمفاتيح الخاصة (Infura, Moralis, Scanners).
39
+ 3. يتتبع إحصائيات الجلسة (Session Stats) لجميع الطلبات.
40
  """
41
+ def __init__(self, http_client: httpx.AsyncClient):
42
  self.http_client = http_client
 
43
 
44
+ # 1. تحميل المفاتيح الخاصة من متغيرات البيئة
45
+ self.api_keys = {
46
+ "infura": os.getenv("INFURA_KEY"),
47
+ "moralis": os.getenv("MORALIS_KEY"),
48
+ "etherscan": os.getenv("ETHERSCAN_KEY"),
49
+ "bscscan": os.getenv("BSCSCAN_KEY"),
50
+ "polygonscan": os.getenv("POLYGONSCAN_KEY"),
51
+ # (يمكن إضافة المزيد من مفاتيح Scanners هنا، مثل ARBISCAN_KEY)
52
+ }
53
+ print("✅ [RPCManager V2] تم تحميل المفاتيح الخاصة من البيئة.")
54
+
55
+ # 2. تهيئة منظمات الطلبات (Semaphores)
56
+ # (بناءً على الحدود التي قدمتها)
57
+
58
+ # لـ Infura (500 credits/sec) - سنستخدم 450 كحد أمان
59
+ self.infura_semaphore = asyncio.Semaphore(450)
60
+
61
+ # لـ Moralis (40k/day ~ 0.46/sec) - سنستخدم 1 لضمان طلب واحد في كل مرة
62
+ self.moralis_semaphore = asyncio.Semaphore(1)
63
+
64
+ # لـ Etherscan, BscScan, PolygonScan (الحد المشترك 5/sec)
65
+ self.scanner_semaphore = asyncio.Semaphore(5)
66
 
67
+ # لـ CoinGecko (عام، سنكون حذرين)
68
+ self.coingecko_semaphore = asyncio.Semaphore(1)
69
+
70
+ # لـ مجمع RPC العام (Public Pool) (لحمايتهم من الضغط)
71
+ self.public_rpc_semaphore = asyncio.Semaphore(10)
72
+
73
  self.last_coingecko_call = 0.0
74
+ self.last_moralis_call = 0.0
75
+
76
+ # 3. تهيئة إحصائيات الجلسة
77
+ self.session_stats = defaultdict(int)
78
 
79
+ # 4. تهيئة إعدادات الشبكة ونقاط RPC
80
  self.network_configs = self._initialize_network_configs(DEFAULT_NETWORK_CONFIGS)
81
 
82
+ # 5. نظام تتبع الصحة (فقط لنقاط RPC العامة)
83
  self.endpoint_health = defaultdict(lambda: defaultdict(lambda: {
84
  "latency": deque(maxlen=RPC_HEALTH_CHECK_WINDOW),
85
+ "consecutive_errors": 0, "total_errors": 0, "last_error_time": None,
 
 
86
  "circuit_open": False,
87
  }))
88
 
89
+ print("✅ [RPCManager V2] مدير RPC/API الذكي (V2) مهيأ.")
90
+
91
+ def _load_rpc_from_csv(self, csv_file_path: str) -> Dict[str, List[str]]:
92
+ """
93
+ (جديد V2)
94
+ قراءة ملف rpc_endpoints (1).csv ودمج النقاط.
95
+ يتوقع الملف أن يحتوي على عمودين: 'network' و 'url'
96
+ """
97
+ custom_rpcs = defaultdict(list)
98
+ if not os.path.exists(csv_file_path):
99
+ print(f"⚠️ [RPCManager V2] ملف CSV المخصص '{csv_file_path}' غير موجود. سيتم تخطيه.")
100
+ return custom_rpcs
101
+
102
+ try:
103
+ with open(csv_file_path, mode='r', encoding='utf-8') as f:
104
+ reader = csv.DictReader(f)
105
+ for row in reader:
106
+ network = row.get('network')
107
+ url = row.get('url')
108
+ if network and url and url.startswith('http'):
109
+ custom_rpcs[network].append(url)
110
+ print(f"✅ [RPCManager V2] تم تحميل {sum(len(v) for v in custom_rpcs.values())} نقطة RPC مخصصة من {csv_file_path}")
111
+ return custom_rpcs
112
+ except Exception as e:
113
+ print(f"❌ [RPCManager V2] فشل في قراءة ملف CSV '{csv_file_path}': {e}")
114
+ return defaultdict(list)
115
 
116
  def _initialize_network_configs(self, configs):
117
  """
118
+ (محدث V2)
119
+ يقوم بدمج CSV، وحقن مفاتيح API، وإعداد الشبكات.
120
  """
121
  initialized_configs = {}
122
+
123
+ # 1. تحميل نقاط CSV المخصصة
124
+ custom_rpcs = self._load_rpc_from_csv(CUSTOM_RPC_CSV_FILE)
125
+
126
  for network, config in configs.items():
127
  new_config = config.copy()
128
 
129
+ # 2. حقن مفاتيح Infura
130
  new_config['rpc_endpoints'] = self._inject_api_keys(
131
  config['rpc_endpoints'],
132
  self.api_keys.get('infura')
133
  )
134
 
135
+ # 3. دمج نقاط CSV
136
+ if network in custom_rpcs:
137
+ new_config['rpc_endpoints'].extend(custom_rpcs[network])
138
+ print(f" ... دمج {len(custom_rpcs[network])} نقاط مخصصة لشبكة {network}")
139
+
140
+ # 4. خلط القائمة النهائية (لضمان التوزيع)
141
+ random.shuffle(new_config['rpc_endpoints'])
142
+
143
+ # 5. حقن مفاتيح المستكشف (Explorer)
144
  if config.get('explorer'):
145
  explorer_key_name = config['explorer'].get('api_key_name')
146
  if explorer_key_name and explorer_key_name in self.api_keys:
 
161
 
162
  return [ep.replace("<INFURA_KEY>", infura_key) for ep in endpoints]
163
 
164
+ # --- (دوال مساعدة للحصول على الإعدادات) ---
165
+
166
  def get_network_configs(self):
 
 
 
167
  return self.network_configs
168
 
169
  def get_explorer_config(self, network: str):
 
 
 
170
  config = self.network_configs.get(network, {})
171
  return config.get('explorer')
172
 
173
+ def get_api_key(self, key_name: str) -> Optional[str]:
174
+ """(جديد V2) جلب مفتاح API بأمان"""
175
+ return self.api_keys.get(key_name)
176
+
177
+ # --- (دوال إدارة الإحصائيات V2) ---
178
+
179
+ def reset_session_stats(self):
180
+ """(جديد V2) تصفير عدادات الإحصائيات للجلسة الجديدة"""
181
+ self.session_stats = defaultdict(int)
182
+ print("📊 [RPCManager V2] تم تصفير عدادات إحصائيات الجلسة.")
183
+
184
+ def get_session_stats(self) -> Dict[str, int]:
185
+ """(جديد V2) إرجاع نسخة من الإحصائيات الحالية"""
186
+ return self.session_stats.copy()
187
+
188
+ # --- (دوال الصحة لـ Public RPCs - لا تغيير) ---
189
+
190
+ def _get_healthy_public_endpoints(self, network: str):
191
  """
192
+ (معدل V2)
193
+ يرتب نقاط RPC العامة (فقط) بناءً على الصحة.
194
  """
195
+ if network not in self.network_configs: return []
 
196
 
197
  endpoints = self.network_configs[network]['rpc_endpoints']
198
  healthy_endpoints = []
 
199
  current_time = time.time()
200
 
201
  for ep in endpoints:
202
+ # (تخطي Infura، هذا المجمع للعام فقط)
203
+ if "infura.io" in ep:
204
+ continue
205
+
206
  health = self.endpoint_health[network][ep]
 
 
207
  if health['circuit_open']:
208
  if current_time - health['last_error_time'] > RPC_CIRCUIT_BREAKER_DURATION:
209
+ health['circuit_open'] = False; health['consecutive_errors'] = 0
 
 
 
210
  else:
211
  continue # النقطة لا تزال معطلة مؤقتاً
212
 
 
213
  avg_latency = sum(health['latency']) / len(health['latency']) if health['latency'] else float('inf')
 
 
214
  healthy_endpoints.append((ep, avg_latency, health['consecutive_errors']))
215
 
 
216
  healthy_endpoints.sort(key=lambda x: (x[2], x[1]))
217
 
218
+ # (فصل السليم تماماً عن غير السليم)
219
  healthy_list = [ep[0] for ep in healthy_endpoints if ep[2] == 0]
220
+ random.shuffle(healthy_list) # خلط السليم للتوزيع
 
221
  unhealthy_list = [ep[0] for ep in healthy_endpoints if ep[2] > 0]
222
 
223
  return healthy_list + unhealthy_list
224
 
225
  def _update_health(self, network: str, endpoint: str, success: bool, latency: float):
226
+ """(لا تغيير) تحديث إحصائيات صحة نقطة RPC العامة."""
 
 
227
  health = self.endpoint_health[network][endpoint]
 
228
  if success:
229
  health['latency'].append(latency)
230
  health['consecutive_errors'] = 0
231
  health['circuit_open'] = False
232
  else:
233
+ health['consecutive_errors'] += 1; health['total_errors'] += 1
 
234
  health['last_error_time'] = time.time()
 
 
 
235
  if health['consecutive_errors'] >= RPC_ERROR_THRESHOLD:
 
236
  health['circuit_open'] = True
237
+ print(f"🚨 [RPCManager V2] قاطع الدائرة مفعل (عام)! إيقاف مؤقت لـ: {endpoint.split('//')[-1]}")
238
+
239
+ # --- (دوال الاتصال الأساسية V2 - محدثة بالكامل) ---
240
 
241
+ async def post_rpc(self, network: str, payload: dict, timeout: float = 20.0):
242
  """
243
+ (محدث V2)
244
+ إرسال طلب POST (JSON-RPC)
245
+ سيحاول مع Infura أولاً (إذا كان متاحاً)، ثم يلجأ إلى المجمع العام.
246
  """
247
+
248
+ # 1. محاولة Infura أولاً (الأولوية)
249
+ infura_key = self.api_keys.get('infura')
250
+ infura_endpoint = next((ep for ep in self.network_configs.get(network, {}).get('rpc_endpoints', []) if "infura.io" in ep), None)
251
+
252
+ if infura_key and infura_endpoint:
253
+ start_time = time.time()
254
+ try:
255
+ async with self.infura_semaphore:
256
+ response = await self.http_client.post(infura_endpoint, json=payload, timeout=timeout)
257
+ response.raise_for_status()
258
+
259
+ self.session_stats['infura_success'] += 1
260
+ latency = time.time() - start_time
261
+ print(f"✅ [RPC Infura] {network} - {latency:.2f}s")
262
+ return response.json()
263
+
264
+ except Exception as e:
265
+ self.session_stats['infura_fail'] += 1
266
+ print(f"⚠️ [RPC Infura] فشل {network}: {type(e).__name__}. اللجوء إلى المجمع العام...")
267
+
268
+ # 2. اللجوء إلى المجمع العام (Public Pool)
269
+ endpoints = self._get_healthy_public_endpoints(network)
270
  if not endpoints:
271
+ print(f"❌ [RPCManager V2] لا توجد نقاط RPC ع��مة متاحة لشبكة {network}")
272
  return None
273
 
274
+ for endpoint in endpoints[:3]: # محاولة أفضل 3
 
275
  start_time = time.time()
276
+ ep_name = endpoint.split('//')[-1].split('/')[0]
 
277
  try:
278
+ async with self.public_rpc_semaphore:
279
  response = await self.http_client.post(endpoint, json=payload, timeout=timeout)
280
+ response.raise_for_status()
281
 
282
  latency = time.time() - start_time
283
  self._update_health(network, endpoint, success=True, latency=latency)
284
+ self.session_stats['public_rpc_success'] += 1
285
+ print(f"✅ [RPC Public] {network} ({ep_name}) - {latency:.2f}s")
286
  return response.json()
287
 
 
 
 
 
 
288
  except Exception as e:
289
  latency = time.time() - start_time
290
  self._update_health(network, endpoint, success=False, latency=latency)
291
+ self.session_stats['public_rpc_fail'] += 1
292
+ print(f"⚠️ [RPC Public] فشل {network} ({ep_name}): {type(e).__name__}")
293
+ continue
294
 
295
+ print(f"❌ [RPCManager V2] فشلت جميع محاولات RPC لشبكة {network}")
296
  return None
297
 
298
+ async def get_scanner_api(self, base_url: str, params: dict, timeout: float = 15.0):
299
+ """
300
+ (جديد V2)
301
+ إجراء طلب GET لواجهات Scanners (Etherscan, BscScan, etc.)
302
+ يستخدم المنظم المشترك (5/ثانية).
303
+ """
304
+ self.session_stats['scanner_total'] += 1
305
+ try:
306
+ async with self.scanner_semaphore:
307
+ response = await self.http_client.get(base_url, params=params, headers=None, timeout=timeout)
308
+ response.raise_for_status()
309
+ self.session_stats['scanner_success'] += 1
310
+ return response.json()
311
+ except Exception as e:
312
+ self.session_stats['scanner_fail'] += 1
313
+ print(f"❌ [Scanner API] فشل الطلب من {base_url.split('//')[-1]}: {e}")
314
+ return None
315
+
316
+ async def get_moralis_api(self, base_url: str, params: dict, timeout: float = 20.0):
317
  """
318
+ (جديد V2)
319
+ إجراء طلب GET لـ Moralis API.
320
+ يستخدم المنظم الخاص به (1/ثانية) ومفتاح API.
321
  """
322
+ moralis_key = self.api_keys.get('moralis')
323
+ if not moralis_key:
324
+ print("❌ [Moralis API] لا يوجد مفتاح MORALIS_KEY.")
325
+ return None
326
+
327
+ headers = {"accept": "application/json", "X-API-Key": moralis_key}
328
+ self.session_stats['moralis_total'] += 1
329
 
330
  try:
331
+ async with self.moralis_semaphore:
332
+ # (ضمان وجود ثانية واحدة على الأقل بين الطلبات لتوزيع 40k على اليوم)
333
+ current_time = time.time()
334
+ if current_time - self.last_moralis_call < 1.0:
335
+ await asyncio.sleep(1.0 - (current_time - self.last_moralis_call))
336
+ self.last_moralis_call = time.time()
337
+
338
+ response = await self.http_client.get(base_url, params=params, headers=headers, timeout=timeout)
339
+ response.raise_for_status()
340
 
341
+ self.session_stats['moralis_success'] += 1
342
+ return response.json()
343
+ except Exception as e:
344
+ self.session_stats['moralis_fail'] += 1
345
+ print(f"❌ [Moralis API] فشل الطلب: {e}")
346
+ return None
347
+
348
+ async def get_coingecko_api(self, params: dict, headers: dict = None, timeout: float = 15.0):
349
+ """
350
+ (معدل V2)
351
+ إجراء طلب GET لـ CoinGecko (يستخدم الآن إحصائيات ومنظم خاص).
352
+ """
353
+ base_url = COINGECKO_BASE_URL
354
+ self.session_stats['coingecko_total'] += 1
355
+ try:
356
+ async with self.coingecko_semaphore:
357
  # (تطبيق "الخنق" لـ CoinGecko)
358
+ current_time = time.time()
359
+ time_since_last = current_time - self.last_coingecko_call
360
+ if time_since_last < COINGECKO_REQUEST_DELAY:
361
+ wait_time = COINGECKO_REQUEST_DELAY - time_since_last
362
+ await asyncio.sleep(wait_time)
363
+ self.last_coingecko_call = time.time()
 
 
 
 
 
364
 
365
  response = await self.http_client.get(base_url, params=params, headers=headers, timeout=timeout)
366
 
367
  if response.status_code == 429: # Too Many Requests
 
368
  wait_duration = 15.0
369
+ print(f"⚠️ [CoinGecko] خطأ 429 (Rate Limit). الانتظار {wait_duration} ثوان...")
370
  await asyncio.sleep(wait_duration)
371
+ self.last_coingecko_call = time.time()
 
 
 
 
 
372
  response = await self.http_client.get(base_url, params=params, headers=headers, timeout=timeout)
373
 
374
  response.raise_for_status()
375
 
376
+ self.session_stats['coingecko_success'] += 1
377
  return response.json()
378
 
 
 
 
 
 
 
 
 
 
379
  except Exception as e:
380
+ self.session_stats['coingecko_fail'] += 1
381
+ print(f"❌ [CoinGecko] فشل الطلب: {e}")
382
  return None