diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml index a836125..ac387d3 100644 --- a/bots/bot-sniper-1-geyser.yaml +++ b/bots/bot-sniper-1-geyser.yaml @@ -8,7 +8,7 @@ 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 +enabled: true # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" @@ -31,7 +31,7 @@ trade: exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" #take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%) #stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%) - max_hold_time: 15 # Maximum hold time in seconds + max_hold_time: 5 # Maximum hold time in seconds for TP/SL strategy, for time_based - see wait_after_buy #price_check_interval: 2 # Check price every 2 seconds # EXTREME FAST mode configuration @@ -61,8 +61,9 @@ compute_units: # Reduces CU cost from 16k to ~128 CU by limiting loaded account data. # Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead. # Note: Savings don't show in "consumed CU" but improve tx priority/cost. + # Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue. # Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit - account_data_size: 512_000 # 512KB limit + account_data_size: 12_500_000 # Filters for token selection filters: @@ -77,7 +78,7 @@ filters: 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_after_buy: 5 # Holding period after buy transaction wait_before_new_token: 15 # Pause between token trades # Token and account management diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml index 9564921..3b50b47 100644 --- a/bots/bot-sniper-2-logs.yaml +++ b/bots/bot-sniper-2-logs.yaml @@ -24,14 +24,14 @@ geyser: # 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.2 # Maximum acceptable price deviation (0.2 = 20%) + buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) sell_slippage: 0.3 # Exit strategy configuration exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" take_profit_percentage: 0.4 # Take profit at 40% gain (0.4 = 40%) stop_loss_percentage: 0.4 # Stop loss at 40% loss (0.4 = 40%) - max_hold_time: 60 # Maximum hold time in seconds + max_hold_time: 15 # Maximum hold time in seconds for TP/SL strategy, for time_based - see wait_after_buy price_check_interval: 2 # Check price every 2 seconds # EXTREME FAST mode configuration @@ -61,8 +61,9 @@ compute_units: # Reduces CU cost from 16k to ~128 CU by limiting loaded account data. # Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead. # Note: Savings don't show in "consumed CU" but improve tx priority/cost. + # Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue. # Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit - account_data_size: 512_000 # 512KB limit + account_data_size: 12_500_000 # Filters for token selection filters: diff --git a/bots/bot-sniper-3-blocks.yaml b/bots/bot-sniper-3-blocks.yaml index 2eae5f4..3701122 100644 --- a/bots/bot-sniper-3-blocks.yaml +++ b/bots/bot-sniper-3-blocks.yaml @@ -8,7 +8,7 @@ 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 +enabled: false # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" @@ -31,7 +31,7 @@ trade: exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" #take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%) #stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%) - max_hold_time: 15 # Maximum hold time in seconds + max_hold_time: 15 # Maximum hold time in seconds for TP/SL strategy, for time_based - see wait_after_buy #price_check_interval: 2 # Check price every 2 seconds # EXTREME FAST mode configuration @@ -61,8 +61,9 @@ compute_units: # Reduces CU cost from 16k to ~128 CU by limiting loaded account data. # Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead. # Note: Savings don't show in "consumed CU" but improve tx priority/cost. + # Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue. # Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit - account_data_size: 512_000 # 512KB limit + account_data_size: 12_500_000 # Filters for token selection filters: diff --git a/bots/bot-sniper-4-pp.yaml b/bots/bot-sniper-4-pp.yaml index f27257d..23af58f 100644 --- a/bots/bot-sniper-4-pp.yaml +++ b/bots/bot-sniper-4-pp.yaml @@ -29,7 +29,7 @@ trade: exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%) stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%) - max_hold_time: 600 # Maximum hold time in seconds (600 = 10 minutes) + max_hold_time: 15 # Maximum hold time in seconds for TP/SL strategy, for time_based - see wait_after_buy price_check_interval: 2 # Check price every 2 seconds # EXTREME FAST mode configuration @@ -59,8 +59,9 @@ compute_units: # Reduces CU cost from 16k to ~128 CU by limiting loaded account data. # Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead. # Note: Savings don't show in "consumed CU" but improve tx priority/cost. + # Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue. # Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit - account_data_size: 512_000 # 512KB limit + account_data_size: 12_500_000 # Filters for token selection filters: diff --git a/src/core/client.py b/src/core/client.py index a32481e..45d6a32 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -271,6 +271,152 @@ async def confirm_transaction( logger.exception(f"Failed to confirm transaction {signature}") return False + async def get_transaction_token_balance( + self, signature: str, user_pubkey: Pubkey, mint: Pubkey + ) -> int | None: + """Get the user's token balance after a transaction from postTokenBalances. + + Args: + signature: Transaction signature + user_pubkey: User's wallet public key + mint: Token mint address + + Returns: + Token balance (raw amount) after transaction, or None if not found + """ + result = await self._get_transaction_result(signature) + if not result: + return None + + meta = result.get("meta", {}) + post_token_balances = meta.get("postTokenBalances", []) + + user_str = str(user_pubkey) + mint_str = str(mint) + + for balance in post_token_balances: + if balance.get("owner") == user_str and balance.get("mint") == mint_str: + ui_amount = balance.get("uiTokenAmount", {}) + amount_str = ui_amount.get("amount") + if amount_str: + return int(amount_str) + + return None + + async def get_buy_transaction_details( + self, signature: str, mint: Pubkey, sol_destination: Pubkey + ) -> tuple[int | None, int | None]: + """Get actual tokens received and SOL spent from a buy transaction. + + Uses preBalances/postBalances to find exact SOL transferred to the + pool/curve and pre/post token balance diff to find tokens received. + + Args: + signature: Transaction signature + mint: Token mint address + sol_destination: Address where SOL is sent (bonding curve for pump.fun, + quote_vault for letsbonk) + + Returns: + Tuple of (tokens_received_raw, sol_spent_lamports), or (None, None) + """ + result = await self._get_transaction_result(signature) + if not result: + return None, None + + meta = result.get("meta", {}) + mint_str = str(mint) + + # Get tokens received from pre/post token balance diff + # This works for Token2022 where owner might be different + tokens_received = None + pre_token_balances = meta.get("preTokenBalances", []) + post_token_balances = meta.get("postTokenBalances", []) + + # Build lookup by account index + pre_by_idx = {b.get("accountIndex"): b for b in pre_token_balances} + post_by_idx = {b.get("accountIndex"): b for b in post_token_balances} + + # Find positive token diff for our mint (user receiving tokens) + all_indices = set(pre_by_idx.keys()) | set(post_by_idx.keys()) + for idx in all_indices: + pre = pre_by_idx.get(idx) + post = post_by_idx.get(idx) + + # Check if this is our mint + balance_mint = (post or pre).get("mint", "") + if balance_mint != mint_str: + continue + + pre_amount = ( + int(pre.get("uiTokenAmount", {}).get("amount", 0)) if pre else 0 + ) + post_amount = ( + int(post.get("uiTokenAmount", {}).get("amount", 0)) if post else 0 + ) + diff = post_amount - pre_amount + + # Positive diff means tokens received (not the bonding curve's negative) + if diff > 0: + tokens_received = diff + logger.info(f"Tokens received from tx: {tokens_received}") + break + + # Get SOL spent from preBalances/postBalances at sol_destination + sol_destination_str = str(sol_destination) + sol_spent = None + pre_balances = meta.get("preBalances", []) + post_balances = meta.get("postBalances", []) + account_keys = ( + result.get("transaction", {}).get("message", {}).get("accountKeys", []) + ) + + for i, key in enumerate(account_keys): + key_str = key if isinstance(key, str) else key.get("pubkey", "") + if key_str == sol_destination_str: + if i < len(pre_balances) and i < len(post_balances): + sol_spent = post_balances[i] - pre_balances[i] + if sol_spent > 0: + logger.info(f"SOL to pool/curve: {sol_spent} lamports") + else: + logger.warning( + f"SOL destination balance change not positive: {sol_spent}" + ) + sol_spent = None + break + + return tokens_received, sol_spent + + async def _get_transaction_result(self, signature: str) -> dict | None: + """Fetch transaction result from RPC. + + Args: + signature: Transaction signature + + Returns: + Transaction result dict or None + """ + body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getTransaction", + "params": [ + signature, + {"encoding": "jsonParsed", "commitment": "confirmed"}, + ], + } + + response = await self.post_rpc(body) + if not response or "result" not in response: + logger.warning(f"Failed to get transaction {signature}") + return None + + result = response["result"] + if not result or "meta" not in result: + return None + + return result + async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None: """ Send a raw RPC request to the Solana node. diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 576b19f..8f616ad 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -130,6 +130,35 @@ async def execute(self, token_info: TokenInfo) -> TradeResult: if success: logger.info(f"Buy transaction confirmed: {tx_signature}") + + # Fetch actual tokens and SOL spent from transaction + # Uses preBalances/postBalances to get exact amounts + sol_destination = self._get_sol_destination( + token_info, address_provider + ) + tokens_raw, sol_spent = await self.client.get_buy_transaction_details( + str(tx_signature), token_info.mint, sol_destination + ) + + if tokens_raw is not None and sol_spent is not None: + actual_amount = tokens_raw / 10**TOKEN_DECIMALS + actual_price = (sol_spent / LAMPORTS_PER_SOL) / actual_amount + logger.info( + f"Actual tokens received: {actual_amount:.6f} " + f"(expected: {token_amount:.6f})" + ) + logger.info( + f"Actual SOL spent: {sol_spent / LAMPORTS_PER_SOL:.10f} SOL" + ) + logger.info(f"Actual price: {actual_price:.10f} SOL/token") + token_amount = actual_amount + token_price_sol = actual_price + else: + raise ValueError( + f"Failed to parse transaction details: tokens={tokens_raw}, " + f"sol_spent={sol_spent}" + ) + return TradeResult( success=True, platform=token_info.platform, @@ -165,6 +194,42 @@ def _get_pool_address( # Fallback to deriving the address using platform provider return address_provider.derive_pool_address(token_info.mint) + def _get_sol_destination( + self, token_info: TokenInfo, address_provider: AddressProvider + ) -> Pubkey: + """Get the address where SOL is sent during a buy transaction. + + For pump.fun: SOL goes to the bonding curve + For letsbonk: SOL goes to the quote_vault (WSOL vault) + + Args: + token_info: Token information + address_provider: Platform-specific address provider + + Returns: + Address where SOL is transferred during buy + + Raises: + NotImplementedError: If platform SOL destination is not implemented + """ + if token_info.platform == Platform.PUMP_FUN: + # For pump.fun, SOL goes directly to bonding curve + if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: + return token_info.bonding_curve + return address_provider.derive_pool_address(token_info.mint) + elif token_info.platform == Platform.LETS_BONK: + # For letsbonk, SOL goes to quote_vault (WSOL vault) + if hasattr(token_info, "quote_vault") and token_info.quote_vault: + return token_info.quote_vault + # Derive quote_vault if not available + return address_provider.derive_quote_vault(token_info.mint) + + raise NotImplementedError( + f"SOL destination not implemented for platform {token_info.platform.value}. " + f"Add platform-specific logic to _get_sol_destination() to specify where " + f"SOL is transferred during buy transactions for this platform." + ) + def _get_cu_override(self, operation: str, platform: Platform) -> int | None: """Get compute unit override from configuration. @@ -202,8 +267,35 @@ def __init__( self.max_retries = max_retries self.compute_units = compute_units or {} - async def execute(self, token_info: TokenInfo) -> TradeResult: - """Execute sell operation using platform-specific implementations.""" + async def execute( + self, token_info: TokenInfo, token_amount: float, token_price: float + ) -> TradeResult: + """Execute sell operation using platform-specific implementations. + + Args: + token_info: Token information for the sell operation + token_amount: Token amount to sell (from buy result). Required to avoid + RPC balance query delays. + token_price: Token price in SOL (from buy result). Required to avoid + RPC pool state query delays. + + Returns: + TradeResult with operation outcome + + Raises: + ValueError: If required parameters are not provided + """ + if token_amount is None: + raise ValueError( + "token_amount is required for sell operation. " + "Pass the amount from buy result to avoid RPC delays." + ) + if token_price is None or token_price <= 0: + raise ValueError( + "token_price is required for sell operation and must be positive. " + "Pass the price from buy result to avoid RPC delays." + ) + try: # Get platform-specific implementations implementations = get_platform_implementations( @@ -211,19 +303,14 @@ async def execute(self, token_info: TokenInfo) -> TradeResult: ) address_provider = implementations.address_provider instruction_builder = implementations.instruction_builder - curve_manager = implementations.curve_manager - # Get user's token account and balance - user_token_account = address_provider.derive_user_token_account( - self.wallet.pubkey, token_info.mint - ) + # Use pre-known amount and price (no RPC delay) + token_balance_decimal = token_amount + token_balance = int(token_amount * 10**TOKEN_DECIMALS) + token_price_sol = token_price - token_balance = await self.client.get_token_account_balance( - user_token_account - ) - token_balance_decimal = token_balance / 10**TOKEN_DECIMALS - - logger.info(f"Token balance: {token_balance_decimal}") + logger.info(f"Token balance: {token_balance_decimal:.6f}") + logger.info(f"Price per Token (from buy): {token_price_sol:.8f} SOL") if token_balance == 0: logger.info("No tokens to sell.") @@ -233,38 +320,19 @@ async def execute(self, token_info: TokenInfo) -> TradeResult: error_message="No tokens to sell", ) - # Get pool address and current price using platform-agnostic method - pool_address = self._get_pool_address(token_info, address_provider) - # Fetch pool state to get price and mayhem mode status - pool_state = await curve_manager.get_pool_state(pool_address) - token_price_sol = pool_state.get("price_per_token") - - # Validate price_per_token is present and positive - if token_price_sol is None or token_price_sol <= 0: - raise ValueError( - f"Invalid price_per_token: {token_price_sol} for pool {pool_address} " - f"(mint: {token_info.mint}) - cannot execute sell with zero/invalid price" - ) - - # Set is_mayhem_mode from bonding curve state - token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False) - - logger.info(f"Price per Token: {token_price_sol:.8f} SOL") - - # Calculate expected SOL output + # Calculate expected SOL output with slippage protection expected_sol_output = token_balance_decimal * token_price_sol - - # Calculate minimum SOL output with slippage protection - min_sol_output = int( - (expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL + min_sol_output = max( + 1, + int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL), ) - logger.info( f"Selling {token_balance_decimal} tokens on {token_info.platform.value}" ) - logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL") + logger.info(f"Expected SOL output: {expected_sol_output:.10f} SOL") logger.info( - f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL" + f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): " + f"{min_sol_output / LAMPORTS_PER_SOL:.10f} SOL ({min_sol_output} lamports)" ) # Build sell instructions using platform-specific builder diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index e4e7a20..6f4350c 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -193,7 +193,9 @@ def __init__( # State tracking self.traded_mints: set[Pubkey] = set() - self.traded_token_programs: dict[str, Pubkey] = {} # Maps mint (as string) to token_program_id + self.traded_token_programs: dict[ + str, Pubkey + ] = {} # Maps mint (as string) to token_program_id self.token_queue: asyncio.Queue = asyncio.Queue() self.processing: bool = False self.processed_tokens: set[str] = set() @@ -329,8 +331,7 @@ async def _cleanup_resources(self) -> None: # Build parallel lists of mints and token_program_ids mints_list = list(self.traded_mints) token_program_ids = [ - self.traded_token_programs.get(str(mint)) - for mint in mints_list + self.traded_token_programs.get(str(mint)) for mint in mints_list ] await handle_cleanup_post_session( self.solana_client, @@ -465,7 +466,7 @@ async def _handle_successful_buy( if self.exit_strategy == "tp_sl": await self._handle_tp_sl_exit(token_info, buy_result) elif self.exit_strategy == "time_based": - await self._handle_time_based_exit(token_info) + await self._handle_time_based_exit(token_info, buy_result) elif self.exit_strategy == "manual": logger.info("Manual exit strategy - position will remain open") else: @@ -512,13 +513,23 @@ async def _handle_tp_sl_exit( # Monitor position until exit condition is met await self._monitor_position_until_exit(token_info, position) - async def _handle_time_based_exit(self, token_info: TokenInfo) -> None: - """Handle legacy time-based exit strategy.""" + async def _handle_time_based_exit( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: + """Handle legacy time-based exit strategy. + + Args: + token_info: Token information + buy_result: Result from the buy operation (contains token amount) + """ 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) + # Pass token amount and price from buy result to avoid RPC delays + sell_result: TradeResult = await self.seller.execute( + token_info, token_amount=buy_result.amount, token_price=buy_result.price + ) if sell_result.success: logger.info(f"Successfully sold {token_info.symbol}") @@ -575,8 +586,12 @@ async def _monitor_position_until_exit( f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)" ) - # Execute sell - sell_result = await self.seller.execute(token_info) + # Execute sell with position quantity and entry price to avoid RPC delays + sell_result = await self.seller.execute( + token_info, + token_amount=position.quantity, + token_price=position.entry_price, + ) if sell_result.success: # Close position with actual exit price