Update whale_news_data.py
Browse files- whale_news_data.py +207 -183
whale_news_data.py
CHANGED
|
@@ -52,7 +52,11 @@ class EnhancedWhaleMonitor:
|
|
| 52 |
self._initialize_contracts_db(contracts_db or {})
|
| 53 |
|
| 54 |
self.data_manager = None
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
self.r2_service = r2_service
|
| 57 |
|
| 58 |
self.address_labels = {}
|
|
@@ -202,9 +206,8 @@ class EnhancedWhaleMonitor:
|
|
| 202 |
print(f"⚠️ مفتاح {env_var} غير متوفر أو فارغ.")
|
| 203 |
setattr(self, attr_name, None)
|
| 204 |
else:
|
| 205 |
-
# إذا لم تكن الخاصية موجودة أصلاً
|
| 206 |
print(f"ℹ️ خاصية المفتاح {attr_name} (لـ {env_var}) غير معرفة في الكلاس.")
|
| 207 |
-
# يمكنك تعيينها إلى None هنا إذا أردت التأكد
|
| 208 |
setattr(self, attr_name, None)
|
| 209 |
|
| 210 |
def _get_ethereum_rpc_endpoints(self):
|
|
@@ -295,14 +298,15 @@ class EnhancedWhaleMonitor:
|
|
| 295 |
]
|
| 296 |
|
| 297 |
def _get_solana_rpc_endpoints(self):
|
| 298 |
-
"""الحصول على نقاط RPC مجانية لشبكة Solana (بدون مفاتيح API)"""
|
| 299 |
endpoints = [
|
| 300 |
-
'https://api.mainnet-beta.solana.com',
|
| 301 |
-
'https://
|
| 302 |
-
'https://
|
| 303 |
-
'https://solana-mainnet.
|
| 304 |
-
'https://mainnet.
|
| 305 |
-
'https://rpc.ankr.com/solana',
|
|
|
|
| 306 |
]
|
| 307 |
print(f"ℹ️ قائمة Solana RPC Endpoints (مجانية وبدون مفاتيح) المستخدمة: {endpoints}")
|
| 308 |
return endpoints
|
|
@@ -387,7 +391,6 @@ class EnhancedWhaleMonitor:
|
|
| 387 |
print(f" ℹ️ تم تحديث صيغة {updated_count} عقد إلى الصيغة الجديدة.")
|
| 388 |
await self._save_contracts_to_r2()
|
| 389 |
|
| 390 |
-
|
| 391 |
except ClientError as e:
|
| 392 |
if e.response['Error']['Code'] == 'NoSuchKey':
|
| 393 |
print("⚠️ لم يتم العثور على قاعدة بيانات العقود في R2. ستبدأ فارغة.")
|
|
@@ -413,7 +416,6 @@ class EnhancedWhaleMonitor:
|
|
| 413 |
contract_info = None
|
| 414 |
network = None
|
| 415 |
|
| 416 |
-
# 1. البحث عن عقد العملة والشبكة
|
| 417 |
if contract_address and isinstance(contract_address, str):
|
| 418 |
network = self._detect_network_from_address(contract_address)
|
| 419 |
contract_info_from_db = await self._find_contract_address_enhanced(symbol)
|
|
@@ -436,14 +438,12 @@ class EnhancedWhaleMonitor:
|
|
| 436 |
|
| 437 |
print(f"🌐 البحث في الشبكة المحددة: {network_final} للعقد: {contract_address_final}")
|
| 438 |
|
| 439 |
-
# 3. جلب بيانات التحويلات
|
| 440 |
transfers = await self._get_targeted_transfer_data(contract_address_final, network_final)
|
| 441 |
|
| 442 |
if not transfers:
|
| 443 |
print(f"⚠️ لم يتم العثور على تحويلات للعملة {symbol} على شبكة {network_final}")
|
| 444 |
return self._create_no_transfers_response(symbol)
|
| 445 |
|
| 446 |
-
# 4. ترشيح التحويلات الحديثة
|
| 447 |
recent_transfers = self._filter_recent_transfers(transfers, max_minutes=120)
|
| 448 |
|
| 449 |
if not recent_transfers:
|
|
@@ -452,7 +452,6 @@ class EnhancedWhaleMonitor:
|
|
| 452 |
|
| 453 |
print(f"📊 تم جلب {len(recent_transfers)} تحويلة حديثة فريدة للعملة {symbol}")
|
| 454 |
|
| 455 |
-
# 5. تحليل تأثير الحيتان
|
| 456 |
analysis = await self._analyze_enhanced_whale_impact(recent_transfers, symbol, contract_address_final, network_final)
|
| 457 |
|
| 458 |
return analysis
|
|
@@ -573,9 +572,10 @@ class EnhancedWhaleMonitor:
|
|
| 573 |
endpoint_name = endpoint.split('//')[1].split('/')[0] if '//' in endpoint else endpoint
|
| 574 |
print(f" 🔍 محاولة جلب سجلات التحويلات لـ {network} عبر {endpoint_name}...")
|
| 575 |
|
| 576 |
-
# ✅ الإصلاح: استخدام "self.
|
| 577 |
-
|
| 578 |
-
|
|
|
|
| 579 |
response_block.raise_for_status()
|
| 580 |
json_response_block = response_block.json()
|
| 581 |
result_block = json_response_block.get('result')
|
|
@@ -586,8 +586,9 @@ class EnhancedWhaleMonitor:
|
|
| 586 |
continue
|
| 587 |
latest_block = int(result_block, 16)
|
| 588 |
|
| 589 |
-
|
| 590 |
-
|
|
|
|
| 591 |
latest_block_timestamp_approx = int(time.time())
|
| 592 |
if response_latest_time.status_code == 200:
|
| 593 |
latest_block_data = response_latest_time.json().get('result')
|
|
@@ -602,7 +603,8 @@ class EnhancedWhaleMonitor:
|
|
| 602 |
"params": [{"fromBlock": hex(from_block), "toBlock": hex(latest_block), "address": contract_address_checksum, "topics": [TRANSFER_EVENT_SIGNATURE]}],
|
| 603 |
"id": int(time.time())+2
|
| 604 |
}
|
| 605 |
-
|
|
|
|
| 606 |
response_logs.raise_for_status()
|
| 607 |
json_response_logs = response_logs.json()
|
| 608 |
logs = json_response_logs.get('result')
|
|
@@ -706,8 +708,9 @@ class EnhancedWhaleMonitor:
|
|
| 706 |
"params": [ token_address, {"limit": limit, "commitment": "confirmed"} ]
|
| 707 |
}
|
| 708 |
|
| 709 |
-
# ✅ الإصلاح: استخدام "self.
|
| 710 |
-
|
|
|
|
| 711 |
|
| 712 |
if response_signatures.status_code == 403:
|
| 713 |
print(f" ⚠️ Solana endpoint {endpoint_name} failed: 403 Forbidden")
|
|
@@ -741,10 +744,11 @@ class EnhancedWhaleMonitor:
|
|
| 741 |
|
| 742 |
processed_count = 0
|
| 743 |
transaction_tasks = []
|
| 744 |
-
|
|
|
|
| 745 |
for signature in signatures[:20]:
|
| 746 |
transaction_tasks.append(
|
| 747 |
-
self._get_solana_transaction_detail_with_retry(signature, endpoint, self.http_client)
|
| 748 |
)
|
| 749 |
transaction_details = await asyncio.gather(*transaction_tasks)
|
| 750 |
|
|
@@ -794,12 +798,16 @@ class EnhancedWhaleMonitor:
|
|
| 794 |
return []
|
| 795 |
|
| 796 |
|
|
|
|
| 797 |
async def _get_solana_transaction_detail_with_retry(self, signature, endpoint, client: httpx.AsyncClient, retries=2, delay=0.5):
|
| 798 |
"""جلب تفاصيل معاملة Solana مع محاولات إعادة بسيطة باستخدام client مشترك"""
|
| 799 |
last_exception = None
|
| 800 |
for attempt in range(retries):
|
| 801 |
try:
|
| 802 |
-
|
|
|
|
|
|
|
|
|
|
| 803 |
if detail:
|
| 804 |
return detail
|
| 805 |
elif detail is None:
|
|
@@ -835,6 +843,7 @@ class EnhancedWhaleMonitor:
|
|
| 835 |
"method": "getTransaction",
|
| 836 |
"params": [ signature, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0, "commitment": "confirmed"} ]
|
| 837 |
}
|
|
|
|
| 838 |
response = await client.post(endpoint, json=payload, timeout=20.0)
|
| 839 |
response.raise_for_status()
|
| 840 |
|
|
@@ -974,7 +983,6 @@ class EnhancedWhaleMonitor:
|
|
| 974 |
if sender and receiver and amount_raw > 0: break
|
| 975 |
if sender and receiver and amount_raw > 0: break
|
| 976 |
|
| 977 |
-
|
| 978 |
if sender and receiver and amount_raw > 0:
|
| 979 |
return {
|
| 980 |
'hash': signature, 'from': sender, 'to': receiver,
|
|
@@ -1017,41 +1025,47 @@ class EnhancedWhaleMonitor:
|
|
| 1017 |
"endblock": 999999999, "sort": "desc", "apikey": config['key']
|
| 1018 |
}
|
| 1019 |
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
if "INVALID API KEY" in str(error_message).upper():
|
| 1048 |
-
print(f"🚨 خطأ فادح: مفتاح API الخاص بـ {network.upper()} غير صالح!")
|
| 1049 |
-
return []
|
| 1050 |
else:
|
| 1051 |
-
|
|
|
|
|
|
|
|
|
|
| 1052 |
return []
|
|
|
|
|
|
|
|
|
|
| 1053 |
|
| 1054 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
| 1055 |
print(f"❌ فشل جلب بيانات Explorer لـ {network}: {e}")
|
| 1056 |
return []
|
| 1057 |
|
|
@@ -1257,7 +1271,10 @@ class EnhancedWhaleMonitor:
|
|
| 1257 |
"params": [{"to": contract_address, "data": "0x313ce567"}, "latest"],
|
| 1258 |
"id": int(time.time())
|
| 1259 |
}
|
| 1260 |
-
|
|
|
|
|
|
|
|
|
|
| 1261 |
if response.status_code == 200:
|
| 1262 |
result = response.json().get('result')
|
| 1263 |
if result and result != '0x' and result.startswith('0x'):
|
|
@@ -1284,7 +1301,6 @@ class EnhancedWhaleMonitor:
|
|
| 1284 |
print(f" ⚠️ استخدام قيمة decimals تقديرية لـ Solana: {estimated_decimals}")
|
| 1285 |
return estimated_decimals
|
| 1286 |
|
| 1287 |
-
|
| 1288 |
print(f"❌ فشل جلب الكسور العشرية للعقد {contract_address} على شبكة {network} من جميع المصادر.")
|
| 1289 |
return None
|
| 1290 |
|
|
@@ -1331,7 +1347,7 @@ class EnhancedWhaleMonitor:
|
|
| 1331 |
'ATOM': 'cosmos', 'UNI': 'uniswap', 'AAVE': 'aave', 'KCS': 'kucoin-shares',
|
| 1332 |
'MAVIA': 'heroes-of-mavia', 'COMMON': 'commonwealth', 'WLFI': 'wolfi',
|
| 1333 |
'PINGPONG': 'pingpong', 'YB': 'yourbusd', 'REACT': 'react', 'XMN': 'xmine',
|
| 1334 |
-
'ANOME': 'anome', 'ZEN': 'zencash', 'AKT': 'akash-network', 'UB': 'unibit', 'WLD': 'worldcoin'
|
| 1335 |
}
|
| 1336 |
|
| 1337 |
base_symbol = symbol.split('/')[0].upper()
|
|
@@ -1347,52 +1363,55 @@ class EnhancedWhaleMonitor:
|
|
| 1347 |
max_retries = 2
|
| 1348 |
for attempt in range(max_retries):
|
| 1349 |
async with self.coingecko_semaphore:
|
| 1350 |
-
# ✅ الإصلاح:
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
return price
|
| 1367 |
-
else:
|
| 1368 |
-
print(f"⚠️ استجابة CoinGecko تحتوي على سعر غير صالح لـ {symbol}: {price}")
|
| 1369 |
-
return 0
|
| 1370 |
else:
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1396 |
|
| 1397 |
print(f"❌ فشل جلب السعر من CoinGecko لـ {symbol} بعد {max_retries} محاولات.")
|
| 1398 |
return 0
|
|
@@ -1405,7 +1424,9 @@ class EnhancedWhaleMonitor:
|
|
| 1405 |
"""دالة مساعدة للبحث عن معرف CoinGecko"""
|
| 1406 |
try:
|
| 1407 |
search_url = f"https://api.coingecko.com/api/v3/search?query={symbol}"
|
| 1408 |
-
|
|
|
|
|
|
|
| 1409 |
response.raise_for_status()
|
| 1410 |
data = response.json()
|
| 1411 |
coins = data.get('coins', [])
|
|
@@ -1587,82 +1608,85 @@ class EnhancedWhaleMonitor:
|
|
| 1587 |
max_retries = 2
|
| 1588 |
for attempt in range(max_retries):
|
| 1589 |
async with self.coingecko_semaphore:
|
| 1590 |
-
|
| 1591 |
-
|
| 1592 |
-
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
if response.status_code == 429:
|
| 1596 |
-
wait_time = 3 * (attempt + 1)
|
| 1597 |
-
print(f" ⏰ Rate limit (Search) for {symbol} - Waiting {wait_time}s")
|
| 1598 |
-
await asyncio.sleep(wait_time)
|
| 1599 |
-
continue
|
| 1600 |
-
|
| 1601 |
-
response.raise_for_status()
|
| 1602 |
-
data = response.json()
|
| 1603 |
-
coins = data.get('coins', [])
|
| 1604 |
-
|
| 1605 |
-
if not coins:
|
| 1606 |
-
print(f" ❌ No matching coins found for {symbol} on CoinGecko")
|
| 1607 |
-
return None
|
| 1608 |
-
|
| 1609 |
-
print(f" 📊 Found {len(coins)} potential matches for {symbol}")
|
| 1610 |
-
|
| 1611 |
-
best_coin = None
|
| 1612 |
-
search_symbol_lower = symbol.lower()
|
| 1613 |
-
for coin in coins:
|
| 1614 |
-
coin_symbol = coin.get('symbol', '').lower()
|
| 1615 |
-
coin_name = coin.get('name', '').lower()
|
| 1616 |
-
if coin_symbol == search_symbol_lower:
|
| 1617 |
-
best_coin = coin; print(f" ✅ Exact symbol match: {coin.get('name')}"); break
|
| 1618 |
-
if not best_coin and coin_name == search_symbol_lower:
|
| 1619 |
-
best_coin = coin; print(f" ✅ Exact name match: {coin.get('name')}")
|
| 1620 |
-
if not best_coin:
|
| 1621 |
-
best_coin = coins[0]; print(f" ⚠️ Using first result: {best_coin.get('name')}")
|
| 1622 |
-
|
| 1623 |
-
coin_id = best_coin.get('id')
|
| 1624 |
-
if not coin_id: print(f" ❌ No ID found"); return None
|
| 1625 |
-
|
| 1626 |
-
print(f" 🔍 Fetching details for CoinGecko ID: {coin_id}")
|
| 1627 |
-
detail_url = f"https://api.coingecko.com/api/v3/coins/{coin_id}"
|
| 1628 |
-
await asyncio.sleep(1.1)
|
| 1629 |
-
detail_response = await client.get(detail_url)
|
| 1630 |
-
|
| 1631 |
-
if detail_response.status_code == 429:
|
| 1632 |
-
wait_time = 5 * (attempt + 1)
|
| 1633 |
-
print(f" ⏰ Rate limit (Details) for {coin_id} - Waiting {wait_time}s")
|
| 1634 |
-
await asyncio.sleep(wait_time)
|
| 1635 |
-
detail_response = await client.get(detail_url)
|
| 1636 |
-
|
| 1637 |
-
detail_response.raise_for_status()
|
| 1638 |
-
detail_data = detail_response.json()
|
| 1639 |
-
platforms = detail_data.get('platforms', {})
|
| 1640 |
-
if not platforms: print(f" ❌ No platform data found"); return None
|
| 1641 |
-
|
| 1642 |
-
network_priority = ['ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom', 'solana']
|
| 1643 |
-
network_map = {'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom', 'solana': 'solana'}
|
| 1644 |
-
|
| 1645 |
-
for platform_cg in network_priority:
|
| 1646 |
-
address = platforms.get(platform_cg)
|
| 1647 |
-
if address and isinstance(address, str) and address.strip():
|
| 1648 |
-
network = network_map.get(platform_cg)
|
| 1649 |
-
if network:
|
| 1650 |
-
print(f" ✅ Found contract on {network}: {address}")
|
| 1651 |
-
return address, network
|
| 1652 |
-
|
| 1653 |
-
print(f" ❌ No contract found on supported platforms for {coin_id}")
|
| 1654 |
-
return None
|
| 1655 |
|
| 1656 |
-
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
-
|
| 1660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1661 |
return None
|
| 1662 |
-
|
| 1663 |
-
|
| 1664 |
-
|
| 1665 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1666 |
|
| 1667 |
print(f" ❌ Failed to get contract from CoinGecko for {symbol} after {max_retries} attempts.")
|
| 1668 |
return None
|
|
@@ -1756,7 +1780,7 @@ class EnhancedWhaleMonitor:
|
|
| 1756 |
}
|
| 1757 |
|
| 1758 |
whale_signal = whale_data.get('trading_signal', {})
|
| 1759 |
-
analysis_details = whale_data
|
| 1760 |
|
| 1761 |
return {
|
| 1762 |
'action': whale_signal.get('action', 'HOLD'),
|
|
@@ -1787,4 +1811,4 @@ class EnhancedWhaleMonitor:
|
|
| 1787 |
print(f"⚠️ خطأ أثناء إغلاق HTTP client لمراقب الحيتان: {e}")
|
| 1788 |
|
| 1789 |
|
| 1790 |
-
print("✅ EnhancedWhaleMonitor loaded -
|
|
|
|
| 52 |
self._initialize_contracts_db(contracts_db or {})
|
| 53 |
|
| 54 |
self.data_manager = None
|
| 55 |
+
# ✅ الإصلاح: إضافة منظم طلبات لـ CoinGecko
|
| 56 |
+
self.coingecko_semaphore = asyncio.Semaphore(1) # 1 طلب متزامن فقط لـ CoinGecko
|
| 57 |
+
# ✅ الإصلاح: إضافة منظم طلبات لـ RPC
|
| 58 |
+
self.rpc_semaphore = asyncio.Semaphore(5) # 5 طلبات RPC متزامنة كحد أقصى (لجميع الشبكات)
|
| 59 |
+
|
| 60 |
self.r2_service = r2_service
|
| 61 |
|
| 62 |
self.address_labels = {}
|
|
|
|
| 206 |
print(f"⚠️ مفتاح {env_var} غير متوفر أو فارغ.")
|
| 207 |
setattr(self, attr_name, None)
|
| 208 |
else:
|
| 209 |
+
# إذا لم تكن الخاصية موجودة أصلاً
|
| 210 |
print(f"ℹ️ خاصية المفتاح {attr_name} (لـ {env_var}) غير معرفة في الكلاس.")
|
|
|
|
| 211 |
setattr(self, attr_name, None)
|
| 212 |
|
| 213 |
def _get_ethereum_rpc_endpoints(self):
|
|
|
|
| 298 |
]
|
| 299 |
|
| 300 |
def _get_solana_rpc_endpoints(self):
|
| 301 |
+
"""الحصول على نقاط RPC مجانية لشبكة Solana (بدون مفاتيح API) - محدثة"""
|
| 302 |
endpoints = [
|
| 303 |
+
'https://api.mainnet-beta.solana.com', # الرسمي (قد يكون محدود الطلبات بشدة)
|
| 304 |
+
'https://solana-mainnet.publicnode.com', # PublicNode (جديد وموثوق)
|
| 305 |
+
'https://ssc-dao.genesysgo.net/', # GenesysGo (قديم نسبياً)
|
| 306 |
+
'https://solana-mainnet.rpc.extrnode.com', # Extrnode (مزود عام)
|
| 307 |
+
'https://solana-mainnet.phantom.tech/', # Phantom Wallet RPC (قد يكون للاستخدام المحدود)
|
| 308 |
+
'https://rpc.ankr.com/solana', # Ankr (يسبب 403 غالباً بدون مفتاح)
|
| 309 |
+
# 'https://mainnet.rpc.solana.pyth.network', # تمت إزالته (خطأ DNS)
|
| 310 |
]
|
| 311 |
print(f"ℹ️ قائمة Solana RPC Endpoints (مجانية وبدون مفاتيح) المستخدمة: {endpoints}")
|
| 312 |
return endpoints
|
|
|
|
| 391 |
print(f" ℹ️ تم تحديث صيغة {updated_count} عقد إلى الصيغة الجديدة.")
|
| 392 |
await self._save_contracts_to_r2()
|
| 393 |
|
|
|
|
| 394 |
except ClientError as e:
|
| 395 |
if e.response['Error']['Code'] == 'NoSuchKey':
|
| 396 |
print("⚠️ لم يتم العثور على قاعدة بيانات العقود في R2. ستبدأ فارغة.")
|
|
|
|
| 416 |
contract_info = None
|
| 417 |
network = None
|
| 418 |
|
|
|
|
| 419 |
if contract_address and isinstance(contract_address, str):
|
| 420 |
network = self._detect_network_from_address(contract_address)
|
| 421 |
contract_info_from_db = await self._find_contract_address_enhanced(symbol)
|
|
|
|
| 438 |
|
| 439 |
print(f"🌐 البحث في الشبكة المحددة: {network_final} للعقد: {contract_address_final}")
|
| 440 |
|
|
|
|
| 441 |
transfers = await self._get_targeted_transfer_data(contract_address_final, network_final)
|
| 442 |
|
| 443 |
if not transfers:
|
| 444 |
print(f"⚠️ لم يتم العثور على تحويلات للعملة {symbol} على شبكة {network_final}")
|
| 445 |
return self._create_no_transfers_response(symbol)
|
| 446 |
|
|
|
|
| 447 |
recent_transfers = self._filter_recent_transfers(transfers, max_minutes=120)
|
| 448 |
|
| 449 |
if not recent_transfers:
|
|
|
|
| 452 |
|
| 453 |
print(f"📊 تم جلب {len(recent_transfers)} تحويلة حديثة فريدة للعملة {symbol}")
|
| 454 |
|
|
|
|
| 455 |
analysis = await self._analyze_enhanced_whale_impact(recent_transfers, symbol, contract_address_final, network_final)
|
| 456 |
|
| 457 |
return analysis
|
|
|
|
| 572 |
endpoint_name = endpoint.split('//')[1].split('/')[0] if '//' in endpoint else endpoint
|
| 573 |
print(f" 🔍 محاولة جلب سجلات التحويلات لـ {network} عبر {endpoint_name}...")
|
| 574 |
|
| 575 |
+
# ✅ الإصلاح: استخدام "self.rpc_semaphore"
|
| 576 |
+
async with self.rpc_semaphore:
|
| 577 |
+
payload_block = {"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": int(time.time())}
|
| 578 |
+
response_block = await self.http_client.post(endpoint, json=payload_block, timeout=15.0)
|
| 579 |
response_block.raise_for_status()
|
| 580 |
json_response_block = response_block.json()
|
| 581 |
result_block = json_response_block.get('result')
|
|
|
|
| 586 |
continue
|
| 587 |
latest_block = int(result_block, 16)
|
| 588 |
|
| 589 |
+
async with self.rpc_semaphore:
|
| 590 |
+
payload_latest_block_time = {"jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": [hex(latest_block), False], "id": int(time.time())+1}
|
| 591 |
+
response_latest_time = await self.http_client.post(endpoint, json=payload_latest_block_time, timeout=15.0)
|
| 592 |
latest_block_timestamp_approx = int(time.time())
|
| 593 |
if response_latest_time.status_code == 200:
|
| 594 |
latest_block_data = response_latest_time.json().get('result')
|
|
|
|
| 603 |
"params": [{"fromBlock": hex(from_block), "toBlock": hex(latest_block), "address": contract_address_checksum, "topics": [TRANSFER_EVENT_SIGNATURE]}],
|
| 604 |
"id": int(time.time())+2
|
| 605 |
}
|
| 606 |
+
async with self.rpc_semaphore:
|
| 607 |
+
response_logs = await self.http_client.post(endpoint, json=payload_logs, timeout=45.0)
|
| 608 |
response_logs.raise_for_status()
|
| 609 |
json_response_logs = response_logs.json()
|
| 610 |
logs = json_response_logs.get('result')
|
|
|
|
| 708 |
"params": [ token_address, {"limit": limit, "commitment": "confirmed"} ]
|
| 709 |
}
|
| 710 |
|
| 711 |
+
# ✅ الإصلاح: استخدام "self.rpc_semaphore"
|
| 712 |
+
async with self.rpc_semaphore:
|
| 713 |
+
response_signatures = await self.http_client.post(endpoint, json=payload_signatures, timeout=20.0)
|
| 714 |
|
| 715 |
if response_signatures.status_code == 403:
|
| 716 |
print(f" ⚠️ Solana endpoint {endpoint_name} failed: 403 Forbidden")
|
|
|
|
| 744 |
|
| 745 |
processed_count = 0
|
| 746 |
transaction_tasks = []
|
| 747 |
+
|
| 748 |
+
# ✅ الإصلاح: تمرير "self" إلى الدالة المساعدة
|
| 749 |
for signature in signatures[:20]:
|
| 750 |
transaction_tasks.append(
|
| 751 |
+
self._get_solana_transaction_detail_with_retry(self, signature, endpoint, self.http_client)
|
| 752 |
)
|
| 753 |
transaction_details = await asyncio.gather(*transaction_tasks)
|
| 754 |
|
|
|
|
| 798 |
return []
|
| 799 |
|
| 800 |
|
| 801 |
+
# ✅ الإصلاح: إضافة "self" كباراميتر
|
| 802 |
async def _get_solana_transaction_detail_with_retry(self, signature, endpoint, client: httpx.AsyncClient, retries=2, delay=0.5):
|
| 803 |
"""جلب تفاصيل معاملة Solana مع محاولات إعادة بسيطة باستخدام client مشترك"""
|
| 804 |
last_exception = None
|
| 805 |
for attempt in range(retries):
|
| 806 |
try:
|
| 807 |
+
# ✅ الإصلاح: استخدام "self.rpc_semaphore"
|
| 808 |
+
async with self.rpc_semaphore:
|
| 809 |
+
detail = await self._get_solana_transaction_detail(signature, endpoint, client)
|
| 810 |
+
|
| 811 |
if detail:
|
| 812 |
return detail
|
| 813 |
elif detail is None:
|
|
|
|
| 843 |
"method": "getTransaction",
|
| 844 |
"params": [ signature, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0, "commitment": "confirmed"} ]
|
| 845 |
}
|
| 846 |
+
# ✅ الإصلاح: لا يوجد semaphore هنا، يتم تطبيقه في الدالة التي تستدعيها
|
| 847 |
response = await client.post(endpoint, json=payload, timeout=20.0)
|
| 848 |
response.raise_for_status()
|
| 849 |
|
|
|
|
| 983 |
if sender and receiver and amount_raw > 0: break
|
| 984 |
if sender and receiver and amount_raw > 0: break
|
| 985 |
|
|
|
|
| 986 |
if sender and receiver and amount_raw > 0:
|
| 987 |
return {
|
| 988 |
'hash': signature, 'from': sender, 'to': receiver,
|
|
|
|
| 1025 |
"endblock": 999999999, "sort": "desc", "apikey": config['key']
|
| 1026 |
}
|
| 1027 |
|
| 1028 |
+
# ✅ الإصلاح: استخدام "self.http_client" بدلاً من إنشاء client جديد
|
| 1029 |
+
# ويفضل استخدام semaphore خاص بالمستكشفات إذا كان لديهم حد طلبات صارم
|
| 1030 |
+
# حالياً، سنستخدم semaphore الـ RPC العام كإجراء وقائي
|
| 1031 |
+
async with self.rpc_semaphore:
|
| 1032 |
+
response = await self.http_client.get(config['url'], params=params, timeout=20.0)
|
| 1033 |
+
|
| 1034 |
+
if response.status_code == 200:
|
| 1035 |
+
data = response.json()
|
| 1036 |
+
status = str(data.get('status', '0'))
|
| 1037 |
+
message = data.get('message', '').upper()
|
| 1038 |
+
|
| 1039 |
+
if status == '1' and message == 'OK':
|
| 1040 |
+
transfers = data.get('result', [])
|
| 1041 |
+
processed_transfers = []
|
| 1042 |
+
for tf in transfers:
|
| 1043 |
+
if tf.get('tokenSymbol') and tf.get('contractAddress','').lower() == contract_address.lower():
|
| 1044 |
+
tf['network'] = network
|
| 1045 |
+
if 'from' in tf: tf['from'] = tf['from'].lower()
|
| 1046 |
+
if 'to' in tf: tf['to'] = tf['to'].lower()
|
| 1047 |
+
if 'logIndex' not in tf: tf['logIndex'] = 'N/A'
|
| 1048 |
+
processed_transfers.append(tf)
|
| 1049 |
+
|
| 1050 |
+
print(f" ✅ Explorer {network}: تم جلب {len(processed_transfers)} تحويلة توكن للعقد المحدد.")
|
| 1051 |
+
return processed_transfers
|
| 1052 |
+
elif status == '0' and "NO TRANSACTIONS FOUND" in message:
|
| 1053 |
+
print(f" ✅ Explorer {network}: لا توجد تحويلات.")
|
| 1054 |
+
return []
|
|
|
|
|
|
|
|
|
|
| 1055 |
else:
|
| 1056 |
+
error_message = data.get('result', message)
|
| 1057 |
+
print(f"⚠️ Explorer {network} returned error: Status={status}, Message={message}, Result={error_message}")
|
| 1058 |
+
if "INVALID API KEY" in str(error_message).upper():
|
| 1059 |
+
print(f"🚨 خطأ فادح: مفتاح API الخاص بـ {network.upper()} غير صالح!")
|
| 1060 |
return []
|
| 1061 |
+
else:
|
| 1062 |
+
print(f"⚠️ Explorer {network} request failed: {response.status_code}")
|
| 1063 |
+
return []
|
| 1064 |
|
| 1065 |
except Exception as e:
|
| 1066 |
+
if "Cannot reopen a client instance" in str(e):
|
| 1067 |
+
print(f" ❌ فشل فادح (Explorer): محاولة استخدام client مغلق. {e}")
|
| 1068 |
+
return []
|
| 1069 |
print(f"❌ فشل جلب بيانات Explorer لـ {network}: {e}")
|
| 1070 |
return []
|
| 1071 |
|
|
|
|
| 1271 |
"params": [{"to": contract_address, "data": "0x313ce567"}, "latest"],
|
| 1272 |
"id": int(time.time())
|
| 1273 |
}
|
| 1274 |
+
# ✅ الإصلاح: استخدام "self.rpc_semaphore"
|
| 1275 |
+
async with self.rpc_semaphore:
|
| 1276 |
+
response = await self.http_client.post(endpoint, json=payload, timeout=10.0)
|
| 1277 |
+
|
| 1278 |
if response.status_code == 200:
|
| 1279 |
result = response.json().get('result')
|
| 1280 |
if result and result != '0x' and result.startswith('0x'):
|
|
|
|
| 1301 |
print(f" ⚠️ استخدام قيمة decimals تقديرية لـ Solana: {estimated_decimals}")
|
| 1302 |
return estimated_decimals
|
| 1303 |
|
|
|
|
| 1304 |
print(f"❌ فشل جلب الكسور العشرية للعقد {contract_address} على شبكة {network} من جميع المصادر.")
|
| 1305 |
return None
|
| 1306 |
|
|
|
|
| 1347 |
'ATOM': 'cosmos', 'UNI': 'uniswap', 'AAVE': 'aave', 'KCS': 'kucoin-shares',
|
| 1348 |
'MAVIA': 'heroes-of-mavia', 'COMMON': 'commonwealth', 'WLFI': 'wolfi',
|
| 1349 |
'PINGPONG': 'pingpong', 'YB': 'yourbusd', 'REACT': 'react', 'XMN': 'xmine',
|
| 1350 |
+
'ANOME': 'anome', 'ZEN': 'zencash', 'AKT': 'akash-network', 'UB': 'unibit', 'WLD': 'worldcoin'
|
| 1351 |
}
|
| 1352 |
|
| 1353 |
base_symbol = symbol.split('/')[0].upper()
|
|
|
|
| 1363 |
max_retries = 2
|
| 1364 |
for attempt in range(max_retries):
|
| 1365 |
async with self.coingecko_semaphore:
|
| 1366 |
+
# ✅ الإصلاح: استخدام "self.http_client"
|
| 1367 |
+
try:
|
| 1368 |
+
response = await self.http_client.get(url, headers=headers, timeout=10.0) # استخدام headers محلي
|
| 1369 |
+
if response.status_code == 429:
|
| 1370 |
+
wait_time = 3 * (attempt + 1)
|
| 1371 |
+
print(f"⏰ Rate limit من CoinGecko لـ {symbol} (محاولة {attempt + 1}) - الانتظار {wait_time}s")
|
| 1372 |
+
await asyncio.sleep(wait_time)
|
| 1373 |
+
continue
|
| 1374 |
+
|
| 1375 |
+
response.raise_for_status()
|
| 1376 |
+
data = response.json()
|
| 1377 |
+
price_data = data.get(coingecko_id)
|
| 1378 |
+
if price_data and 'usd' in price_data:
|
| 1379 |
+
price = price_data['usd']
|
| 1380 |
+
if isinstance(price, (int, float)) and price > 0:
|
| 1381 |
+
return price
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
else:
|
| 1383 |
+
print(f"⚠️ استجابة CoinGecko تحتوي على سعر غير صالح لـ {symbol}: {price}")
|
| 1384 |
+
return 0
|
| 1385 |
+
else:
|
| 1386 |
+
if attempt == 0:
|
| 1387 |
+
print(f"⚠️ لم يتم العثور على سعر لـ {coingecko_id} في CoinGecko، محاولة البحث عن المعرف...")
|
| 1388 |
+
await asyncio.sleep(1.1)
|
| 1389 |
+
# ✅ الإصلاح: استخدام "self.http_client" للبحث
|
| 1390 |
+
found_id = await self._find_coingecko_id_via_search(base_symbol, self.http_client)
|
| 1391 |
+
if found_id and found_id != coingecko_id:
|
| 1392 |
+
print(f" 🔄 تم العثور على معرف بديل: {found_id}. إعادة المحاولة...")
|
| 1393 |
+
coingecko_id = found_id
|
| 1394 |
+
url = f"https://api.coingecko.com/api/v3/simple/price?ids={coingecko_id}&vs_currencies=usd"
|
| 1395 |
+
continue
|
| 1396 |
+
else:
|
| 1397 |
+
print(f" ❌ لم يتم العثور على معرف بديل لـ {base_symbol}.")
|
| 1398 |
+
return 0
|
| 1399 |
+
else:
|
| 1400 |
+
print(f"⚠️ لم يتم العثور على سعر لـ {coingecko_id} في استجابة CoinGecko بعد البحث.")
|
| 1401 |
+
return 0
|
| 1402 |
+
|
| 1403 |
+
except Exception as inner_e:
|
| 1404 |
+
if "Cannot reopen a client instance" in str(inner_e):
|
| 1405 |
+
print(f" ❌ فشل فادح (CoinGecko): محاولة استخدام client مغلق. {inner_e}")
|
| 1406 |
+
return 0 # لا يمكن المتابعة
|
| 1407 |
+
# التعامل مع الأخطاء الأخرى
|
| 1408 |
+
if isinstance(inner_e, httpx.HTTPStatusError):
|
| 1409 |
+
print(f"❌ خطأ HTTP من CoinGecko لـ {symbol}: {inner_e.response.status_code}")
|
| 1410 |
+
elif isinstance(inner_e, httpx.RequestError):
|
| 1411 |
+
print(f"❌ خطأ اتصال بـ CoinGecko لـ {symbol}: {inner_e}")
|
| 1412 |
+
else:
|
| 1413 |
+
print(f"❌ خطأ غير متوقع أثناء جلب السعر من CoinGecko لـ {symbol}: {inner_e}")
|
| 1414 |
+
return 0 # فشل في هذه المحاولة
|
| 1415 |
|
| 1416 |
print(f"❌ فشل جلب السعر من CoinGecko لـ {symbol} بعد {max_retries} محاولات.")
|
| 1417 |
return 0
|
|
|
|
| 1424 |
"""دالة مساعدة للبحث عن معرف CoinGecko"""
|
| 1425 |
try:
|
| 1426 |
search_url = f"https://api.coingecko.com/api/v3/search?query={symbol}"
|
| 1427 |
+
# ✅ الإصلاح: استخدام "self.coingecko_semaphore"
|
| 1428 |
+
async with self.coingecko_semaphore:
|
| 1429 |
+
response = await client.get(search_url, timeout=10.0)
|
| 1430 |
response.raise_for_status()
|
| 1431 |
data = response.json()
|
| 1432 |
coins = data.get('coins', [])
|
|
|
|
| 1608 |
max_retries = 2
|
| 1609 |
for attempt in range(max_retries):
|
| 1610 |
async with self.coingecko_semaphore:
|
| 1611 |
+
# ✅ الإصلاح: استخدام "self.http_client"
|
| 1612 |
+
try:
|
| 1613 |
+
print(f" 🔍 CoinGecko Search API Call (Attempt {attempt + 1}) for {symbol}")
|
| 1614 |
+
response = await self.http_client.get(search_url, headers=headers, timeout=15.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1615 |
|
| 1616 |
+
if response.status_code == 429:
|
| 1617 |
+
wait_time = 3 * (attempt + 1)
|
| 1618 |
+
print(f" ⏰ Rate limit (Search) for {symbol} - Waiting {wait_time}s")
|
| 1619 |
+
await asyncio.sleep(wait_time)
|
| 1620 |
+
continue
|
| 1621 |
+
|
| 1622 |
+
response.raise_for_status()
|
| 1623 |
+
data = response.json()
|
| 1624 |
+
coins = data.get('coins', [])
|
| 1625 |
+
|
| 1626 |
+
if not coins:
|
| 1627 |
+
print(f" ❌ No matching coins found for {symbol} on CoinGecko")
|
| 1628 |
return None
|
| 1629 |
+
|
| 1630 |
+
print(f" 📊 Found {len(coins)} potential matches for {symbol}")
|
| 1631 |
+
|
| 1632 |
+
best_coin = None
|
| 1633 |
+
search_symbol_lower = symbol.lower()
|
| 1634 |
+
for coin in coins:
|
| 1635 |
+
coin_symbol = coin.get('symbol', '').lower()
|
| 1636 |
+
coin_name = coin.get('name', '').lower()
|
| 1637 |
+
if coin_symbol == search_symbol_lower:
|
| 1638 |
+
best_coin = coin; print(f" ✅ Exact symbol match: {coin.get('name')}"); break
|
| 1639 |
+
if not best_coin and coin_name == search_symbol_lower:
|
| 1640 |
+
best_coin = coin; print(f" ✅ Exact name match: {coin.get('name')}")
|
| 1641 |
+
if not best_coin:
|
| 1642 |
+
best_coin = coins[0]; print(f" ⚠️ Using first result: {best_coin.get('name')}")
|
| 1643 |
+
|
| 1644 |
+
coin_id = best_coin.get('id')
|
| 1645 |
+
if not coin_id: print(f" ❌ No ID found"); return None
|
| 1646 |
+
|
| 1647 |
+
print(f" 🔍 Fetching details for CoinGecko ID: {coin_id}")
|
| 1648 |
+
detail_url = f"https://api.coingecko.com/api/v3/coins/{coin_id}"
|
| 1649 |
+
await asyncio.sleep(1.1)
|
| 1650 |
+
detail_response = await self.http_client.get(detail_url, headers=headers, timeout=15.0)
|
| 1651 |
+
|
| 1652 |
+
if detail_response.status_code == 429:
|
| 1653 |
+
wait_time = 5 * (attempt + 1)
|
| 1654 |
+
print(f" ⏰ Rate limit (Details) for {coin_id} - Waiting {wait_time}s")
|
| 1655 |
+
await asyncio.sleep(wait_time)
|
| 1656 |
+
detail_response = await self.http_client.get(detail_url, headers=headers, timeout=15.0)
|
| 1657 |
+
|
| 1658 |
+
detail_response.raise_for_status()
|
| 1659 |
+
detail_data = detail_response.json()
|
| 1660 |
+
platforms = detail_data.get('platforms', {})
|
| 1661 |
+
if not platforms: print(f" ❌ No platform data found"); return None
|
| 1662 |
+
|
| 1663 |
+
network_priority = ['ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom', 'solana']
|
| 1664 |
+
network_map = {'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom', 'solana': 'solana'}
|
| 1665 |
+
|
| 1666 |
+
for platform_cg in network_priority:
|
| 1667 |
+
address = platforms.get(platform_cg)
|
| 1668 |
+
if address and isinstance(address, str) and address.strip():
|
| 1669 |
+
network = network_map.get(platform_cg)
|
| 1670 |
+
if network:
|
| 1671 |
+
print(f" ✅ Found contract on {network}: {address}")
|
| 1672 |
+
return address, network
|
| 1673 |
+
|
| 1674 |
+
print(f" ❌ No contract found on supported platforms for {coin_id}")
|
| 1675 |
+
return None
|
| 1676 |
+
|
| 1677 |
+
except Exception as inner_e:
|
| 1678 |
+
if "Cannot reopen a client instance" in str(inner_e):
|
| 1679 |
+
print(f" ❌ فشل فادح (CoinGecko): محاولة استخدام client مغلق. {inner_e}")
|
| 1680 |
+
return None
|
| 1681 |
+
if isinstance(inner_e, httpx.HTTPStatusError):
|
| 1682 |
+
print(f" �� HTTP Error from CoinGecko ({inner_e.request.url}): {inner_e.response.status_code}")
|
| 1683 |
+
elif isinstance(inner_e, httpx.RequestError):
|
| 1684 |
+
print(f" ❌ Connection Error with CoinGecko ({inner_e.request.url}): {inner_e}")
|
| 1685 |
+
else:
|
| 1686 |
+
print(f" ❌ Unexpected error during CoinGecko API call (Attempt {attempt + 1}): {inner_e}")
|
| 1687 |
+
|
| 1688 |
+
if attempt < max_retries - 1: await asyncio.sleep(1); continue
|
| 1689 |
+
else: return None
|
| 1690 |
|
| 1691 |
print(f" ❌ Failed to get contract from CoinGecko for {symbol} after {max_retries} attempts.")
|
| 1692 |
return None
|
|
|
|
| 1780 |
}
|
| 1781 |
|
| 1782 |
whale_signal = whale_data.get('trading_signal', {})
|
| 1783 |
+
analysis_details = whale_data
|
| 1784 |
|
| 1785 |
return {
|
| 1786 |
'action': whale_signal.get('action', 'HOLD'),
|
|
|
|
| 1811 |
print(f"⚠️ خطأ أثناء إغلاق HTTP client لمراقب الحيتان: {e}")
|
| 1812 |
|
| 1813 |
|
| 1814 |
+
print("✅ EnhancedWhaleMonitor loaded - RPC Rate Limiting (Semaphore) & Updated Endpoints")
|