feat: add support for multiple bots

This commit is contained in:
smypmsa
2025-04-24 20:32:47 +00:00
parent 2aac2f8f64
commit 40e4a4b130
14 changed files with 875 additions and 460 deletions
+71
View File
@@ -0,0 +1,71 @@
# This file defines comprehensive parameters and settings for the trading bot.
# Carefully review and adjust values to match your trading strategy and risk tolerance.
# Bot identification and connection settings
name: "bot-sniper-1"
env_file: ".env"
rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}"
enabled: true # You can turn off the bot w/o removing its config
separate_process: true
# Geyser configuration (fastest method for getting updates)
geyser:
endpoint: "${GEYSER_ENDPOINT}"
api_token: "${GEYSER_API_TOKEN}"
# Trading parameters
# Control trade execution: amount of SOL per trade and acceptable price deviation
trade:
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3
# EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
# The bot buys the specified number of tokens directly, making the process faster but less precise.
extreme_fast_mode: true
extreme_fast_token_amount: 20 # Amount of tokens to buy
# Priority fee configuration
# Manage transaction speed and cost on the Solana network.
# Note: dynamic mode requires an additional RPC call, which slows down the buying process.
priority_fees:
enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees)
enable_fixed: true # Use fixed amount below
fixed_amount: 1_000_000 # Base fee in microlamports
extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%)
hard_cap: 1_000_000 # Maximum allowable fee in microlamports to prevent excessive spending
# Filters for token selection
filters:
match_string: null # Only process tokens with this string in name/symbol
bro_address: null # Only trade tokens created by this user address
listener_type: "geyser" # Method for detecting new tokens: "logs", "blocks", or "geyser"
max_token_age: 0.001 # Maximum token age in seconds for processing
marry_mode: false # Only buy tokens, skip selling
yolo_mode: false # Continuously trade tokens
# Retry and timeout settings
retries:
max_attempts: 1 # Number of attempts for transaction submission
wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled)
wait_after_buy: 15 # Holding period after buy transaction
wait_before_new_token: 15 # Pause between token trades
# Token and account management
cleanup:
# Cleanup mode determines when to manage token accounts. Options:
# "disabled": no cleanup will occur.
# "on_fail": only clean up if a buy transaction fails.
# "after_sell": clean up after selling.
# "post_session": clean up all empty accounts after a trading session ends.
mode: "post_session"
force_close_with_burn: false # Force burning remaining tokens before closing account
with_priority_fee: false # Use priority fees for cleanup transactions
# Node provider configuration (not implemented)
node:
max_rps: 25 # Maximum requests per second
+71
View File
@@ -0,0 +1,71 @@
# This file defines comprehensive parameters and settings for the trading bot.
# Carefully review and adjust values to match your trading strategy and risk tolerance.
# Bot identification and connection settings
name: "bot-sniper-2"
env_file: ".env"
rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}"
enabled: false # You can turn off the bot w/o removing its config
separate_process: true
# Geyser configuration (fastest method for getting updates)
geyser:
endpoint: "${GEYSER_ENDPOINT}"
api_token: "${GEYSER_API_TOKEN}"
# Trading parameters
# Control trade execution: amount of SOL per trade and acceptable price deviation
trade:
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3
# EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
# The bot buys the specified number of tokens directly, making the process faster but less precise.
extreme_fast_mode: true
extreme_fast_token_amount: 20 # Amount of tokens to buy
# Priority fee configuration
# Manage transaction speed and cost on the Solana network.
# Note: dynamic mode requires an additional RPC call, which slows down the buying process.
priority_fees:
enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees)
enable_fixed: true # Use fixed amount below
fixed_amount: 200_000 # Base fee in microlamports
extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%)
hard_cap: 200_000 # Maximum allowable fee in microlamports to prevent excessive spending
# Filters for token selection
filters:
match_string: null # Only process tokens with this string in name/symbol
bro_address: null # Only trade tokens created by this user address
listener_type: "logs" # Method for detecting new tokens: "logs", "blocks", or "geyser"
max_token_age: 0.001 # Maximum token age in seconds for processing
marry_mode: false # Only buy tokens, skip selling
yolo_mode: false # Continuously trade tokens
# Retry and timeout settings
retries:
max_attempts: 1 # Number of attempts for transaction submission
wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled)
wait_after_buy: 15 # Holding period after buy transaction
wait_before_new_token: 15 # Pause between token trades
# Token and account management
cleanup:
# Cleanup mode determines when to manage token accounts. Options:
# "disabled": no cleanup will occur.
# "on_fail": only clean up if a buy transaction fails.
# "after_sell": clean up after selling.
# "post_session": clean up all empty accounts after a trading session ends.
mode: "post_session"
force_close_with_burn: false # Force burning remaining tokens before closing account
with_priority_fee: false # Use priority fees for cleanup transactions
# Node provider configuration (not implemented)
node:
max_rps: 25 # Maximum requests per second
+2 -1
View File
@@ -18,6 +18,7 @@ dependencies = [
"grpcio>=1.71.0",
"grpcio-tools>=1.71.0",
"protobuf>=5.29.4",
"pyyaml>=6.0.2",
]
[project.optional-dependencies]
@@ -26,7 +27,7 @@ dev = [
]
[project.scripts]
pump_bot = "cli:sync_main"
pump_bot = "bot_runner:main"
[tool.ruff]
exclude = [
+152
View File
@@ -0,0 +1,152 @@
import asyncio
import logging
import multiprocessing
from datetime import datetime
from pathlib import Path
from config_loader import load_bot_config, print_config_summary
from trading.trader import PumpTrader
from utils.logger import setup_file_logging
def setup_logging(bot_name: str):
"""
Set up logging to file for a specific bot instance.
Args:
bot_name: Name of the bot for the log file
"""
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_filename = log_dir / f"{bot_name}_{timestamp}.log"
setup_file_logging(str(log_filename))
async def start_bot(config_path: str):
"""
Start a trading bot with the configuration from the specified path.
Args:
config_path: Path to the YAML configuration file
"""
cfg = load_bot_config(config_path)
setup_logging(cfg["name"])
print_config_summary(cfg)
trader = PumpTrader(
# Connection settings
rpc_endpoint=cfg["rpc_endpoint"],
wss_endpoint=cfg["wss_endpoint"],
private_key=cfg["private_key"],
# Trade parameters
buy_amount=cfg["trade"]["buy_amount"],
buy_slippage=cfg["trade"]["buy_slippage"],
sell_slippage=cfg["trade"]["sell_slippage"],
# Extreme fast mode settings
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
# Listener configuration
listener_type=cfg["filters"]["listener_type"],
# Geyser configuration (if applicable)
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
# Priority fee configuration
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False),
enable_fixed_priority_fee=cfg.get("priority_fees", {}).get("enable_fixed", True),
fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000),
extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0),
hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000),
# Retry and timeout settings
max_retries=cfg.get("retries", {}).get("max_attempts", 10),
wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15),
wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15),
wait_time_before_new_token=cfg.get("retries", {}).get("wait_before_new_token", 15),
max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001),
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 30),
# Cleanup settings
cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"),
cleanup_force_close_with_burn=cfg.get("cleanup", {}).get("force_close_with_burn", False),
cleanup_with_priority_fee=cfg.get("cleanup", {}).get("with_priority_fee", False),
# Trading filters
match_string=cfg["filters"].get("match_string"),
bro_address=cfg["filters"].get("bro_address"),
marry_mode=cfg["filters"].get("marry_mode", False),
yolo_mode=cfg["filters"].get("yolo_mode", False),
)
await trader.start()
def run_all_bots():
"""
Run all bots defined in YAML files in the 'bots' directory.
Only runs bots that have enabled=True (or where enabled is not specified).
Bots can be run in separate processes based on their configuration.
"""
bot_dir = Path("bots")
if not bot_dir.exists():
logging.error(f"Bot directory '{bot_dir}' not found")
return
bot_files = list(bot_dir.glob("*.yaml"))
if not bot_files:
logging.error(f"No bot configuration files found in '{bot_dir}'")
return
logging.info(f"Found {len(bot_files)} bot configuration files")
processes = []
skipped_bots = 0
for file in bot_files:
try:
cfg = load_bot_config(str(file))
bot_name = cfg.get("name", file.stem)
# Skip bots with enabled=False
if not cfg.get("enabled", True):
logging.info(f"Skipping disabled bot '{bot_name}'")
skipped_bots += 1
continue
if cfg.get("separate_process", False):
logging.info(f"Starting bot '{bot_name}' in separate process")
p = multiprocessing.Process(
target=lambda path=str(file): asyncio.run(start_bot(path)),
name=f"bot-{bot_name}"
)
p.start()
processes.append(p)
else:
logging.info(f"Starting bot '{bot_name}' in main process")
asyncio.run(start_bot(str(file)))
except Exception as e:
logging.exception(f"Failed to start bot from {file}: {e}")
logging.info(f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots")
for p in processes:
p.join()
logging.info(f"Process {p.name} completed")
def main() -> None:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
run_all_bots()
if __name__ == "__main__":
main()
+8 -2
View File
@@ -1,7 +1,8 @@
import asyncio
from solders.pubkey import Pubkey
from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account
from config import CLEANUP_FORCE_CLOSE_WITH_BURN
from core.client import SolanaClient
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import SystemAddresses
@@ -19,6 +20,7 @@ class AccountCleanupManager:
wallet: Wallet,
priority_fee_manager: PriorityFeeManager,
use_priority_fee: bool = False,
force_burn: bool = False,
):
"""
Args:
@@ -29,6 +31,7 @@ class AccountCleanupManager:
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
self.use_priority_fee = use_priority_fee
self.close_with_force_burn = force_burn
async def cleanup_ata(self, mint: Pubkey) -> None:
"""
@@ -44,6 +47,9 @@ class AccountCleanupManager:
else None
)
logger.info("Waiting for 15 seconds for RPC node to synchronize...")
await asyncio.sleep(15)
try:
info = await solana_client.get_account_info(ata, encoding="base64")
if not info.value:
@@ -53,7 +59,7 @@ class AccountCleanupManager:
balance = await self.client.get_token_account_balance(ata)
instructions = []
if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN:
if balance > 0 and self.close_with_force_burn:
logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...")
burn_ix = burn(
BurnParams(
+21 -16
View File
@@ -1,37 +1,42 @@
from cleanup.manager import AccountCleanupManager
from config import CLEANUP_MODE, CLEANUP_WITH_PRIORITY_FEE
from utils.logger import get_logger
logger = get_logger(__name__)
def should_cleanup_after_failure() -> bool:
return CLEANUP_MODE == "on_fail"
def should_cleanup_after_failure(cleanup_mode) -> bool:
return cleanup_mode == "on_fail"
def should_cleanup_after_sell() -> bool:
return CLEANUP_MODE == "after_sell"
def should_cleanup_after_sell(cleanup_mode) -> bool:
return cleanup_mode == "after_sell"
def should_cleanup_post_session() -> bool:
return CLEANUP_MODE == "post_session"
def should_cleanup_post_session(cleanup_mode) -> bool:
return cleanup_mode == "post_session"
async def handle_cleanup_after_failure(client, wallet, mint, priority_fee_manager):
if should_cleanup_after_failure():
async def handle_cleanup_after_failure(
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
):
if should_cleanup_after_failure(cleanup_mode):
logger.info("[Cleanup] Triggered by failed buy transaction.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
await manager.cleanup_ata(mint)
async def handle_cleanup_after_sell(client, wallet, mint, priority_fee_manager):
if should_cleanup_after_sell():
async def handle_cleanup_after_sell(
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
):
if should_cleanup_after_sell(cleanup_mode):
logger.info("[Cleanup] Triggered after token sell.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
await manager.cleanup_ata(mint)
async def handle_cleanup_post_session(client, wallet, mints, priority_fee_manager):
if should_cleanup_post_session():
async def handle_cleanup_post_session(
client, wallet, mints, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
):
if should_cleanup_post_session(cleanup_mode):
logger.info("[Cleanup] Triggered post trading session.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
for mint in mints:
await manager.cleanup_ata(mint)
-142
View File
@@ -1,142 +0,0 @@
#!/usr/bin/env python3
"""
Command-line interface for the pump.fun trading bot.
"""
import argparse
import asyncio
import os
import sys
from dotenv import load_dotenv
import config
from trading.trader import PumpTrader
from utils.logger import get_logger, setup_file_logging
load_dotenv()
logger = get_logger(__name__)
def parse_args() -> argparse.Namespace:
"""Parse command line arguments.
Returns:
Parsed arguments
"""
parser = argparse.ArgumentParser(description="Trade tokens on pump.fun.")
parser.add_argument(
"--yolo", action="store_true", help="Run in YOLO mode (continuous trading)"
)
parser.add_argument(
"--match",
type=str,
help="Only trade tokens with names or symbols matching this string",
)
parser.add_argument(
"--bro", type=str, help="Only trade tokens created by this user address"
)
parser.add_argument(
"--marry", action="store_true", help="Only buy tokens, skip selling"
)
parser.add_argument(
"--amount",
type=float,
help=f"Amount of SOL to spend on each buy (default: {config.BUY_AMOUNT})",
)
parser.add_argument(
"--buy-slippage",
type=float,
help=f"Buy slippage tolerance (default: {config.BUY_SLIPPAGE})",
)
parser.add_argument(
"--sell-slippage",
type=float,
help=f"Sell slippage tolerance (default: {config.SELL_SLIPPAGE})",
)
return parser.parse_args()
async def main() -> None:
"""Main entry point for the CLI."""
setup_file_logging("pump_trading.log")
args = parse_args()
# Get configuration values, preferring command line args over config.py
rpc_endpoint: str | None = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY")
geyser_endpoint: str | None = os.environ.get("GEYSER_ENDPOINT")
geyser_api_token: str | None = os.environ.get("GEYSER_API_TOKEN")
# Validate configuration values
if not rpc_endpoint or not rpc_endpoint.startswith(("http://", "https://")):
logger.error("Invalid RPC endpoint. Must start with http:// or https://")
sys.exit(1)
if not wss_endpoint or not wss_endpoint.startswith(("ws://", "wss://")):
logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://")
sys.exit(1)
if not private_key or len(private_key) < 80:
logger.error("Invalid private key. Key appears to be missing or too short")
sys.exit(1)
if config.LISTENER_TYPE.lower() == "geyser":
if not geyser_endpoint:
logger.error("GEYSER_ENDPOINT environment variable is not set")
sys.exit(1)
if not geyser_api_token:
logger.error("GEYSER_API_TOKEN environment variable is not set")
sys.exit(1)
# Get trading parameters
buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT
buy_slippage: float = (
args.buy_slippage if args.buy_slippage is not None else config.BUY_SLIPPAGE
)
sell_slippage: float = (
args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE
)
trader: PumpTrader = PumpTrader(
rpc_endpoint=rpc_endpoint, # type: ignore
wss_endpoint=wss_endpoint, # type: ignore
private_key=private_key,
buy_amount=buy_amount,
buy_slippage=buy_slippage,
sell_slippage=sell_slippage,
max_retries=config.MAX_RETRIES,
listener_type=config.LISTENER_TYPE,
geyser_endpoint=geyser_endpoint,
geyser_api_token=geyser_api_token,
)
try:
await trader.start(
match_string=args.match,
bro_address=args.bro,
marry_mode=args.marry,
yolo_mode=args.yolo,
)
except KeyboardInterrupt:
logger.info("Trading stopped by user")
except Exception as e:
logger.error(f"Trading stopped due to error: {e!s}")
finally:
try:
await trader.solana_client.close()
except Exception:
pass
def sync_main() -> None:
asyncio.run(main())
if __name__ == "__main__":
asyncio.run(main())
-112
View File
@@ -1,112 +0,0 @@
"""
Configuration for the pump.fun trading bot.
This file defines comprehensive parameters and settings for the trading bot.
Carefully review and adjust values to match your trading strategy and risk tolerance.
"""
# Trading parameters
# Control trade execution: amount of SOL per trade and acceptable price deviation
BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying
BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%)
SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy
# EXTREME FAST Mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
# The bot buys the specified number of tokens directly, making the process faster but less precise.
EXTREME_FAST_MODE: bool = True
# Amount of tokens to buy in EXTREME FAST mode. No price calculation is done; the bot buys exactly this amount.
EXTREME_FAST_TOKEN_AMOUNT: int = 30
# Priority fee configuration
# Manage transaction speed and cost on the Solana network
ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation
ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee
FIXED_PRIORITY_FEE: int = 500_000 # Base fee in microlamports
EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%)
HARD_CAP_PRIOR_FEE: int = 500_000 # Maximum allowable fee to prevent excessive spending in microlamports
# Listener configuration
# Choose method for detecting new tokens on the network
# "logs": Recommended for more stable token detection
# "blocks": Unstable method, potentially less reliable
# "geyser": The fastest way for getting updates, requires Geyser endpoint
LISTENER_TYPE = "logs"
# Retry and timeout settings
# Control bot resilience and transaction handling
MAX_RETRIES: int = 10 # Number of attempts for transaction submission
# Waiting periods in seconds between actions (TODO: to be replaced with retry mechanism)
WAIT_TIME_AFTER_CREATION: int | float = 15 # Seconds to wait after token creation
WAIT_TIME_AFTER_BUY: int | float = 15 # Holding period after buy transaction
WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades
# Token and account management
# Control token processing and account cleanup strategies
MAX_TOKEN_AGE: int | float = 0.001 # Maximum token age in seconds for processing
# Cleanup mode determines when to manage token accounts. Options:
# "disabled": No cleanup will occur.
# "on_fail": Only clean up if a buy transaction fails.
# "after_sell": Clean up after selling, but only if the balance is zero.
# "post_session": Clean up all empty accounts after a trading session ends.
CLEANUP_MODE: str = "disabled"
CLEANUP_FORCE_CLOSE_WITH_BURN: bool = False # Burn remaining tokens before closing account, else skip ATA with non-zero balances
CLEANUP_WITH_PRIORITY_FEE: bool = False # Use priority fees for cleanup transactions
# Node provider configuration (TODO: to be implemented)
# Manage RPC node interaction to prevent rate limiting
MAX_RPS: int = 25 # Maximum requests per second
def validate_configuration() -> None:
"""
Comprehensive validation of bot configuration.
Checks:
- Type correctness
- Value ranges
- Logical consistency of settings
"""
# Configuration validation checks
config_checks = [
# (value, type, min_value, max_value, error_message)
(BUY_AMOUNT, (int, float), 0, float('inf'), "BUY_AMOUNT must be a positive number"),
(BUY_SLIPPAGE, float, 0, 1, "BUY_SLIPPAGE must be between 0 and 1"),
(SELL_SLIPPAGE, float, 0, 1, "SELL_SLIPPAGE must be between 0 and 1"),
(FIXED_PRIORITY_FEE, int, 0, float('inf'), "FIXED_PRIORITY_FEE must be a non-negative integer"),
(EXTRA_PRIORITY_FEE, float, 0, 1, "EXTRA_PRIORITY_FEE must be between 0 and 1"),
(HARD_CAP_PRIOR_FEE, int, 0, float('inf'), "HARD_CAP_PRIOR_FEE must be a non-negative integer"),
(MAX_RETRIES, int, 0, 100, "MAX_RETRIES must be between 0 and 100")
]
for value, expected_type, min_val, max_val, error_msg in config_checks:
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}")
# Logical consistency checks
if ENABLE_DYNAMIC_PRIORITY_FEE and ENABLE_FIXED_PRIORITY_FEE:
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
# Validate listener type
if LISTENER_TYPE not in ["logs", "blocks", "geyser"]:
raise ValueError("LISTENER_TYPE must be either 'logs' or 'blocks'")
# Validate cleanup mode
valid_cleanup_modes = ["disabled", "on_fail", "after_sell", "post_session"]
if CLEANUP_MODE not in valid_cleanup_modes:
raise ValueError(f"CLEANUP_MODE must be one of {valid_cleanup_modes}")
# Validate configuration on import
validate_configuration()
+188
View File
@@ -0,0 +1,188 @@
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)
else:
# If not found relative to config, try relative to current working directory
load_dotenv(env_file)
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)
+3 -2
View File
@@ -47,7 +47,8 @@ class SolanaClient:
self._cached_blockhash = blockhash
except Exception as e:
logger.warning(f"Blockhash fetch failed: {e!s}")
await asyncio.sleep(interval)
finally:
await asyncio.sleep(interval)
async def get_cached_blockhash(self) -> Hash:
"""Return the most recently cached blockhash."""
@@ -195,7 +196,7 @@ class SolanaClient:
"""
client = await self.get_client()
try:
await client.confirm_transaction(signature, commitment=commitment)
await client.confirm_transaction(signature, commitment=commitment, sleep_seconds=1)
return True
except Exception as e:
logger.error(f"Failed to confirm transaction {signature}: {e!s}")
+352 -153
View File
@@ -10,7 +10,6 @@ from datetime import datetime
from solders.pubkey import Pubkey
import config
from cleanup.modes import (
handle_cleanup_after_failure,
handle_cleanup_after_sell,
@@ -34,7 +33,6 @@ logger = get_logger(__name__)
class PumpTrader:
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
def __init__(
self,
rpc_endpoint: str,
@@ -43,13 +41,40 @@ class PumpTrader:
buy_amount: float,
buy_slippage: float,
sell_slippage: float,
max_retries: int = 5,
listener_type: str = "logs",
geyser_endpoint: str | None = None,
geyser_api_token: str | None = None,
extreme_fast_mode: bool = False,
extreme_fast_token_amount: int = 30,
# Priority fee configuration
enable_dynamic_priority_fee: bool = False,
enable_fixed_priority_fee: bool = True,
fixed_priority_fee: int = 200_000,
extra_priority_fee: float = 0.0,
hard_cap_prior_fee: int = 200_000,
# Retry and timeout settings
max_retries: int = 3,
wait_time_after_creation: int = 15, # here and further - seconds
wait_time_after_buy: int = 15,
wait_time_before_new_token: int = 15,
max_token_age: int | float = 0.001,
token_wait_timeout: int = 30,
# Cleanup settings
cleanup_mode: str = "disabled",
cleanup_force_close_with_burn: bool = False,
cleanup_with_priority_fee: bool = False,
# Trading filters
match_string: str | None = None,
bro_address: str | None = None,
marry_mode: bool = False,
yolo_mode: bool = False,
):
"""Initialize the pump trader.
Args:
rpc_endpoint: RPC endpoint URL
wss_endpoint: WebSocket endpoint URL
@@ -57,24 +82,47 @@ class PumpTrader:
buy_amount: Amount of SOL to spend on buys
buy_slippage: Slippage tolerance for buys
sell_slippage: Slippage tolerance for sells
max_retries: Maximum number of retry attempts
listener_type: Type of listener to use ('logs', 'blocks', or 'geyser')
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
geyser_api_token: Geyser API token (required for geyser listener)
extreme_fast_mode: Whether to enable extreme fast mode
extreme_fast_token_amount: Maximum token amount for extreme fast mode
enable_dynamic_priority_fee: Whether to enable dynamic priority fees
enable_fixed_priority_fee: Whether to enable fixed priority fees
fixed_priority_fee: Fixed priority fee amount
extra_priority_fee: Extra percentage for priority fees
hard_cap_prior_fee: Hard cap for priority fees
max_retries: Maximum number of retry attempts
wait_time_after_creation: Time to wait after token creation (seconds)
wait_time_after_buy: Time to wait after buying a token (seconds)
wait_time_before_new_token: Time to wait before processing a new token (seconds)
max_token_age: Maximum age of token to process (seconds)
token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds)
cleanup_mode: Cleanup mode ("disabled", "auto", or "manual")
cleanup_force_close_with_burn: Whether to force close with burn during cleanup
cleanup_with_priority_fee: Whether to use priority fees during cleanup
match_string: Optional string to match in token name/symbol
bro_address: Optional creator address to filter by
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, trade continuously
"""
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
self.curve_manager = BondingCurveManager(self.solana_client)
self.priority_fee_manager = PriorityFeeManager(
client=self.solana_client,
enable_dynamic_fee=config.ENABLE_DYNAMIC_PRIORITY_FEE,
enable_fixed_fee=config.ENABLE_FIXED_PRIORITY_FEE,
fixed_fee=config.FIXED_PRIORITY_FEE,
extra_fee=config.EXTRA_PRIORITY_FEE,
hard_cap=config.HARD_CAP_PRIOR_FEE,
enable_dynamic_fee=enable_dynamic_priority_fee,
enable_fixed_fee=enable_fixed_priority_fee,
fixed_fee=fixed_priority_fee,
extra_fee=extra_priority_fee,
hard_cap=hard_cap_prior_fee,
)
self.buyer = TokenBuyer(
self.solana_client,
self.wallet,
@@ -83,10 +131,9 @@ class PumpTrader:
buy_amount,
buy_slippage,
max_retries,
config.EXTREME_FAST_TOKEN_AMOUNT,
config.EXTREME_FAST_MODE
extreme_fast_token_amount,
extreme_fast_mode
)
self.seller = TokenSeller(
self.solana_client,
self.wallet,
@@ -95,7 +142,7 @@ class PumpTrader:
sell_slippage,
max_retries,
)
# Initialize the appropriate listener type
listener_type = listener_type.lower()
if listener_type == "geyser":
@@ -114,69 +161,166 @@ class PumpTrader:
else:
self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using blockSubscribe listener for token monitoring")
# Trading parameters
self.buy_amount = buy_amount
self.buy_slippage = buy_slippage
self.sell_slippage = sell_slippage
self.max_retries = max_retries
self.max_token_age = config.MAX_TOKEN_AGE
self.extreme_fast_mode = extreme_fast_mode
self.extreme_fast_token_amount = extreme_fast_token_amount
# Timing parameters
self.wait_time_after_creation = wait_time_after_creation
self.wait_time_after_buy = wait_time_after_buy
self.wait_time_before_new_token = wait_time_before_new_token
self.max_token_age = max_token_age
self.token_wait_timeout = token_wait_timeout
# Cleanup parameters
self.cleanup_mode = cleanup_mode
self.cleanup_force_close_with_burn = cleanup_force_close_with_burn
self.cleanup_with_priority_fee = cleanup_with_priority_fee
# Trading filters/modes
self.match_string = match_string
self.bro_address = bro_address
self.marry_mode = marry_mode
self.yolo_mode = yolo_mode
# State tracking
self.traded_mints: set[Pubkey] = set()
# Token processing state
self.token_queue = asyncio.Queue()
self.processing = False
self.token_queue: asyncio.Queue = asyncio.Queue()
self.processing: bool = False
self.processed_tokens: set[str] = set()
self.token_timestamps: dict[str, float] = {}
async def start(
self,
match_string: str | None = None,
bro_address: str | None = None,
marry_mode: bool = False,
yolo_mode: bool = False,
) -> None:
"""Start the trading bot.
Args:
match_string: Optional string to match in token name/symbol
bro_address: Optional creator address to filter by
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, trade continuously
"""
async def start(self) -> None:
"""Start the trading bot and listen for new tokens."""
logger.info("Starting pump.fun trader")
logger.info(f"Match filter: {match_string if match_string else 'None'}")
logger.info(f"Creator filter: {bro_address if bro_address else 'None'}")
logger.info(f"Marry mode: {marry_mode}")
logger.info(f"YOLO mode: {yolo_mode}")
logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}")
logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}")
logger.info(f"Marry mode: {self.marry_mode}")
logger.info(f"YOLO mode: {self.yolo_mode}")
logger.info(f"Max token age: {self.max_token_age} seconds")
# Start processor task
processor_task = asyncio.create_task(
self._process_token_queue(marry_mode, yolo_mode)
)
try:
await self.token_listener.listen_for_tokens(
lambda token: self._queue_token(token),
match_string,
bro_address,
)
# Choose operating mode based on yolo_mode
if not self.yolo_mode:
# Single token mode: process one token and exit
logger.info("Running in single token mode - will process one token and exit")
token_info = await self._wait_for_token()
if token_info:
await self._handle_token(token_info)
logger.info("Finished processing single token. Exiting...")
else:
logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...")
else:
# Continuous mode: process tokens until interrupted
logger.info("Running in continuous mode - will process tokens until interrupted")
processor_task = asyncio.create_task(
self._process_token_queue()
)
try:
await self.token_listener.listen_for_tokens(
lambda token: self._queue_token(token),
self.match_string,
self.bro_address,
)
except Exception as e:
logger.error(f"Token listening stopped due to error: {e!s}")
finally:
processor_task.cancel()
try:
await processor_task
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Trading stopped due to error: {e!s}")
processor_task.cancel()
await self.solana_client.close()
finally:
processor_task.cancel()
if self.traded_mints:
# Close ATA if enabled
await handle_cleanup_post_session(self.solana_client, self.wallet, list(self.traded_mints), self.priority_fee_manager)
await self.solana_client.close()
await self._cleanup_resources()
logger.info("Pump trader has shut down")
async def _queue_token(self, token_info: TokenInfo) -> None:
"""Queue a token for processing if not already processed."""
async def _wait_for_token(self) -> TokenInfo | None:
"""Wait for a single token to be detected.
Returns:
TokenInfo or None if timeout occurs
"""
# Create a one-time event to signal when a token is found
token_found = asyncio.Event()
found_token = None
async def token_callback(token: TokenInfo) -> None:
nonlocal found_token
token_key = str(token.mint)
# Only process if not already processed and fresh
if token_key not in self.processed_tokens:
# Record when the token was discovered
self.token_timestamps[token_key] = asyncio.get_event_loop().time()
found_token = token
self.processed_tokens.add(token_key)
token_found.set()
listener_task = asyncio.create_task(
self.token_listener.listen_for_tokens(
token_callback,
self.match_string,
self.bro_address,
)
)
# Wait for a token with a timeout
try:
logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...")
await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout)
logger.info(f"Found token: {found_token.symbol} ({found_token.mint})")
return found_token
except TimeoutError:
logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token")
return None
finally:
listener_task.cancel()
try:
await listener_task
except asyncio.CancelledError:
pass
async def _cleanup_resources(self) -> None:
"""Perform cleanup operations before shutting down."""
if self.traded_mints:
try:
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
await handle_cleanup_post_session(
self.solana_client,
self.wallet,
list(self.traded_mints),
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
self.cleanup_force_close_with_burn
)
except Exception as e:
logger.error(f"Error during cleanup: {e!s}")
old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens}
for key in old_keys:
self.token_timestamps.pop(key, None)
await self.solana_client.close()
async def _queue_token(
self, token_info: TokenInfo
) -> None:
"""Queue a token for processing if not already processed.
Args:
token_info: Token information to queue
"""
token_key = str(token_info.mint)
if token_key in self.processed_tokens:
@@ -189,126 +333,178 @@ class PumpTrader:
await self.token_queue.put(token_info)
logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})")
async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None:
async def _process_token_queue(self) -> None:
"""Continuously process tokens from the queue, only if they're fresh."""
while True:
token_info = await self.token_queue.get()
token_key = str(token_info.mint)
try:
token_info = await self.token_queue.get()
token_key = str(token_info.mint)
# Check if token is still "fresh"
current_time = asyncio.get_event_loop().time()
token_age = current_time - self.token_timestamps.get(
token_key, current_time
)
if token_age > self.max_token_age:
logger.info(
f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)"
# Check if token is still "fresh"
current_time = asyncio.get_event_loop().time()
token_age = current_time - self.token_timestamps.get(
token_key, current_time
)
if token_age > self.max_token_age:
logger.info(
f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)"
)
#self.token_queue.task_done()
continue
self.processed_tokens.add(token_key)
logger.info(
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
)
await self._handle_token(token_info)
except asyncio.CancelledError:
# Handle cancellation gracefully
logger.info("Token queue processor was cancelled")
break
except Exception as e:
logger.error(f"Error in token queue processor: {e!s}")
finally:
self.token_queue.task_done()
continue
self.processed_tokens.add(token_key)
logger.info(
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
)
await self._handle_token(token_info, marry_mode, yolo_mode)
self.token_queue.task_done()
async def _handle_token(
self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool
self, token_info: TokenInfo
) -> None:
"""Handle a new token creation event.
Args:
token_info: Token information
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, continue trading after this token
"""
try:
# Save token info to file
await self._save_token_info(token_info)
if not config.EXTREME_FAST_MODE:
# Wait for bonding curve to stabilize (unless in extreme fast mode)
if not self.extreme_fast_mode:
logger.info(
f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..."
f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..."
)
await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION)
await asyncio.sleep(self.wait_time_after_creation)
# Buy token
logger.info(
f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..."
)
buy_result: TradeResult = await self.buyer.execute(token_info)
if buy_result.success:
logger.info(f"Successfully bought {token_info.symbol}")
self._log_trade(
"buy",
token_info,
buy_result.price, # type: ignore
buy_result.amount, # type: ignore
buy_result.tx_signature,
)
self.traded_mints.add(token_info.mint)
await self._handle_successful_buy(token_info, buy_result)
else:
logger.error(
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
)
# Close ATA if enabled
await handle_cleanup_after_failure(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager)
await self._handle_failed_buy(token_info, buy_result)
# Sell token if not in marry mode
if not marry_mode and buy_result.success:
# Only wait for next token in yolo mode
if self.yolo_mode:
logger.info(
f"Waiting for {config.WAIT_TIME_AFTER_BUY} seconds before selling..."
f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..."
)
await asyncio.sleep(config.WAIT_TIME_AFTER_BUY)
logger.info(f"Selling {token_info.symbol}...")
sell_result: TradeResult = await self.seller.execute(token_info)
if sell_result.success:
logger.info(f"Successfully sold {token_info.symbol}")
self._log_trade(
"sell",
token_info,
sell_result.price, # type: ignore
sell_result.amount, # type: ignore
sell_result.tx_signature,
)
# Close ATA if enabled
await handle_cleanup_after_sell(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager)
else:
logger.error(
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
)
elif marry_mode:
logger.info("Marry mode enabled. Skipping sell operation.")
# Wait before looking for the next token
if yolo_mode:
logger.info(
f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..."
)
await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN)
await asyncio.sleep(self.wait_time_before_new_token)
except Exception as e:
logger.error(f"Error handling token {token_info.symbol}: {e!s}")
async def _save_token_info(self, token_info: TokenInfo) -> None:
async def _handle_successful_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
"""Handle successful token purchase.
Args:
token_info: Token information
buy_result: The result of the buy operation
"""
logger.info(f"Successfully bought {token_info.symbol}")
self._log_trade(
"buy",
token_info,
buy_result.price, # type: ignore
buy_result.amount, # type: ignore
buy_result.tx_signature,
)
self.traded_mints.add(token_info.mint)
# Sell token if not in marry mode
if not self.marry_mode:
logger.info(
f"Waiting for {self.wait_time_after_buy} seconds before selling..."
)
await asyncio.sleep(self.wait_time_after_buy)
logger.info(f"Selling {token_info.symbol}...")
sell_result: TradeResult = await self.seller.execute(token_info)
if sell_result.success:
logger.info(f"Successfully sold {token_info.symbol}")
self._log_trade(
"sell",
token_info,
sell_result.price, # type: ignore
sell_result.amount, # type: ignore
sell_result.tx_signature,
)
# Close ATA if enabled
await handle_cleanup_after_sell(
self.solana_client,
self.wallet,
token_info.mint,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
self.cleanup_force_close_with_burn
)
else:
logger.error(
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
)
else:
logger.info("Marry mode enabled. Skipping sell operation.")
async def _handle_failed_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
"""Handle failed token purchase.
Args:
token_info: Token information
buy_result: The result of the buy operation
"""
logger.error(
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
)
# Close ATA if enabled
await handle_cleanup_after_failure(
self.solana_client,
self.wallet,
token_info.mint,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
self.cleanup_force_close_with_burn
)
async def _save_token_info(
self, token_info: TokenInfo
) -> None:
"""Save token information to a file.
Args:
token_info: Token information
"""
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
try:
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
with open(file_name, "w") as file:
file.write(json.dumps(token_info.to_dict(), indent=2))
with open(file_name, "w") as file:
file.write(json.dumps(token_info.to_dict(), indent=2))
logger.info(f"Token information saved to {file_name}")
logger.info(f"Token information saved to {file_name}")
except Exception as e:
logger.error(f"Failed to save token information: {e!s}")
def _log_trade(
self,
@@ -327,17 +523,20 @@ class PumpTrader:
amount: Trade amount in SOL
tx_hash: Transaction hash
"""
os.makedirs("trades", exist_ok=True)
try:
os.makedirs("trades", exist_ok=True)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"action": action,
"token_address": str(token_info.mint),
"symbol": token_info.symbol,
"price": price,
"amount": amount,
"tx_hash": str(tx_hash),
}
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"action": action,
"token_address": str(token_info.mint),
"symbol": token_info.symbol,
"price": price,
"amount": amount,
"tx_hash": str(tx_hash) if tx_hash else None,
}
with open("trades/trades.log", "a") as log_file:
log_file.write(json.dumps(log_entry) + "\n")
with open("trades/trades.log", "a") as log_file:
log_file.write(json.dumps(log_entry) + "\n")
except Exception as e:
logger.error(f"Failed to log trade information: {e!s}")
+5 -11
View File
@@ -3,7 +3,6 @@ Logging utilities for the pump.fun trading bot.
"""
import logging
import sys
# Global dict to store loggers
_loggers: dict[str, logging.Logger] = {}
@@ -27,16 +26,6 @@ def get_logger(name: str, level: int = logging.INFO) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(level)
if not logger.handlers:
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
_loggers[name] = logger
return logger
@@ -52,6 +41,11 @@ def setup_file_logging(
"""
root_logger = logging.getLogger()
# Check if file handler with same filename already exists
for handler in root_logger.handlers:
if isinstance(handler, logging.FileHandler) and handler.baseFilename == filename:
return # File handler already added
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
@@ -1,9 +0,0 @@
{
"name": "RAKAMAKAFO",
"symbol": "MAKAFO",
"uri": "https://cf-ipfs.com/ipfs/QmYyNfffJL8aLzsQQBxSDEmbQ2STSPS35rfFwE7isjJiVu",
"mint": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump",
"bondingCurve": "9UznPeXvYtPqq8saSyM65cC8LCtyAKPnLCGG7Mq1pUTq",
"associatedBondingCurve": "CTe6pwwgzEUvRmPGRE2EZXrESpiWawK5TwyJhgvFgJ6T",
"user": "Ev5WEEJ2Y1qKGE8KCNvBNVEgy9fJUuttA93pMo2fcGFd"
}
+2 -12
View File
@@ -1,12 +1,2 @@
{"timestamp": "2024-08-22T10:51:30.306580", "action": "buy", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "XZDRuEnaBNEeuFAiGyJsqBmVF3uZvFqAQUKsZVKTxsThHxN43LiRqy3T4cSH4dRXGB7c5A4855iYQDLY4BNJJhP"}
{"timestamp": "2024-08-22T10:51:56.116951", "action": "sell", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "WQHnEoyztfDf5FJyQRXDGxxtqf2EcMGs8SET43KuYU8Dpdunruku22SHnfiH2xsUi1hTBjC1msvTcnU78fFKrc2"}
{"timestamp": "2025-03-06T21:53:16.417950", "action": "buy", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.3860819583417394e-08, "amount": 29.532657871333047, "tx_hash": "4fUuh16912cK7xiZsvY1scu5P5NtWGwowcAYXQvvr1FWAvPD57mPKfrq5PMMzMULhMiVn86fiDCUfwWt6B7NPNxV"}
{"timestamp": "2025-03-06T21:53:33.704455", "action": "sell", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.38403327639918e-08, "amount": 29.532657, "tx_hash": "4TRFmKYkibrgn2fX4UT7mM8i7Lpu3FgXKvY1d6AsEUaxLj11JxWo61xxqYGW7B6wzdaY9LWVWZnTSDTZUUNs5pFU"}
{"timestamp": "2025-03-06T22:04:01.067407", "action": "buy", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8543914418196173e-08, "amount": 35.03373732659877, "tx_hash": "VCWbh79TmNWtt9kjex5LLtWKGHjc3nPBQVJdynC7B85nbnXx4DBgwguqAe6StYVAUS7jzP17pN5FWWGvbiUBRfk"}
{"timestamp": "2025-03-06T22:04:48.843998", "action": "sell", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8056120698010214e-08, "amount": 35.033737, "tx_hash": "RLx8hFVd5DVZdUg5vN41Fs8Yz66yTEUEvegasuHwYQ7DTJhPcCdggoNBtipb93VPVdtQLZydxc9ZKRZ1uzK6KzH"}
{"timestamp": "2025-03-06T22:05:11.444317", "action": "buy", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.383038210726932e-08, "amount": 29.559228649242023, "tx_hash": "3E4RT5kvRezWJmQKJ9wmcAiDcYZefohxFVNdEZPweE9vhq215rSmqBn4Dv4ypH5KUUuNJzKdhjyrGU2b47woWr9e"}
{"timestamp": "2025-03-06T22:05:25.789876", "action": "sell", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.3830384158620706e-08, "amount": 29.559228, "tx_hash": "3oyJVzsRvnQV5qBsEp4hSk97eFWzUjzfWWa16GMUXtMd7JBmbQkTkmvs7N8nagYcJvqfFhzhCKZEG6onxjYYQbwX"}
{"timestamp": "2025-04-03T21:55:46.849123", "action": "buy", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.3333333333333334e-08, "amount": 30, "tx_hash": "35UncvxypdvuKC4gD3jXnmmtwfBfEhFc2Y2JLfszzvHqsZumcjp2CMkXH4HD7iexgkPEpG1je73bwnFWbNQjxNUS"}
{"timestamp": "2025-04-03T21:56:05.816803", "action": "sell", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.1716731316027684e-08, "amount": 30.0, "tx_hash": "2NoMsScE5tJu3gzvrSn1uarnVZ4DcgXVLsc7BEwJ1ndU7T69xPbgyk53YW4MVCEfrSvdoaCup6VyM9pnf6YR6pGc"}
{"timestamp": "2025-04-03T21:56:49.686823", "action": "buy", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 5.067234074416521e-08, "amount": 19.734632055953472, "tx_hash": "52hQYXPjtP1Rr945UW5peef6qbxThUubyBbEbcnGCCJ146nxx6bZXj3mCpWtpqpmMe82oNkEFwMms26JqhAaJMXL"}
{"timestamp": "2025-04-03T21:57:09.656288", "action": "sell", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 2.8113188162567704e-08, "amount": 19.734632, "tx_hash": "xzpCFN8PgtPvXyRfQjkhHkPQcdzTNb6JXtCsQixtsMQabiBt9yNMSb7JLxRCT295rbVZuuocRNxYe6AqAiRnE91"}
{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"}
{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"}