Files
pumpfun-bonkfun-bot_github/src/config_loader.py
T
2025-04-29 11:56:55 +00:00

188 lines
6.5 KiB
Python

import os
from typing import Any
import yaml
from dotenv import load_dotenv
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
VALID_VALUES = {
"filters.listener_type": ["logs", "blocks", "geyser"],
"cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"]
}
def load_bot_config(path: str) -> dict:
"""
Load and validate a bot configuration from a YAML file.
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)
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.
Args:
config: Configuration dictionary to validate
Raises:
ValueError: If the configuration is invalid
"""
for field in REQUIRED_FIELDS:
get_nested_value(config, field)
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
def print_config_summary(config: dict) -> None:
"""
Print a summary of the loaded configuration.
Args:
config: Configuration dictionary
"""
print(f"Bot name: {config.get('name', 'unnamed')}")
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__":
config = load_bot_config("bots/bot-sniper.yaml")
print_config_summary(config)