#!/usr/bin/env python3
"""
Raydium Token Validator - Test Script
====================================
Focused script to detect and validate tokens with verified security features:
- Mint authority revoked
- LP token locked
- Valid pool association
"""

import asyncio
import base58
import base64
import json
import logging
import sys
import time
from typing import Dict, Optional, List, Any

import websockets

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("raydium_validator.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger("raydium_validator")

# Fix for Windows
if sys.platform.startswith('win'):
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

# Configuration
HELIUS_WS_URL = "wss://winny-rychu7-fast-mainnet.helius-rpc.com"
HELIUS_RPC_URL = "https://winny-rychu7-fast-mainnet.helius-rpc.com"
RAYDIUM_PROGRAM_ID = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"
WSOL_MINT = "So11111111111111111111111111111111111111112"
KEYPAIR_PATH = r"C:\solana_rust_bot\keypair.bin"  # Binary keypair path

# Security features layout positions - Validated from multiple token examples
SECURITY_OFFSETS = {
    "mint_authority": 72,  # Offset for mint authority status
    "lp_lock": 88         # Offset for LP lock status
}

# Alternative security check offsets for validation
ALT_SECURITY_OFFSETS = [
    {"mint_authority": 72, "lp_lock": 88},  # Primary (most common)
    {"mint_authority": 73, "lp_lock": 96},  # Alternative 1
    {"mint_authority": 74, "lp_lock": 80}   # Alternative 2
]

class RaydiumTokenValidator:
    def __init__(self, keypair_path=KEYPAIR_PATH):
        self.pools_seen = set()
        self.valid_tokens = []
        self.keypair_path = keypair_path
        
        # Statistics
        self.stats = {
            "pools_scanned": 0,
            "valid_tokens_found": 0,
            "invalid_tokens": 0,
            "start_time": time.time(),
            "connection_errors": 0,
            "reconnections": 0
        }
    
    def is_valid_solana_address(self, address: str) -> bool:
        """Validate a Solana address"""
        if not address or len(address) < 32 or len(address) > 44:
            return False
            
        try:
            decoded = base58.b58decode(address)
            return len(decoded) == 32
        except:
            return False
            
        return True
    
    def bytes_to_pubkey(self, data: bytes) -> str:
        """Convert bytes to Solana pubkey"""
        if not data or len(data) == 0:
            return ""
        return base58.b58encode(data).decode('utf-8')
    
    def verify_security_features(self, data: bytes) -> tuple:
        """
        Verify token security features with multiple validation attempts
        
        Returns (mint_revoked, lp_locked)
        """
        results = []
        
        # Try all security offset combinations
        for offsets in ALT_SECURITY_OFFSETS:
            mint_offset = offsets["mint_authority"]
            lp_offset = offsets["lp_lock"]
            
            try:
                if len(data) <= mint_offset or len(data) < lp_offset + 8:
                    continue
                    
                # Check mint authority (0xf4 = revoked)
                mint_byte = data[mint_offset]
                mint_revoked = (mint_byte == 0xf4)
                
                # Check LP lock (non-zero = locked)
                lp_raw_value = int.from_bytes(data[lp_offset:lp_offset+8], "little")
                lp_locked = (lp_raw_value > 0)
                
                results.append((mint_revoked, lp_locked))
            except Exception:
                continue
        
        # First check primary offsets (most common layout)
        primary_result = next((r for i, r in enumerate(results) if i == 0), None)
        if primary_result and primary_result[0] and primary_result[1]:
            return primary_result
            
        # Check if any layout shows both features secured
        secured_result = next((r for r in results if r[0] and r[1]), None)
        if secured_result:
            return secured_result
            
        # Return primary result or default to False, False
        return primary_result if primary_result else (False, False)
    
    def extract_token_and_pool(self, data: bytes) -> tuple:
        """
        Extract token mint and verify if it's a valid SOL pair
        
        Returns (token_mint, is_sol_pair)
        """
        try:
            # Raydium layout - Token mints are at these offsets
            base_mint_offset = 368
            quote_mint_offset = 400
            
            if len(data) < quote_mint_offset + 32:
                return None, False
                
            # Extract token mints
            base_mint_data = data[base_mint_offset:base_mint_offset+32]
            quote_mint_data = data[quote_mint_offset:quote_mint_offset+32]
            
            base_mint = self.bytes_to_pubkey(base_mint_data)
            quote_mint = self.bytes_to_pubkey(quote_mint_data)
            
            # Verify pubkeys
            if not self.is_valid_solana_address(base_mint) or not self.is_valid_solana_address(quote_mint):
                return None, False
            
            # Check if this is a SOL pair
            if base_mint == WSOL_MINT:
                return quote_mint, True
            elif quote_mint == WSOL_MINT:
                return base_mint, True
            else:
                return None, False
                
        except Exception as e:
            logger.error(f"Error extracting token: {e}")
            return None, False
    
    def process_pool_data(self, pool_address: str, data: bytes) -> Dict:
        """Process a Raydium pool account"""
        try:
            # Verify this is a Raydium AMM account
            if len(data) < 8 or data[0] != 0x06:  # Raydium AMM discriminator = 0x06
                return None
                
            # Extract token mint
            token_mint, is_sol_pair = self.extract_token_and_pool(data)
            if not token_mint or not is_sol_pair:
                return None
                
            # Verify security features
            mint_revoked, lp_locked = self.verify_security_features(data)
            
            # Construct result
            result = {
                "pool_address": pool_address,
                "token_mint": token_mint,
                "mint_revoked": mint_revoked,
                "lp_locked": lp_locked,
                "valid": mint_revoked and lp_locked
            }
            
            if result["valid"]:
                self.valid_tokens.append(result)
                self.stats["valid_tokens_found"] += 1
                logger.info(f"✅ VALID TOKEN: {token_mint} in pool {pool_address}")
                logger.info(f"   Security: Mint Revoked: {mint_revoked}, LP Locked: {lp_locked}")
            else:
                self.stats["invalid_tokens"] += 1
                logger.debug(f"❌ INVALID TOKEN: {token_mint} in pool {pool_address}")
                logger.debug(f"   Security: Mint Revoked: {mint_revoked}, LP Locked: {lp_locked}")
                
            return result
            
        except Exception as e:
            logger.error(f"Error processing pool {pool_address}: {e}")
            return None
    
    def display_stats(self):
        """Display validation statistics"""
        elapsed = time.time() - self.stats["start_time"]
        minutes = int(elapsed // 60)
        seconds = int(elapsed % 60)
        
        print("\n" + "=" * 70)
        print("📊 RAYDIUM TOKEN VALIDATOR STATISTICS")
        print("=" * 70)
        print(f"⏱️ Runtime: {minutes}m {seconds}s")
        print(f"🔍 Pools Scanned: {self.stats['pools_scanned']}")
        print(f"✅ Valid Tokens Found: {self.stats['valid_tokens_found']}")
        print(f"❌ Invalid Tokens: {self.stats['invalid_tokens']}")
        print(f"🔄 Connection Errors: {self.stats['connection_errors']}")
        print(f"🔄 Reconnections: {self.stats['reconnections']}")
        
        if self.stats["pools_scanned"] > 0:
            valid_ratio = (self.stats["valid_tokens_found"] / self.stats["pools_scanned"]) * 100
            print(f"📊 Valid Ratio: {valid_ratio:.2f}%")
            
        print("=" * 70)    
    def display_valid_tokens(self, count=None):
        """Display valid tokens found"""
        if not self.valid_tokens:
            print("\n❌ No valid tokens found yet")
            return
            
        tokens_to_show = self.valid_tokens
        if count is not None:
            tokens_to_show = tokens_to_show[:count]
            
        print("\n" + "=" * 70)
        print(f"✅ VALID TOKENS WITH VERIFIED SECURITY ({len(tokens_to_show)})")
        print("=" * 70)
        
        for i, token in enumerate(tokens_to_show, 1):
            print(f"{i}. Token: {token['token_mint']}")
            print(f"   Pool: {token['pool_address']}")
            print(f"   Security: Mint Revoked ✅ | LP Locked ✅")
            print()
            
        print("=" * 70)
    
    async def scan_tokens(self, duration_seconds=300):
        """
        Scan for tokens with verified security for a specified duration
        """
        print("🚀 RAYDIUM TOKEN VALIDATOR - SECURITY VERIFICATION")
        print(f"⏱️ Test will run for {duration_seconds} seconds")
        print("📡 Connecting to Helius WebSocket...")
        
        start_time = time.time()
        end_time = start_time + duration_seconds
        
        # Setup retry mechanism
        max_retries = 5
        retry_count = 0
        backoff_time = 1
        
        while time.time() < end_time and retry_count < max_retries:
            try:
                async with websockets.connect(HELIUS_WS_URL) as ws:
                    print("✅ Connected to Helius WebSocket")
                    retry_count = 0  # Reset retry counter on successful connection
                    self.stats["reconnections"] += 1
                    
                    # Subscribe to Raydium program
                    subscribe_msg = json.dumps({
                        "jsonrpc": "2.0",
                        "id": 1,
                        "method": "programSubscribe",
                        "params": [
                            RAYDIUM_PROGRAM_ID,
                            {"encoding": "base64", "commitment": "confirmed"}
                        ]
                    })
                    
                    await ws.send(subscribe_msg)
                    response = await ws.recv()
                    response_json = json.loads(response)
                    
                    if "result" not in response_json:
                        print(f"❌ Failed to subscribe: {response_json}")
                        if "error" in response_json and "429" in str(response_json["error"]):
                            print("⚠️ Rate limit hit, waiting to retry...")
                            await asyncio.sleep(backoff_time)
                            backoff_time = min(30, backoff_time * 2)  # Exponential backoff
                            retry_count += 1
                            continue
                        else:
                            return
                        
                    subscription_id = response_json["result"]
                    print(f"✅ Subscribed to Raydium program (ID: {subscription_id})")
                    print("🔍 SCANNING FOR TOKENS WITH VERIFIED SECURITY...")
                    print("   Validating: Mint Authority Revoked + LP Tokens Locked")
                    
                    last_stats_time = time.time()
                    
                    while time.time() < end_time:
                        # Set timeout to ensure we check time remaining
                        try:
                            msg = await asyncio.wait_for(ws.recv(), timeout=5)
                        except asyncio.TimeoutError:
                            # Check if test duration has elapsed
                            if time.time() >= end_time:
                                break
                            continue
                            
                        data = json.loads(msg)
                        
                        # Handle errors - especially rate limits
                        if "error" in data:
                            logger.warning(f"WebSocket error: {data['error']}")
                            if "429" in str(data["error"]):
                                print("⚠️ Rate limit hit, reconnecting...")
                                break  # Break inner loop to reconnect
                            continue
                        
                        # Process program notifications
                        if "method" in data and data["method"] == "programNotification":
                            params = data.get("params", {})
                            result = params.get("result", {})
                            
                            if "value" in result:
                                value = result["value"]
                                pool_address = value["pubkey"]
                                
                                # Skip if already seen
                                if pool_address in self.pools_seen:
                                    continue
                                    
                                # Mark as seen
                                self.pools_seen.add(pool_address)
                                self.stats["pools_scanned"] += 1
                                
                                # Process pool
                                try:
                                    # Decode account data
                                    encoding = value["account"].get("encoding", "base64")
                                    if encoding != "base64":
                                        continue
                                        
                                    data_list = value["account"].get("data", ["", ""])
                                    if not data_list or len(data_list) < 1:
                                        continue
                                        
                                    raw_data = base64.b64decode(data_list[0])
                                    
                                    # Process pool data
                                    self.process_pool_data(pool_address, raw_data)
                                    
                                    # Show stats periodically
                                    current_time = time.time()
                                    if current_time - last_stats_time >= 30:  # Every 30 seconds
                                        last_stats_time = current_time
                                        self.display_stats()
                                        
                                        # Show top valid tokens
                                        if self.valid_tokens:
                                            self.display_valid_tokens(5)
                                            
                                        # Display time remaining
                                        remaining = end_time - current_time
                                        minutes = int(remaining // 60)
                                        seconds = int(remaining % 60)
                                        print(f"⏱️ Time remaining: {minutes}m {seconds}s")
                                        
                                except Exception as e:
                                    logger.error(f"Error processing pool {pool_address}: {e}")
                                    continue
                                    
            except websockets.exceptions.ConnectionClosed as e:
                logger.error(f"WebSocket connection closed: {e}")
                self.stats["connection_errors"] += 1
                retry_count += 1
                backoff_time = min(30, backoff_time * 2)  # Exponential backoff
                print(f"⚠️ Connection closed. Retrying in {backoff_time} seconds... ({retry_count}/{max_retries})")
                await asyncio.sleep(backoff_time)
                
            except Exception as e:
                logger.error(f"Connection error: {e}")
                self.stats["connection_errors"] += 1
                retry_count += 1
                backoff_time = min(30, backoff_time * 2)  # Exponential backoff
                print(f"⚠️ Error occurred. Retrying in {backoff_time} seconds... ({retry_count}/{max_retries})")
                await asyncio.sleep(backoff_time)
        
        # Display final results
        print("\n✅ Test complete!")
        self.display_stats()
        self.display_valid_tokens()

async def main():
    """Main entry point"""
    # Duration to run test (in seconds)
    test_duration = 300  # 5 minutes
    
    # Initialize validator
    validator = RaydiumTokenValidator()
    
    # Run scan
    await validator.scan_tokens(test_duration)

if __name__ == "__main__":
    try:
        if sys.platform.startswith('win'):
            asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(main())
        else:
            asyncio.run(main())
    except KeyboardInterrupt:
        print("\n🛑 Test stopped by user")
    except Exception as e:
        print(f"❌ Critical error: {e}")
        import traceback
        traceback.print_exc() please will u write this so it doesnt have the charm error please     stream.write(msg + self.terminator)
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeEncodeError: 'charmap' codec can't encode character '\u2705' in position 33: character maps to <undefined>
Call stack:
  File "C:\solana_rust_bot\src\python\ws2.py", line 421, in <module>
    loop.run_until_complete(main())
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\base_events.py", line 641, in run_until_complete
    self.run_forever()
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\base_events.py", line 608, in run_forever
    self._run_once()
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\base_events.py", line 1936, in _run_once
    handle._run()
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\events.py", line 84, in _run
    self._context.run(self._callback, *self._args)
  File "C:\solana_rust_bot\src\python\ws2.py", line 413, in main
    await validator.scan_tokens(test_duration)
  File "C:\solana_rust_bot\src\python\ws2.py", line 361, in scan_tokens
    self.process_pool_data(pool_address, raw_data)
  File "C:\solana_rust_bot\src\python\ws2.py", line 198, in process_pool_data
    logger.info(f"✅ VALID TOKEN: {token_mint} in pool {pool_address}")
Message: '✅ VALID TOKEN: 4tTuaHHYmRPLLGtF6aNN7ZoKaYkH8dwhyRzb1tfSJ8AT in pool EsXcHtyGrsqfDHvoJiUu5F2yCmv1YZ5y9ZBP8NE5TdJR'
Arguments: ()
2025-04-20 15:38:08,333 - INFO - ✅ VALID TOKEN: 4tTuaHHYmRPLLGtF6aNN7ZoKaYkH8dwhyRzb1tfSJ8AT in pool EsXcHtyGrsqfDHvoJiUu5F2yCmv1YZ5y9ZBP8NE5TdJR
2025-04-20 15:38:08,335 - INFO -    Security: Mint Revoked: True, LP Locked: True
--- Logging error ---
Traceback (most recent call last):
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\logging\__init__.py", line 1113, in emit
    stream.write(msg + self.terminator)
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeEncodeError: 'charmap' codec can't encode character '\u2705' in position 33: character maps to <undefined>
Call stack:
  File "C:\solana_rust_bot\src\python\ws2.py", line 421, in <module>
    loop.run_until_complete(main())
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\base_events.py", line 641, in run_until_complete
    self.run_forever()
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\base_events.py", line 608, in run_forever
    self._run_once()
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\base_events.py", line 1936, in _run_once
    handle._run()
  File "C:\Users\Orsen\anaconda3\envs\quantum_beast\Lib\asyncio\events.py", line 84, in _run
    self._context.run(self._callback, *self._args)
  File "C:\solana_rust_bot\src\python\ws2.py", line 413, in main
    await validator.scan_tokens(test_duration)
  File "C:\solana_rust_bot\src\python\ws2.py", line 361, in scan_tokens
    self.process_pool_data(pool_address, raw_data)
  File "C:\solana_rust_bot\src\python\ws2.py", line 198, in process_pool_data i use windows pc so i need it rewritten just copy my same code just fix the charm error