Merge pull request #71 from chainstacklabs/feature/cleanup-ata-support

Integrate ATA closing into trader
This commit is contained in:
Anton
2025-03-28 08:29:13 +00:00
committed by GitHub
6 changed files with 245 additions and 58 deletions
View File
+98
View File
@@ -0,0 +1,98 @@
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
from core.wallet import Wallet
from utils.logger import get_logger
logger = get_logger(__name__)
class AccountCleanupManager:
"""Handles safe cleanup of token accounts (ATA) after trading sessions."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
priority_fee_manager: PriorityFeeManager,
use_priority_fee: bool = False,
):
"""
Args:
client: Solana RPC client
wallet: Wallet for signing transactions
"""
self.client = client
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
self.use_priority_fee = use_priority_fee
async def cleanup_ata(self, mint: Pubkey) -> None:
"""
Attempt to burn any remaining tokens and close the ATA.
Skips if account doesn't exist or is already empty/closed.
"""
ata = self.wallet.get_associated_token_address(mint)
solana_client = await self.client.get_client()
priority_fee = (
await self.priority_fee_manager.calculate_priority_fee([ata])
if self.use_priority_fee
else None
)
try:
info = await solana_client.get_account_info(ata)
if not info.value:
logger.info(f"ATA {ata} does not exist or already closed.")
return
balance = await self.client.get_token_account_balance(ata)
if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN:
logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...")
burn_ix = burn(
BurnParams(
account=ata,
mint=mint,
owner=self.wallet.pubkey,
amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM,
)
)
await self.client.build_and_send_transaction(
[burn_ix],
self.wallet.keypair,
skip_preflight=True,
priority_fee=priority_fee,
)
logger.info(f"✅ Burned successfully from ATA {ata}")
elif balance > 0:
logger.info(
f"Skipping ATA {ata} with non-zero balance ({balance} tokens) "
f"because CLEANUP_FORCE_CLOSE_WITH_BURN is disabled."
)
return
logger.info(f"Closing ATA: {ata}")
close_ix = close_account(
CloseAccountParams(
account=ata,
dest=self.wallet.pubkey,
owner=self.wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM,
)
)
tx_sig = await self.client.build_and_send_transaction(
[close_ix],
self.wallet.keypair,
skip_preflight=True,
priority_fee=priority_fee,
)
await self.client.confirm_transaction(tx_sig)
logger.info(f"Closed successfully: {ata}")
except Exception as e:
logger.warning(f"Cleanup failed for ATA {ata}: {e!s}")
+37
View File
@@ -0,0 +1,37 @@
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_sell() -> bool:
return CLEANUP_MODE == "after_sell"
def should_cleanup_post_session() -> bool:
return CLEANUP_MODE == "post_session"
async def handle_cleanup_after_failure(client, wallet, mint, priority_fee_manager):
if should_cleanup_after_failure():
logger.info("[Cleanup] Triggered by failed buy transaction.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
await manager.cleanup_ata(mint)
async def handle_cleanup_after_sell(client, wallet, mint, priority_fee_manager):
if should_cleanup_after_sell():
logger.info("[Cleanup] Triggered after token sell.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
await manager.cleanup_ata(mint)
async def handle_cleanup_post_session(client, wallet, mints, priority_fee_manager):
if should_cleanup_post_session():
logger.info("[Cleanup] Triggered post trading session.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
for mint in mints:
await manager.cleanup_ata(mint)
+3 -3
View File
@@ -8,7 +8,7 @@ import asyncio
import os import os
import sys import sys
import config as config import config
from trading.trader import PumpTrader from trading.trader import PumpTrader
from utils.logger import get_logger, setup_file_logging from utils.logger import get_logger, setup_file_logging
@@ -109,7 +109,7 @@ async def main() -> None:
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Trading stopped by user") logger.info("Trading stopped by user")
except Exception as e: except Exception as e:
logger.error(f"Trading stopped due to error: {str(e)}") logger.error(f"Trading stopped due to error: {e!s}")
finally: finally:
try: try:
await trader.solana_client.close() await trader.solana_client.close()
@@ -117,7 +117,7 @@ async def main() -> None:
pass pass
def sync_main(): def sync_main() -> None:
asyncio.run(main()) asyncio.run(main())
+82 -51
View File
@@ -1,72 +1,103 @@
""" """
Configuration for the pump.fun trading bot. 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 # 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_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying
BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%)
SELL_SLIPPAGE: float = 0.4 # 40% slippage tolerance for selling SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy
# Configuration for priority fee settings # Priority fee configuration
ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Enable dynamic priority fee calculation # Manage transaction speed and cost on the Solana network
ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation
FIXED_PRIORITY_FEE: int = 2_000 # Fixed priority fee in microlamports ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee
EXTRA_PRIORITY_FEE: float = ( FIXED_PRIORITY_FEE: int = 2_000 # Base fee in microlamports
0.0 # Percentage increase applied to priority fee (0.1 = 10%) EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%)
) HARD_CAP_PRIOR_FEE: int = 200_000 # Maximum allowable fee to prevent excessive spending in microlamports
HARD_CAP_PRIOR_FEE: int = (
200_000 # Maximum allowed priority fee in microlamports (hard cap)
)
# Listener configuration # Listener configuration
LISTENER_TYPE = "block" # Options: "block" or "logs" # Choose method for detecting new tokens on the network
# "logs": Recommended for more stable token detection
# "blocks": Unstable method, potentially less reliable
LISTENER_TYPE = "logs"
# Retries and timeouts # Retry and timeout settings
MAX_RETRIES: int = 10 # Number of retries for transaction sending # Control bot resilience and transaction handling
# TODO: waiting times will be replaced with retries to shorten delays MAX_RETRIES: int = 10 # Number of attempts for transaction submission
WAIT_TIME_AFTER_CREATION: int | float = (
15 # Time to wait after token creation (in seconds) # Waiting periods in seconds between actions (TODO: to be replaced with retry mechanism)
# Too short a delay may cause the RPC node to be unaware of the bonding curve account 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_AFTER_BUY: int | float = ( WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades
15 # Time to wait after a buy transaction is confirmed (in seconds)
# Acts as a simple holding period
# Too short delay may cause the RPC node to be unaware of account balance
)
WAIT_TIME_BEFORE_NEW_TOKEN: int | float = (
5 # Time to wait after a sell transaction is confirmed (in seconds)
# Provides a pause between completed trades, can be set to 0
)
# Maximum age (in seconds) for a token to be considered "fresh" and eligible for processing. # Token and account management
# This threshold is checked before processing starts - tokens older than this are skipped # Control token processing and account cleanup strategies
# since they likely contain outdated information from the websocket stream MAX_TOKEN_AGE: int | float = 0.1 # Maximum token age in seconds for processing
MAX_TOKEN_AGE: int | float = 0.1
# 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 # Node provider configuration (TODO: to be implemented)
# Tested with Chainstack nodes (https://console.chainstack.com), but you can use any node provider # Manage RPC node interaction to prevent rate limiting
# You can get a trader node https://docs.chainstack.com/docs/solana-trader-nodes MAX_RPS: int = 25 # Maximum requests per second
MAX_RPS: int = 25 # TODO: not implemented. Max RPS to avoid rate limit errors
def validate_priority_fee_config() -> None: def validate_configuration() -> None:
"""Validate priority fee configuration values.""" """
if not isinstance(ENABLE_DYNAMIC_PRIORITY_FEE, bool): Comprehensive validation of bot configuration.
raise ValueError("ENABLE_DYNAMIC_PRIORITY_FEE must be a boolean")
if not isinstance(ENABLE_FIXED_PRIORITY_FEE, bool): Checks:
raise ValueError("ENABLE_FIXED_PRIORITY_FEE must be a boolean") - Type correctness
if not isinstance(FIXED_PRIORITY_FEE, int) or FIXED_PRIORITY_FEE < 0: - Value ranges
raise ValueError("FIXED_PRIORITY_FEE must be a non-negative integer") - Logical consistency of settings
if not isinstance(EXTRA_PRIORITY_FEE, float) or EXTRA_PRIORITY_FEE < 0: """
raise ValueError("EXTRA_PRIORITY_FEE must be a non-negative float") # Configuration validation checks
if not isinstance(HARD_CAP_PRIOR_FEE, int) or HARD_CAP_PRIOR_FEE < 0: config_checks = [
raise ValueError("HARD_CAP_PRIOR_FEE must be a non-negative integer") # (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"]:
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 config on import # Validate configuration on import
validate_priority_fee_config() validate_configuration()
+25 -4
View File
@@ -8,7 +8,14 @@ import json
import os import os
from datetime import datetime from datetime import datetime
import config as config from solders.pubkey import Pubkey
import config
from cleanup.modes import (
handle_cleanup_after_failure,
handle_cleanup_after_sell,
handle_cleanup_post_session,
)
from core.client import SolanaClient from core.client import SolanaClient
from core.curve import BondingCurveManager from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager from core.priority_fee.manager import PriorityFeeManager
@@ -96,6 +103,8 @@ class PumpTrader:
self.max_retries = max_retries self.max_retries = max_retries
self.max_token_age = config.MAX_TOKEN_AGE self.max_token_age = config.MAX_TOKEN_AGE
self.traded_mints: set[Pubkey] = set()
# Token processing state # Token processing state
self.token_queue = asyncio.Queue() self.token_queue = asyncio.Queue()
self.processing = False self.processing = False
@@ -137,10 +146,17 @@ class PumpTrader:
) )
except Exception as e: except Exception as e:
logger.error(f"Trading stopped due to error: {str(e)}") logger.error(f"Trading stopped due to error: {e!s}")
processor_task.cancel() processor_task.cancel()
await self.solana_client.close() 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()
async def _queue_token(self, token_info: TokenInfo) -> None: async def _queue_token(self, token_info: TokenInfo) -> None:
"""Queue a token for processing if not already processed.""" """Queue a token for processing if not already processed."""
token_key = str(token_info.mint) token_key = str(token_info.mint)
@@ -153,7 +169,7 @@ class PumpTrader:
self.token_timestamps[token_key] = asyncio.get_event_loop().time() self.token_timestamps[token_key] = asyncio.get_event_loop().time()
await self.token_queue.put(token_info) await self.token_queue.put(token_info)
# logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") 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, marry_mode: bool, yolo_mode: bool) -> None:
"""Continuously process tokens from the queue, only if they're fresh.""" """Continuously process tokens from the queue, only if they're fresh."""
@@ -215,10 +231,13 @@ class PumpTrader:
buy_result.amount, # type: ignore buy_result.amount, # type: ignore
buy_result.tx_signature, buy_result.tx_signature,
) )
self.traded_mints.add(token_info.mint)
else: else:
logger.error( logger.error(
f"Failed to buy {token_info.symbol}: {buy_result.error_message}" 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)
# Sell token if not in marry mode # Sell token if not in marry mode
if not marry_mode and buy_result.success: if not marry_mode and buy_result.success:
@@ -239,6 +258,8 @@ class PumpTrader:
sell_result.amount, # type: ignore sell_result.amount, # type: ignore
sell_result.tx_signature, 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: else:
logger.error( logger.error(
f"Failed to sell {token_info.symbol}: {sell_result.error_message}" f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
@@ -254,7 +275,7 @@ class PumpTrader:
await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN)
except Exception as e: except Exception as e:
logger.error(f"Error handling token {token_info.symbol}: {str(e)}") logger.error(f"Error handling token {token_info.symbol}: {e!s}")
async def _save_token_info(self, token_info: TokenInfo) -> None: async def _save_token_info(self, token_info: TokenInfo) -> None:
"""Save token information to a file. """Save token information to a file.