mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
323 lines
9.3 KiB
Python
323 lines
9.3 KiB
Python
"""
|
|
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}") |