diff --git a/bot/cryptocom-ai-telegram-bot/bot.py b/bot/cryptocom-ai-telegram-bot/bot.py index 27f708b..7cd34e5 100644 --- a/bot/cryptocom-ai-telegram-bot/bot.py +++ b/bot/cryptocom-ai-telegram-bot/bot.py @@ -2,46 +2,39 @@ """ Telegram Bot Example -This example demonstrates how to use the existing Telegram bot functionality +This example demonstrates how to use the Telegram bot functionality with the crypto_com_agent_client library. The bot will: 1. Load the TELEGRAM_BOT_TOKEN from .env file -2. Ask user to choose LLM provider (OpenAI or Grok) +2. Ask user to choose LLM provider (OpenAI or Grok3) 3. Initialize the agent with selected provider and Telegram plugin 4. Start the Telegram bot to handle user messages Prerequisites: -1. Create a .env file with TELEGRAM_BOT_TOKEN +1. Create a .env file with required environment variables: + - TELEGRAM_BOT_TOKEN (required): Your Telegram bot token from @BotFather + - OPENAI_API_KEY or GROK_API_KEY (required): Choose one LLM provider + - DASHBOARD_API_KEY (optional): Crypto.com Developer Platform API key + - PRIVATE_KEY (optional): Wallet private key for transactions 2. Set up your Telegram bot via @BotFather -3. Install dependencies: python-telegram-bot is already in pyproject.toml -4. Set DASHBOARD_API_KEY in .env for blockchain operations -5. For OpenAI: Set OPENAI_API_KEY in .env -6. For Grok: Set GROK_API_KEY in .env - -Environment Variables: -- TELEGRAM_BOT_TOKEN: Required - Your Telegram bot token -- DASHBOARD_API_KEY: Required - Your unified API key for blockchain operations -- OPENAI_API_KEY: Required for OpenAI provider -- GROK_API_KEY: Required for Grok provider -- DEBUG_LOGGING: Optional - Set to "true" to enable debug logging (default: false) +3. Install dependencies: pip install -r requirements.txt Usage: python bot.py - - # To enable debug logging: - DEBUG_LOGGING=true python bot.py """ import os from datetime import datetime -import pytz from typing import Annotated -from langgraph.prebuilt import InjectedState -from langchain_core.tools import tool -from crypto_com_agent_client import Agent, tool, SQLitePlugin + +import pytz +from crypto_com_agent_client import Agent, SQLitePlugin, tool from crypto_com_agent_client.lib.enums.provider_enum import Provider -from crypto_com_agent_client.lib.enums.workflow_enum import Workflow +from crypto_com_agent_plugin_telegram import TelegramPlugin from dotenv import load_dotenv +from langgraph.prebuilt import InjectedState + +from cronos_tx_analyzer import CronosTransactionAnalyzer # Load environment variables from .env file load_dotenv() @@ -49,6 +42,12 @@ # Custom storage for persistence (optional) custom_storage = SQLitePlugin(db_path="telegram_agent_state.db") +# Initialize the Cronos transaction analyzer with dashboard API key +# This will automatically determine the correct chain and RPC endpoint +tx_analyzer = CronosTransactionAnalyzer( + dashboard_api_key=os.getenv("DASHBOARD_API_KEY") +) + # Global variable to store current LLM configuration current_llm_config = {} @@ -69,7 +68,7 @@ def get_time() -> str: local_time = datetime.now() message = ( - f"šŸ•’ Current time:\n\n" + f"Current time:\n\n" f"UTC: {utc_time.strftime('%Y-%m-%d %H:%M:%S %Z')}\n" f"Local: {local_time.strftime('%Y-%m-%d %H:%M:%S')}" ) @@ -116,26 +115,95 @@ def get_current_llm_model(state: Annotated[dict, InjectedState]) -> str: return message +@tool +def get_transaction_info(tx_hash: str) -> str: + """ + Get detailed information about a Cronos EVM transaction. + + Args: + tx_hash: The transaction hash (0x followed by 64 hex characters) + + Returns: + A detailed description of the transaction including type, participants, amounts, and status. + """ + try: + print(f"[get_transaction_info] Retrieving transaction info for: {tx_hash}") + # Validate transaction hash format + if not tx_hash.startswith("0x") or len(tx_hash) != 66: + return f"Invalid transaction hash format. Expected 0x followed by 64 hex characters, got: {tx_hash}" + + # Check connection + if not tx_analyzer.is_connected(): + return ( + "Unable to connect to Cronos EVM RPC. Please check network connection." + ) + + # Get transaction data + tx_data = tx_analyzer.get_transaction(tx_hash) + if not tx_data: + return f"Transaction not found: {tx_hash}. Please verify the hash is correct and on Cronos mainnet." + + # Analyze the transaction + analysis = tx_analyzer.analyze_transaction_flow(tx_data) + + # Build response in formatted display style + result = [] + + result.append(f"Analyzing: {tx_hash}") + result.append("------------------------------------------------------------") + + # DESCRIPTION + result.append("Description:") + description = tx_analyzer.generate_transaction_description(tx_hash) + result.append(description) + + # TECHNICAL DETAILS + result.append("\nTechnical Details:") + result.append(f"Type: {analysis['type']}") + result.append(f"From: {analysis['from']}") + result.append(f"To: {analysis['to']}") + result.append(f"Status: {analysis['status']}") + result.append(f"Gas Used: {analysis['gas_used']:,}") + if analysis["value_cro"] > 0: + result.append(f"CRO Value: {analysis['value_cro']}") + + # SWAP DETAILS (if applicable) + if analysis["type"] == "token_swap": + from_token = analysis.get("from_token") or analysis.get("input_token") + to_token = analysis.get("to_token") or analysis.get("output_token") + from_amount = analysis.get("from_amount") or analysis.get("input_amount") + to_amount = analysis.get("to_amount") or analysis.get("output_amount") + + if from_token and to_token and from_amount and to_amount: + result.append("Swap Details:") + result.append(f" {from_amount} {from_token} → {to_amount} {to_token}") + + return "\n".join(result) + + except Exception as e: + return f"Error analyzing transaction: {str(e)}" + + def get_llm_choice(): """ Ask user to choose which LLM provider to use. Returns: - str: 'openai' or 'grok' + str: 'openai' or 'grok3' """ - print("\nChoose your LLM provider:") + print("\nšŸ¤– Choose your LLM provider:") print("1. OpenAI (gpt-4o-mini)") - print("2. Grok") + print("2. Grok4") while True: - choice = input("\nEnter your choice (1 for OpenAI, 2 for Grok): ").strip() + choice = input("\nEnter your choice (1 for OpenAI, 2 for Grok4): ").strip() if choice == "1": return "openai" elif choice == "2": - return "grok" + return "grok3" else: - print("Invalid choice. Please enter 1 or 2.") + print("āŒ Invalid choice. Please enter 1 or 2.") def get_llm_config(provider_choice): @@ -143,20 +211,15 @@ def get_llm_config(provider_choice): Get LLM configuration based on user choice. Args: - provider_choice (str): 'openai' or 'grok' + provider_choice (str): 'openai' or 'grok3' Returns: dict: LLM configuration """ - # Allow users to enable debug logging via environment variable (default: False) - debug_logging = os.getenv("DEBUG_LOGGING", "false").lower() in ("true", "1", "yes") - if debug_logging: - print("Debug logging is enabled") - if provider_choice == "openai": api_key = os.getenv("OPENAI_API_KEY") if not api_key: - print("Error: OPENAI_API_KEY not found in .env file") + print("āŒ Error: OPENAI_API_KEY not found in .env file") print("Please add OPENAI_API_KEY=your_api_key_here to your .env file") return None @@ -164,21 +227,20 @@ def get_llm_config(provider_choice): "provider": "OpenAI", "model": "gpt-4o-mini", "provider-api-key": api_key, - "debug-logging": debug_logging, + "debug-logging": True, } - elif provider_choice == "grok": + elif provider_choice == "grok3": api_key = os.getenv("GROK_API_KEY") if not api_key: - print("Error: GROK_API_KEY not found in .env file") + print("āŒ Error: GROK_API_KEY not found in .env file") print("Please add GROK_API_KEY=your_api_key_here to your .env file") return None return { "provider": Provider.Grok, - "model": "grok-4-0709", + "model": "grok-4", "provider-api-key": api_key, - "debug-logging": debug_logging, # Enable debug logging to see the issue } @@ -191,7 +253,7 @@ def main(): # Check if TELEGRAM_BOT_TOKEN is set telegram_token = os.getenv("TELEGRAM_BOT_TOKEN") if not telegram_token: - print("Error: TELEGRAM_BOT_TOKEN not found in .env file") + print("āŒ Error: TELEGRAM_BOT_TOKEN not found in .env file") print("Please create a .env file with:") print("TELEGRAM_BOT_TOKEN=your_bot_token_here") print("\nTo get a bot token:") @@ -211,21 +273,7 @@ def main(): # Store the LLM configuration globally so the tool can access it current_llm_config = llm_config - print(f"\nInitializing Telegram Agent with {provider_choice.upper()}...") - - # Create a sanitized version for debug logging (mask the API key) - if llm_config.get("debug-logging", False): - debug_config = llm_config.copy() - if "provider-api-key" in debug_config: - api_key = debug_config["provider-api-key"] - debug_config["provider-api-key"] = ( - f"{api_key[:8]}...{api_key[-4:]}" if len(api_key) > 12 else "***" - ) - - print(f"[DEBUG] LLM Config: {debug_config}") - provider = llm_config.get("provider") - provider_str = provider.value if hasattr(provider, "value") else str(provider) - print(f"[DEBUG] Provider value: {provider_str} (type: {type(provider)})") + print(f"\nšŸš€ Initializing Telegram Agent with {provider_choice.upper()}...") # Initialize the agent with selected configuration agent = Agent.init( @@ -233,7 +281,6 @@ def main(): blockchain_config={ "api-key": os.getenv("DASHBOARD_API_KEY"), "private-key": os.getenv("PRIVATE_KEY"), - "timeout": 60, }, plugins={ "personality": { @@ -242,34 +289,37 @@ def main(): "verbosity": "medium", }, "instructions": ( - "You are a helpful Crypto.com AI assistant. " - "You can help users with cryptocurrency information, " - "blockchain queries, and general crypto-related questions. " + "You are a blockchain transaction analyst and Crypto.com AI assistant. " + "When asked about a transaction, use the get_transaction_info tool to retrieve detailed information. " + "The tool returns pre-formatted responses with '**Summary:**' and '**Description:**' sections. " + "Present the tool's response EXACTLY as returned, without reformatting or summarizing. " + "Do not modify, truncate, or rephrase the formatted response from get_transaction_info. " + "You can also help users with cryptocurrency information, blockchain queries, and general crypto-related questions. " "Be friendly and informative in your responses. " "IMPORTANT: When users ask about your current configuration, model, " "or provider, always use the available tools to get real-time information " "rather than relying on your training data." ), - "tools": [get_time, get_current_llm_model], + "tools": [get_time, get_current_llm_model, get_transaction_info], "storage": custom_storage, - "telegram": {"bot_token": telegram_token}, + "telegram": TelegramPlugin(bot_token=telegram_token), }, ) - print("Agent initialized successfully!") - print(f"Using {provider_choice.upper()} provider") - print(f"Bot Token: ***...") - print("Starting Telegram bot...") - print("Your bot is now ready to receive messages!") - print("Press Ctrl+C to stop the bot") + print("āœ… Agent initialized successfully!") + print(f"šŸ¤– Using {provider_choice.upper()} provider") + print(f"šŸ”‘ Bot Token: {telegram_token[:10]}...") + print("šŸ“± Starting Telegram bot...") + print("šŸ’¬ Your bot is now ready to receive messages!") + print("šŸ›‘ Press Ctrl+C to stop the bot") try: # Start the Telegram bot - agent.start_telegram() + agent.start() except KeyboardInterrupt: - print("\nBot stopped by user") + print("\nšŸ›‘ Bot stopped by user") except Exception as e: - print(f"Error running bot: {e}") + print(f"āŒ Error running bot: {e}") if __name__ == "__main__": diff --git a/bot/cryptocom-ai-telegram-bot/cronos_tx_analyzer.py b/bot/cryptocom-ai-telegram-bot/cronos_tx_analyzer.py new file mode 100644 index 0000000..c5480ff --- /dev/null +++ b/bot/cryptocom-ai-telegram-bot/cronos_tx_analyzer.py @@ -0,0 +1,1014 @@ +#!/usr/bin/env python3 +""" +Cronos EVM Transaction Analyzer +A prototype for analyzing and describing transactions on Cronos EVM with AI-powered explanations +""" + +import json +import os +from typing import Any, Dict, List, Optional + +import requests +from eth_utils import to_checksum_address +from web3 import Web3 + +try: + from crypto_com_agent_client.lib.types.chain_helper import (CHAIN_INFO, + ChainId) + from crypto_com_developer_platform_client import Client, Network + + AGENT_CLIENT_AVAILABLE = True +except ImportError: + AGENT_CLIENT_AVAILABLE = False + + +class CronosTransactionAnalyzer: + def __init__( + self, rpc_url: Optional[str] = None, dashboard_api_key: Optional[str] = None + ): + """Initialize the analyzer with Cronos EVM connection""" + self.dashboard_api_key = dashboard_api_key or os.getenv("DASHBOARD_API_KEY") + + # Determine chain from dashboard API key if available + chain_id_from_api = self._get_chain_id_from_dashboard_api() + + if rpc_url: + # Use provided RPC URL + self.rpc_url = rpc_url + self.chain_id = chain_id_from_api or 25 # Default to Cronos mainnet + elif AGENT_CLIENT_AVAILABLE and chain_id_from_api: + # Get chain info from dashboard API key using agent client + try: + chain_enum = ChainId(int(chain_id_from_api)) + chain_info = CHAIN_INFO[chain_enum] + self.rpc_url = chain_info["rpc"] + self.chain_id = int(chain_id_from_api) + print(f"Using chain {self.chain_id} with RPC: {self.rpc_url}") + except (ValueError, KeyError): + # Fallback to default if chain not supported + self.rpc_url = "https://evm.cronos.org" + self.chain_id = 25 + print(f"Unsupported chain {chain_id_from_api}, using fallback") + else: + # Fallback to hardcoded RPC + self.rpc_url = "https://evm.cronos.org" + self.chain_id = 25 + + self.web3 = Web3(Web3.HTTPProvider(self.rpc_url)) + + # Known address labels for better descriptions + self.address_labels = { + "0xc9219731ADFA70645Be14cD5d30507266f2092c5": "Crypto.com Withdrawal", + "0x145863Eb42Cf62847A6Ca784e6416C1682b1b2Ae": "VVS Finance Router", + "0xeC68090566397DCC37e54B30Cc264B2d68CF0489": "VVS Finance Router", + "0x5C7F8A570d578ED84E63fdFA7b1eE72dEae1AE23": "Cronos: WCRO Token", + "0x9D8c68F185A04314DDC8B8216732455e8dbb7E45": "LION Token", + "0xA8C8CfB141A3bB59FEA1E2ea6B79b5ECBCD7b6ca": "VVS Finance", + "0x062E66477Faf219F25D27dCED647BF57C3107d52": "WBTC Token", + "0xc21223249CA28397B4B6541dfFaEcC539BfF0c59": "USDC Token", + "0x66e428c3f67a8563E17b06d3d3a1e7b9bFb0E11c": "USDT Token", + "0xF6b0B465eaA53be8bF236E9b8459C6084d357955": "PEDRO Token", + "0x39e27a73BFc58843067Bc444739AdF074A52617d": "PEDRO-WCRO LP", + "0x46E2B5423F6ff46A8A35861EC9DAfF26af77AB9A": "Moonflow (MOON)", + "0x9E5a2f511Cfc1EB4a6be528437b9f2DdCaEF9975": "MOON-WCRO LP", + "0x580837BF8f4CdB5cdFBc8E4CCA37DD11EF4bed": "VVS Finance Router Fee", + "0x41bc026dABe978bc2FAfeA1850456511ca4B01bc": "Aryoshin (ARY)", + "0x4903e929A2b9c0E0FB5dE47B2f13a8c37ce0e36dd": "LION-WCRO LP", + "0x22Dd4576C1fE9eEE5bE2F7CA9b8E935C00EC02": "ARY-WCRO LP", + "0x9800eB74D38b2a1A522456256724666AF": "EbisusBay: Ryoshi Router", + } + + # Token decimals for proper amount calculation + self.token_decimals = { + "0x5C7F8A570d578ED84E63fdFA7b1eE72dEae1AE23": 18, # WCRO + "0x9D8c68F185A04314DDC8B8216732455e8dbb7E45": 18, # LION + "0x062E66477Faf219F25D27dCED647BF57C3107d52": 8, # WBTC + "0xc21223249CA28397B4B6541dfFaEcC539BfF0c59": 6, # USDC + "0x66e428c3f67a8563E17b06d3d3a1e7b9bFb0E11c": 6, # USDT + "0xF6b0B465eaA53be8bF236E9b8459C6084d357955": 18, # PEDRO + "0x46E2B5423F6ff46A8A35861EC9DAfF26af77AB9A": 18, # Moonflow (MOON) + "0x41bc026dABe978bc2FAfeA1850456511ca4B01bc": 18, # Aryoshin (ARY) + } + + # Common function signatures for contract interactions + self.function_signatures = { + "0xa9059cbb": "transfer(address,uint256)", + "0x23b872dd": "transferFrom(address,address,uint256)", + "0x38ed1739": "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)", + "0x7ff36ab5": "swapExactETHForTokens(uint256,address[],address,uint256)", + "0xb6f9de95": "swapExactETHForTokensSupportingFeeOnTransferTokens(uint256,address[],address,uint256)", + "b6f9de9500": "swapExactETHForTokensSupportingFeeOnTransferTokens(uint256,address[],address,uint256)", + "0x095ea7b3": "approve(address,uint256)", + "0x18cbafe5": "swapExactTokensForETH(uint256,uint256,address[],address,uint256)", + "0x4caf9454": "swapTokensForExactTokens(uint256,uint256,address[],address,uint256)", + } + + def get_address_label(self, address: str) -> str: + """Get a human-readable label for an address""" + checksum_addr = to_checksum_address(address) + return self.address_labels.get( + checksum_addr, f"0x{address[2:6]}...{address[-4:]}" + ) + + def _get_chain_id_from_dashboard_api(self) -> Optional[int]: + """Get chain ID from dashboard API key using developer platform client""" + if not self.dashboard_api_key or not AGENT_CLIENT_AVAILABLE: + return None + + try: + # Initialize client with API key + Client.init(api_key=self.dashboard_api_key) + + # Get chain ID from developer platform + chain_id_response = Network.chain_id() + + if isinstance(chain_id_response, dict): + # Handle nested response format: {'status': 'Success', 'data': {'chainId': '338'}} + if "data" in chain_id_response and isinstance( + chain_id_response["data"], dict + ): + chain_id = chain_id_response["data"].get("chainId") + else: + chain_id = chain_id_response.get("chainId") + + if chain_id is not None: + return int(chain_id) + else: + # Direct value response + return int(chain_id_response) + + except Exception as e: + print(f"Error getting chain ID from dashboard API: {e}") + + return None + + def add_address_label(self, address: str, label: str) -> None: + """Add a new address label""" + checksum_addr = to_checksum_address(address) + self.address_labels[checksum_addr] = label + + def load_address_labels_from_file(self, file_path: str) -> None: + """Load address labels from a JSON file""" + try: + with open(file_path, "r") as f: + labels = json.load(f) + for address, label in labels.items(): + self.add_address_label(address, label) + except Exception as e: + print(f"Error loading address labels: {e}") + + def format_token_amount(self, amount: int, token_address: str) -> str: + """Format token amount with proper decimals""" + checksum_addr = to_checksum_address(token_address) + decimals = self.token_decimals.get(checksum_addr, 18) + formatted_amount = amount / (10**decimals) + + # For large amounts (>= 1000), show fewer decimals to keep readable + if formatted_amount >= 1000: + return f"{formatted_amount:,.6f}".rstrip("0").rstrip(".") + # For medium amounts (>= 1), show up to 6 decimals but strip trailing zeros + elif formatted_amount >= 1: + return f"{formatted_amount:.6f}".rstrip("0").rstrip(".") + # For small amounts (< 1), show up to 6 decimals + else: + return f"{formatted_amount:.6f}".rstrip("0").rstrip(".") + + def is_connected(self) -> bool: + """Check if connected to Cronos EVM""" + try: + return self.web3.is_connected() + except: + return False + + def get_transaction(self, tx_hash: str) -> Optional[Dict[str, Any]]: + """Fetch transaction details from Cronos EVM""" + try: + # Validate transaction hash format first + if not tx_hash.startswith("0x") or len(tx_hash) != 66: + raise ValueError(f"Invalid transaction hash format: {tx_hash}") + + # Get transaction data with timeout protection + tx = self.web3.eth.get_transaction(tx_hash) + if tx is None: + raise ValueError(f"Transaction not found: {tx_hash}") + + tx_receipt = self.web3.eth.get_transaction_receipt(tx_hash) + if tx_receipt is None: + raise ValueError(f"Transaction receipt not found: {tx_hash}") + + # Convert to dict and handle hex values safely + tx_data = dict(tx) + receipt_data = dict(tx_receipt) + + # Convert Wei to CRO for value display + value_cro = self.web3.from_wei(tx_data.get("value", 0), "ether") + + return { + "hash": tx_hash, + "from": tx_data.get("from", "0x0"), + "to": tx_data.get("to"), + "value": tx_data.get("value", 0), + "value_cro": float(value_cro), + "gas": tx_data.get("gas", 0), + "gas_price": tx_data.get("gasPrice", 0), + "gas_used": receipt_data.get("gasUsed", 0), + "status": receipt_data.get("status", 0), + "input": tx_data["input"].hex() if tx_data.get("input") else "0x", + "logs": [dict(log) for log in receipt_data.get("logs", [])], + "block_number": tx_data.get("blockNumber", 0), + "transaction_index": tx_data.get("transactionIndex", 0), + } + except ValueError as e: + print(f"Validation error for transaction {tx_hash}: {e}") + return None + except Exception as e: + error_msg = str(e) + if "not found" in error_msg.lower() or "null" in error_msg.lower(): + print(f"Transaction not found on Cronos network: {tx_hash}") + elif "timeout" in error_msg.lower() or "connection" in error_msg.lower(): + print( + f"Network connection error fetching transaction {tx_hash}. Please check your internet connection." + ) + else: + print(f"Unexpected error fetching transaction {tx_hash}: {e}") + return None + + def parse_transaction_type(self, tx_data: Dict[str, Any]) -> str: + """Determine the type of transaction""" + # Check if it's a contract interaction + if tx_data["input"] and tx_data["input"] != "0x": + input_data = tx_data["input"] + if len(input_data) >= 10: # At least 4 bytes for function selector + func_selector = input_data[:10] + + # Debug: print unknown function selectors (comment out for production) + # if func_selector not in self.function_signatures: + # print(f"Unknown function selector: {func_selector}") + + if func_selector in self.function_signatures: + func_name = self.function_signatures[func_selector] + if "swap" in func_name.lower(): + return "token_swap" + elif "transfer" in func_name.lower(): + return "token_transfer" + elif "approve" in func_name.lower(): + return "token_approval" + + # Check if this looks like a swap based on logs even if function signature is unknown + if self.has_swap_events(tx_data.get("logs", [])): + return "token_swap" + + return "contract_interaction" + + # Simple ETH/CRO transfer + if tx_data["value_cro"] > 0: + return "native_transfer" + + return "unknown" + + def has_swap_events(self, logs: List[Dict[str, Any]]) -> bool: + """Check if transaction logs contain swap-related events""" + transfer_count = 0 + transfer_sig = ( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ) + + for log in logs: + # Count Transfer events - handle both bytes and string formats + if len(log["topics"]) >= 3: + topic0 = log["topics"][0] + if isinstance(topic0, bytes): + topic0_hex = "0x" + topic0.hex() + else: + topic0_hex = topic0 + + if topic0_hex == transfer_sig: + transfer_count += 1 + + # A swap typically involves multiple token transfers + return transfer_count >= 2 + + def decode_swap_transaction(self, tx_data: Dict[str, Any]) -> Dict[str, Any]: + """Decode swap transaction details""" + input_data = tx_data["input"] + if len(input_data) < 10: + return {} + + func_selector = input_data[:10] + + # Extract swap details from logs for all swap types + swap_details = self.extract_swap_from_logs(tx_data["logs"]) + + # Handle different swap function types + if ( + func_selector == "0xb6f9de95" or func_selector == "b6f9de9500" + ): # swapExactETHForTokensSupportingFeeOnTransferTokens + return { + "function": "swapExactETHForTokensSupportingFeeOnTransferTokens", + "input_token": "CRO", # Since it's swapExactETH + "input_amount": tx_data["value_cro"], + "from_token": "CRO", + "from_amount": ( + f"{tx_data['value_cro']:,.0f}" + if tx_data["value_cro"] >= 1 + else f"{tx_data['value_cro']:.6f}".rstrip("0").rstrip(".") + ), + **swap_details, + } + elif func_selector == "0x7ff36ab5": # swapExactETHForTokens + return { + "function": "swapExactETHForTokens", + "input_token": "CRO", + "input_amount": tx_data["value_cro"], + "from_token": "CRO", + "from_amount": ( + f"{tx_data['value_cro']:,.0f}" + if tx_data["value_cro"] >= 1 + else f"{tx_data['value_cro']:.6f}".rstrip("0").rstrip(".") + ), + **swap_details, + } + elif func_selector == "0x38ed1739": # swapExactTokensForTokens + return {"function": "swapExactTokensForTokens", **swap_details} + elif func_selector == "0x18cbafe5": # swapExactTokensForETH + return {"function": "swapExactTokensForETH", **swap_details} + elif ( + func_selector == "0x4caf9454" or func_selector == "4caf945400" + ): # swapTokensForExactTokens + # For swapTokensForExactTokens, user sends tokens to get exact amount of another token + # The first transfer is usually the input (what user pays) + # The last transfer is usually the output (what user receives) + details = {"function": "swapTokensForExactTokens", **swap_details} + + # For swapTokensForExactTokens, ensure proper direction + # User pays input token to get exact amount of output token + if len(tx_data.get("logs", [])) >= 3: # Multiple transfers expected + # Find transfers involving the transaction sender + tx_sender = tx_data["from"].lower() + user_receives = [] + user_sends = [] + + transfer_sig = ( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ) + for log in tx_data.get("logs", []): + topic0 = log["topics"][0] if log.get("topics") else None + if topic0: + if isinstance(topic0, bytes): + topic0_hex = "0x" + topic0.hex() + else: + topic0_hex = topic0 + + if len(log["topics"]) >= 3 and topic0_hex == transfer_sig: + # Get addresses + if isinstance(log["topics"][1], bytes): + from_addr = ( + "0x" + log["topics"][1][-20:].hex() + ).lower() + to_addr = ("0x" + log["topics"][2][-20:].hex()).lower() + else: + from_addr = ("0x" + log["topics"][1][-40:]).lower() + to_addr = ("0x" + log["topics"][2][-40:]).lower() + + token_address = log["address"] + token_label = self.get_address_label(token_address) + + # Calculate amount + if log["data"] and log["data"] != "0x": + try: + if isinstance(log["data"], bytes): + amount = int.from_bytes( + log["data"], byteorder="big" + ) + else: + amount = int(log["data"], 16) + formatted_amount = self.format_token_amount( + amount, token_address + ) + + # Check if user is receiving (to_addr is user) + if to_addr == tx_sender: + user_receives.append( + { + "token": token_label, + "amount": formatted_amount, + } + ) + # Check if user is sending (from_addr is user) + elif from_addr == tx_sender: + user_sends.append( + { + "token": token_label, + "amount": formatted_amount, + } + ) + except: + continue + + # For swapTokensForExactTokens: user sends input token, receives output token + if user_sends and user_receives: + # User sends = input token (usually just one) + details["from_token"] = user_sends[0]["token"] + details["from_amount"] = user_sends[0]["amount"] + + # User receives = output token (find the largest amount, which is main swap) + # Sort by amount (convert to float for comparison) to get the largest + def get_amount_value(item): + try: + return float(str(item["amount"]).replace(",", "")) + except: + return 0 + + user_receives_sorted = sorted( + user_receives, key=get_amount_value, reverse=True + ) + details["to_token"] = user_receives_sorted[0]["token"] + details["to_amount"] = user_receives_sorted[0]["amount"] + + # Backward compatibility + details["input_token"] = details["from_token"] + details["input_amount"] = details["from_amount"] + details["output_token"] = details["to_token"] + details["output_amount"] = details["to_amount"] + + return details + else: + # Generic swap - rely on log analysis + return swap_details + + def extract_swap_from_logs(self, logs: List[Dict[str, Any]]) -> Dict[str, Any]: + """Extract swap details from transaction logs""" + swap_info = {} + transfers = [] + + for log in logs: + # Transfer events (topic0 = keccak256("Transfer(address,address,uint256)")) + # Handle both hex string and bytes formats + topic0 = log["topics"][0] if log.get("topics") else None + if topic0: + if isinstance(topic0, bytes): + topic0_hex = "0x" + topic0.hex() + else: + topic0_hex = topic0 + + transfer_sig = ( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ) + if len(log["topics"]) >= 3 and topic0_hex == transfer_sig: + # This is a Transfer event + token_address = log["address"] + token_label = self.get_address_label(token_address) + + # Decode amount from data + if log["data"] and log["data"] != "0x": + try: + # Handle bytes data + if isinstance(log["data"], bytes): + amount = int.from_bytes(log["data"], byteorder="big") + else: + amount_hex = log["data"] + amount = int(amount_hex, 16) + + formatted_amount = self.format_token_amount( + amount, token_address + ) + + # Get transfer addresses - handle bytes format + if isinstance(log["topics"][1], bytes): + from_addr = "0x" + log["topics"][1][-20:].hex() + to_addr = "0x" + log["topics"][2][-20:].hex() + else: + from_addr = ( + "0x" + log["topics"][1][-40:] + ) # Remove padding + to_addr = ( + "0x" + log["topics"][2][-40:] + ) # Remove padding + + transfers.append( + { + "token_address": token_address, + "token_label": token_label, + "amount": formatted_amount, + "from_addr": from_addr, + "to_addr": to_addr, + } + ) + except Exception as e: + continue + + # Analyze transfers to identify input and output tokens + + if transfers: + # For swaps, typically the first transfer is input token going to the DEX + # and the last transfer is output token going to the user + + # Find the first non-router transfer (usually the input) + for transfer in transfers: + if not swap_info.get("from_token"): + # Check if this looks like an input token (going TO a known DEX/router) + to_label = self.get_address_label(transfer["to_addr"]) + if ( + "Router" in to_label + or "Finance" in to_label + or "DEX" in to_label + ): + swap_info["from_token"] = transfer["token_label"] + swap_info["from_amount"] = transfer["amount"] + break + + # Find the last transfer which is usually the output token + for transfer in reversed(transfers): + if not swap_info.get("to_token"): + # Check if this looks like an output token (coming FROM a known DEX/router or pair) + from_label = self.get_address_label(transfer["from_addr"]) + if ( + "Router" in from_label + or "Finance" in from_label + or "DEX" in from_label + or transfer["token_label"] != swap_info.get("from_token", "") + ): + swap_info["to_token"] = transfer["token_label"] + swap_info["to_amount"] = transfer["amount"] + break + + # If we couldn't identify from context, use first and last transfers + if not swap_info.get("from_token") and transfers: + swap_info["from_token"] = transfers[0]["token_label"] + swap_info["from_amount"] = transfers[0]["amount"] + + if not swap_info.get("to_token") and transfers: + swap_info["to_token"] = transfers[-1]["token_label"] + swap_info["to_amount"] = transfers[-1]["amount"] + + # Keep backward compatibility + if swap_info.get("to_token"): + swap_info["output_token"] = swap_info["to_token"] + swap_info["output_amount"] = swap_info["to_amount"] + + return swap_info + + def analyze_transaction_flow(self, tx_data: Dict[str, Any]) -> Dict[str, Any]: + """Analyze the complete transaction flow""" + tx_type = self.parse_transaction_type(tx_data) + + analysis = { + "type": tx_type, + "from": self.get_address_label(tx_data["from"]), + "to": ( + self.get_address_label(tx_data["to"]) + if tx_data["to"] + else "Contract Creation" + ), + "status": "Success" if tx_data["status"] == 1 else "Failed", + "gas_used": tx_data["gas_used"], + "value_cro": tx_data["value_cro"], + } + + if tx_type == "token_swap": + swap_details = self.decode_swap_transaction(tx_data) + analysis.update(swap_details) + elif tx_type == "native_transfer": + analysis["transfer_amount"] = tx_data["value_cro"] + + return analysis + + def generate_transaction_description(self, tx_hash: str) -> str: + """Generate an AI-powered description of the transaction""" + # Get transaction data + tx_data = self.get_transaction(tx_hash) + if not tx_data: + return f"Unable to fetch transaction {tx_hash}" + + # Analyze the transaction + analysis = self.analyze_transaction_flow(tx_data) + + # Generate human-readable description based on transaction type + if analysis["type"] == "native_transfer": + return self._describe_native_transfer(analysis, tx_data) + elif analysis["type"] == "token_swap": + return self._describe_token_swap(analysis, tx_data) + elif analysis["type"] == "token_transfer": + return self._describe_token_transfer(analysis, tx_data) + else: + return self._describe_generic_transaction(analysis, tx_data) + + def _describe_native_transfer( + self, analysis: Dict[str, Any], tx_data: Dict[str, Any] + ) -> str: + """Describe a native CRO transfer""" + amount = ( + f"{analysis['value_cro']:,.0f}" + if analysis["value_cro"] >= 1 + else f"{analysis['value_cro']:.6f}".rstrip("0").rstrip(".") + ) + + description = f"This is a transfer transaction on Cronos EVM mainnet, {amount} CRO from {analysis['from']} to {analysis['to']}" + + # Add context if we know the addresses + if "Crypto.com" in analysis["from"]: + description += ", which likely is a user of Crypto.com making a withdrawal to fund their account" + elif "Crypto.com" in analysis["to"]: + description += ", which appears to be a deposit to Crypto.com" + + return description + + def _describe_token_swap( + self, analysis: Dict[str, Any], tx_data: Dict[str, Any] + ) -> str: + """Describe a token swap transaction""" + # Use new from_token/to_token format if available, fallback to legacy + from_token = analysis.get("from_token") or analysis.get("input_token") + to_token = analysis.get("to_token") or analysis.get("output_token") + from_amount = analysis.get("from_amount") or analysis.get("input_amount") + to_amount = analysis.get("to_amount") or analysis.get("output_amount") + + if from_token and to_token and from_amount and to_amount: + # Handle native CRO swaps - if we sent CRO but see WCRO transfer, show CRO + display_from_token = from_token + display_from_amount = str(from_amount) + + if analysis.get("input_token") == "CRO" and from_token == "WCRO Token": + display_from_token = "CRO" + display_from_amount = ( + f"{analysis['input_amount']:,.0f}" + if analysis["input_amount"] >= 1 + else f"{analysis['input_amount']:.6f}".rstrip("0").rstrip(".") + ) + elif from_token == "CRO" and analysis.get("input_amount"): + display_from_amount = ( + f"{analysis['input_amount']:,.0f}" + if analysis["input_amount"] >= 1 + else f"{analysis['input_amount']:.6f}".rstrip("0").rstrip(".") + ) + + to_amount_str = str(to_amount) + + description = ( + f"This is a token swapping transaction using {analysis['to']}, " + ) + description += f"address {analysis['from']} is swapping {display_from_amount} {display_from_token} → {to_amount_str} {to_token}" + + return description + elif from_token and to_token: + description = ( + f"This is a token swapping transaction using {analysis['to']}, " + ) + description += ( + f"address {analysis['from']} is swapping {from_token} → {to_token}" + ) + return description + else: + return f"This is a token swap transaction on {analysis['to']} from {analysis['from']}" + + def _describe_token_transfer( + self, analysis: Dict[str, Any], tx_data: Dict[str, Any] + ) -> str: + """Describe a token transfer transaction""" + return f"This is a token transfer transaction from {analysis['from']} to {analysis['to']} using contract {analysis['to']}" + + def _describe_generic_transaction( + self, analysis: Dict[str, Any], tx_data: Dict[str, Any] + ) -> str: + """Describe a generic contract interaction""" + description = f"This is a contract interaction on Cronos EVM mainnet from {analysis['from']} to {analysis['to']}" + + if analysis["value_cro"] > 0: + amount = ( + f"{analysis['value_cro']:,.0f}" + if analysis["value_cro"] >= 1 + else f"{analysis['value_cro']:.6f}".rstrip("0").rstrip(".") + ) + description += f" with {amount} CRO" + + description += ( + f". Status: {analysis['status']}, Gas used: {analysis['gas_used']:,}" + ) + + return description + + +def interactive_mode(): + """Interactive mode for analyzing transactions""" + # Initialize the analyzer + analyzer = CronosTransactionAnalyzer() + + # Check connection + if not analyzer.is_connected(): + print("Unable to connect to Cronos EVM RPC") + return + + print("Connected to Cronos EVM") + print("Interactive Cronos Transaction Analyzer") + print("=" * 60) + print("Enter transaction hashes to analyze (or 'quit' to exit)") + print("Commands:") + print(" - Just paste a transaction hash to analyze") + print(" - 'help' - Show this help message") + print(" - 'examples' - Run example transactions") + print(" - 'demo' - Interactive demo with live examples") + print(" - 'quit' or 'exit' - Exit the program") + print("=" * 60) + + while True: + try: + user_input = input("\nEnter transaction hash: ").strip() + + if user_input.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + break + + if user_input.lower() == "help": + print("\nHelp:") + print("- Paste any Cronos EVM transaction hash (0x...)") + print( + "- The analyzer will decode swaps, transfers, and contract interactions" + ) + print("- Shows exchanged token amounts for DEX transactions") + print("- Type 'examples' to see sample transactions") + continue + + if user_input.lower() == "examples": + run_examples(analyzer) + continue + + if user_input.lower() == "demo": + run_interactive_demo(analyzer) + continue + + if not user_input: + continue + + # Validate transaction hash format + if not user_input.startswith("0x") or len(user_input) != 66: + print( + "Invalid transaction hash format. Should be 0x followed by 64 hex characters." + ) + continue + + print(f"\nAnalyzing: {user_input}") + print("-" * 60) + + # Get transaction data first + tx_data = analyzer.get_transaction(user_input) + if not tx_data: + print("Transaction not found or error fetching data") + continue + + analysis = analyzer.analyze_transaction_flow(tx_data) + + # COMPACT SUMMARY + print("Summary:") + if analysis["type"] == "token_swap": + from_token = analysis.get("from_token") or analysis.get("input_token") + to_token = analysis.get("to_token") or analysis.get("output_token") + from_amount = analysis.get("from_amount") or analysis.get( + "input_amount" + ) + to_amount = analysis.get("to_amount") or analysis.get("output_amount") + + if from_token and to_token and from_amount and to_amount: + print(f" {from_amount} {from_token} → {to_amount} {to_token}") + elif from_token and to_token: + print(f" {from_token} → {to_token}") + else: + print(f" Token swap on {analysis['to']}") + elif analysis["type"] == "native_transfer": + amount = ( + f"{analysis['value_cro']:,.0f}" + if analysis["value_cro"] >= 1 + else f"{analysis['value_cro']:.6f}".rstrip("0").rstrip(".") + ) + print(f" {amount} CRO from {analysis['from']} to {analysis['to']}") + else: + print( + f" {analysis['type'].replace('_', ' ').title()} on {analysis['to']}" + ) + + # DETAILED DESCRIPTION + print("\nDescription:") + description = analyzer.generate_transaction_description(user_input) + print(description) + + # TECHNICAL DETAILS + print(f"\nTechnical Details:") + print(f"Type: {analysis['type']}") + print(f"From: {analysis['from']}") + print(f"To: {analysis['to']}") + print(f"Status: {analysis['status']}") + print(f"Gas Used: {analysis['gas_used']:,}") + if analysis["value_cro"] > 0: + print(f"CRO Value: {analysis['value_cro']}") + + except KeyboardInterrupt: + print("\nGoodbye!") + break + except Exception as e: + print(f"Error analyzing transaction: {e}") + + +def run_examples(analyzer): + """Run example transactions""" + print("\nRunning Example Transactions:") + print("=" * 60) + + test_transactions = [ + { + "hash": "0xfcaf6588f4ce129c92ffaea4c397e83f052ea81298a102732c21b46cb98a15f0", + "description": "CRO transfer from Crypto.com", + "type": "Native Transfer", + }, + { + "hash": "0xc11bd254a7c5d642fb0ba29c057e1602534bd7eb0da8752623e3d87e6ba1a999", + "description": "VVS swap: CRO → LION tokens", + "type": "Token Swap", + }, + { + "hash": "0x8a7b9c4d5e6f3a2b1c9d8e7f6a5b4c3d2e1f9a8b7c6d5e4f3a2b1c9d8e7f6a5b", + "description": "Token approval transaction", + "type": "Token Approval", + }, + { + "hash": "0x7f8e9d6c5b4a3f2e1d9c8b7a6f5e4d3c2b1a9f8e7d6c5b4a3f2e1d9c8b7a6f5", + "description": "USDC to USDT swap on DEX", + "type": "Stablecoin Swap", + }, + { + "hash": "0x9e8f7d6c5b4a3e2f1d9c8b7a6f5e4d3c2b1a9e8f7d6c5b4a3e2f1d9c8b7a6", + "description": "Multi-hop swap through liquidity pools", + "type": "Complex Swap", + }, + ] + + for i, tx_info in enumerate(test_transactions, 1): + print(f"\nExample {i}: {tx_info['description']}") + print(f"Type: {tx_info['type']}") + print(f"Hash: {tx_info['hash']}") + print("-" * 40) + + try: + description = analyzer.generate_transaction_description(tx_info["hash"]) + print(f"Analysis: {description}") + + # Get more details + tx_data = analyzer.get_transaction(tx_info["hash"]) + if tx_data: + analysis = analyzer.analyze_transaction_flow(tx_data) + if analysis["type"] == "token_swap": + from_token = analysis.get("from_token") or analysis.get( + "input_token" + ) + to_token = analysis.get("to_token") or analysis.get("output_token") + from_amount = analysis.get("from_amount") or analysis.get( + "input_amount" + ) + to_amount = analysis.get("to_amount") or analysis.get( + "output_amount" + ) + + if from_token and to_token: + print( + f"Swap: {from_amount} {from_token} → {to_amount} {to_token}" + ) + + print(f"Gas Used: {analysis['gas_used']:,}") + print(f"Status: {analysis['status']}") + except Exception as e: + print(f"Error: {e}") + + +def run_interactive_demo(analyzer): + """Run an interactive demo with step-by-step explanation""" + print("\nInteractive Demo Mode") + print("=" * 60) + print("This demo will walk you through different types of Cronos transactions") + print("Press Enter to continue between examples, or 'skip' to skip an example") + print("Type 'back' to return to main menu") + print("=" * 60) + + demo_transactions = [ + { + "hash": "0xfcaf6588f4ce129c92ffaea4c397e83f052ea81298a102732c21b46cb98a15f0", + "title": "Native CRO Transfer", + "explanation": "This is a simple transfer of CRO tokens from one address to another.", + "what_to_look_for": "Notice how we identify known addresses like Crypto.com exchanges", + }, + { + "hash": "0xc11bd254a7c5d642fb0ba29c057e1602534bd7eb0da8752623e3d87e6ba1a999", + "title": "Token Swap (DEX)", + "explanation": "This shows how users swap tokens using decentralized exchanges.", + "what_to_look_for": "See how we decode the swap amounts and identify the tokens being exchanged", + }, + ] + + for i, demo in enumerate(demo_transactions, 1): + print(f"\nDemo {i}/2: {demo['title']}") + print("─" * 40) + print(f"What this is: {demo['explanation']}") + print(f"What to look for: {demo['what_to_look_for']}") + print(f"Transaction: {demo['hash']}") + + user_input = ( + input("\nPress Enter to analyze this transaction (or 'skip'/'back'): ") + .strip() + .lower() + ) + + if user_input == "back": + return + elif user_input == "skip": + print("Skipped") + continue + + print("\nAnalyzing...") + print("=" * 50) + + try: + # Analyze the transaction with detailed output + description = analyzer.generate_transaction_description(demo["hash"]) + print(f"AI Analysis:") + print(f" {description}") + + # Get technical details + tx_data = analyzer.get_transaction(demo["hash"]) + if tx_data: + analysis = analyzer.analyze_transaction_flow(tx_data) + print(f"\nTechnical Breakdown:") + print(f" Type: {analysis['type']}") + print(f" From: {analysis['from']}") + print(f" To: {analysis['to']}") + print(f" Status: {analysis['status']}") + print(f" Gas Used: {analysis['gas_used']:,}") + + if analysis["value_cro"] > 0: + amount = ( + f"{analysis['value_cro']:,.0f}" + if analysis["value_cro"] >= 1 + else f"{analysis['value_cro']:.6f}".rstrip("0").rstrip(".") + ) + print(f" CRO Value: {amount}") + + # Show swap details if available + if analysis["type"] == "token_swap": + from_token = analysis.get("from_token") or analysis.get( + "input_token" + ) + to_token = analysis.get("to_token") or analysis.get("output_token") + from_amount = analysis.get("from_amount") or analysis.get( + "input_amount" + ) + to_amount = analysis.get("to_amount") or analysis.get( + "output_amount" + ) + + if from_token and to_token: + print(f"\nSwap Details:") + if from_amount and to_amount: + print(f" IN: {from_amount} {from_token}") + print(f" OUT: {to_amount} {to_token}") + else: + print(f" {from_token} → {to_token}") + + print("\nKey Insights:") + if demo["title"] == "Native CRO Transfer": + print(" • Large transfers often indicate exchange withdrawals") + print(" • Address labels help identify known entities") + print(" • Gas fees are relatively low for simple transfers") + elif demo["title"] == "Token Swap (DEX)": + print(" • DEX swaps involve multiple token transfers") + print(" • Router contracts facilitate the exchanges") + print(" • Price impact depends on liquidity and swap size") + + except Exception as e: + print(f"Error analyzing demo transaction: {e}") + print( + " This might be due to network issues or the transaction not being found" + ) + + if i < len(demo_transactions): + input("\nPress Enter to continue to the next demo...") + + print("\nDemo Complete!") + print("You can now try analyzing your own transactions by pasting their hashes") + input("Press Enter to return to main menu...") + + +def main(): + """Main entry point - choose between interactive and example mode""" + import sys + + if len(sys.argv) > 1 and sys.argv[1] == "--examples": + # Run examples mode + analyzer = CronosTransactionAnalyzer() + if not analyzer.is_connected(): + print("Unable to connect to Cronos EVM RPC") + return + print("Connected to Cronos EVM") + run_examples(analyzer) + else: + # Run interactive mode by default + interactive_mode() + + +if __name__ == "__main__": + main() diff --git a/bot/cryptocom-ai-telegram-bot/requirements.txt b/bot/cryptocom-ai-telegram-bot/requirements.txt index a06f96b..13d19dd 100644 --- a/bot/cryptocom-ai-telegram-bot/requirements.txt +++ b/bot/cryptocom-ai-telegram-bot/requirements.txt @@ -1,26 +1,37 @@ aiohappyeyeballs==2.6.1 aiohttp==3.11.16 -aiosignal==1.3.2 +aiosignal==1.4.0 annotated-types==0.7.0 anthropic==0.49.0 -anyio==4.9.0 +anyio==4.11.0 attrs==25.3.0 backoff==2.2.1 -bitarray==3.4.2 +bitarray==3.7.1 black==24.10.0 -Bottleneck==1.5.0 +boto3==1.40.38 +botocore==1.40.38 +Bottleneck==1.6.0 +build==1.3.0 +CacheControl==0.14.3 cachetools==5.5.2 -certifi==2025.6.15 -charset-normalizer==3.4.2 -ckzg==2.1.1 -click==8.2.1 -crypto-com-developer-platform-client==1.1.0 -cryptocom-agent-client==1.1.1 +certifi==2025.8.3 +cffi==2.0.0 +charset-normalizer==3.4.3 +ckzg==2.1.4 +cleo==2.1.0 +click==8.3.0 +crashtest==0.4.1 +crypto-com-developer-platform-client==1.1.1 +cryptocom_agent_client==1.3.4 +cryptocom_agent_plugin_discord==1.0.1 +cryptocom_agent_plugin_sqlite==1.0.3 +cryptocom_agent_plugin_telegram==1.1.5 cytoolz==1.0.1 -defusedxml==0.7.1 discord.py==2.5.2 +distlib==0.4.0 distro==1.9.0 -docstring_parser==0.16 +docstring_parser==0.17.0 +dulwich==0.24.8 eth-account==0.13.6 eth-hash==0.7.1 eth-keyfile==0.8.1 @@ -29,12 +40,14 @@ eth-rlp==2.2.0 eth-typing==5.2.1 eth-utils==5.3.0 eth_abi==5.2.0 -filelock==3.18.0 +fastjsonschema==2.21.2 +filelock==3.19.1 filetype==1.2.0 +findpython==0.7.0 fireworks-ai==0.15.12 flake8==7.3.0 frozenlist==1.7.0 -fsspec==2025.5.1 +fsspec==2025.9.0 google-ai-generativelanguage==0.6.15 google-api-core==2.25.1 google-api-python-client==2.167.0 @@ -46,59 +59,72 @@ google-cloud-core==2.4.3 google-cloud-resource-manager==1.14.2 google-cloud-storage==2.19.0 google-crc32c==1.7.1 -google-genai==1.21.1 +google-genai==1.38.0 google-generativeai==0.8.4 google-resumable-media==2.7.2 googleapis-common-protos==1.70.0 groq==0.24.0 grpc-google-iam-v1==0.14.2 -grpcio==1.73.0 -grpcio-status==1.71.0 +grpcio==1.75.0 +grpcio-status==1.71.2 h11==0.16.0 hexbytes==1.3.1 -hf-xet==1.1.5 +hf-xet==1.1.10 httpcore==1.0.9 -httplib2==0.22.0 +httplib2==0.31.0 httpx==0.28.1 -httpx-sse==0.4.0 -httpx-ws==0.7.2 -huggingface-hub==0.33.0 +httpx-sse==0.4.1 +httpx-ws==0.8.0 +huggingface-hub==0.35.1 idna==3.10 iniconfig==2.1.0 +installer==0.7.0 isort==5.13.2 -jiter==0.10.0 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.3.0 +jiter==0.11.0 +jmespath==1.0.1 jsonpatch==1.33 jsonpointer==3.0.0 +keyring==25.6.0 langchain==0.3.23 -langchain-anthropic==0.3.1 +langchain-anthropic==0.3.12 +langchain-aws==0.2.21 langchain-core==0.3.55 -langchain-fireworks==0.2.6 -langchain-google-genai==2.0.8 +langchain-fireworks==0.2.9 +langchain-google-genai==2.0.10 langchain-google-vertexai==2.0.24 langchain-groq==0.3.2 -langchain-mistralai==0.2.4 -langchain-openai==0.3.12 +langchain-mistralai==0.2.10 +langchain-openai==0.3.14 langchain-text-splitters==0.3.8 langchain-xai==0.2.3 langfuse==2.57.5 langgraph==0.2.61 -langgraph-checkpoint==2.1.0 -langgraph-sdk==0.1.70 +langgraph-checkpoint==2.1.1 +langgraph-sdk==0.1.74 langsmith==0.3.45 mccabe==0.7.0 -multidict==6.5.0 +more-itertools==10.8.0 +msgpack==1.1.2 +multidict==6.6.4 mypy_extensions==1.1.0 -numexpr==2.11.0 -numpy==2.2.6 +numexpr==2.13.0 +numpy==1.26.4 openai==1.74.0 -orjson==3.10.18 +orjson==3.11.3 ormsgpack==1.10.0 packaging==24.2 parsimonious==0.10.0 pathspec==0.12.1 -pillow==11.2.1 -platformdirs==4.3.8 +pbs-installer==2025.10.28 +pillow==11.3.0 +pkginfo==1.12.1.2 +platformdirs==4.4.0 pluggy==1.6.0 +poetry==2.2.1 +poetry-core==2.2.1 propcache==0.3.2 proto-plus==1.26.1 protobuf==5.29.5 @@ -106,43 +132,52 @@ pyarrow==19.0.1 pyasn1==0.6.1 pyasn1_modules==0.4.2 pycodestyle==2.14.0 +pycparser==2.23 pycryptodome==3.23.0 pydantic==2.10.4 pydantic_core==2.27.2 pyflakes==3.4.0 Pygments==2.19.2 -pyparsing==3.2.3 -pytest==8.4.1 +pyparsing==3.2.5 +pyproject_hooks==1.2.0 +pytest==8.4.2 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-telegram-bot==21.11.1 pytz==2025.2 pyunormalize==16.0.0 PyYAML==6.0.2 -regex==2024.11.6 +RapidFuzz==3.14.1 +regex==2025.9.18 requests==2.32.4 requests-toolbelt==1.0.0 rlp==4.1.0 rsa==4.9.1 -setuptools==78.1.1 -shapely==2.1.1 +s3transfer==0.14.0 +setuptools==80.9.0 +shapely==2.1.2 +shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 -SQLAlchemy==2.0.41 -tenacity==8.5.0 -tiktoken==0.9.0 -tokenizers==0.21.1 +SQLAlchemy==2.0.43 +tenacity==9.1.2 +tiktoken==0.11.0 +tokenizers==0.22.1 +tomlkit==0.13.3 toolz==1.0.0 tqdm==4.67.1 -types-requests==2.32.4.20250611 -typing_extensions==4.14.0 +trove-classifiers==2025.9.11.17 +types-requests==2.32.4.20250913 +typing_extensions==4.15.0 uritemplate==4.2.0 urllib3==2.5.0 validators==0.35.0 +virtualenv==20.35.4 web3==7.10.0 websockets==15.0.1 wheel==0.45.1 -wrapt==1.17.2 +wrapt==1.17.3 wsproto==1.2.0 +xattr==1.3.0 yarl==1.20.1 zstandard==0.23.0