#!/usr/bin/env python3
"""
Raydium MEV Bot - Production Trading System
Optimized for detecting and executing sandwich trades on Raydium AMM
With improved balance management and price tracking
"""

import os
import json
import asyncio
import base64
import base58
import time
import signal
import re
from datetime import datetime
import traceback
from typing import Dict, List, Optional, Tuple, Any, Set, Union
import websockets
import torch
import decimal

# Import custom modules
from config_loader import config
from solana_rust_bot import TradingCore, TradeParams
from solana_logger import logger
from solana_rust_bot_extension import  solana_rust_bot_extension  

class RaydiumMEVBot:
    """Raydium MEV Bot for sandwich trading opportunities with enhanced balance management"""
    
    def __init__(self):
        """Initialize the Raydium MEV bot"""
        # Load configuration
        self.setup_environment()
        # Initialize CUDA if available
        self.setup_cuda()
        # Initialize trading components
        self.init_components()
        # Setup performance tracking
        self.setup_metrics()
        # Setup safety features
        self.setup_safety_features()
        # Log startup information
        self.log_startup_info()
        
    def setup_environment(self):
        """Setup environment and load configuration"""
        # Core addresses and constants
        self.WSOL_MINT = "So11111111111111111111111111111111111111112"
        self.LAMPORTS_PER_SOL = 1_000_000_000
        
        # Network settings
        network_config = config.get_network_config()
        self.RPC_URL = network_config.get("rpc_url")
        self.WS_URL = network_config.get("ws_url")
        self.API_KEY = network_config.get("api_key")
        self.WALLET = config.get("SOLANA_PUBLIC_KEY")
        self.WSOL_ACCOUNT = config.get("WSOL_TOKEN_ACCOUNT")
        
        # Raydium program ID
        self.RAYDIUM_PROGRAM_ID = config.get("RAYDIUM_PROGRAM_ID")
        
        # Trading parameters
        trade_config = config.get_trade_config()
        self.POSITION_SIZE = trade_config.get("front_run_amount", 0.4)
        self.MIN_VICTIM_SIZE = trade_config.get("min_victim_size", 5.0)
        self.MAX_VICTIM_SIZE = trade_config.get("max_victim_size", 320.0)
        self.MIN_PROFIT = trade_config.get("min_profit", 0.002)
        self.COMPUTE_LIMIT = trade_config.get("compute_limit", 50000)
        self.PRIORITY_FEE = trade_config.get("priority_fee", 750000)
        
        # Solana price tracking
        self.PRICE_UPDATE_INTERVAL = 60  # seconds
        self.solana_price_usd = 0.0
        self.last_price_update = 0
        
        # Balance management
        balance_config = config.get_balance_config()
        self.SOL_WRAP_THRESHOLD = balance_config.get("min_sol_balance", 0.03) 
        self.SOL_KEEP_AMOUNT = balance_config.get("sol_keep_amount", 0.05)  # Amount of SOL to keep unwrapped
        self.WSOL_UNWRAP_THRESHOLD = balance_config.get("wsol_unwrap_threshold", 0.01)  # When to unwrap WSOL
        self.SOL_WRAP_RATIO = balance_config.get("wsol_target_ratio", 0.70)  # Target % of SOL to wrap to WSOL
        self.BALANCE_CHECK_INTERVAL = balance_config.get("wsol_wrap_interval", 30)  # Balance management frequency
        
        # Debugging options
        self.debug_mode = os.environ.get("DEBUG_MODE", "0") == "1"
        
    def setup_cuda(self):
        """Setup CUDA for optimized performance"""
        if not torch.cuda.is_available():
            logger.warning("CUDA not available - profit calculations may be slower")
            self.cuda_enabled = False
        else:
            # Setup CUDA optimizations
            torch.cuda.empty_cache()
            torch.set_float32_matmul_precision("high")
            torch.backends.cudnn.benchmark = True
            torch.backends.cudnn.enabled = True
            torch.backends.cuda.matmul.allow_tf32 = True
            
            # Create CUDA stream with priority
            self.cuda_stream = torch.cuda.Stream(priority=-1)
            self.cuda_enabled = True
            
            logger.system(f"CUDA initialized: {torch.cuda.get_device_name(0)}")
            
    def init_components(self):
        """Initialize core trading components"""
        try:
            # Initialize trading core with RPC URL
            self.trading_core = TradingCore(self.RPC_URL)
            
            # Initialize basic state
            self.active = True
            self.shutdown_requested = False
            
            # Initialize token validation cache
            self.validation_cache = {}
            
            # Initialize caches and state
            self.processed_txs: Set[str] = set()
            self.recent_trades: List[Dict[str, Any]] = []
            
            logger.system("Core components initialized")
        except Exception as e:
            raise RuntimeError(f"Failed to initialize components: {e}")
            
    def setup_metrics(self):
        """Initialize performance metrics"""
        self.trades_found = 0
        self.trades_executed = 0
        self.successful_trades = 0
        self.total_profit = 0.0
        self.start_time = datetime.now()
        self.balances = {"SOL": 0.0, "WSOL": 0.0}
        self.execution_times = []
        self.wrap_unwrap_operations = {"wrap": 0, "unwrap": 0}
        
    def setup_safety_features(self):
        """Setup safety features to prevent trading errors"""
        # Concurrency controls
        self.trade_lock = asyncio.Lock()
        self.balance_lock = asyncio.Lock()
        self.active_trades = 0
        self.MAX_ACTIVE_TRADES = 1
        
        # Minimum balance thresholds
        balance_config = config.get_balance_config()
        self.min_sol_balance = balance_config.get("min_sol_balance", 0.03)
        self.min_wsol_balance = balance_config.get("min_wsol_balance", 0.02)
        
        # Trading state
        self.is_trading_paused = False
        self.last_trade_time = 0
        self.last_balance_management = 0
        
    def log_startup_info(self):
        """Log startup information and configuration"""
        logger.system(f"""
🚀 Raydium MEV Bot Starting
============================
Position Size: {self.POSITION_SIZE} SOL
Trade Range: {self.MIN_VICTIM_SIZE}-{self.MAX_VICTIM_SIZE} SOL
Min Profit: {self.MIN_PROFIT} SOL
CUDA Enabled: {self.cuda_enabled}
Balance Management: Auto-enabled
============================
Wallet: {self.WALLET}
WSOL Account: {self.WSOL_ACCOUNT}
============================
""")
    
    def extract_ray_log_data(self, log_line: str) -> Optional[bytes]:
        """Extract raw data from ray_log message with improved parsing"""
        try:
            if "ray_log:" not in log_line:
                return None
                
            # Extract the base64 encoded part after ray_log:
            match = re.search(r'ray_log:\s*([A-Za-z0-9+/=]+)', log_line)
            if not match:
                logger.debug(f"No base64 data found in ray_log line: {log_line[:50]}...")
                return None
                
            raw_base64 = match.group(1).strip()
            
            # Decode the base64 data
            raw_data = base64.b64decode(raw_base64)
            
            if len(raw_data) < 9:
                logger.debug(f"Decoded ray_log data too short: {len(raw_data)} bytes")
                return None
                
            return raw_data
                
        except Exception as e:
            logger.error(f"Error extracting ray_log data: {e}")
            if self.debug_mode:
                logger.debug(f"Problematic log line: {log_line[:100]}...")
            return None
    
    def decode_ray_log(self, log_line: str) -> Optional[bytes]:
        """
        Decode Raydium ray_log data from log line with robust error handling
        Returns raw bytes for further processing
        """
        try:
            raw_data = self.extract_ray_log_data(log_line)
            if not raw_data:
                return None
                
            return raw_data
                
        except Exception as e:
            logger.error(f"Ray log decode error: {e}")
            if self.debug_mode:
                logger.debug(traceback.format_exc())
            return None
            
    def parse_trade_data(self, raw_data: bytes, tx_id: str = None) -> Optional[dict]:
        """
        Parse Raydium trade data from raw bytes with enhanced validation and support for different formats
        
        Args:
            raw_data: Raw bytes from ray_log
            tx_id: Optional transaction ID for tracking
            
        Returns:
            Dictionary with trade details or None if invalid
        """
        try:
            # Skip if already processed
            if tx_id and tx_id in self.processed_txs:
                return None
            
            # Validate minimum data length for instruction type and amount
            if len(raw_data) < 9:
                logger.debug(f"Raw data too short for basic parsing: {len(raw_data)} bytes")
                return None
            
            # Extract instruction type and amount
            instruction_type = raw_data[0]
            amount = int.from_bytes(raw_data[1:9], "little") / self.LAMPORTS_PER_SOL
            
            # Validate instruction type (Raydium swap instructions)
            valid_instructions = [0x03, 0x09, 0x0a]
            if instruction_type not in valid_instructions:
                logger.debug(f"Invalid instruction type: 0x{instruction_type:02x}")
                return None
            
            # Validate amount is within our trading range
            if not (self.MIN_VICTIM_SIZE <= amount <= self.MAX_VICTIM_SIZE):
                logger.debug(f"Amount outside trading range: {amount:.4f} SOL")
                return None
            
            # Extract token mints with proper validation and fallbacks
            token_a_mint = None
            token_b_mint = None
            
            # Try to use Rust decoder first (usually faster and more reliable)
            try:
                token_a_mint, token_b_mint, amount_lamports = self.trading_core.decode_ray_log_py(raw_data)
                # Double-check amount with our Python parsing
                if abs(amount_lamports/self.LAMPORTS_PER_SOL - amount) > 0.000001:
                    logger.debug(f"Amount mismatch: Python={amount}, Rust={amount_lamports/self.LAMPORTS_PER_SOL}")
            except Exception as e:
                # Fall back to our Python implementation
                logger.debug(f"Rust decoder failed, using Python fallback: {e}")
                
                # Handle different data formats for different Raydium versions
                # For standard format (bytes 40-72 for Token A)
                if len(raw_data) >= 72:
                    try:
                        token_a_bytes = raw_data[40:72]
                        token_a_mint = base58.b58encode(token_a_bytes).decode()
                        
                        # Basic validation of base58 encoded string
                        if len(token_a_mint) < 32 or not all(c in '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' for c in token_a_mint):
                            logger.debug(f"Invalid Token A format: {token_a_mint}")
                            
                            # Try alternative position (sometimes tokens are at different offsets)
                            if len(raw_data) >= 64:
                                alt_token_bytes = raw_data[32:64]
                                token_a_mint = base58.b58encode(alt_token_bytes).decode()
                                logger.debug(f"Using alternative position for Token A: {token_a_mint}")
                    except Exception as e:
                        logger.debug(f"Token A decode error: {e}")
                        
                        # Try to extract from alternative position as fallback
                        if len(raw_data) >= 64:
                            try:
                                alt_token_bytes = raw_data[32:64]
                                token_a_mint = base58.b58encode(alt_token_bytes).decode()
                                logger.debug(f"Using fallback position for Token A: {token_a_mint}")
                            except Exception:
                                pass
                
                # If we still don't have a valid token_a_mint, try one last approach
                if not token_a_mint and len(raw_data) >= 57:
                    try:
                        # For short formats, try extracting from any 32-byte segment
                        for i in range(9, len(raw_data) - 32, 8):
                            candidate_bytes = raw_data[i:i+32]
                            candidate = base58.b58encode(candidate_bytes).decode()
                            
                            # Check if it looks like a valid Solana address
                            if len(candidate) >= 32 and all(c in '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' for c in candidate):
                                token_a_mint = candidate
                                logger.debug(f"Found potential Token A at offset {i}: {token_a_mint}")
                                break
                    except Exception as e:
                        logger.debug(f"Alternative Token A extraction error: {e}")
                
                # If still no token_a_mint, we can't process this trade
                if not token_a_mint:
                    logger.debug(f"Failed to extract Token A from data of length {len(raw_data)}")
                    return None
                
                # For Token B, we can default to WSOL if not found
                if len(raw_data) >= 104:
                    try:
                        token_b_bytes = raw_data[72:104]
                        token_b_mint = base58.b58encode(token_b_bytes).decode()
                        
                        # Validate token_b_mint format
                        if len(token_b_mint) < 32 or not all(c in '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' for c in token_b_mint):
                            logger.debug(f"Invalid Token B format, using WSOL")
                            token_b_mint = self.WSOL_MINT
                    except Exception:
                        token_b_mint = self.WSOL_MINT
                else:
                    # Default to WSOL if token B data not found
                    token_b_mint = self.WSOL_MINT
            
            # Calculate profit potential
            net_profit = self.calculate_profit(amount, token_a_mint, token_b_mint)
            
            # Skip if not profitable
            if net_profit <= self.MIN_PROFIT:
                logger.debug(f"Not profitable: {net_profit:.6f} SOL (min={self.MIN_PROFIT})")
                return None
            
            # Mark as processed if tx_id provided
            if tx_id:
                self.processed_txs.add(tx_id)
                
                # Prevent the processed_txs set from growing too large
                if len(self.processed_txs) > 10000:
                    # Keep only the most recent 5000 transactions
                    self.processed_txs = set(list(self.processed_txs)[-5000:])
            
            # Return trade opportunity
            return {
                "victim_data": raw_data,
                "amount": amount,
                "token_a_mint": token_a_mint,
                "token_b_mint": token_b_mint,
                "net_profit": net_profit,
                "instruction_type": instruction_type,
                "transaction_id": tx_id or "unknown",
                "timestamp": time.time(),
                "profit_usd": net_profit * self.solana_price_usd
            }
            
        except Exception as e:
            logger.error(f"Trade data parse error: {e}")
            if self.debug_mode:
                logger.debug(traceback.format_exc())
            return None
            
    async def check_initial_balances(self):
        """Check initial SOL and WSOL balances"""
        try:
            # Get SOL balance
            sol_balance = await self.trading_core.get_sol_balance()
            if sol_balance is not None:
                self.balances["SOL"] = sol_balance
            else:
                logger.error("Failed to get SOL balance")
            
            # Get WSOL balance
            wsol_balance = await self.trading_core.get_wsol_balance()
            if wsol_balance is not None:
                self.balances["WSOL"] = wsol_balance
            else:
                logger.error("Failed to get WSOL balance")
            
            # Log initial balances
            logger.info(f"Initial balances - SOL: {self.balances['SOL']:.4f}, WSOL: {self.balances['WSOL']:.4f}")
            
            # Check if balances are sufficient
            if self.balances["SOL"] < self.min_sol_balance:
                logger.warning(f"Low SOL balance: {self.balances['SOL']:.4f}")
                
            if self.balances["WSOL"] < self.POSITION_SIZE:
                logger.warning(f"WSOL balance below position size: {self.balances['WSOL']:.4f}")
            
            return self.balances
        except Exception as e:
            logger.error(f"Balance check error: {e}")
            logger.debug(traceback.format_exc())
            return self.balances
    
    def calculate_profit(self, amount: float, token_a_mint: str, token_b_mint: str) -> float:
        """Calculate estimated profit for a trade opportunity"""
        if not self.cuda_enabled:
            # CPU fallback calculation
            if amount >= 40:
                profit_rate = 0.025
            elif amount >= 20:
                profit_rate = 0.020
            elif amount >= 5:
                profit_rate = 0.015
            else:
                return 0.0
                
            # Calculate costs
            base_cost = 0.002  # Base transaction cost (gas + priority)
            impact = 0.001 * amount  # Price impact (0.1% of amount)
            
            # Calculate net profit
            gross_profit = amount * profit_rate
            net_profit = gross_profit - (impact * 2) - base_cost
            
            return max(0, net_profit)
        else:
            # GPU accelerated calculation
            with torch.cuda.stream(self.cuda_stream):
                # Convert to tensor
                amount_tensor = torch.tensor(amount, device="cuda")
                
                # Calculate profit rate based on amount
                if amount >= 40:
                    profit_rate = 0.025
                elif amount >= 20:
                    profit_rate = 0.020
                elif amount >= 5:
                    profit_rate = 0.015
                else:
                    return 0.0
                    
                profit_rate_tensor = torch.tensor(profit_rate, device="cuda")
                
                # Calculate price impact and costs
                impact = 0.001 * amount_tensor  # 0.1% impact
                gas_cost = torch.tensor(0.000125, device="cuda")
                priority_fee = torch.tensor(0.00185, device="cuda")
                
                # Calculate gross profit and costs
                gross_profit = amount_tensor * profit_rate_tensor
                total_cost = (impact * 2) + gas_cost + priority_fee
                
                # Calculate net profit
                net_profit = gross_profit - total_cost
                
                # Return result, ensuring it's at least the minimum profit
                if net_profit <= self.MIN_PROFIT:
                    return 0.0
                    
                return float(net_profit)
    
    async def monitor_balances(self):
        """Monitor SOL and WSOL balances with automatic wrapping/unwrapping"""
        retry_delay = 1
        max_retry_delay = 30
        
        while self.active:
            try:
                async with websockets.connect(
                    self.WS_URL,
                    extra_headers={
                        "api-key": self.API_KEY,
                        "User-Agent": "solana-client/1.18.23",
                    },
                    ping_interval=20,
                    ping_timeout=30,
                    close_timeout=60
                ) as ws:
                    # Reset retry delay on successful connection
                    retry_delay = 1
                    
                    # Define accounts to monitor
                    accounts = {
                        "SOL": self.WALLET,
                        "WSOL": self.WSOL_ACCOUNT
                    }
                    
                    # Subscribe to account changes
                    for acc_type, address in accounts.items():
                        await ws.send(json.dumps({
                            "jsonrpc": "2.0",
                            "id": 1 if acc_type == "SOL" else 2,
                            "method": "accountSubscribe",
                            "params": [
                                address,
                                {"encoding": "base64", "commitment": "confirmed"}
                            ]
                        }))
                        
                    # Track subscriptions
                    subs = {}
                    
                    # Process messages
                    while self.active:
                        try:
                            msg = await ws.recv()
                            data = json.loads(msg)
                            
                            # Handle subscription confirmation
                            if "result" in data:
                                sub_id = data["result"]
                                acc_type = "SOL" if data["id"] == 1 else "WSOL"
                                subs[sub_id] = acc_type
                                logger.debug(f"{acc_type} subscription active (ID: {sub_id})")
                                continue
                            
                            # Handle account notification
                            if "method" in data and data["method"] == "accountNotification":
                                params = data["params"]
                                sub_id = params["subscription"]
                                
                                if sub_id in subs:
                                    acc_type = subs[sub_id]
                                    value = params["result"]["value"]
                                    
                                    # Update SOL balance
                                    if acc_type == "SOL":
                                        lamports = int(value["lamports"])
                                        balance = lamports / self.LAMPORTS_PER_SOL
                                        
                                        # Skip if no significant change
                                        if abs(balance - self.balances[acc_type]) < 0.00001:
                                            continue
                                            
                                        # Record change
                                        old_balance = self.balances[acc_type]
                                        self.balances[acc_type] = balance
                                        change = balance - old_balance
                                        
                                        logger.info(f"SOL Balance: {balance:.4f} ({'+' if change >= 0 else ''}{change:.4f})")
                                        await self.check_balance_management()
                                    
                                    # Update WSOL balance  
                                    elif acc_type == "WSOL":
                                        try:
                                            if "data" in value:
                                                token_data = base64.b64decode(value["data"][0])
                                                if len(token_data) >= 72:
                                                    amount = int.from_bytes(token_data[64:72], "little")
                                                    balance = amount / self.LAMPORTS_PER_SOL
                                                    
                                                    # Skip if no significant change
                                                    if abs(balance - self.balances[acc_type]) < 0.00001:
                                                        continue
                                                        
                                                    # Record change
                                                    old_balance = self.balances[acc_type]
                                                    self.balances[acc_type] = balance
                                                    change = balance - old_balance
                                                    
                                                    logger.info(f"WSOL Balance: {balance:.4f} ({'+' if change >= 0 else ''}{change:.4f})")
                                                    await self.check_balance_management()
                                        except Exception as e:
                                            logger.warning(f"WSOL data parse error: {e}")
                                            continue
                                    
                                    # Check for low balances
                                    if self.balances["SOL"] < self.min_sol_balance:
                                        logger.warning(f"Low SOL balance: {self.balances['SOL']:.4f}")
                                        
                                    if self.balances["WSOL"] < self.POSITION_SIZE:
                                        if not self.is_trading_paused:
                                            logger.warning(f"WSOL balance below position size, pausing trading: {self.balances['WSOL']:.4f}")
                                            self.is_trading_paused = True
                                    elif self.is_trading_paused and self.balances["WSOL"] >= self.POSITION_SIZE:
                                        logger.info(f"WSOL balance restored, resuming trading: {self.balances['WSOL']:.4f}")
                                        self.is_trading_paused = False
                        
                        except json.JSONDecodeError:
                            logger.warning("Invalid JSON received in balance WebSocket")
                            continue
                        except Exception as e:
                            if not self.shutdown_requested:
                                logger.error(f"Balance monitoring error: {e}")
                            break
                    
            except websockets.ConnectionClosed:
                logger.warning(f"Balance WebSocket disconnected, reconnecting in {retry_delay}s...")
                await asyncio.sleep(retry_delay)
                retry_delay = min(retry_delay * 2, max_retry_delay)
            except Exception as e:
                if not self.shutdown_requested:
                    logger.error(f"Balance monitor error: {e}")
                await asyncio.sleep(retry_delay)
                retry_delay = min(retry_delay * 2, max_retry_delay)
    
    async def check_balance_management(self):
        """Check and manage SOL/WSOL balances to maintain target ratios"""
        # Only check periodically and not during active trades
        current_time = time.time()
        if (self.last_balance_management and 
            current_time - self.last_balance_management < self.BALANCE_CHECK_INTERVAL):
            return
            
        # Skip if trading is active
        if self.active_trades > 0:
            return
            
        async with self.balance_lock:
            self.last_balance_management = current_time
            
            # Calculate total balance
            total_balance = self.balances["SOL"] + self.balances["WSOL"]
            
            # Calculate target WSOL amount (% of total)
            target_wsol = total_balance * self.SOL_WRAP_RATIO
            
            # Calculate how much SOL should be kept unwrapped
            target_sol = total_balance - target_wsol
            
            # Determine if we need to wrap or unwrap
            if self.balances["SOL"] < self.SOL_KEEP_AMOUNT and self.balances["WSOL"] > self.WSOL_UNWRAP_THRESHOLD:
                # Need to unwrap some WSOL to SOL
                unwrap_amount = min(
                    self.SOL_KEEP_AMOUNT - self.balances["SOL"] + 0.002,  # Add buffer for fees
                    self.balances["WSOL"] - self.WSOL_UNWRAP_THRESHOLD
                )
                
                if unwrap_amount > 0.01:  # Only unwrap if significant
                    logger.info(f"Unwrapping {unwrap_amount:.4f} WSOL to SOL")
                    try:
                        result = await self.unwrap_sol(unwrap_amount)
                        if result:
                            logger.info(f"Successfully unwrapped {unwrap_amount:.4f} WSOL to SOL")
                            self.wrap_unwrap_operations["unwrap"] += 1
                        else:
                            logger.warning(f"Failed to unwrap {unwrap_amount:.4f} WSOL to SOL")
                    except Exception as e:
                        logger.error(f"Error unwrapping WSOL: {e}")
                        
            elif (self.balances["SOL"] > self.SOL_KEEP_AMOUNT + 0.01 and 
                  self.balances["WSOL"] < target_wsol):
                # Need to wrap some SOL to WSOL
                wrap_amount = min(
                    self.balances["SOL"] - self.SOL_KEEP_AMOUNT - 0.002,  # Keep buffer for fees
                    target_wsol - self.balances["WSOL"]
                )
                
                if wrap_amount > 0.01:  # Only wrap if significant
                    logger.info(f"Wrapping {wrap_amount:.4f} SOL to WSOL")
                    try:
                        result = await self.wrap_sol(wrap_amount)
                        if result:
                            logger.info(f"Successfully wrapped {wrap_amount:.4f} SOL to WSOL")
                            self.wrap_unwrap_operations["wrap"] += 1
                        else:
                            logger.warning(f"Failed to wrap {wrap_amount:.4f} SOL to WSOL")
                    except Exception as e:
                        logger.error(f"Error wrapping SOL: {e}")
    
    async def wrap_sol(self, amount: float) -> bool:
        """Wrap SOL to WSOL"""
        try:
            # Convert amount to lamports
            lamports = int(amount * self.LAMPORTS_PER_SOL)
            
            # Wrap SOL using trading core
            await self.trading_core.wrap_sol_py(lamports)
            return True
        except Exception as e:
            logger.error(f"SOL wrap error: {e}")
            return False
            
    async def unwrap_sol(self, amount: float) -> bool:
        """Unwrap WSOL to SOL"""
        try:
            # Convert amount to lamports
            lamports = int(amount * self.LAMPORTS_PER_SOL)
            
            # Unwrap WSOL using trading core
            await self.trading_core.unwrap_sol_py(lamports)
            return True
        except Exception as e:
            logger.error(f"WSOL unwrap error: {e}")
            return False
    
    async def update_solana_price(self):
        """Update Solana price periodically using API"""
        retry_delay = 5
        max_retry_delay = 60
        
        while self.active:
            try:
                current_time = time.time()
                # Only update price if enough time has passed
                if current_time - self.last_price_update >= self.PRICE_UPDATE_INTERVAL:
                    # Use CoinGecko API for price
                    async with websockets.connect(
                        "wss://stream.binance.com:9443/ws/solusdt@ticker"
                    ) as ws:
                        msg = await ws.recv()
                        data = json.loads(msg)
                        price = float(data.get("c", 0))  # Current price
                        
                        if price > 0:
                            old_price = self.solana_price_usd
                            self.solana_price_usd = price
                            self.last_price_update = current_time
                            
                            # Log price if it changed significantly
                            if old_price == 0 or abs((price - old_price) / old_price) > 0.005:  # >0.5% change
                                logger.info(f"Solana price updated: ${price:.2f} ({'+' if price > old_price else ''}{((price/old_price)-1)*100:.2f}%)")
                    
                    # Reset retry delay after successful update
                    retry_delay = 5
                
                # Wait before next check
                await asyncio.sleep(retry_delay)
                
            except Exception as e:
                logger.warning(f"Solana price update failed: {e}")
                await asyncio.sleep(retry_delay)
                retry_delay = min(retry_delay * 2, max_retry_delay)
    
    async def monitor_raydium_swaps(self):
        """Monitor Raydium program for swap opportunities with enhanced parsing and rate limiting"""
        retry_delay = 1
        max_retry_delay = 30
        # Add rate limiting
        last_rpc_call = 0
        MIN_RPC_INTERVAL = 0.2  # 200ms minimum between RPC calls (max 5 per second)
        
        while self.active:
            try:
                async with websockets.connect(
                    self.WS_URL,
                    extra_headers={
                        "api-key": self.API_KEY,
                        "User-Agent": "solana-client/1.18.23",
                    },
                    ping_interval=20,
                    ping_timeout=30,
                    close_timeout=60
                ) as ws:
                    # Reset retry delay on successful connection
                    retry_delay = 1
                    logger.info("Connected to mempool WebSocket")
                    
                    # Subscribe to Raydium program logs
                    await ws.send(json.dumps({
                        "jsonrpc": "2.0",
                        "id": 1,
                        "method": "logsSubscribe",
                        "params": [
                            {"mentions": [self.RAYDIUM_PROGRAM_ID]},
                            {"commitment": "processed"}
                        ]
                    }))
                    
                    # Wait for subscription confirmation
                    confirmation = await ws.recv()
                    logger.info("Raydium logs subscription active")
                    
                    # Process messages
                    while self.active:
                        try:
                            msg = await ws.recv()
                            data = json.loads(msg)
                            
                            if "params" in data:
                                result = data["params"].get("result", {})
                                logs = result.get("value", {}).get("logs", [])
                                tx_id = result.get("value", {}).get("signature", "unknown")
                                
                                # Skip if no logs
                                if not logs:
                                    continue
                                    
                                # Skip if not from Raydium
                                if not logs[0].startswith(f"Program {self.RAYDIUM_PROGRAM_ID}"):
                                    continue
                                
                                # Skip if Jupiter is involved (avoid competing with aggregators)
                                if any("Program JUP" in log for log in logs):
                                    continue
                                
                                # Skip if already processed transaction
                                if tx_id in self.processed_txs:
                                    continue
                                    
                                # Check for ray_logs in transaction logs
                                for log in logs:
                                    if "ray_log:" in log:
                                        # Decode raw data
                                        raw_data = self.decode_ray_log(log)
                                        if not raw_data:
                                            logger.debug(f"Failed to decode ray_log: {log[:50]}...")
                                            continue
                                            
                                        # Print full raw data for debugging during early runs
                                        if self.debug_mode:
                                            logger.debug(f"Raw data length: {len(raw_data)} bytes")
                                            logger.debug(f"Raw data hex: {raw_data[:min(50, len(raw_data))].hex()}")
                                        
                                        # Try to parse trade data
                                        trade = self.parse_trade_data(raw_data, tx_id)
                                        
                                        # Process trade if valid opportunity found
                                        if trade:
                                            self.trades_found += 1
                                                
                                            # Skip if trading is paused
                                            if self.is_trading_paused:
                                                logger.info(f"Found opportunity but trading is paused: {trade['amount']:.2f} SOL")
                                                continue
                                                
                                            # Skip if trading is too frequent (rate limiting)
                                            if time.time() - self.last_trade_time < 0.2:  # 200ms cooldown
                                                logger.info(f"Found opportunity but cooldown active: {trade['amount']:.2f} SOL")
                                                continue
                                                
                                            # Skip if already at maximum active trades
                                            if self.active_trades >= self.MAX_ACTIVE_TRADES:
                                                logger.info(f"Maximum active trades reached, skipping opportunity: {trade['amount']:.2f} SOL")
                                                continue
                                                
                                            # RPC rate limiting
                                            current_time = time.time()
                                            time_since_last_rpc = current_time - last_rpc_call
                                            if time_since_last_rpc < MIN_RPC_INTERVAL:
                                                await asyncio.sleep(MIN_RPC_INTERVAL - time_since_last_rpc)
                                                
                                            last_rpc_call = time.time()
                                                
                                            logger.info(f"""
Trade opportunity:
Size: {trade['amount']:.4f} SOL
Token A: {trade['token_a_mint']}
Token B: {trade['token_b_mint']}
Est. Profit: {trade['net_profit']:.6f} SOL (${trade['profit_usd']:.2f})
Tx: {tx_id[:8]}...
""")
                                            # Execute trade
                                            await self.execute_trade(trade)
                                        
                        except json.JSONDecodeError:
                            logger.warning("Invalid JSON received")
                            continue
                        except Exception as e:
                            if not self.shutdown_requested:
                                logger.error(f"Raydium monitoring error: {e}")
                                if self.debug_mode:
                                    logger.debug(traceback.format_exc())
                            break
                    
            except websockets.ConnectionClosed:
                logger.warning(f"Raydium WebSocket disconnected, reconnecting in {retry_delay}s...")
                await asyncio.sleep(retry_delay)
                retry_delay = min(retry_delay * 2, max_retry_delay)
            except Exception as e:
                if not self.shutdown_requested:
                    logger.error(f"Raydium monitor error: {e}")
                    if self.debug_mode:
                        logger.debug(traceback.format_exc())
                await asyncio.sleep(retry_delay)
                retry_delay = min(retry_delay * 2, max_retry_delay)
                
    async def execute_trade(self, trade: dict) -> bool:
        """Execute a trade based on the opportunity with proper error handling and retries"""
        # Skip if already at maximum active trades
        if self.active_trades >= self.MAX_ACTIVE_TRADES:
            logger.info("Maximum active trades reached, skipping opportunity")
            return False
            
        # Skip if not enough WSOL balance
        if self.balances["WSOL"] < self.POSITION_SIZE:
            logger.warning(f"Insufficient WSOL balance: {self.balances['WSOL']:.4f}")
            return False
        
        # Acquire lock to ensure no concurrent trades
        async with self.trade_lock:
            try:
                # Mark trade as active
                self.active_trades += 1
                self.last_trade_time = time.time()
                start_time = time.time()
                entry_tx = None
                exit_tx = None
                
                # Convert position size to lamports
                position_lamports = int(self.POSITION_SIZE * self.LAMPORTS_PER_SOL)
                
                logger.info(f"""
🎯 Executing trade:
Size: {trade['amount']:.4f} SOL 
Token A: {trade['token_a_mint']}
Token B: {trade['token_b_mint']}
Est. Profit: {trade['net_profit']:.6f} SOL
""")
                
                # Prepare Raydium accounts - using correct account list for V4
                market_accounts = [
                    self.RAYDIUM_PROGRAM_ID,
                    config.get("AMM_ID", "CZza3Ej4Mc58MnxWA385itCC9jCo3L1D7zc3LKy1bZMR"),  # AMM ID
                    config.get("MARKET_AMM_AUTHORITY"),  # AMM Authority
                    config.get("MARKET_AMM_OPEN_ORDERS"),  # AMM Open Orders
                    config.get("MARKET_AMM_TARGET"),  # AMM Target
                    config.get("MARKET_POOL_TOKEN_ACCOUNT"),  # Pool Token Coin Account
                    config.get("MARKET_POOL_PC_TOKEN_ACCOUNT", "6DfxGFP8TQnAqkVFEGX3pd5xFhGNkrLhwVR9MYrjpt6h"),  # Pool Token PC Account
                    config.get("SERUM_PROGRAM_ID"),  # Serum Program
                    config.get("MARKET_SERUM_MARKET"),  # Serum Market
                    config.get("MARKET_SERUM_BIDS"),  # Serum Bids
                    config.get("MARKET_SERUM_ASKS"),  # Serum Asks
                    config.get("MARKET_EVENT_QUEUE"),  # Serum Event Queue
                    config.get("MARKET_BASE_VAULT"),  # Serum Base Vault
                    config.get("MARKET_QUOTE_VAULT"),  # Serum Quote Vault
                    config.get("BASE_VAULT_SIGNER"),  # Serum Vault Signer
                    config.get("TOKEN_PROGRAM_ID", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),  # Token Program
                    config.get("SYSTEM_PROGRAM_ID", "11111111111111111111111111111111"),  # System Program
                    config.get("ASSOCIATED_TOKEN_PROGRAM_ID", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")  # Associated Token Program
                ]
                
                # Validate all accounts are present
                if None in market_accounts or len(market_accounts) < 15:
                    logger.error(f"Missing required Raydium accounts, found {len(market_accounts)}/15")
                    return False
                
                # Entry trade parameters
                entry_params = TradeParams(
                    accounts=market_accounts,
                    amount_in=position_lamports,
                    is_buy=True,
                    token_a_mint=trade['token_a_mint'],
                    token_b_mint=trade['token_b_mint'],
                    victim_data=trade['victim_data'],
                    sell_all=False
                )
                
                # Execute entry trade with retries
                MAX_ENTRY_RETRIES = 1  # Only one attempt for entry - must be fast
                for attempt in range(MAX_ENTRY_RETRIES):
                    try:
                        entry_tx = await self.trading_core.execute_trade_py(entry_params)
                        logger.info(f"Entry transaction sent: {entry_tx}")
                        break
                    except Exception as e:
                        logger.error(f"Entry trade attempt {attempt+1} failed: {e}")
                        # Add short delay between retries, but keep it minimal
                        if attempt < MAX_ENTRY_RETRIES - 1:
                            await asyncio.sleep(0.05)
                        else:
                            # All entry attempts failed
                            logger.error(f"All entry attempts failed after {MAX_ENTRY_RETRIES} tries")
                            self.active_trades -= 1
                            return False
                
                # Small delay between entry and exit (time-sensitive)
                await asyncio.sleep(0.0001)
                
                # Exit trade parameters - sell 100% of tokens
                exit_params = TradeParams(
                    accounts=market_accounts,
                    amount_in=None,  # Use sell_all instead
                    is_buy=False,
                    token_a_mint=trade['token_a_mint'],
                    token_b_mint=trade['token_b_mint'],
                    victim_data=None,
                    sell_all=True
                )
                
                # Execute exit trade with more retries
                MAX_EXIT_RETRIES = 3  # Multiple attempts for exit to ensure we don't get stuck with tokens
                for attempt in range(MAX_EXIT_RETRIES):
                    try:
                        exit_tx = await self.trading_core.execute_trade_py(exit_params)
                        logger.info(f"Exit transaction sent: {exit_tx}")
                        break
                    except Exception as e:
                        logger.error(f"Exit trade attempt {attempt+1} failed: {e}")
                        # Add delay between retries, increasing with each failure
                        if attempt < MAX_EXIT_RETRIES - 1:
                            await asyncio.sleep(0.2 * (attempt + 1))
                        else:
                            # All exit attempts failed, but entry succeeded
                            logger.error(f"All exit attempts failed after {MAX_EXIT_RETRIES} tries")
                            # Still count as executed but not successful
                            self.trades_executed += 1
                            self.active_trades -= 1
                            return False
                
                # Record performance
                execution_time = time.time() - start_time
                self.execution_times.append(execution_time)
                
                # Update metrics
                self.trades_executed += 1
                self.successful_trades += 1
                self.total_profit += trade['net_profit']
                
                # Log success
                logger.info(f"""
💰 Trade completed:
Entry TX: {entry_tx}
Exit TX: {exit_tx}
Profit: {trade['net_profit']:.6f} SOL (${trade['profit_usd']:.2f})
Total Profit: {self.total_profit:.6f} SOL (${self.total_profit * self.solana_price_usd:.2f})
Execution Time: {execution_time*1000:.2f}ms
""")
                
                return True
                
            except Exception as e:
                logger.error(f"Trade execution error: {e}")
                if self.debug_mode:
                    logger.debug(traceback.format_exc())
                return False
                
            finally:
                self.active_trades -= 1
    
    async def periodic_maintenance(self):
        """Perform periodic maintenance tasks"""
        while self.active:
            try:
                # Clean up processed transactions every hour
                if len(self.processed_txs) > 5000:
                    logger.info(f"Cleaning up processed transactions cache (size: {len(self.processed_txs)})")
                    self.processed_txs = set(list(self.processed_txs)[-2000:])
                
                # Show performance periodically
                await self.show_performance()
                
                # Wait for next maintenance cycle
                await asyncio.sleep(300)  # 5 minutes
                
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.error(f"Maintenance error: {e}")
                await asyncio.sleep(60)
    
    async def show_performance(self):
        """Display current performance metrics"""
        runtime = (datetime.now() - self.start_time).total_seconds()
        
        hours = int(runtime // 3600)
        minutes = int((runtime % 3600) // 60)
        seconds = int(runtime % 60)
        
        # Calculate USD values
        total_profit_usd = self.total_profit * self.solana_price_usd
        profit_per_hour = (self.total_profit/(runtime/3600) if runtime > 0 else 0)
        profit_per_hour_usd = profit_per_hour * self.solana_price_usd
        
        logger.system(f"""
🏁 Performance Report
============================
Runtime: {hours:02d}:{minutes:02d}:{seconds:02d}

Trade Statistics:
----------------
Opportunities Found: {self.trades_found}
Trades Executed: {self.trades_executed}
Successful Trades: {self.successful_trades}
Success Rate: {(self.successful_trades/max(1,self.trades_executed)*100):.1f}%

Profit Metrics:
--------------
Total Profit: {self.total_profit:.6f} SOL (${total_profit_usd:.2f})
Profit per Hour: {profit_per_hour:.6f} SOL/hr (${profit_per_hour_usd:.2f}/hr)
Average Profit per Trade: {(self.total_profit/max(1,self.successful_trades)):.6f} SOL

Current Balances:
----------------
SOL: {self.balances['SOL']:.4f} (${self.balances['SOL'] * self.solana_price_usd:.2f})
WSOL: {self.balances['WSOL']:.4f} (${self.balances['WSOL'] * self.solana_price_usd:.2f})
Total: {(self.balances['SOL'] + self.balances['WSOL']):.4f} (${(self.balances['SOL'] + self.balances['WSOL']) * self.solana_price_usd:.2f})

Balance Management:
-----------------
Wrap Operations: {self.wrap_unwrap_operations['wrap']}
Unwrap Operations: {self.wrap_unwrap_operations['unwrap']}
SOL Price: ${self.solana_price_usd:.2f}
============================
""")
    
    async def shutdown(self):
        """Shutdown the bot gracefully"""
        logger.system("Shutdown requested, cleaning up...")
        self.shutdown_requested = True
        self.active = False
        
        # Show final performance
        await self.show_performance()
        
        # Clear CUDA cache if needed
        if self.cuda_enabled:
            torch.cuda.empty_cache()
            
        logger.system("Shutdown complete")
    
    async def run(self):
        """Run the bot with all monitoring systems"""
        # Set up signal handlers for graceful shutdown, with Windows compatibility
        if os.name != 'nt':  # Skip on Windows
            try:
                loop = asyncio.get_running_loop()
                for signame in {'SIGINT', 'SIGTERM'}:
                    if hasattr(signal, signame):
                        loop.add_signal_handler(
                            getattr(signal, signame),
                            lambda: asyncio.create_task(self.shutdown())
                        )
            except NotImplementedError:
                # Fallback for Windows
                pass
        
        try:
            # Check initial balances
            await self.check_initial_balances()
            
            # Start all tasks
            tasks = [
                asyncio.create_task(self.monitor_balances()),
                asyncio.create_task(self.monitor_raydium_swaps()),
                asyncio.create_task(self.update_solana_price()),
                asyncio.create_task(self.periodic_maintenance())
            ]
            
            # Wait for tasks to complete or shutdown
            await asyncio.gather(*tasks)
            
        except asyncio.CancelledError:
            logger.system("Tasks cancelled")
        except Exception as e:
            logger.error(f"Fatal error: {e}")
            logger.debug(traceback.format_exc())
        finally:
            await self.shutdown()

async def main():
    """Entry point with proper setup for Windows"""
    if os.name == "nt":
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    
    try:
        print("\n" + "="*80)
        print("Raydium MEV Bot - Starting")
        print("="*80 + "\n")
        
        # Enable debug mode for better logging during initial runs
        os.environ["DEBUG_MODE"] = "1"
        
        # Initialize and run bot
        bot = RaydiumMEVBot()
        await bot.run()
        
    except KeyboardInterrupt:
        print("\nShutdown requested by user")
    except Exception as e:
        logger.error(f"Fatal error: {e}")
        logger.debug(traceback.format_exc())

if __name__ == "__main__":
    asyncio.run(main())    COMBINE THE 2 AND WRTE ME THE FULLS CRIPT PLEASE DONT ALTER THE CODE JUST COMBINE AND MAKE 1 FULLL SCRIPT FROM IT PLEASE