""" Updated configuration validation with platform support. This extends the existing config_loader.py to support platform selection and validation while maintaining backward compatibility. """ import os from typing import Any import yaml from dotenv import load_dotenv from interfaces.core import Platform # Existing validation rules (keeping all existing ones) REQUIRED_FIELDS = [ "name", "rpc_endpoint", "wss_endpoint", "private_key", "trade.buy_amount", "trade.buy_slippage", "trade.sell_slippage", "filters.listener_type", "filters.max_token_age", ] CONFIG_VALIDATION_RULES = [ # (path, type, min_value, max_value, error_message) ( "trade.buy_amount", (int, float), 0, float("inf"), "trade.buy_amount must be a positive number", ), ("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"), ("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"), ( "priority_fees.fixed_amount", int, 0, float("inf"), "priority_fees.fixed_amount must be a non-negative integer", ), ( "priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1", ), ( "priority_fees.hard_cap", int, 0, float("inf"), "priority_fees.hard_cap must be a non-negative integer", ), ( "retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100", ), ( "filters.max_token_age", (int, float), 0, float("inf"), "filters.max_token_age must be a non-negative number", ), ] # Valid values for enum-like fields (extended with platform support) VALID_VALUES = { "filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"], "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"], "trade.exit_strategy": ["time_based", "tp_sl", "manual"], "platform": ["pump_fun", "lets_bonk"], # New platform validation } def load_bot_config(path: str) -> dict: """ Load and validate a bot configuration from a YAML file. Extended to support platform selection with backward compatibility. Args: path: Path to the YAML configuration file (relative or absolute) Returns: Validated configuration dictionary Raises: FileNotFoundError: If the configuration file doesn't exist ValueError: If the configuration is invalid """ with open(path) as f: config = yaml.safe_load(f) env_file = config.get("env_file") if env_file: env_path = os.path.join(os.path.dirname(path), env_file) if os.path.exists(env_path): load_dotenv(env_path, override=True) else: # If not found relative to config, try relative to current working directory load_dotenv(env_file, override=True) resolve_env_vars(config) # Set default platform if not specified (backward compatibility) if "platform" not in config: config["platform"] = "pump_fun" validate_config(config) return config def resolve_env_vars(config: dict) -> None: """ Recursively resolve environment variables in the configuration. Args: config: Configuration dictionary to process """ def resolve_env(value): if isinstance(value, str) and value.startswith("${") and value.endswith("}"): env_var = value[2:-1] env_value = os.getenv(env_var) if env_value is None: raise ValueError(f"Environment variable '{env_var}' not found") return env_value return value def resolve_all(d): for k, v in d.items(): if isinstance(v, dict): resolve_all(v) else: d[k] = resolve_env(v) resolve_all(config) def get_nested_value(config: dict, path: str) -> Any: """ Get a nested value from the configuration using dot notation. Args: config: Configuration dictionary path: Path to the value using dot notation (e.g., "trade.buy_amount") Returns: The value at the specified path Raises: ValueError: If the path doesn't exist in the configuration """ keys = path.split(".") value = config for key in keys: if not isinstance(value, dict) or key not in value: raise ValueError(f"Missing required config key: {path}") value = value[key] return value def validate_config(config: dict) -> None: """ Validate the configuration against defined rules. Extended to include platform-specific validation. Args: config: Configuration dictionary to validate Raises: ValueError: If the configuration is invalid """ # Validate required fields for field in REQUIRED_FIELDS: get_nested_value(config, field) # Validate config rules for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES: try: value = get_nested_value(config, path) if not isinstance(value, expected_type): raise ValueError(f"Type error: {error_msg}") if isinstance(value, (int, float)) and not (min_val <= value <= max_val): raise ValueError(f"Range error: {error_msg}") except ValueError as e: # Re-raise if it's our own error if str(e).startswith(("Type error:", "Range error:")): raise # Otherwise, the field might be missing continue # Validate enum-like fields for path, valid_values in VALID_VALUES.items(): try: value = get_nested_value(config, path) if value not in valid_values: raise ValueError(f"{path} must be one of {valid_values}") except ValueError: # Skip if the field is missing continue # Cannot enable both dynamic and fixed priority fees try: dynamic = get_nested_value(config, "priority_fees.enable_dynamic") fixed = get_nested_value(config, "priority_fees.enable_fixed") if dynamic and fixed: raise ValueError( "Cannot enable both dynamic and fixed priority fees simultaneously" ) except ValueError: # Skip if one of the fields is missing pass # Platform-specific validation platform_str = config.get("platform", "pump_fun") try: platform = Platform(platform_str) validate_platform_config(config, platform) except ValueError as e: if "is not a valid" in str(e): raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}") raise def validate_platform_config(config: dict, platform: Platform) -> None: """ Validate platform-specific configuration requirements. Args: config: Configuration dictionary platform: Platform enum value Raises: ValueError: If platform-specific config is invalid """ if platform == Platform.PUMP_FUN: # pump.fun doesn't require additional config beyond base requirements pass elif platform == Platform.LETS_BONK: # LetsBonk may require additional configuration in the future # For now, it uses the same base configuration as pump.fun pass def get_platform_from_config(config: dict) -> Platform: """ Extract platform enum from configuration. Args: config: Configuration dictionary Returns: Platform enum value Raises: ValueError: If platform is invalid """ platform_str = config.get("platform", "pump_fun") try: return Platform(platform_str) except ValueError: raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}") def print_config_summary(config: dict) -> None: """ Print a summary of the loaded configuration. Extended to show platform information. Args: config: Configuration dictionary """ platform_str = config.get("platform", "pump_fun") print(f"Bot name: {config.get('name', 'unnamed')}") print(f"Platform: {platform_str}") print( f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}" ) trade = config.get("trade", {}) print("Trade settings:") print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL") print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%") print( f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}" ) fees = config.get("priority_fees", {}) print("Priority fees:") if fees.get("enable_dynamic"): print(" - Dynamic fees enabled") elif fees.get("enable_fixed"): print( f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports" ) print("Configuration loaded successfully!") if __name__ == "__main__": # Example usage with platform configuration config = load_bot_config("bots/bot-sniper.yaml") print_config_summary(config) platform = get_platform_from_config(config) print(f"Detected platform: {platform}")