|
|
|
|
|
import os |
|
|
import asyncio |
|
|
import httpx |
|
|
import json |
|
|
import traceback |
|
|
import time |
|
|
from datetime import datetime, timedelta |
|
|
from collections import defaultdict, deque |
|
|
import ccxt.pro as ccxt |
|
|
import numpy as np |
|
|
import logging |
|
|
import ssl |
|
|
from botocore.exceptions import ClientError |
|
|
import base58 |
|
|
|
|
|
|
|
|
logging.getLogger("httpx").setLevel(logging.WARNING) |
|
|
logging.getLogger("httpcore").setLevel(logging.WARNING) |
|
|
|
|
|
|
|
|
TRANSFER_EVENT_SIGNATURE = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" |
|
|
|
|
|
class EnhancedWhaleMonitor: |
|
|
def __init__(self, contracts_db=None, r2_service=None): |
|
|
|
|
|
self.ssl_context = ssl.create_default_context() |
|
|
|
|
|
self.http_client = httpx.AsyncClient( |
|
|
timeout=30.0, |
|
|
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), |
|
|
follow_redirects=True, |
|
|
verify=self.ssl_context |
|
|
) |
|
|
|
|
|
|
|
|
self.moralis_key = os.getenv("MORALIS_KEY") |
|
|
self.etherscan_key = os.getenv("ETHERSCAN_KEY") |
|
|
self.infura_key = os.getenv("INFURA_KEY") |
|
|
|
|
|
self.bscscan_key = os.getenv("BSCSCAN_KEY") |
|
|
self.polygonscan_key = os.getenv("POLYGONSCAN_KEY") |
|
|
|
|
|
|
|
|
|
|
|
self._validate_api_keys() |
|
|
|
|
|
self.whale_threshold_usd = 50000 |
|
|
|
|
|
|
|
|
self.contracts_db = {} |
|
|
self._initialize_contracts_db(contracts_db or {}) |
|
|
|
|
|
self.data_manager = None |
|
|
|
|
|
self.coingecko_semaphore = asyncio.Semaphore(1) |
|
|
|
|
|
self.rpc_semaphore = asyncio.Semaphore(5) |
|
|
|
|
|
self.r2_service = r2_service |
|
|
|
|
|
self.address_labels = {} |
|
|
self._initialize_comprehensive_exchange_addresses() |
|
|
|
|
|
self.token_price_cache = {} |
|
|
self.token_decimals_cache = {} |
|
|
|
|
|
|
|
|
self.supported_networks = { |
|
|
'ethereum': { |
|
|
'name': 'Ethereum', |
|
|
'chain_id': '0x1', |
|
|
'native_coin': 'ETH', |
|
|
'explorer': 'etherscan', |
|
|
'rpc_endpoints': self._get_ethereum_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'bsc': { |
|
|
'name': 'Binance Smart Chain', |
|
|
'chain_id': '0x38', |
|
|
'native_coin': 'BNB', |
|
|
'explorer': 'bscscan', |
|
|
'rpc_endpoints': self._get_bsc_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'polygon': { |
|
|
'name': 'Polygon', |
|
|
'chain_id': '0x89', |
|
|
'native_coin': 'MATIC', |
|
|
'explorer': 'polygonscan', |
|
|
'rpc_endpoints': self._get_polygon_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'arbitrum': { |
|
|
'name': 'Arbitrum', |
|
|
'chain_id': '0xa4b1', |
|
|
'native_coin': 'ETH', |
|
|
'explorer': 'arbiscan', |
|
|
'rpc_endpoints': self._get_arbitrum_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'optimism': { |
|
|
'name': 'Optimism', |
|
|
'chain_id': '0xa', |
|
|
'native_coin': 'ETH', |
|
|
'explorer': 'optimistic', |
|
|
'rpc_endpoints': self._get_optimism_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'avalanche': { |
|
|
'name': 'Avalanche', |
|
|
'chain_id': '0xa86a', |
|
|
'native_coin': 'AVAX', |
|
|
'explorer': 'snowtrace', |
|
|
'rpc_endpoints': self._get_avalanche_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'fantom': { |
|
|
'name': 'Fantom', |
|
|
'chain_id': '0xfa', |
|
|
'native_coin': 'FTM', |
|
|
'explorer': 'ftmscan', |
|
|
'rpc_endpoints': self._get_fantom_rpc_endpoints(), |
|
|
'type': 'evm' |
|
|
}, |
|
|
'solana': { |
|
|
'name': 'Solana', |
|
|
'chain_id': 'mainnet-beta', |
|
|
'native_coin': 'SOL', |
|
|
'explorer': 'solscan', |
|
|
'rpc_endpoints': self._get_solana_rpc_endpoints(), |
|
|
'type': 'solana' |
|
|
} |
|
|
} |
|
|
|
|
|
if self.r2_service: |
|
|
asyncio.create_task(self._load_contracts_from_r2()) |
|
|
|
|
|
def _initialize_contracts_db(self, initial_contracts): |
|
|
"""تهيئة قاعدة بيانات العقود مع تحسين تخزين الشبكات""" |
|
|
print("🔄 تهيئة قاعدة بيانات العقود...") |
|
|
|
|
|
for symbol, contract_data in initial_contracts.items(): |
|
|
symbol_lower = symbol.lower() |
|
|
if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data: |
|
|
self.contracts_db[symbol_lower] = contract_data |
|
|
elif isinstance(contract_data, str): |
|
|
self.contracts_db[symbol_lower] = { |
|
|
'address': contract_data, |
|
|
'network': self._detect_network_from_address(contract_data) |
|
|
} |
|
|
|
|
|
|
|
|
print(f"✅ تم تحميل {len(self.contracts_db)} عقد في قاعدة البيانات") |
|
|
|
|
|
|
|
|
def _detect_network_from_address(self, address): |
|
|
"""اكتشاف الشبكة من عنوان العقد""" |
|
|
if not isinstance(address, str): |
|
|
return 'ethereum' |
|
|
|
|
|
address_lower = address.lower() |
|
|
|
|
|
if address_lower.startswith('0x') and len(address_lower) == 42: |
|
|
|
|
|
|
|
|
return 'ethereum' |
|
|
elif len(address_lower) >= 32 and len(address_lower) <= 44: |
|
|
base58_chars = set("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") |
|
|
if all(char in base58_chars for char in address): |
|
|
try: |
|
|
base58.b58decode(address) |
|
|
return 'solana' |
|
|
except ValueError: |
|
|
return 'ethereum' |
|
|
else: |
|
|
return 'ethereum' |
|
|
else: |
|
|
return 'ethereum' |
|
|
|
|
|
def _validate_api_keys(self): |
|
|
"""التحقق من صحة مفاتيح API المطلوبة فقط""" |
|
|
print("🔍 التحقق من صحة مفاتيح API...") |
|
|
|
|
|
|
|
|
api_keys_to_check = { |
|
|
'MORALIS_KEY': 'moralis_key', |
|
|
'ETHERSCAN_KEY': 'etherscan_key', |
|
|
'INFURA_KEY': 'infura_key', |
|
|
'BSCSCAN_KEY': 'bscscan_key', |
|
|
'POLYGONSCAN_KEY': 'polygonscan_key' |
|
|
|
|
|
} |
|
|
|
|
|
for env_var, attr_name in api_keys_to_check.items(): |
|
|
|
|
|
if hasattr(self, attr_name): |
|
|
key_value = getattr(self, attr_name, None) |
|
|
if key_value: |
|
|
if isinstance(key_value, str) and len(key_value) >= 10: |
|
|
print(f"✅ مفتاح {env_var} موجود وصالح مبدئيًا.") |
|
|
else: |
|
|
print(f"❌ مفتاح {env_var} موجود ولكنه غير صالح. سيتم التعطيل.") |
|
|
setattr(self, attr_name, None) |
|
|
else: |
|
|
print(f"⚠️ مفتاح {env_var} غير متوفر أو فارغ.") |
|
|
setattr(self, attr_name, None) |
|
|
else: |
|
|
|
|
|
print(f"ℹ️ خاصية المفتاح {attr_name} (لـ {env_var}) غير معرفة في الكلاس.") |
|
|
setattr(self, attr_name, None) |
|
|
|
|
|
def _get_ethereum_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة Ethereum""" |
|
|
endpoints = [] |
|
|
if self.infura_key: |
|
|
endpoints.append(f'https://mainnet.infura.io/v3/{self.infura_key}') |
|
|
endpoints.extend([ |
|
|
'https://rpc.ankr.com/eth', |
|
|
'https://cloudflare-eth.com', |
|
|
'https://eth.llamarpc.com', |
|
|
'https://ethereum.publicnode.com', |
|
|
'https://1rpc.io/eth', |
|
|
]) |
|
|
return endpoints |
|
|
|
|
|
def _get_bsc_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة BSC""" |
|
|
return [ |
|
|
'https://bsc-dataseed.binance.org/', |
|
|
'https://bsc-dataseed1.defibit.io/', |
|
|
'https://bsc.publicnode.com', |
|
|
'https://binance.llamarpc.com', |
|
|
'https://1rpc.io/bnb', |
|
|
'https://rpc.ankr.com/bsc' |
|
|
] |
|
|
|
|
|
def _get_polygon_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة Polygon""" |
|
|
endpoints = [] |
|
|
if self.infura_key: |
|
|
endpoints.append(f'https://polygon-mainnet.infura.io/v3/{self.infura_key}') |
|
|
endpoints.extend([ |
|
|
'https://polygon-rpc.com', |
|
|
'https://polygon.llamarpc.com', |
|
|
'https://polygon-bor.publicnode.com', |
|
|
'https://1rpc.io/matic', |
|
|
'https://rpc.ankr.com/polygon' |
|
|
]) |
|
|
return endpoints |
|
|
|
|
|
def _get_arbitrum_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة Arbitrum""" |
|
|
endpoints = [] |
|
|
if self.infura_key: |
|
|
endpoints.append(f'https://arbitrum-mainnet.infura.io/v3/{self.infura_key}') |
|
|
endpoints.extend([ |
|
|
'https://arb1.arbitrum.io/rpc', |
|
|
'https://arbitrum.llamarpc.com', |
|
|
'https://arbitrum.publicnode.com', |
|
|
'https://1rpc.io/arb', |
|
|
'https://rpc.ankr.com/arbitrum' |
|
|
]) |
|
|
return endpoints |
|
|
|
|
|
def _get_optimism_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة Optimism""" |
|
|
endpoints = [] |
|
|
if self.infura_key: |
|
|
endpoints.append(f'https://optimism-mainnet.infura.io/v3/{self.infura_key}') |
|
|
endpoints.extend([ |
|
|
'https://mainnet.optimism.io', |
|
|
'https://optimism.llamarpc.com', |
|
|
'https://optimism.publicnode.com', |
|
|
'https://1rpc.io/op', |
|
|
'https://rpc.ankr.com/optimism' |
|
|
]) |
|
|
return endpoints |
|
|
|
|
|
def _get_avalanche_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة Avalanche""" |
|
|
return [ |
|
|
'https://api.avax.network/ext/bc/C/rpc', |
|
|
'https://avalanche.publicnode.com', |
|
|
'https://avax.llamarpc.com', |
|
|
'https://1rpc.io/avax/c', |
|
|
'https://rpc.ankr.com/avalanche' |
|
|
] |
|
|
|
|
|
def _get_fantom_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC لشبكة Fantom""" |
|
|
return [ |
|
|
'https://rpc.ftm.tools/', |
|
|
'https://fantom.publicnode.com', |
|
|
'https://fantom.llamarpc.com', |
|
|
'https://1rpc.io/ftm', |
|
|
'https://rpc.ankr.com/fantom' |
|
|
] |
|
|
|
|
|
def _get_solana_rpc_endpoints(self): |
|
|
"""الحصول على نقاط RPC مجانية لشبكة Solana (بدون مفاتيح API) - محدثة""" |
|
|
endpoints = [ |
|
|
'https://api.mainnet-beta.solana.com', |
|
|
'https://solana-mainnet.publicnode.com', |
|
|
'https://ssc-dao.genesysgo.net/', |
|
|
'https://solana-mainnet.rpc.extrnode.com', |
|
|
'https://solana-mainnet.phantom.tech/', |
|
|
'https://rpc.ankr.com/solana', |
|
|
|
|
|
] |
|
|
print(f"ℹ️ قائمة Solana RPC Endpoints (مجانية وبدون مفاتيح) المستخدمة: {endpoints}") |
|
|
return endpoints |
|
|
|
|
|
def _initialize_comprehensive_exchange_addresses(self): |
|
|
"""تهيئة قاعدة بيانات شاملة لعناوين كل المنصات""" |
|
|
self.exchange_addresses = { |
|
|
'kucoin': [ |
|
|
'0x2b5634c42055806a59e9107ed44d43c426e58258', '0x689c56aef474df92d44a1b70850f808488f9769c', |
|
|
'0xa1d8d972560c2f8144af871db508f0b0b10a3fbf', '0x4ad64983349c49defe8d7a4686202d24b25d0ce8', |
|
|
'0x1692e170361cefd1eb7240ec13d048fd9af6d667', '0xd6216fc19db775df9774a6e33526131da7d19a2c', |
|
|
'0xe59cd29be3be4461d79c0881a238c467a2b3775c', '0x899b5d52671830f567bf43a14684eb14e1f945fe' |
|
|
], |
|
|
'binance': [ |
|
|
'0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be', '0xd551234ae421e3bcba99a0da6d736074f22192ff', |
|
|
'0x564286362092d8e7936f0549571a803b203aaced', '0x0681d8db095565fe8a346fa0277bffde9c0edbbf', |
|
|
'0xfe9e8709d3215310075d67e3ed32a380ccf451c8', '0x28c6c06298d514db089934071355e5743bf21d60' |
|
|
], |
|
|
'coinbase': [ |
|
|
'0x71660c4005ba85c37ccec55d0c4493e66fe775d3', '0x503828976d22510aad0201ac7ec88293211d23da', |
|
|
'0xddfabcdc4d8ffc6d5beaf154f18b778f892a0740' |
|
|
], |
|
|
'kraken': [ '0x2910543af39aba0cd09dbb2d50200b3e800a63d2', '0xa160cdab225685da1d56aa342ad8841c3b53f291' ], |
|
|
'okx': [ '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b', '0x2c8fbb630289363ac80705a1a61273f76fd5a161' ], |
|
|
'gate': [ '0x0d0707963952f2fba59dd06f2b425ace40b492fe', '0xc6f9741f5a5a676b91171804cf3c500ab438bb6e' ], |
|
|
'uniswap': [ '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45' ], |
|
|
'pancakeswap': [ '0x10ed43c718714eb63d5aa57b78b54704e256024e', '0x13f4ea83d0bd40e75c8222255bc855a974568dd4' ], |
|
|
'solana_kucoin': ['F3a5ZLPKUCrCj6CGKP2m9wS9Y2L8RcUBoJq9L2D2sUYi', '2AQdpHJ2JpcEgPiATFnAKfV9hPMvouWJAhKvamQ2Krqy' ], |
|
|
'solana_binance': ['5tzFkiKscXHK5ZXCGbXZJpXaWuBtZ6RrH8eL2a7Yi7Vn', '9WzDXwBbmkg8ZTbNMqUxvQRApF22xwsgMj2SUZrchK2E'], |
|
|
'solana_coinbase': ['CeBiLnAE3UAW9mCDSjD1VULKLeQjefjN4d941fVKazSM'], |
|
|
'solana_wormhole': ['worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth'] |
|
|
} |
|
|
|
|
|
self.address_categories = {'exchange': set(), 'cex': set(), 'dex': set(), 'bridge': set(), 'whale': set(), 'unknown': set()} |
|
|
|
|
|
for category, addresses in self.exchange_addresses.items(): |
|
|
for address in addresses: |
|
|
if not isinstance(address, str): continue |
|
|
addr_lower = address.lower() |
|
|
self.address_labels[addr_lower] = category |
|
|
self.address_categories['exchange'].add(addr_lower) |
|
|
|
|
|
if category in ['kucoin', 'binance', 'coinbase', 'kraken', 'okx', 'gate', 'solana_kucoin', 'solana_binance', 'solana_coinbase']: |
|
|
self.address_categories['cex'].add(addr_lower) |
|
|
elif category in ['uniswap', 'pancakeswap']: |
|
|
self.address_categories['dex'].add(addr_lower) |
|
|
elif 'wormhole' in category: |
|
|
self.address_categories['bridge'].add(addr_lower) |
|
|
|
|
|
print(f"✅ تم تهيئة {len(self.address_labels)} عنوان منصة/بروتوكول معروف.") |
|
|
|
|
|
|
|
|
async def _load_contracts_from_r2(self): |
|
|
"""تحميل قاعدة بيانات العقود من R2""" |
|
|
if not self.r2_service: return |
|
|
try: |
|
|
key = "contracts.json" |
|
|
response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key) |
|
|
contracts_data = json.loads(response['Body'].read()) |
|
|
|
|
|
loaded_count = 0 |
|
|
updated_count = 0 |
|
|
updated_contracts_db = self.contracts_db.copy() |
|
|
for symbol, contract_data in contracts_data.items(): |
|
|
symbol_lower = symbol.lower() |
|
|
if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data: |
|
|
updated_contracts_db[symbol_lower] = contract_data |
|
|
loaded_count += 1 |
|
|
elif isinstance(contract_data, str): |
|
|
new_format = { |
|
|
'address': contract_data, |
|
|
'network': self._detect_network_from_address(contract_data) |
|
|
} |
|
|
updated_contracts_db[symbol_lower] = new_format |
|
|
loaded_count += 1 |
|
|
updated_count +=1 |
|
|
|
|
|
self.contracts_db = updated_contracts_db |
|
|
|
|
|
print(f"✅ تم تحميل {loaded_count} عقد من R2.") |
|
|
if updated_count > 0: |
|
|
print(f" ℹ️ تم تحديث صيغة {updated_count} عقد إلى الصيغة الجديدة.") |
|
|
await self._save_contracts_to_r2() |
|
|
|
|
|
except ClientError as e: |
|
|
if e.response['Error']['Code'] == 'NoSuchKey': |
|
|
print("⚠️ لم يتم العثور على قاعدة بيانات العقود في R2. ستبدأ فارغة.") |
|
|
else: |
|
|
print(f"❌ خطأ ClientError أثناء تحميل العقود من R2: {e}") |
|
|
except Exception as e: |
|
|
print(f"❌ خطأ عام أثناء تحميل العقود من R2: {e}") |
|
|
|
|
|
async def get_symbol_whale_activity(self, symbol, contract_address=None): |
|
|
""" |
|
|
مراقبة تحركات الحيتان لعملة محددة مع معالجة العملات الأصلية |
|
|
""" |
|
|
try: |
|
|
print(f"🔍 بدء مراقبة الحيتان الحقيقية للعملة: {symbol}") |
|
|
|
|
|
native_coins = ['BTC', 'LTC', 'ETH', 'BNB', 'ADA', 'DOT', 'XRP', 'DOGE', 'BCH', 'XLM', 'TRX', 'EOS', 'XMR'] |
|
|
base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper() |
|
|
|
|
|
if base_symbol in native_coins: |
|
|
print(f"ℹ️ {symbol} عملة أصلية - لا توجد بيانات حيتان للتوكنات") |
|
|
return self._create_native_coin_response(symbol) |
|
|
|
|
|
contract_info = None |
|
|
network = None |
|
|
|
|
|
if contract_address and isinstance(contract_address, str): |
|
|
network = self._detect_network_from_address(contract_address) |
|
|
contract_info_from_db = await self._find_contract_address_enhanced(symbol) |
|
|
if contract_info_from_db and contract_info_from_db.get('address', '').lower() != contract_address.lower(): |
|
|
print(f" ⚠️ تم توفير عنوان عقد ({contract_address}) يختلف عن المخزن لـ {symbol}. سيتم استخدام العنوان المُقدم.") |
|
|
contract_info = {'address': contract_address, 'network': network} |
|
|
elif contract_info_from_db: |
|
|
contract_info = contract_info_from_db |
|
|
else: |
|
|
contract_info = {'address': contract_address, 'network': network} |
|
|
else: |
|
|
contract_info = await self._find_contract_address_enhanced(symbol) |
|
|
|
|
|
if not contract_info or not contract_info.get('address') or not contract_info.get('network'): |
|
|
print(f"❌ لم يتم العثور على عقد أو شبكة للعملة {symbol}") |
|
|
return self._create_no_contract_response(symbol) |
|
|
|
|
|
contract_address_final = contract_info['address'] |
|
|
network_final = contract_info['network'] |
|
|
|
|
|
print(f"🌐 البحث في الشبكة المحددة: {network_final} للعقد: {contract_address_final}") |
|
|
|
|
|
transfers = await self._get_targeted_transfer_data(contract_address_final, network_final) |
|
|
|
|
|
if not transfers: |
|
|
print(f"⚠️ لم يتم العثور على تحويلات للعملة {symbol} على شبكة {network_final}") |
|
|
return self._create_no_transfers_response(symbol) |
|
|
|
|
|
recent_transfers = self._filter_recent_transfers(transfers, max_minutes=120) |
|
|
|
|
|
if not recent_transfers: |
|
|
print(f"⚠️ لا توجد تحويلات حديثة للعملة {symbol}") |
|
|
return self._create_no_recent_activity_response(symbol) |
|
|
|
|
|
print(f"📊 تم جلب {len(recent_transfers)} تحويلة حديثة فريدة للعملة {symbol}") |
|
|
|
|
|
analysis = await self._analyze_enhanced_whale_impact(recent_transfers, symbol, contract_address_final, network_final) |
|
|
|
|
|
return analysis |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ خطأ في مراقبة الحيتان للعملة {symbol}: {e}") |
|
|
traceback.print_exc() |
|
|
return self._create_error_response(symbol, str(e)) |
|
|
|
|
|
def _create_native_coin_response(self, symbol): |
|
|
"""إنشاء رد للعملات الأصلية التي ليس لها tokens""" |
|
|
return { |
|
|
'symbol': symbol, 'data_available': False, 'error': 'NATIVE_COIN_NO_TOKEN', |
|
|
'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': f'{symbol} عملة أصلية - لا توجد بيانات حيتان للتوكنات'}, |
|
|
'llm_friendly_summary': {'whale_activity_summary': f'{symbol} عملة أصلية - نظام مراقبة الحيتان الحالي مصمم للتوكنات فقط', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NOT_APPLICABLE'}} |
|
|
} |
|
|
|
|
|
async def _get_targeted_transfer_data(self, contract_address, network): |
|
|
"""جلب بيانات التحويلات من الشبكة المحددة فقط""" |
|
|
print(f"🌐 جلب بيانات التحويلات من شبكة {network} للعقد: {contract_address}") |
|
|
|
|
|
if isinstance(contract_address, dict): |
|
|
contract_address = contract_address.get(network) |
|
|
if not contract_address: |
|
|
print(f"❌ لم يتم العثور على عنوان للشبكة {network}") |
|
|
return [] |
|
|
|
|
|
if not isinstance(contract_address, str): |
|
|
print(f"❌ عنوان العقد غير صالح: {type(contract_address)}") |
|
|
return [] |
|
|
|
|
|
if network in self.supported_networks: |
|
|
net_config = self.supported_networks[network] |
|
|
if net_config['type'] == 'evm': |
|
|
return await self._get_evm_network_transfer_data(contract_address, network) |
|
|
elif net_config['type'] == 'solana': |
|
|
return await self._get_solana_transfer_data(contract_address, network) |
|
|
|
|
|
print(f"⚠️ نوع الشبكة {network} غير مدعوم حاليًا.") |
|
|
return [] |
|
|
|
|
|
async def _get_evm_network_transfer_data(self, contract_address, network): |
|
|
"""جلب بيانات التحويلات من شبكة EVM محددة (باستخدام RPC و Explorer)""" |
|
|
try: |
|
|
transfers = [] |
|
|
rpc_successful = False |
|
|
|
|
|
try: |
|
|
rpc_transfers = await self._get_rpc_token_data_targeted(contract_address, network) |
|
|
if rpc_transfers: |
|
|
transfers.extend(rpc_transfers) |
|
|
rpc_successful = True |
|
|
print(f"✅ RPC {network}: تم جلب {len(rpc_transfers)} تحويلة عبر eth_getLogs") |
|
|
else: |
|
|
print(f"⚠️ RPC {network}: لم يتم العثور على تحويلات عبر eth_getLogs") |
|
|
except Exception as rpc_error: |
|
|
print(f"❌ خطأ في جلب بيانات RPC {network}: {rpc_error}") |
|
|
|
|
|
if not rpc_successful or len(transfers) < 5: |
|
|
print(f"ℹ️ محاولة جلب بيانات إضافية من Explorer لـ {network}...") |
|
|
try: |
|
|
explorer_transfers = await self._get_explorer_token_data(contract_address, network) |
|
|
if explorer_transfers: |
|
|
existing_hashes = {f"{t.get('hash')}-{t.get('logIndex','N/A')}" for t in transfers} |
|
|
new_explorer_transfers = [] |
|
|
for et in explorer_transfers: |
|
|
explorer_key = f"{et.get('hash')}-{et.get('logIndex','N/A')}" |
|
|
if explorer_key not in existing_hashes: |
|
|
new_explorer_transfers.append(et) |
|
|
|
|
|
if new_explorer_transfers: |
|
|
transfers.extend(new_explorer_transfers) |
|
|
print(f"✅ Explorer {network}: تم إضافة {len(new_explorer_transfers)} تحويلة جديدة.") |
|
|
else: |
|
|
print(f"⚠️ Explorer {network}: لم يتم العثور على تحويلات جديدة.") |
|
|
else: |
|
|
print(f"⚠️ Explorer {network}: لم يتم العثور على تحويلات.") |
|
|
except Exception as explorer_error: |
|
|
print(f"❌ خطأ في جلب بيانات Explorer {network}: {explorer_error}") |
|
|
|
|
|
print(f"📊 إجمالي التحويلات المجمعة لـ {network}: {len(transfers)}") |
|
|
final_transfers = [] |
|
|
seen_keys = set() |
|
|
for t in transfers: |
|
|
key = f"{t.get('hash')}-{t.get('logIndex','N/A')}" |
|
|
if key not in seen_keys: |
|
|
final_transfers.append(t) |
|
|
seen_keys.add(key) |
|
|
|
|
|
return final_transfers |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ خطأ عام في جلب بيانات {network}: {e}") |
|
|
return [] |
|
|
|
|
|
async def _get_rpc_token_data_targeted(self, contract_address, network='ethereum', blocks_to_scan=500): |
|
|
"""جلب تحويلات التوكن ERC20 عبر RPC باستخدام eth_getLogs""" |
|
|
try: |
|
|
if network not in self.supported_networks or self.supported_networks[network]['type'] != 'evm': |
|
|
print(f"⚠️ شبكة {network} غير مدعومة أو ليست EVM لـ eth_getLogs.") |
|
|
return [] |
|
|
|
|
|
if not isinstance(contract_address, str) or not contract_address.startswith('0x'): |
|
|
print(f"❌ عنوان العقد غير صالح لـ EVM: {contract_address}") |
|
|
return [] |
|
|
contract_address_checksum = contract_address |
|
|
|
|
|
endpoints = self.supported_networks[network]['rpc_endpoints'] |
|
|
if not endpoints: |
|
|
print(f" ❌ لا توجد نقاط RPC معرفة لشبكة {network}.") |
|
|
return [] |
|
|
|
|
|
transfers = [] |
|
|
successful_endpoint = None |
|
|
|
|
|
for endpoint in endpoints: |
|
|
try: |
|
|
endpoint_name = endpoint.split('//')[1].split('/')[0] if '//' in endpoint else endpoint |
|
|
print(f" 🔍 محاولة جلب سجلات التحويلات لـ {network} عبر {endpoint_name}...") |
|
|
|
|
|
|
|
|
async with self.rpc_semaphore: |
|
|
payload_block = {"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": int(time.time())} |
|
|
response_block = await self.http_client.post(endpoint, json=payload_block, timeout=15.0) |
|
|
response_block.raise_for_status() |
|
|
json_response_block = response_block.json() |
|
|
result_block = json_response_block.get('result') |
|
|
if not result_block: |
|
|
rpc_error = json_response_block.get('error') |
|
|
error_msg = rpc_error.get('message') if rpc_error else 'No block number result' |
|
|
print(f" ❌ لم يتم الحصول على رقم أحدث كتلة من {endpoint_name}: {error_msg}") |
|
|
continue |
|
|
latest_block = int(result_block, 16) |
|
|
|
|
|
async with self.rpc_semaphore: |
|
|
payload_latest_block_time = {"jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": [hex(latest_block), False], "id": int(time.time())+1} |
|
|
response_latest_time = await self.http_client.post(endpoint, json=payload_latest_block_time, timeout=15.0) |
|
|
latest_block_timestamp_approx = int(time.time()) |
|
|
if response_latest_time.status_code == 200: |
|
|
latest_block_data = response_latest_time.json().get('result') |
|
|
if latest_block_data and latest_block_data.get('timestamp'): |
|
|
latest_block_timestamp_approx = int(latest_block_data['timestamp'], 16) |
|
|
|
|
|
from_block = max(0, latest_block - blocks_to_scan) |
|
|
print(f" 📦 فحص الكتل من {from_block} إلى {latest_block} (آخر {blocks_to_scan} كتل تقريباً)") |
|
|
|
|
|
payload_logs = { |
|
|
"jsonrpc": "2.0", "method": "eth_getLogs", |
|
|
"params": [{"fromBlock": hex(from_block), "toBlock": hex(latest_block), "address": contract_address_checksum, "topics": [TRANSFER_EVENT_SIGNATURE]}], |
|
|
"id": int(time.time())+2 |
|
|
} |
|
|
async with self.rpc_semaphore: |
|
|
response_logs = await self.http_client.post(endpoint, json=payload_logs, timeout=45.0) |
|
|
response_logs.raise_for_status() |
|
|
json_response_logs = response_logs.json() |
|
|
logs = json_response_logs.get('result') |
|
|
|
|
|
if logs is None: |
|
|
rpc_error = json_response_logs.get('error') |
|
|
error_msg = rpc_error.get('message') if rpc_error else 'Invalid logs response' |
|
|
print(f" ❌ استجابة السجلات غير صالحة من {endpoint_name}: {error_msg}") |
|
|
continue |
|
|
|
|
|
if not logs: |
|
|
print(f" ✅ لا توجد سجلات تحويل لـ {contract_address} في آخر {blocks_to_scan} كتل عبر {endpoint_name}") |
|
|
successful_endpoint = endpoint_name |
|
|
break |
|
|
|
|
|
print(f" 📊 تم العثور على {len(logs)} سجل تحويل محتمل.") |
|
|
|
|
|
for log in logs: |
|
|
try: |
|
|
topics = log.get('topics', []) |
|
|
data = log.get('data', '0x') |
|
|
block_num_hex = log.get('blockNumber') |
|
|
tx_hash = log.get('transactionHash') |
|
|
log_index_hex = log.get('logIndex', '0x0') |
|
|
|
|
|
if len(topics) == 3 and data != '0x' and block_num_hex and tx_hash: |
|
|
sender = '0x' + topics[1][26:] |
|
|
receiver = '0x' + topics[2][26:] |
|
|
value = str(int(data, 16)) |
|
|
block_num = str(int(block_num_hex, 16)) |
|
|
log_index = str(int(log_index_hex, 16)) |
|
|
|
|
|
transfers.append({ |
|
|
'hash': tx_hash, 'from': sender.lower(), 'to': receiver.lower(), |
|
|
'value': value, 'timeStamp': str(latest_block_timestamp_approx), |
|
|
'blockNumber': block_num, 'network': network, 'logIndex': log_index |
|
|
}) |
|
|
else: |
|
|
print(f" ⚠️ سجل غير متوافق مع Transfer Event: Topics={len(topics)}, Data={data[:10]}...") |
|
|
|
|
|
except (ValueError, TypeError, KeyError, IndexError) as log_parse_error: |
|
|
print(f" ⚠️ خطأ في تحليل سجل فردي: {log_parse_error} - Log: {log}") |
|
|
continue |
|
|
|
|
|
print(f" ✅ تم تحليل {len(transfers)} تحويلة بنجاح من {endpoint_name}") |
|
|
successful_endpoint = endpoint_name |
|
|
break |
|
|
|
|
|
except ssl.SSLCertVerificationError as ssl_err: |
|
|
print(f" ❌ خطأ SSL مع {endpoint_name}: {ssl_err}") |
|
|
continue |
|
|
except httpx.HTTPStatusError as http_err: |
|
|
print(f" ❌ خطأ HTTP من {endpoint_name}: {http_err.response.status_code} - {http_err}") |
|
|
continue |
|
|
except (httpx.RequestError, asyncio.TimeoutError) as req_err: |
|
|
print(f" ❌ خطأ اتصال/مهلة بـ {endpoint_name}: {req_err}") |
|
|
continue |
|
|
except json.JSONDecodeError as json_err: |
|
|
print(f" ❌ خطأ في تحليل استجابة JSON من {endpoint_name}: {json_err}") |
|
|
continue |
|
|
except Exception as e: |
|
|
if "Cannot reopen a client instance" in str(e): |
|
|
print(f" ❌ فشل فادح: محاولة استخدام client مغلق. {e}") |
|
|
return transfers |
|
|
print(f" ❌ فشل غير متوقع مع {endpoint_name}: {e}") |
|
|
traceback.print_exc() |
|
|
continue |
|
|
|
|
|
if successful_endpoint: |
|
|
print(f"✅ اكتمل جلب بيانات RPC لـ {network} بنجاح باستخدام {successful_endpoint}. إجمالي التحويلات الأولية: {len(transfers)}") |
|
|
else: |
|
|
print(f"❌ فشلت جميع نقاط RPC لـ {network} في الاستجابة بشكل صحيح.") |
|
|
|
|
|
return transfers |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ فشل عام في جلب بيانات RPC لـ {network}: {e}") |
|
|
traceback.print_exc() |
|
|
return [] |
|
|
|
|
|
|
|
|
async def _get_solana_transfer_data(self, token_address, network='solana', limit=50): |
|
|
"""جلب بيانات التحويلات من شبكة Solana مع تحسين التعامل مع الأخطاء""" |
|
|
try: |
|
|
print(f" 🔍 جلب تحويلات Solana للتوكن: {token_address}") |
|
|
transfers = [] |
|
|
endpoints = self.supported_networks[network]['rpc_endpoints'] |
|
|
if not endpoints: |
|
|
print(f" ❌ لا توجد نقاط RPC معرفة لشبكة Solana.") |
|
|
return [] |
|
|
|
|
|
signatures_found = False |
|
|
successful_endpoint_name = None |
|
|
|
|
|
for endpoint in endpoints: |
|
|
endpoint_name = endpoint.split('//')[-1] |
|
|
try: |
|
|
payload_signatures = { |
|
|
"jsonrpc": "2.0", "id": int(time.time()), |
|
|
"method": "getSignaturesForAddress", |
|
|
"params": [ token_address, {"limit": limit, "commitment": "confirmed"} ] |
|
|
} |
|
|
|
|
|
|
|
|
async with self.rpc_semaphore: |
|
|
response_signatures = await self.http_client.post(endpoint, json=payload_signatures, timeout=20.0) |
|
|
|
|
|
if response_signatures.status_code == 403: |
|
|
print(f" ⚠️ Solana endpoint {endpoint_name} failed: 403 Forbidden") |
|
|
continue |
|
|
elif response_signatures.status_code != 200: |
|
|
print(f" ⚠️ Solana endpoint {endpoint_name} failed: {response_signatures.status_code}") |
|
|
continue |
|
|
|
|
|
data_signatures = response_signatures.json() |
|
|
|
|
|
if 'error' in data_signatures: |
|
|
print(f" ❌ خطأ RPC (getSignatures) من {endpoint_name}: {data_signatures['error'].get('message', 'Unknown RPC error')}") |
|
|
continue |
|
|
if 'result' not in data_signatures or data_signatures['result'] is None: |
|
|
print(f" ✅ Solana: لا توجد معاملات حديثة ({endpoint_name})") |
|
|
successful_endpoint_name = endpoint_name |
|
|
signatures_found = True |
|
|
break |
|
|
|
|
|
signatures_data = data_signatures['result'] |
|
|
if not signatures_data: |
|
|
print(f" ✅ Solana: لا توجد معاملات حديثة ({endpoint_name})") |
|
|
successful_endpoint_name = endpoint_name |
|
|
signatures_found = True |
|
|
break |
|
|
|
|
|
signatures = [tx['signature'] for tx in signatures_data] |
|
|
print(f" 📊 Solana: وجد {len(signatures)} توقيع معاملة محتملة ({endpoint_name})") |
|
|
signatures_found = True |
|
|
successful_endpoint_name = endpoint_name |
|
|
|
|
|
processed_count = 0 |
|
|
transaction_tasks = [] |
|
|
|
|
|
|
|
|
for signature in signatures[:20]: |
|
|
transaction_tasks.append( |
|
|
self._get_solana_transaction_detail_with_retry(self, signature, endpoint, self.http_client) |
|
|
) |
|
|
transaction_details = await asyncio.gather(*transaction_tasks) |
|
|
|
|
|
for detail_result in transaction_details: |
|
|
if detail_result and self._is_solana_token_transfer(detail_result, token_address): |
|
|
transfer = await self._parse_solana_transfer(detail_result, token_address) |
|
|
if transfer: |
|
|
transfers.append(transfer) |
|
|
processed_count += 1 |
|
|
|
|
|
print(f" ✅ Solana: تم تحليل {processed_count} تحويلة توكن فعلية من {endpoint_name}") |
|
|
break |
|
|
|
|
|
except ssl.SSLCertVerificationError as ssl_err: |
|
|
print(f" ❌ فشل endpoint في Solana ({endpoint_name}): خطأ في شهادة SSL - {ssl_err}") |
|
|
continue |
|
|
except httpx.HTTPStatusError as http_err: |
|
|
if http_err.response.status_code == 403: print(f" ⚠️ Solana endpoint {endpoint_name} failed: 403 Forbidden") |
|
|
else: print(f" ❌ خطأ HTTP من Solana ({endpoint_name}): {http_err.response.status_code}") |
|
|
continue |
|
|
except (httpx.RequestError, asyncio.TimeoutError) as req_err: |
|
|
print(f" ❌ خطأ اتصال/مهلة بـ Solana ({endpoint_name}): {req_err}") |
|
|
continue |
|
|
except json.JSONDecodeError as json_err: |
|
|
print(f" ❌ خطأ في تحليل استجابة JSON من Solana ({endpoint_name}): {json_err}") |
|
|
continue |
|
|
except Exception as e: |
|
|
if "Cannot reopen a client instance" in str(e): |
|
|
print(f" ❌ فشل فادح: محاولة استخدام client مغلق. {e}") |
|
|
return transfers |
|
|
print(f" ❌ فشل غير متوقع مع endpoint Solana ({endpoint_name}): {e}") |
|
|
traceback.print_exc() |
|
|
continue |
|
|
|
|
|
if successful_endpoint_name: |
|
|
print(f"✅ اكتمل جلب بيانات Solana بنجاح باستخدام {successful_endpoint_name}. إجمالي التحويلات النهائية: {len(transfers)}") |
|
|
elif not signatures_found: |
|
|
print(f" ❌ فشلت جميع نقاط RPC لـ Solana في الاستجابة بشكل صحيح لجلب التوقيعات.") |
|
|
else: |
|
|
print(f"⚠️ تم العثور على توقيعات Solana ولكن حدثت مشاكل في جلب التفاصيل أو تحليلها. التحويلات النهائية: {len(transfers)}") |
|
|
|
|
|
return transfers |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ فشل عام في جلب بيانات Solana: {e}") |
|
|
traceback.print_exc() |
|
|
return [] |
|
|
|
|
|
|
|
|
|
|
|
async def _get_solana_transaction_detail_with_retry(self, signature, endpoint, client: httpx.AsyncClient, retries=2, delay=0.5): |
|
|
"""جلب تفاصيل معاملة Solana مع محاولات إعادة بسيطة باستخدام client مشترك""" |
|
|
last_exception = None |
|
|
for attempt in range(retries): |
|
|
try: |
|
|
|
|
|
async with self.rpc_semaphore: |
|
|
detail = await self._get_solana_transaction_detail(signature, endpoint, client) |
|
|
|
|
|
if detail: |
|
|
return detail |
|
|
elif detail is None: |
|
|
return None |
|
|
print(f" ⚠️ نتيجة فارغة غير متوقعة لـ {signature[:10]}... (محاولة {attempt + 1}/{retries})") |
|
|
last_exception = Exception("Empty result without error") |
|
|
|
|
|
except (httpx.RequestError, asyncio.TimeoutError, ssl.SSLError) as e: |
|
|
print(f" ⏳ خطأ مؤقت ({type(e).__name__}) في جلب تفاصيل {signature[:10]}... (محاولة {attempt + 1}/{retries})") |
|
|
last_exception = e |
|
|
if attempt < retries - 1: |
|
|
await asyncio.sleep(delay * (attempt + 1)) |
|
|
else: |
|
|
print(f" ❌ فشل جلب تفاصيل {signature[:10]}... بعد {retries} محاولات.") |
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
print(f" ❌ خطأ غير متوقع في جلب تفاصيل {signature[:10]}...: {e}") |
|
|
return None |
|
|
|
|
|
if attempt < retries - 1 and not isinstance(last_exception, (httpx.RequestError, asyncio.TimeoutError, ssl.SSLError)): |
|
|
await asyncio.sleep(delay * (attempt + 1)) |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
async def _get_solana_transaction_detail(self, signature, endpoint, client: httpx.AsyncClient): |
|
|
"""جلب تفاصيل معاملة Solana (يستخدم الآن client مُمرر)""" |
|
|
try: |
|
|
payload = { |
|
|
"jsonrpc": "2.0", |
|
|
"id": f"gtx-{signature[:8]}-{int(time.time()*1000)}", |
|
|
"method": "getTransaction", |
|
|
"params": [ signature, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0, "commitment": "confirmed"} ] |
|
|
} |
|
|
|
|
|
response = await client.post(endpoint, json=payload, timeout=20.0) |
|
|
response.raise_for_status() |
|
|
|
|
|
data = response.json() |
|
|
if 'error' in data: |
|
|
print(f" ❌ خطأ RPC في getTransaction لـ {signature[:10]}... : {data['error'].get('message')}") |
|
|
return None |
|
|
result = data.get('result') |
|
|
if result is None: |
|
|
print(f" ℹ️ لم يتم العثور على نتيجة للمعاملة {signature[:10]}...") |
|
|
return None |
|
|
return result |
|
|
|
|
|
except httpx.HTTPStatusError as http_err: |
|
|
print(f" ⚠️ فشل HTTP في getTransaction لـ {signature[:10]}... : {http_err.response.status_code}") |
|
|
return None |
|
|
except (httpx.RequestError, asyncio.TimeoutError) as req_err: |
|
|
print(f" ⏳ خطأ اتصال/مهلة في getTransaction لـ {signature[:10]}...") |
|
|
raise req_err |
|
|
except ssl.SSLError as ssl_err: |
|
|
print(f" ❌ خطأ SSL في getTransaction لـ {signature[:10]}... : {ssl_err}") |
|
|
raise ssl_err |
|
|
except json.JSONDecodeError as json_err: |
|
|
print(f" ❌ خطأ JSON في getTransaction لـ {signature[:10]}... : {response.text[:200]}") |
|
|
return None |
|
|
except Exception as e: |
|
|
print(f" ❌ استثناء غير متوقع في getTransaction لـ {signature[:10]}... : {e}") |
|
|
traceback.print_exc() |
|
|
return None |
|
|
|
|
|
|
|
|
def _is_solana_token_transfer(self, transaction, token_address): |
|
|
"""التحقق إذا كانت المعاملة تحويل توكن في Solana""" |
|
|
try: |
|
|
if not transaction or 'meta' not in transaction or transaction.get('meta') is None: |
|
|
return False |
|
|
|
|
|
meta = transaction['meta'] |
|
|
post_balances = meta.get('postTokenBalances') |
|
|
pre_balances = meta.get('preTokenBalances') |
|
|
|
|
|
if post_balances is None and pre_balances is None: |
|
|
inner_instructions = meta.get('innerInstructions', []) |
|
|
for inner_inst_set in inner_instructions: |
|
|
for inst in inner_inst_set.get('instructions', []): |
|
|
parsed = inst.get('parsed') |
|
|
if isinstance(parsed, dict) and parsed.get('type') in ['transfer', 'transferChecked'] and parsed.get('info', {}).get('mint') == token_address: |
|
|
return True |
|
|
return False |
|
|
|
|
|
all_balances = (post_balances or []) + (pre_balances or []) |
|
|
for balance in all_balances: |
|
|
if isinstance(balance, dict) and balance.get('mint') == token_address: |
|
|
pre_amount = next((pb.get('uiTokenAmount',{}).get('amount','0') for pb in (pre_balances or []) if isinstance(pb, dict) and pb.get('owner') == balance.get('owner') and pb.get('mint') == token_address), '0') |
|
|
post_amount = balance.get('uiTokenAmount',{}).get('amount','0') |
|
|
try: |
|
|
if int(post_amount) != int(pre_amount): |
|
|
return True |
|
|
except (ValueError, TypeError): |
|
|
pass |
|
|
|
|
|
return False |
|
|
except Exception as e: |
|
|
return False |
|
|
|
|
|
async def _parse_solana_transfer(self, transaction, token_address): |
|
|
"""تحليل معاملة Solana لاستخراج بيانات التحويل""" |
|
|
try: |
|
|
if not transaction or 'transaction' not in transaction or not transaction['transaction'].get('signatures'): |
|
|
return None |
|
|
signature = transaction['transaction']['signatures'][0] |
|
|
block_time = transaction.get('blockTime') |
|
|
timestamp_to_use = str(block_time) if block_time else str(int(time.time())) |
|
|
slot = transaction.get('slot', 0) |
|
|
|
|
|
sender = None |
|
|
receiver = None |
|
|
amount_raw = 0 |
|
|
|
|
|
meta = transaction.get('meta') |
|
|
if not meta: return None |
|
|
|
|
|
pre_balances = {bal.get('owner'): bal for bal in meta.get('preTokenBalances', []) if isinstance(bal, dict) and bal.get('mint') == token_address} |
|
|
post_balances = {bal.get('owner'): bal for bal in meta.get('postTokenBalances', []) if isinstance(bal, dict) and bal.get('mint') == token_address} |
|
|
|
|
|
all_owners = set(list(pre_balances.keys()) + list(post_balances.keys())) |
|
|
|
|
|
for owner in all_owners: |
|
|
pre_bal_data = pre_balances.get(owner, {}).get('uiTokenAmount', {}) |
|
|
post_bal_data = post_balances.get(owner, {}).get('uiTokenAmount', {}) |
|
|
|
|
|
pre_amount_str = pre_bal_data.get('amount', '0') |
|
|
post_amount_str = post_bal_data.get('amount', '0') |
|
|
|
|
|
try: |
|
|
pre_amount_raw = int(pre_amount_str) |
|
|
post_amount_raw = int(post_amount_str) |
|
|
diff_raw = post_amount_raw - pre_amount_raw |
|
|
|
|
|
if diff_raw > 0: |
|
|
receiver = owner |
|
|
amount_raw = diff_raw |
|
|
elif diff_raw < 0: |
|
|
sender = owner |
|
|
|
|
|
except (ValueError, TypeError): |
|
|
print(f" ⚠️ تحذير: فشل تحليل القيمة الخام لـ Solana ({owner}).") |
|
|
pre_ui = pre_bal_data.get('uiAmount', 0.0) or 0.0 |
|
|
post_ui = post_bal_data.get('uiAmount', 0.0) or 0.0 |
|
|
diff_ui = post_ui - pre_ui |
|
|
if diff_ui > 1e-9: |
|
|
receiver = owner |
|
|
decimals = await self._get_token_decimals(token_address, 'solana') |
|
|
if decimals is not None: amount_raw = int(diff_ui * (10**decimals)) |
|
|
else: amount_raw = 0 |
|
|
elif diff_ui < -1e-9: |
|
|
sender = owner |
|
|
|
|
|
if not sender or not receiver or amount_raw == 0: |
|
|
inner_instructions = meta.get('innerInstructions', []) |
|
|
for inner_inst_set in inner_instructions: |
|
|
for inst in inner_inst_set.get('instructions', []): |
|
|
parsed = inst.get('parsed') |
|
|
if isinstance(parsed, dict) and parsed.get('type') in ['transfer', 'transferChecked'] and parsed.get('info', {}).get('mint') == token_address: |
|
|
info = parsed.get('info', {}) |
|
|
sender_inner = info.get('source') or info.get('authority') |
|
|
receiver_inner = info.get('destination') |
|
|
amount_inner_str = info.get('tokenAmount', {}).get('amount') or info.get('amount') |
|
|
try: |
|
|
amount_inner_raw = int(amount_inner_str) |
|
|
if sender_inner and receiver_inner and amount_inner_raw > 0: |
|
|
sender = sender_inner |
|
|
receiver = receiver_inner |
|
|
amount_raw = amount_inner_raw |
|
|
break |
|
|
except (ValueError, TypeError): continue |
|
|
if sender and receiver and amount_raw > 0: break |
|
|
if sender and receiver and amount_raw > 0: break |
|
|
|
|
|
if sender and receiver and amount_raw > 0: |
|
|
return { |
|
|
'hash': signature, 'from': sender, 'to': receiver, |
|
|
'value': str(amount_raw), 'timeStamp': timestamp_to_use, |
|
|
'blockNumber': str(slot), 'network': 'solana', |
|
|
'type': 'solana_transfer', 'logIndex': '0' |
|
|
} |
|
|
else: |
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
sig_short = signature[:10] if 'signature' in locals() and signature else "N/A" |
|
|
print(f"❌ خطأ في تحليل معاملة Solana ({sig_short}...): {e}") |
|
|
traceback.print_exc() |
|
|
return None |
|
|
|
|
|
|
|
|
async def _get_explorer_token_data(self, contract_address, network): |
|
|
"""جلب بيانات التحويلات من Explorer""" |
|
|
if network not in self.supported_networks: |
|
|
return [] |
|
|
|
|
|
try: |
|
|
explorer_config = { |
|
|
'ethereum': {'url': 'https://api.etherscan.io/api', 'key': self.etherscan_key}, |
|
|
'bsc': {'url': 'https://api.bscscan.com/api', 'key': self.bscscan_key}, |
|
|
'polygon': {'url': 'https://api.polygonscan.com/api', 'key': self.polygonscan_key} |
|
|
} |
|
|
|
|
|
if network not in explorer_config or not explorer_config[network].get('key'): |
|
|
return [] |
|
|
|
|
|
config = explorer_config[network] |
|
|
address_param = "contractaddress" if network == 'ethereum' else "address" |
|
|
|
|
|
params = { |
|
|
"module": "account", "action": "tokentx", |
|
|
address_param: contract_address, |
|
|
"page": 1, "offset": 100, "startblock": 0, |
|
|
"endblock": 999999999, "sort": "desc", "apikey": config['key'] |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async with self.rpc_semaphore: |
|
|
response = await self.http_client.get(config['url'], params=params, timeout=20.0) |
|
|
|
|
|
if response.status_code == 200: |
|
|
data = response.json() |
|
|
status = str(data.get('status', '0')) |
|
|
message = data.get('message', '').upper() |
|
|
|
|
|
if status == '1' and message == 'OK': |
|
|
transfers = data.get('result', []) |
|
|
processed_transfers = [] |
|
|
for tf in transfers: |
|
|
if tf.get('tokenSymbol') and tf.get('contractAddress','').lower() == contract_address.lower(): |
|
|
tf['network'] = network |
|
|
if 'from' in tf: tf['from'] = tf['from'].lower() |
|
|
if 'to' in tf: tf['to'] = tf['to'].lower() |
|
|
if 'logIndex' not in tf: tf['logIndex'] = 'N/A' |
|
|
processed_transfers.append(tf) |
|
|
|
|
|
print(f" ✅ Explorer {network}: تم جلب {len(processed_transfers)} تحويلة توكن للعقد المحدد.") |
|
|
return processed_transfers |
|
|
elif status == '0' and "NO TRANSACTIONS FOUND" in message: |
|
|
print(f" ✅ Explorer {network}: لا توجد تحويلات.") |
|
|
return [] |
|
|
else: |
|
|
error_message = data.get('result', message) |
|
|
print(f"⚠️ Explorer {network} returned error: Status={status}, Message={message}, Result={error_message}") |
|
|
if "INVALID API KEY" in str(error_message).upper(): |
|
|
print(f"🚨 خطأ فادح: مفتاح API الخاص بـ {network.upper()} غير صالح!") |
|
|
return [] |
|
|
else: |
|
|
print(f"⚠️ Explorer {network} request failed: {response.status_code}") |
|
|
return [] |
|
|
|
|
|
except Exception as e: |
|
|
if "Cannot reopen a client instance" in str(e): |
|
|
print(f" ❌ فشل فادح (Explorer): محاولة استخدام client مغلق. {e}") |
|
|
return [] |
|
|
print(f"❌ فشل جلب بيانات Explorer لـ {network}: {e}") |
|
|
return [] |
|
|
|
|
|
def _filter_recent_transfers(self, transfers, max_minutes=120): |
|
|
"""ترشيح التحويلات الحديثة فقط""" |
|
|
recent_transfers = [] |
|
|
current_time_dt = datetime.now() |
|
|
cutoff_timestamp = int((current_time_dt - timedelta(minutes=max_minutes)).timestamp()) |
|
|
processed_keys = set() |
|
|
|
|
|
for transfer in transfers: |
|
|
try: |
|
|
tx_hash = transfer.get('hash', 'N/A') |
|
|
log_index = transfer.get('logIndex', 'N/A') |
|
|
unique_key = f"{tx_hash}-{log_index}" |
|
|
|
|
|
if unique_key in processed_keys: |
|
|
continue |
|
|
|
|
|
timestamp_str = transfer.get('timeStamp') |
|
|
if not timestamp_str or not timestamp_str.isdigit(): |
|
|
continue |
|
|
timestamp = int(timestamp_str) |
|
|
|
|
|
if timestamp >= cutoff_timestamp: |
|
|
transfer_time_dt = datetime.fromtimestamp(timestamp) |
|
|
time_diff = current_time_dt - transfer_time_dt |
|
|
|
|
|
transfer['minutes_ago'] = round(time_diff.total_seconds() / 60, 2) |
|
|
transfer['human_time'] = transfer_time_dt.isoformat() |
|
|
recent_transfers.append(transfer) |
|
|
processed_keys.add(unique_key) |
|
|
|
|
|
except (ValueError, TypeError, KeyError) as e: |
|
|
print(f" ⚠️ خطأ في ترشيح تحويلة: {e} - البيانات: {transfer}") |
|
|
continue |
|
|
|
|
|
print(f" 🔍 تم ترشيح {len(recent_transfers)} تحويلة حديثة فريدة (آخر {max_minutes} دقيقة).") |
|
|
recent_transfers.sort(key=lambda x: int(x.get('timeStamp', 0)), reverse=True) |
|
|
return recent_transfers |
|
|
|
|
|
|
|
|
async def _analyze_enhanced_whale_impact(self, transfers, symbol, contract_address, network): |
|
|
"""تحليل تأثير تحركات الحيتان المحسن (مع إصلاح KeyError)""" |
|
|
print(f"📊 تحليل {len(transfers)} تحويلة حديثة للعملة {symbol}") |
|
|
|
|
|
exchange_flows = { |
|
|
'to_exchanges': [], 'from_exchanges': [], 'other_transfers': [], |
|
|
'network_breakdown': defaultdict(lambda: {'to_exchanges': 0, 'from_exchanges': 0, 'other': 0}) |
|
|
} |
|
|
total_volume_usd = 0.0 |
|
|
whale_transfers_count = 0 |
|
|
network_stats = defaultdict(int) |
|
|
unique_transfers = {} |
|
|
|
|
|
decimals = await self._get_token_decimals(contract_address, network) |
|
|
if decimals is None: |
|
|
print(f"❌ لا يمكن تحليل الحيتان بدون decimals لـ {symbol} على {network}.") |
|
|
return self._create_error_response(symbol, f"Failed to get decimals on {network}") |
|
|
|
|
|
for transfer in transfers: |
|
|
try: |
|
|
unique_key = f"{transfer.get('hash', '')}-{transfer.get('logIndex', 'N/A')}" |
|
|
if unique_key in unique_transfers: continue |
|
|
unique_transfers[unique_key] = True |
|
|
|
|
|
from_addr = str(transfer.get('from', '')).lower() |
|
|
to_addr = str(transfer.get('to', '')).lower() |
|
|
raw_value_str = transfer.get('value', '0') |
|
|
transfer_network = transfer.get('network', 'unknown') |
|
|
|
|
|
if not raw_value_str or not raw_value_str.isdigit(): continue |
|
|
raw_value = int(raw_value_str) |
|
|
if raw_value == 0: continue |
|
|
|
|
|
value_usd = await self._get_accurate_token_value_optimized(raw_value, decimals, symbol) |
|
|
|
|
|
if value_usd is None or value_usd < 0: |
|
|
print(f" ⚠️ قيمة USD غير صالحة ({value_usd}) للتحويلة {unique_key}. التخطي.") |
|
|
continue |
|
|
|
|
|
total_volume_usd += value_usd |
|
|
network_stats[transfer_network] += 1 |
|
|
|
|
|
if value_usd >= self.whale_threshold_usd: |
|
|
whale_transfers_count += 1 |
|
|
is_to_exchange = to_addr in self.address_categories['exchange'] |
|
|
is_from_exchange = from_addr in self.address_categories['exchange'] |
|
|
|
|
|
if is_to_exchange: |
|
|
exchange_flows['to_exchanges'].append({ |
|
|
'value_usd': value_usd, 'from_type': self._classify_address(from_addr), |
|
|
'to_exchange': self._classify_address(to_addr), |
|
|
'transaction': {'hash': transfer.get('hash'), 'minutes_ago': transfer.get('minutes_ago')}, |
|
|
'network': transfer_network |
|
|
}) |
|
|
exchange_flows['network_breakdown'][transfer_network]['to_exchanges'] += 1 |
|
|
elif is_from_exchange: |
|
|
exchange_flows['from_exchanges'].append({ |
|
|
'value_usd': value_usd, 'from_exchange': self._classify_address(from_addr), |
|
|
'to_type': self._classify_address(to_addr), |
|
|
'transaction': {'hash': transfer.get('hash'), 'minutes_ago': transfer.get('minutes_ago')}, |
|
|
'network': transfer_network |
|
|
}) |
|
|
exchange_flows['network_breakdown'][transfer_network]['from_exchanges'] += 1 |
|
|
else: |
|
|
exchange_flows['other_transfers'].append({ |
|
|
'value_usd': value_usd, 'from_type': self._classify_address(from_addr), |
|
|
'to_type': self._classify_address(to_addr), |
|
|
'transaction': {'hash': transfer.get('hash'), 'minutes_ago': transfer.get('minutes_ago')}, |
|
|
'network': transfer_network |
|
|
}) |
|
|
exchange_flows['network_breakdown'][transfer_network]['other'] += 1 |
|
|
except Exception as analysis_loop_error: |
|
|
print(f" ⚠️ خطأ في تحليل حلقة التحويلات: {analysis_loop_error}") |
|
|
continue |
|
|
|
|
|
total_to_exchanges_usd = sum(t['value_usd'] for t in exchange_flows['to_exchanges']) |
|
|
total_from_exchanges_usd = sum(t['value_usd'] for t in exchange_flows['from_exchanges']) |
|
|
deposit_count = len(exchange_flows['to_exchanges']) |
|
|
withdrawal_count = len(exchange_flows['from_exchanges']) |
|
|
net_flow_usd = total_to_exchanges_usd - total_from_exchanges_usd |
|
|
|
|
|
exchange_flows['deposit_count'] = deposit_count |
|
|
exchange_flows['withdrawal_count'] = withdrawal_count |
|
|
exchange_flows['net_flow_usd'] = net_flow_usd |
|
|
|
|
|
print(f"🐋 تحليل الحيتان لـ {symbol}:") |
|
|
print(f" • إجمالي التحويلات الفريدة: {len(unique_transfers)}") |
|
|
print(f" • تحويلات الحيتان (>${self.whale_threshold_usd:,.0f}): {whale_transfers_count}") |
|
|
print(f" • إيداعات للمنصات: {deposit_count} (${total_to_exchanges_usd:,.2f})") |
|
|
print(f" • سحوبات من المنصات: {withdrawal_count} (${total_from_exchanges_usd:,.2f})") |
|
|
print(f" • صافي التدفق (للمنصات): ${net_flow_usd:,.2f}") |
|
|
|
|
|
signal = self._generate_enhanced_trading_signal( |
|
|
total_to_exchanges_usd, total_from_exchanges_usd, net_flow_usd, |
|
|
deposit_count, withdrawal_count, whale_transfers_count, network_stats |
|
|
) |
|
|
|
|
|
llm_summary = self._create_enhanced_llm_summary(signal, exchange_flows, network_stats, total_volume_usd, whale_transfers_count) |
|
|
|
|
|
return { |
|
|
'symbol': symbol, 'data_available': True, 'analysis_timestamp': datetime.now().isoformat(), |
|
|
'summary': { |
|
|
'total_transfers_analyzed': len(unique_transfers), 'whale_transfers_count': whale_transfers_count, |
|
|
'total_volume_usd': total_volume_usd, 'time_window_minutes': 120, |
|
|
'networks_analyzed': dict(network_stats) |
|
|
}, |
|
|
'exchange_flows': { |
|
|
'to_exchanges_usd': total_to_exchanges_usd, 'from_exchanges_usd': total_from_exchanges_usd, |
|
|
'net_flow_usd': net_flow_usd, 'deposit_count': deposit_count, 'withdrawal_count': withdrawal_count, |
|
|
'network_breakdown': dict(exchange_flows['network_breakdown']), |
|
|
'top_deposits': sorted([{'value_usd': t['value_usd'], 'network': t['network'], 'to': t['to_exchange']} for t in exchange_flows['to_exchanges']], key=lambda x: x['value_usd'], reverse=True)[:3], |
|
|
'top_withdrawals': sorted([{'value_usd': t['value_usd'], 'network': t['network'], 'from': t['from_exchange']} for t in exchange_flows['from_exchanges']], key=lambda x: x['value_usd'], reverse=True)[:3] |
|
|
}, |
|
|
'trading_signal': signal, |
|
|
'llm_friendly_summary': llm_summary |
|
|
} |
|
|
|
|
|
|
|
|
async def _get_accurate_token_value_optimized(self, raw_value, decimals, symbol): |
|
|
"""حساب القيمة بالدولار باستخدام decimals مُمررة والسعر الحالي""" |
|
|
try: |
|
|
if decimals is None or decimals < 0: |
|
|
print(f"⚠️ قيمة decimals غير صالحة ({decimals}) لـ {symbol}. لا يمكن حساب القيمة.") |
|
|
return 0 |
|
|
|
|
|
price = await self._get_token_price(symbol) |
|
|
if price is None or price <= 0: |
|
|
return 0 |
|
|
|
|
|
token_amount = raw_value / (10 ** decimals) |
|
|
value_usd = token_amount * price |
|
|
return value_usd |
|
|
|
|
|
except OverflowError: |
|
|
print(f"⚠️ خطأ OverflowError أثناء حساب قيمة {symbol} (Raw={raw_value}, Decimals={decimals})") |
|
|
return 0 |
|
|
except Exception as e: |
|
|
print(f"❌ خطأ في _get_accurate_token_value_optimized لـ {symbol}: {e}") |
|
|
return 0 |
|
|
|
|
|
async def _get_token_decimals(self, contract_address, network): |
|
|
"""جلب عدد الكسور العشرية للعملة""" |
|
|
try: |
|
|
if isinstance(contract_address, dict): |
|
|
addr_for_network = contract_address.get(network) |
|
|
if not addr_for_network: return None |
|
|
contract_address = addr_for_network |
|
|
|
|
|
if not isinstance(contract_address, str): return None |
|
|
|
|
|
cache_key = f"{contract_address.lower()}_{network}" |
|
|
if cache_key in self.token_decimals_cache: |
|
|
return self.token_decimals_cache[cache_key] |
|
|
|
|
|
if network in self.supported_networks and self.supported_networks[network]['type'] == 'evm': |
|
|
endpoints = self.supported_networks[network]['rpc_endpoints'] |
|
|
for endpoint in endpoints: |
|
|
try: |
|
|
payload = { |
|
|
"jsonrpc": "2.0", "method": "eth_call", |
|
|
"params": [{"to": contract_address, "data": "0x313ce567"}, "latest"], |
|
|
"id": int(time.time()) |
|
|
} |
|
|
|
|
|
async with self.rpc_semaphore: |
|
|
response = await self.http_client.post(endpoint, json=payload, timeout=10.0) |
|
|
|
|
|
if response.status_code == 200: |
|
|
result = response.json().get('result') |
|
|
if result and result != '0x' and result.startswith('0x'): |
|
|
try: |
|
|
decimals = int(result, 16) |
|
|
if isinstance(decimals, int) and decimals >= 0: |
|
|
self.token_decimals_cache[cache_key] = decimals |
|
|
print(f" ℹ️ تم جلب Decimals لـ {contract_address[:10]}... على {network}: {decimals}") |
|
|
return decimals |
|
|
except ValueError: |
|
|
print(f" ⚠️ فشل تحويل نتيجة decimals من RPC ({result}) لـ {contract_address} على {network}") |
|
|
continue |
|
|
except Exception as rpc_decimal_error: |
|
|
if "Cannot reopen a client instance" in str(rpc_decimal_error): |
|
|
print(f" ❌ فشل فادح (Decimals): محاولة استخدام client مغلق. {rpc_decimal_error}") |
|
|
return None |
|
|
print(f" ⚠️ خطأ RPC أثناء جلب decimals من {endpoint.split('//')[-1]}: {rpc_decimal_error}") |
|
|
continue |
|
|
|
|
|
elif network == 'solana': |
|
|
print(f"ℹ️ جلب decimals لـ Solana ({contract_address}) غير مدعوم حاليًا عبر RPC.") |
|
|
estimated_decimals = 9 |
|
|
self.token_decimals_cache[cache_key] = estimated_decimals |
|
|
print(f" ⚠️ استخدام قيمة decimals تقديرية لـ Solana: {estimated_decimals}") |
|
|
return estimated_decimals |
|
|
|
|
|
print(f"❌ فشل جلب الكسور العشرية للعقد {contract_address} على شبكة {network} من جميع المصادر.") |
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ فشل عام في جلب الكسور العشرية للعقد {contract_address} على شبكة {network}: {e}") |
|
|
return None |
|
|
|
|
|
|
|
|
async def _get_token_price(self, symbol): |
|
|
"""جلب سعر العملة من CoinGecko مع fallbacks""" |
|
|
try: |
|
|
base_symbol = symbol.split('/')[0].upper() |
|
|
cache_entry = self.token_price_cache.get(base_symbol) |
|
|
if cache_entry and time.time() - cache_entry.get('timestamp', 0) < 300: |
|
|
return cache_entry['price'] |
|
|
|
|
|
price = await self._get_token_price_from_coingecko(symbol) |
|
|
if price is not None and price > 0: |
|
|
self.token_price_cache[base_symbol] = {'price': price, 'timestamp': time.time()} |
|
|
return price |
|
|
|
|
|
price = await self._get_token_price_from_kucoin(symbol) |
|
|
if price is not None and price > 0: |
|
|
self.token_price_cache[base_symbol] = {'price': price, 'timestamp': time.time()} |
|
|
return price |
|
|
|
|
|
print(f"❌ فشل جميع محاولات جلب سعر العملة {symbol}") |
|
|
self.token_price_cache[base_symbol] = {'price': 0, 'timestamp': time.time()} |
|
|
return 0 |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ فشل عام في جلب سعر العملة {symbol}: {e}") |
|
|
return 0 |
|
|
|
|
|
|
|
|
async def _get_token_price_from_coingecko(self, symbol): |
|
|
"""جلب سعر العملة من CoinGecko مع التعامل مع rate limiting""" |
|
|
try: |
|
|
symbol_mapping = { |
|
|
'BTC': 'bitcoin', 'ETH': 'ethereum', 'BNB': 'binancecoin', 'ADA': 'cardano', |
|
|
'DOT': 'polkadot', 'XRP': 'ripple', 'DOGE': 'dogecoin', 'BCH': 'bitcoin-cash', |
|
|
'LTC': 'litecoin', 'XLM': 'stellar', 'TRX': 'tron', 'EOS': 'eos', 'XMR': 'monero', |
|
|
'SOL': 'solana', 'MATIC': 'matic-network', 'AVAX': 'avalanche-2', 'LINK': 'chainlink', |
|
|
'ATOM': 'cosmos', 'UNI': 'uniswap', 'AAVE': 'aave', 'KCS': 'kucoin-shares', |
|
|
'MAVIA': 'heroes-of-mavia', 'COMMON': 'commonwealth', 'WLFI': 'wolfi', |
|
|
'PINGPONG': 'pingpong', 'YB': 'yourbusd', 'REACT': 'react', 'XMN': 'xmine', |
|
|
'ANOME': 'anome', 'ZEN': 'zencash', 'AKT': 'akash-network', 'UB': 'unibit', 'WLD': 'worldcoin' |
|
|
} |
|
|
|
|
|
base_symbol = symbol.split('/')[0].upper() |
|
|
coingecko_id = symbol_mapping.get(base_symbol) |
|
|
if not coingecko_id: |
|
|
coingecko_id = base_symbol.lower() |
|
|
print(f"ℹ️ رمز {base_symbol} غير موجود في symbol_mapping، استخدام {coingecko_id} لـ CoinGecko.") |
|
|
|
|
|
|
|
|
url = f"https://api.coingecko.com/api/v3/simple/price?ids={coingecko_id}&vs_currencies=usd" |
|
|
headers = {'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'} |
|
|
|
|
|
max_retries = 2 |
|
|
for attempt in range(max_retries): |
|
|
async with self.coingecko_semaphore: |
|
|
|
|
|
try: |
|
|
response = await self.http_client.get(url, headers=headers, timeout=10.0) |
|
|
if response.status_code == 429: |
|
|
wait_time = 3 * (attempt + 1) |
|
|
print(f"⏰ Rate limit من CoinGecko لـ {symbol} (محاولة {attempt + 1}) - الانتظار {wait_time}s") |
|
|
await asyncio.sleep(wait_time) |
|
|
continue |
|
|
|
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
price_data = data.get(coingecko_id) |
|
|
if price_data and 'usd' in price_data: |
|
|
price = price_data['usd'] |
|
|
if isinstance(price, (int, float)) and price > 0: |
|
|
return price |
|
|
else: |
|
|
print(f"⚠️ استجابة CoinGecko تحتوي على سعر غير صالح لـ {symbol}: {price}") |
|
|
return 0 |
|
|
else: |
|
|
if attempt == 0: |
|
|
print(f"⚠️ لم يتم العثور على سعر لـ {coingecko_id} في CoinGecko، محاولة البحث عن المعرف...") |
|
|
await asyncio.sleep(1.1) |
|
|
|
|
|
found_id = await self._find_coingecko_id_via_search(base_symbol, self.http_client) |
|
|
if found_id and found_id != coingecko_id: |
|
|
print(f" 🔄 تم العثور على معرف بديل: {found_id}. إعادة المحاولة...") |
|
|
coingecko_id = found_id |
|
|
url = f"https://api.coingecko.com/api/v3/simple/price?ids={coingecko_id}&vs_currencies=usd" |
|
|
continue |
|
|
else: |
|
|
print(f" ❌ لم يتم العثور على معرف بديل لـ {base_symbol}.") |
|
|
return 0 |
|
|
else: |
|
|
print(f"⚠️ لم يتم العثور على سعر لـ {coingecko_id} في استجابة CoinGecko بعد البحث.") |
|
|
return 0 |
|
|
|
|
|
except Exception as inner_e: |
|
|
if "Cannot reopen a client instance" in str(inner_e): |
|
|
print(f" ❌ فشل فادح (CoinGecko): محاولة استخدام client مغلق. {inner_e}") |
|
|
return 0 |
|
|
|
|
|
if isinstance(inner_e, httpx.HTTPStatusError): |
|
|
print(f"❌ خطأ HTTP من CoinGecko لـ {symbol}: {inner_e.response.status_code}") |
|
|
elif isinstance(inner_e, httpx.RequestError): |
|
|
print(f"❌ خطأ اتصال بـ CoinGecko لـ {symbol}: {inner_e}") |
|
|
else: |
|
|
print(f"❌ خطأ غير متوقع أثناء جلب السعر من CoinGecko لـ {symbol}: {inner_e}") |
|
|
return 0 |
|
|
|
|
|
print(f"❌ فشل جلب السعر من CoinGecko لـ {symbol} بعد {max_retries} محاولات.") |
|
|
return 0 |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ فشل عام في _get_token_price_from_coingecko لـ {symbol}: {e}") |
|
|
return 0 |
|
|
|
|
|
async def _find_coingecko_id_via_search(self, symbol, client: httpx.AsyncClient): |
|
|
"""دالة مساعدة للبحث عن معرف CoinGecko""" |
|
|
try: |
|
|
search_url = f"https://api.coingecko.com/api/v3/search?query={symbol}" |
|
|
|
|
|
async with self.coingecko_semaphore: |
|
|
response = await client.get(search_url, timeout=10.0) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
coins = data.get('coins', []) |
|
|
if coins: |
|
|
search_symbol_lower = symbol.lower() |
|
|
for coin in coins: |
|
|
if coin.get('symbol', '').lower() == search_symbol_lower: |
|
|
return coin.get('id') |
|
|
return coins[0].get('id') |
|
|
return None |
|
|
except Exception as e: |
|
|
print(f" ⚠️ خطأ أثناء البحث عن معرف CoinGecko لـ {symbol}: {e}") |
|
|
return None |
|
|
|
|
|
|
|
|
async def _get_token_price_from_kucoin(self, symbol): |
|
|
"""جلب سعر العملة من KuCoin كبديل (بشكل غير متزامن)""" |
|
|
exchange = None |
|
|
try: |
|
|
exchange = ccxt.kucoin({'enableRateLimit': True}) |
|
|
markets = await exchange.load_markets() |
|
|
if symbol not in markets: |
|
|
print(f"⚠️ الرمز {symbol} غير موجود في أسواق KuCoin.") |
|
|
await exchange.close() |
|
|
return 0 |
|
|
|
|
|
ticker = await exchange.fetch_ticker(symbol) |
|
|
price = ticker.get('last') |
|
|
|
|
|
if price is not None: |
|
|
try: |
|
|
price_float = float(price) |
|
|
if price_float > 0: |
|
|
return price_float |
|
|
else: |
|
|
print(f"⚠️ KuCoin أعاد سعراً غير موجب لـ {symbol}: {price}") |
|
|
return 0 |
|
|
except (ValueError, TypeError): |
|
|
print(f"⚠️ KuCoin أعاد سعراً غير رقمي لـ {symbol}: {price}") |
|
|
return 0 |
|
|
else: |
|
|
print(f"⚠️ KuCoin لم يُعد مفتاح 'last' في ticker لـ {symbol}") |
|
|
return 0 |
|
|
|
|
|
except ccxt.NetworkError as e: |
|
|
print(f"❌ خطأ شبكة أثناء جلب السعر من KuCoin لـ {symbol}: {e}") |
|
|
return 0 |
|
|
except ccxt.ExchangeError as e: |
|
|
print(f"❌ خطأ منصة KuCoin ({type(e).__name__}) لـ {symbol}: {e}") |
|
|
return 0 |
|
|
except Exception as e: |
|
|
print(f"❌ خطأ غير متوقع أثناء جلب السعر من KuCoin لـ {symbol}: {e}") |
|
|
return 0 |
|
|
finally: |
|
|
if exchange: |
|
|
try: |
|
|
await exchange.close() |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
|
|
|
def _generate_enhanced_trading_signal(self, to_exchanges_usd, from_exchanges_usd, net_flow_usd, deposit_count, withdrawal_count, whale_transfers_count, network_stats): |
|
|
"""توليد إشارة تداول محسنة بناء على تحركات الحيتان""" |
|
|
network_strength = min(len(network_stats) / 3.0, 1.0) |
|
|
abs_net_flow = abs(net_flow_usd) |
|
|
|
|
|
|
|
|
total_flow_volume = to_exchanges_usd + from_exchanges_usd |
|
|
|
|
|
confidence_multiplier = min( (abs_net_flow / 500000.0) + (whale_transfers_count / 5.0) , 1.5) |
|
|
base_confidence = 0.5 + (network_strength * 0.1) |
|
|
|
|
|
action = 'HOLD' |
|
|
reason = f'نشاط حيتان عبر {len(network_stats)} شبكة: {whale_transfers_count} تحويلة كبيرة بقيمة إجمالية ${total_flow_volume:,.0f}. صافي التدفق ${net_flow_usd:,.0f}' |
|
|
critical_alert = False |
|
|
|
|
|
if net_flow_usd > 500000 and deposit_count >= 2: |
|
|
action = 'STRONG_SELL' |
|
|
confidence = min(0.7 + (base_confidence * confidence_multiplier * 0.3), 0.95) |
|
|
reason = f'ضغط بيعي قوي عبر {len(network_stats)} شبكة: ${net_flow_usd:,.0f} إيداع للمنصات ({deposit_count} تحويلة).' |
|
|
critical_alert = abs_net_flow > 1000000 |
|
|
elif net_flow_usd > 150000 and (deposit_count >= 1 or whale_transfers_count > 0): |
|
|
action = 'SELL' |
|
|
confidence = min(0.6 + (base_confidence * confidence_multiplier * 0.2), 0.85) |
|
|
reason = f'ضغط بيعي محتمل عبر {len(network_stats)} شبكة: ${net_flow_usd:,.0f} إيداع للمنصات.' |
|
|
critical_alert = abs_net_flow > 750000 |
|
|
elif net_flow_usd < -500000 and withdrawal_count >= 2: |
|
|
action = 'STRONG_BUY' |
|
|
confidence = min(0.7 + (base_confidence * confidence_multiplier * 0.3), 0.95) |
|
|
reason = f'تراكم شرائي قوي عبر {len(network_stats)} شبكة: ${abs_net_flow:,.0f} سحب من المنصات ({withdrawal_count} تحويلة).' |
|
|
critical_alert = abs_net_flow > 1000000 |
|
|
elif net_flow_usd < -150000 and (withdrawal_count >= 1 or whale_transfers_count > 0): |
|
|
action = 'BUY' |
|
|
confidence = min(0.6 + (base_confidence * confidence_multiplier * 0.2), 0.85) |
|
|
reason = f'تراكم شرائي محتمل عبر {len(network_stats)} شبكة: ${abs_net_flow:,.0f} سحب من المنصات.' |
|
|
critical_alert = abs_net_flow > 750000 |
|
|
else: |
|
|
if whale_transfers_count > 0 and abs_net_flow < 100000: |
|
|
confidence = min(base_confidence * 1.1, 0.6) |
|
|
reason += " (صافي التدفق منخفض نسبياً)" |
|
|
elif whale_transfers_count > 0 and abs_net_flow < 50000: |
|
|
confidence = max(0.4, base_confidence * 0.9) |
|
|
reason += " (صافي التدفق شبه متعادل)" |
|
|
elif whale_transfers_count == 0: |
|
|
confidence = 0.3 |
|
|
reason = 'لا توجد تحركات حيتان كبيرة في الساعات الأخيرة' |
|
|
else: |
|
|
confidence = base_confidence |
|
|
|
|
|
final_confidence = max(0.3, min(confidence, 0.95)) |
|
|
|
|
|
return {'action': action, 'confidence': final_confidence, 'reason': reason, 'critical_alert': critical_alert} |
|
|
|
|
|
def _create_enhanced_llm_summary(self, signal, exchange_flows, network_stats, total_volume, whale_count): |
|
|
"""إنشاء ملخص محسن للنموذج الضخم (مع إصلاح KeyError)""" |
|
|
deposit_count_val = exchange_flows.get('deposit_count', 0) |
|
|
withdrawal_count_val = exchange_flows.get('withdrawal_count', 0) |
|
|
exchange_involvement_str = f"{deposit_count_val + withdrawal_count_val} معاملة مع المنصات" |
|
|
|
|
|
return { |
|
|
'whale_activity_summary': signal['reason'], |
|
|
'recommended_action': signal['action'], |
|
|
'confidence': signal['confidence'], |
|
|
'key_metrics': { |
|
|
'total_whale_transfers': whale_count, |
|
|
'total_volume_analyzed': f"${total_volume:,.0f}", |
|
|
'net_flow_direction': 'TO_EXCHANGES' if signal['action'] in ['SELL', 'STRONG_SELL'] else 'FROM_EXCHANGES' if signal['action'] in ['BUY', 'STRONG_BUY'] else 'BALANCED', |
|
|
'whale_movement_impact': 'HIGH' if signal['confidence'] > 0.8 else 'MEDIUM' if signal['confidence'] > 0.6 else 'LOW', |
|
|
'exchange_involvement': exchange_involvement_str, |
|
|
'network_coverage': f"{len(network_stats)} شبكات", |
|
|
'data_quality': 'REAL_TIME' |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async def _find_contract_address_enhanced(self, symbol): |
|
|
"""بحث متقدم عن عقد العملة مع تحديد الشبكة المناسبة""" |
|
|
base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper() |
|
|
symbol_lower = base_symbol.lower() |
|
|
|
|
|
print(f"🔍 البحث عن عقد للعملة: {symbol}") |
|
|
|
|
|
if symbol_lower in self.contracts_db: |
|
|
contract_info = self.contracts_db[symbol_lower] |
|
|
if isinstance(contract_info, str): |
|
|
network = self._detect_network_from_address(contract_info) |
|
|
contract_info = {'address': contract_info, 'network': network} |
|
|
self.contracts_db[symbol_lower] = contract_info |
|
|
if 'address' in contract_info and 'network' in contract_info: |
|
|
print(f" ✅ وجد في قاعدة البيانات المحلية: {contract_info}") |
|
|
return contract_info |
|
|
else: |
|
|
print(f" ⚠️ بيانات العقد غير مكتملة في القاعدة المحلية لـ {symbol}: {contract_info}") |
|
|
|
|
|
print(f" 🔍 البحث في CoinGecko عن {base_symbol}...") |
|
|
coingecko_result = await self._find_contract_via_coingecko(base_symbol) |
|
|
|
|
|
if coingecko_result: |
|
|
address, network = coingecko_result |
|
|
contract_info = {'address': address, 'network': network} |
|
|
self.contracts_db[symbol_lower] = contract_info |
|
|
print(f" ✅ تم العثور على عقد {symbol} عبر CoinGecko على شبكة {network}: {address}") |
|
|
|
|
|
if self.r2_service: |
|
|
await self._save_contracts_to_r2() |
|
|
|
|
|
return contract_info |
|
|
|
|
|
print(f" ❌ فشل العثور على عقد لـ {symbol} في جميع المصادر") |
|
|
return None |
|
|
|
|
|
|
|
|
async def _find_contract_via_coingecko(self, symbol): |
|
|
"""البحث عن عقد العملة عبر CoinGecko مع التعامل مع الأخطاء ومعدل الطلبات""" |
|
|
try: |
|
|
search_url = f"https://api.coingecko.com/api/v3/search?query={symbol}" |
|
|
headers = {'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'} |
|
|
|
|
|
max_retries = 2 |
|
|
for attempt in range(max_retries): |
|
|
async with self.coingecko_semaphore: |
|
|
|
|
|
try: |
|
|
print(f" 🔍 CoinGecko Search API Call (Attempt {attempt + 1}) for {symbol}") |
|
|
response = await self.http_client.get(search_url, headers=headers, timeout=15.0) |
|
|
|
|
|
if response.status_code == 429: |
|
|
wait_time = 3 * (attempt + 1) |
|
|
print(f" ⏰ Rate limit (Search) for {symbol} - Waiting {wait_time}s") |
|
|
await asyncio.sleep(wait_time) |
|
|
continue |
|
|
|
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
coins = data.get('coins', []) |
|
|
|
|
|
if not coins: |
|
|
print(f" ❌ No matching coins found for {symbol} on CoinGecko") |
|
|
return None |
|
|
|
|
|
print(f" 📊 Found {len(coins)} potential matches for {symbol}") |
|
|
|
|
|
best_coin = None |
|
|
search_symbol_lower = symbol.lower() |
|
|
for coin in coins: |
|
|
coin_symbol = coin.get('symbol', '').lower() |
|
|
coin_name = coin.get('name', '').lower() |
|
|
if coin_symbol == search_symbol_lower: |
|
|
best_coin = coin; print(f" ✅ Exact symbol match: {coin.get('name')}"); break |
|
|
if not best_coin and coin_name == search_symbol_lower: |
|
|
best_coin = coin; print(f" ✅ Exact name match: {coin.get('name')}") |
|
|
if not best_coin: |
|
|
best_coin = coins[0]; print(f" ⚠️ Using first result: {best_coin.get('name')}") |
|
|
|
|
|
coin_id = best_coin.get('id') |
|
|
if not coin_id: print(f" ❌ No ID found"); return None |
|
|
|
|
|
print(f" 🔍 Fetching details for CoinGecko ID: {coin_id}") |
|
|
detail_url = f"https://api.coingecko.com/api/v3/coins/{coin_id}" |
|
|
await asyncio.sleep(1.1) |
|
|
detail_response = await self.http_client.get(detail_url, headers=headers, timeout=15.0) |
|
|
|
|
|
if detail_response.status_code == 429: |
|
|
wait_time = 5 * (attempt + 1) |
|
|
print(f" ⏰ Rate limit (Details) for {coin_id} - Waiting {wait_time}s") |
|
|
await asyncio.sleep(wait_time) |
|
|
detail_response = await self.http_client.get(detail_url, headers=headers, timeout=15.0) |
|
|
|
|
|
detail_response.raise_for_status() |
|
|
detail_data = detail_response.json() |
|
|
platforms = detail_data.get('platforms', {}) |
|
|
if not platforms: print(f" ❌ No platform data found"); return None |
|
|
|
|
|
network_priority = ['ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom', 'solana'] |
|
|
network_map = {'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom', 'solana': 'solana'} |
|
|
|
|
|
for platform_cg in network_priority: |
|
|
address = platforms.get(platform_cg) |
|
|
if address and isinstance(address, str) and address.strip(): |
|
|
network = network_map.get(platform_cg) |
|
|
if network: |
|
|
print(f" ✅ Found contract on {network}: {address}") |
|
|
return address, network |
|
|
|
|
|
print(f" ❌ No contract found on supported platforms for {coin_id}") |
|
|
return None |
|
|
|
|
|
except Exception as inner_e: |
|
|
if "Cannot reopen a client instance" in str(inner_e): |
|
|
print(f" ❌ فشل فادح (CoinGecko): محاولة استخدام client مغلق. {inner_e}") |
|
|
return None |
|
|
if isinstance(inner_e, httpx.HTTPStatusError): |
|
|
print(f" ❌ HTTP Error from CoinGecko ({inner_e.request.url}): {inner_e.response.status_code}") |
|
|
elif isinstance(inner_e, httpx.RequestError): |
|
|
print(f" ❌ Connection Error with CoinGecko ({inner_e.request.url}): {inner_e}") |
|
|
else: |
|
|
print(f" ❌ Unexpected error during CoinGecko API call (Attempt {attempt + 1}): {inner_e}") |
|
|
|
|
|
if attempt < max_retries - 1: await asyncio.sleep(1); continue |
|
|
else: return None |
|
|
|
|
|
print(f" ❌ Failed to get contract from CoinGecko for {symbol} after {max_retries} attempts.") |
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ General failure in _find_contract_via_coingecko for {symbol}: {e}") |
|
|
traceback.print_exc() |
|
|
return None |
|
|
|
|
|
|
|
|
def _is_exchange_address(self, address): |
|
|
"""التحقق إذا كان العنوان ينتمي إلى منصة""" |
|
|
try: |
|
|
if not isinstance(address, str): return False |
|
|
return address.lower() in self.address_categories['exchange'] |
|
|
except Exception: return False |
|
|
|
|
|
def _classify_address(self, address): |
|
|
"""تصنيف العنوان إلى نوع محدد""" |
|
|
try: |
|
|
if not isinstance(address, str): return 'unknown' |
|
|
return self.address_labels.get(address.lower(), 'unknown') |
|
|
except Exception: return 'unknown' |
|
|
|
|
|
def _create_no_contract_response(self, symbol): |
|
|
"""إنشاء رد عند عدم العثور على عقد""" |
|
|
return { |
|
|
'symbol': symbol, 'data_available': False, 'error': 'NO_CONTRACT_FOUND', |
|
|
'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': 'لم يتم العثور على عقد العملة - لا توجد بيانات حيتان'}, |
|
|
'llm_friendly_summary': {'whale_activity_summary': 'لا توجد بيانات عن تحركات الحيتان - لم يتم العثور على عقد العملة', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NO_DATA'}} |
|
|
} |
|
|
|
|
|
def _create_no_transfers_response(self, symbol): |
|
|
"""إنشاء رد عند عدم العثور على تحويلات""" |
|
|
return { |
|
|
'symbol': symbol, 'data_available': False, 'error': 'NO_TRANSFERS_FOUND', |
|
|
'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': 'لا توجد تحويلات للعملة - لا توجد بيانات حيتان'}, |
|
|
'llm_friendly_summary': {'whale_activity_summary': 'لا توجد تحويلات حديثة للعملة - لا توجد بيانات حيتان', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NO_DATA'}} |
|
|
} |
|
|
|
|
|
def _create_no_recent_activity_response(self, symbol): |
|
|
"""إنشاء رد عند عدم وجود نشاط حديث""" |
|
|
return { |
|
|
'symbol': symbol, 'data_available': False, 'error': 'NO_RECENT_ACTIVITY', |
|
|
'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': 'لا توجد تحويلات حديثة للعملة - لا توجد بيانات حيتان'}, |
|
|
'llm_friendly_summary': {'whale_activity_summary': 'لا توجد تحويلات حيتان حديثة في آخر 120 دقيقة - لا توجد بيانات حيتان', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NO_DATA'}} |
|
|
} |
|
|
|
|
|
|
|
|
def _create_error_response(self, symbol, error_msg): |
|
|
"""إنشاء رد في حالة حدوث خطأ""" |
|
|
return { |
|
|
'symbol': symbol, 'data_available': False, 'error': f'ANALYSIS_ERROR: {error_msg}', |
|
|
'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': f'خطأ في تحليل الحيتان: {error_msg} - لا توجد بيانات حيتان'}, |
|
|
'llm_friendly_summary': {'whale_activity_summary': f'فشل في تحليل تحركات الحيتان: {error_msg} - لا توجد بيانات حيتان', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'ERROR'}} |
|
|
} |
|
|
|
|
|
async def _save_contracts_to_r2(self): |
|
|
"""حفظ قاعدة البيانات العقود إلى R2""" |
|
|
if not self.r2_service: return |
|
|
try: |
|
|
key = "contracts.json" |
|
|
contracts_to_save = {} |
|
|
for symbol, data in self.contracts_db.items(): |
|
|
if isinstance(data, dict) and 'address' in data and 'network' in data: |
|
|
contracts_to_save[symbol] = data |
|
|
elif isinstance(data, str): |
|
|
contracts_to_save[symbol] = {'address': data, 'network': self._detect_network_from_address(data)} |
|
|
|
|
|
if not contracts_to_save: |
|
|
print("⚠️ لا توجد بيانات عقود صالحة للحفظ في R2.") |
|
|
return |
|
|
|
|
|
data_json = json.dumps(contracts_to_save, indent=2).encode('utf-8') |
|
|
self.r2_service.s3_client.put_object( |
|
|
Bucket="trading", Key=key, Body=data_json, ContentType="application/json" |
|
|
) |
|
|
print(f"✅ تم حفظ قاعدة بيانات العقود ({len(contracts_to_save)} إدخال) إلى R2") |
|
|
except Exception as e: |
|
|
print(f"❌ فشل حفظ قاعدة البيانات العقود: {e}") |
|
|
|
|
|
|
|
|
async def generate_whale_trading_signal(self, symbol, whale_data, market_context): |
|
|
"""توليد إشارة تداول بناءً على بيانات الحيتان""" |
|
|
try: |
|
|
if not whale_data or not whale_data.get('data_available', False): |
|
|
return { |
|
|
'action': 'HOLD', 'confidence': 0.3, |
|
|
'reason': 'لا توجد بيانات كافية عن نشاط الحيتان', |
|
|
'source': 'whale_analysis', 'data_available': False |
|
|
} |
|
|
|
|
|
whale_signal = whale_data.get('trading_signal', {}) |
|
|
analysis_details = whale_data |
|
|
|
|
|
return { |
|
|
'action': whale_signal.get('action', 'HOLD'), |
|
|
'confidence': whale_signal.get('confidence', 0.3), |
|
|
'reason': whale_signal.get('reason', 'تحليل الحيتان غير متوفر'), |
|
|
'source': 'whale_analysis', |
|
|
'critical_alert': whale_signal.get('critical_alert', False), |
|
|
'data_available': True, |
|
|
'whale_analysis_details': analysis_details |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ خطأ في توليد إشارة تداول الحيتان لـ {symbol}: {e}") |
|
|
return { |
|
|
'action': 'HOLD', 'confidence': 0.3, |
|
|
'reason': f'خطأ في تحليل الحيتان: {str(e)} - لا توجد بيانات حيتان', |
|
|
'source': 'whale_analysis', 'data_available': False |
|
|
} |
|
|
|
|
|
|
|
|
async def cleanup(self): |
|
|
"""تنظيف الموارد عند الإغلاق""" |
|
|
if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed: |
|
|
try: |
|
|
await self.http_client.aclose() |
|
|
print("✅ تم إغلاق اتصال HTTP client لمراقب الحيتان.") |
|
|
except Exception as e: |
|
|
print(f"⚠️ خطأ أثناء إغلاق HTTP client لمراقب الحيتان: {e}") |
|
|
|
|
|
|
|
|
print("✅ EnhancedWhaleMonitor loaded - RPC Rate Limiting (Semaphore) & Updated Endpoints") |