diff --git a/.gitignore b/.gitignore index 13525c6..c5d2607 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ trades/* +.vscode .pylintrc .ruff_cache diff --git a/cli.py b/cli.py index c5228d8..7f0c2dc 100644 --- a/cli.py +++ b/cli.py @@ -62,12 +62,8 @@ async def main() -> None: args = parse_args() # Get configuration values, preferring command line args over config.py - rpc_endpoint: str | None = ( - os.environ.get("SOLANA_NODE_RPC_ENDPOINT") or config.PUBLIC_RPC_ENDPOINT - ) - wss_endpoint: str | None = ( - os.environ.get("SOLANA_NODE_WSS_ENDPOINT") or config.PUBLIC_WSS_ENDPOINT - ) + 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") # Validate configuration values diff --git a/config.py b/config.py index 54a316c..4e499be 100644 --- a/config.py +++ b/config.py @@ -3,25 +3,66 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT = 0.000001 # Amount of SOL to spend when buying -BUY_SLIPPAGE = 0.4 # 40% slippage tolerance for buying -SELL_SLIPPAGE = 0.4 # 40% slippage tolerance for selling -ENABLE_DYNAMIC_PRIORITY_FEE = True # TODO: not implemented. getRecentPriorityFee is used to get current priority fee -EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic priority fee +BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying +BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying +SELL_SLIPPAGE: float = 0.4 # 40% slippage tolerance for selling + + +# Configuration for priority fee settings +ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Enable dynamic priority fee calculation +ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee +FIXED_PRIORITY_FEE: int = 2_000 # Fixed priority fee in microlamports +EXTRA_PRIORITY_FEE: float = ( + 0.0 # Percentage increase applied to priority fee (0.1 = 10%) +) +HARD_CAP_PRIOR_FEE: int = ( + 200_000 # Maximum allowed priority fee in microlamports (hard cap) +) + # Retries and timeouts -MAX_RETRIES: int = 2 -WAIT_TIME_AFTER_BUY: int = 15 -WAIT_TIME_BEFORE_NEW_TOKEN: int = 30 -WAIT_TIME_AFTER_CREATION: int = 15 +MAX_RETRIES: int = 10 # Number of retries for transaction sending +# TODO: waiting times will be replaced with retries to shorten delays +WAIT_TIME_AFTER_CREATION: int | float = ( + 15 # Time to wait after token creation (in seconds) + # Too short a delay may cause the RPC node to be unaware of the bonding curve account +) +WAIT_TIME_AFTER_BUY: int | float = ( + 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. # This threshold is checked before processing starts - tokens older than this are skipped # since they likely contain outdated information from the websocket stream -MAX_TOKEN_AGE: float = 0.1 +MAX_TOKEN_AGE: int | float = 0.1 -# Node provier configuration -# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes -MAX_RPS = 25 # TODO: not implemented. Max RPS to avoid rate limit errors -PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" -PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" + +# Node provider configuration +# Tested with Chainstack nodes (https://console.chainstack.com), but you can use any node provider +# You can get a trader node https://docs.chainstack.com/docs/solana-trader-nodes +MAX_RPS: int = 25 # TODO: not implemented. Max RPS to avoid rate limit errors + + +def validate_priority_fee_config() -> None: + """Validate priority fee configuration values.""" + if not isinstance(ENABLE_DYNAMIC_PRIORITY_FEE, bool): + raise ValueError("ENABLE_DYNAMIC_PRIORITY_FEE must be a boolean") + if not isinstance(ENABLE_FIXED_PRIORITY_FEE, bool): + raise ValueError("ENABLE_FIXED_PRIORITY_FEE must be a boolean") + if not isinstance(FIXED_PRIORITY_FEE, int) or FIXED_PRIORITY_FEE < 0: + raise ValueError("FIXED_PRIORITY_FEE must be a non-negative integer") + if not isinstance(EXTRA_PRIORITY_FEE, float) or EXTRA_PRIORITY_FEE < 0: + raise ValueError("EXTRA_PRIORITY_FEE must be a non-negative float") + if not isinstance(HARD_CAP_PRIOR_FEE, int) or HARD_CAP_PRIOR_FEE < 0: + raise ValueError("HARD_CAP_PRIOR_FEE must be a non-negative integer") + + +# Validate config on import +validate_priority_fee_config() diff --git a/requirements.txt b/requirements.txt index e18fee3..c84165e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ construct-typing>=0.5.2 solana==0.36.6 solders>=0.26.0 websockets>=15.0 -python-dotenv>=1.0.1 \ No newline at end of file +python-dotenv>=1.0.1 +aiohttp==3.11.13 \ No newline at end of file diff --git a/src/core/client.py b/src/core/client.py index ab9a485..a6d4b3b 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -3,12 +3,18 @@ Solana client abstraction for blockchain operations. """ import asyncio +import json from typing import Any +import aiohttp from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts +from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price from solders.hash import Hash +from solders.instruction import Instruction +from solders.keypair import Keypair +from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction @@ -88,34 +94,50 @@ class SolanaClient: response = await client.get_latest_blockhash() return response.value.blockhash - async def send_transaction( + async def build_and_send_transaction( self, - transaction: Transaction, + instructions: list[Instruction], + signer_keypair: Keypair, skip_preflight: bool = True, max_retries: int = 3, + priority_fee: int | None = None, ) -> str: - """Send a transaction to the network. + """ + Send a transaction with optional priority fee. Args: - transaction: Prepared transaction - skip_preflight: Whether to skip preflight checks - max_retries: Maximum number of sending attempts + instructions: List of instructions to include in the transaction. + skip_preflight: Whether to skip preflight checks. + max_retries: Maximum number of retry attempts. + priority_fee: Optional priority fee in microlamports. Returns: - Transaction signature - - Raises: - Exception: If transaction fails after all retries + Transaction signature. """ client = await self.get_client() - # Attempt to send with retries + logger.info( + f"Priority fee in microlamports: {priority_fee if priority_fee else 0}" + ) + + # Add priority fee instructions if applicable + if priority_fee is not None: + fee_instructions = [ + set_compute_unit_limit(100_000), # Default compute unit limit + set_compute_unit_price(priority_fee), + ] + instructions = fee_instructions + instructions + + recent_blockhash = await self.get_latest_blockhash() + message = Message(instructions, signer_keypair.pubkey()) + transaction = Transaction([signer_keypair], message, recent_blockhash) + for attempt in range(max_retries): try: tx_opts = TxOpts( skip_preflight=skip_preflight, preflight_commitment=Confirmed ) - response = await client.send_transaction(transaction, opts=tx_opts) + response = await client.send_transaction(transaction, tx_opts) return response.value except Exception as e: @@ -150,3 +172,29 @@ class SolanaClient: except Exception as e: logger.error(f"Failed to confirm transaction {signature}: {str(e)}") return False + + async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None: + """ + Send a raw RPC request to the Solana node. + + Args: + body: JSON-RPC request body. + + Returns: + Optional[Dict[str, Any]]: Parsed JSON response, or None if the request fails. + """ + try: + async with aiohttp.ClientSession() as session: + async with session.post( + self.rpc_endpoint, + json=body, + timeout=aiohttp.ClientTimeout(10), # 10-second timeout + ) as response: + response.raise_for_status() + return await response.json() + except aiohttp.ClientError as e: + logger.error(f"RPC request failed: {str(e)}", exc_info=True) + return None + except json.JSONDecodeError as e: + logger.error(f"Failed to decode RPC response: {str(e)}", exc_info=True) + return None diff --git a/src/core/priority_fee/__init__.py b/src/core/priority_fee/__init__.py new file mode 100644 index 0000000..276aa74 --- /dev/null +++ b/src/core/priority_fee/__init__.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + + +class PriorityFeePlugin(ABC): + """Base class for priority fee calculation plugins.""" + + @abstractmethod + async def get_priority_fee(self) -> int | None: + """ + Calculate the priority fee. + + Returns: + Optional[int]: Priority fee in lamports, or None if no fee should be applied. + """ + pass diff --git a/src/core/priority_fee/dynamic_fee.py b/src/core/priority_fee/dynamic_fee.py new file mode 100644 index 0000000..b8a1ff8 --- /dev/null +++ b/src/core/priority_fee/dynamic_fee.py @@ -0,0 +1,69 @@ +import statistics + +from solders.pubkey import Pubkey + +from core.priority_fee import PriorityFeePlugin +from src.core.client import SolanaClient +from src.utils.logger import get_logger + +logger = get_logger(__name__) + + +class DynamicPriorityFee(PriorityFeePlugin): + """Dynamic priority fee plugin using getRecentPrioritizationFees.""" + + def __init__(self, client: SolanaClient): + """ + Initialize the dynamic fee plugin. + + Args: + client: Solana RPC client for network requests. + """ + self.client = client + + async def get_priority_fee( + self, accounts: list[Pubkey] | None = None + ) -> int | None: + """ + Fetch the recent priority fee using getRecentPrioritizationFees. + + Args: + accounts: List of accounts to consider for the fee calculation. + If None, the fee is calculated without specific account constraints. + + Returns: + Optional[int]: Median priority fee in microlamports, or None if the request fails. + """ + try: + body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getRecentPrioritizationFees", + "params": [[str(account) for account in accounts]] if accounts else [], + } + + response = await self.client.post_rpc(body) + if not response or "result" not in response: + logger.error( + "Failed to fetch recent prioritization fees: invalid response" + ) + return None + + fees = [fee["prioritizationFee"] for fee in response["result"]] + if not fees: + logger.warning("No prioritization fees found in the response") + return None + + # Get the 70th percentile of fees for faster processing + # It means you're paying a fee that's higher than 70% of other transactions + # Higher percentile = faster transactions but more expensive + # Lower percentile = cheaper but slower transactions + prior_fee = int(statistics.quantiles(fees, n=10)[-3]) # 70th percentile + + return prior_fee + + except Exception as e: + logger.error( + f"Failed to fetch recent priority fee: {str(e)}", exc_info=True + ) + return None diff --git a/src/core/priority_fee/fixed_fee.py b/src/core/priority_fee/fixed_fee.py new file mode 100644 index 0000000..64102bc --- /dev/null +++ b/src/core/priority_fee/fixed_fee.py @@ -0,0 +1,25 @@ +from . import PriorityFeePlugin + + +class FixedPriorityFee(PriorityFeePlugin): + """Fixed priority fee plugin.""" + + def __init__(self, fixed_fee: int): + """ + Initialize the fixed fee plugin. + + Args: + fixed_fee: Fixed priority fee in microlamports. + """ + self.fixed_fee = fixed_fee + + async def get_priority_fee(self) -> int | None: + """ + Return the fixed priority fee. + + Returns: + Optional[int]: Fixed priority fee in microlamports, or None if fixed_fee is 0. + """ + if self.fixed_fee == 0: + return None + return self.fixed_fee diff --git a/src/core/priority_fee/manager.py b/src/core/priority_fee/manager.py new file mode 100644 index 0000000..db5b12d --- /dev/null +++ b/src/core/priority_fee/manager.py @@ -0,0 +1,92 @@ +from solders.pubkey import Pubkey + +from src.core.client import SolanaClient +from src.core.priority_fee.dynamic_fee import DynamicPriorityFee +from src.core.priority_fee.fixed_fee import FixedPriorityFee +from src.utils.logger import get_logger + +logger = get_logger(__name__) + + +class PriorityFeeManager: + """Manager for priority fee calculation and validation.""" + + def __init__( + self, + client: SolanaClient, + enable_dynamic_fee: bool, + enable_fixed_fee: bool, + fixed_fee: int, + extra_fee: float, + hard_cap: int, + ): + """ + Initialize the priority fee manager. + + Args: + client: Solana RPC client for dynamic fee calculation. + enable_dynamic_fee: Whether to enable dynamic fee calculation. + enable_fixed_fee: Whether to enable fixed fee. + fixed_fee: Fixed priority fee in microlamports. + extra_fee: Percentage increase to apply to the base fee. + hard_cap: Maximum allowed priority fee in microlamports. + """ + self.client = client + self.enable_dynamic_fee = enable_dynamic_fee + self.enable_fixed_fee = enable_fixed_fee + self.fixed_fee = fixed_fee + self.extra_fee = extra_fee + self.hard_cap = hard_cap + + # Initialize plugins + self.dynamic_fee_plugin = DynamicPriorityFee(client) + self.fixed_fee_plugin = FixedPriorityFee(fixed_fee) + + async def calculate_priority_fee( + self, accounts: list[Pubkey] | None = None + ) -> int | None: + """ + Calculate the priority fee based on the configuration. + + Args: + accounts: List of accounts to consider for dynamic fee calculation. + If None, the fee is calculated without specific account constraints. + + Returns: + Optional[int]: Calculated priority fee in microlamports, or None if no fee should be applied. + """ + base_fee = await self._get_base_fee(accounts) + if base_fee is None: + return None + + # Apply extra fee (percentage increase) + final_fee = int(base_fee * (1 + self.extra_fee)) + + # Enforce hard cap + if final_fee > self.hard_cap: + logger.warning( + f"Calculated priority fee {final_fee} exceeds hard cap {self.hard_cap}. Applying hard cap." + ) + final_fee = self.hard_cap + + return final_fee + + async def _get_base_fee(self, accounts: list[Pubkey] | None = None) -> int | None: + """ + Determine the base fee based on the configuration. + + Returns: + Optional[int]: Base fee in microlamports, or None if no fee should be applied. + """ + # Prefer dynamic fee if both are enabled + if self.enable_dynamic_fee: + dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee(accounts) + if dynamic_fee is not None: + return dynamic_fee + + # Fall back to fixed fee if enabled + if self.enable_fixed_fee: + return await self.fixed_fee_plugin.get_priority_fee() + + # No priority fee if both are disabled + return None diff --git a/src/trading/base.py b/src/trading/base.py index 7144e7f..2359590 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -8,6 +8,8 @@ from typing import Any from solders.pubkey import Pubkey +from core.pubkeys import PumpAddresses + @dataclass class TokenInfo: @@ -80,3 +82,20 @@ class Trader(ABC): TradeResult with operation outcome """ pass + + def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]: + """ + Get the list of accounts relevant for calculating the priority fee. + + Args: + token_info: Token information for the buy/sell operation. + + Returns: + list[Pubkey]: List of relevant accounts. + """ + return [ + token_info.mint, # Token mint address + token_info.bonding_curve, # Bonding curve address + PumpAddresses.PROGRAM, # Pump.fun program address + PumpAddresses.FEE, # Pump.fun fee account + ] diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 1ddc99a..c38dfe7 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -5,13 +5,11 @@ Buy operations for pump.fun tokens. import struct from typing import Final -from solders.hash import Hash from solders.instruction import AccountMeta, Instruction -from solders.message import Message from solders.pubkey import Pubkey -from solders.transaction import Transaction from spl.token.instructions import create_associated_token_account +from core.priority_fee.manager import PriorityFeeManager from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import ( @@ -38,6 +36,7 @@ class TokenBuyer(Trader): client: SolanaClient, wallet: Wallet, curve_manager: BondingCurveManager, + priority_fee_manager: PriorityFeeManager, amount: float, slippage: float = 0.01, max_retries: int = 5, @@ -55,6 +54,7 @@ class TokenBuyer(Trader): self.client = client self.wallet = wallet self.curve_manager = curve_manager + self.priority_fee_manager = priority_fee_manager self.amount = amount self.slippage = slippage self.max_retries = max_retries @@ -69,16 +69,13 @@ class TokenBuyer(Trader): TradeResult with buy outcome """ try: - # Extract token info - mint = token_info.mint - bonding_curve = token_info.bonding_curve - associated_bonding_curve = token_info.associated_bonding_curve - # Convert amount to lamports amount_lamports = int(self.amount * LAMPORTS_PER_SOL) # Fetch token price - curve_state = await self.curve_manager.get_curve_state(bonding_curve) + curve_state = await self.curve_manager.get_curve_state( + token_info.bonding_curve + ) token_price_sol = curve_state.calculate_price() token_amount = self.amount / token_price_sol @@ -92,14 +89,16 @@ class TokenBuyer(Trader): f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" ) - associated_token_account = self.wallet.get_associated_token_address(mint) + associated_token_account = self.wallet.get_associated_token_address( + token_info.mint + ) - await self._ensure_associated_token_account(mint, associated_token_account) + await self._ensure_associated_token_account( + token_info.mint, associated_token_account + ) tx_signature = await self._send_buy_transaction( - mint, - bonding_curve, - associated_bonding_curve, + token_info, associated_token_account, token_amount, max_amount_lamports, @@ -128,7 +127,7 @@ class TokenBuyer(Trader): async def _ensure_associated_token_account( self, mint: Pubkey, associated_token_account: Pubkey ) -> None: - """Ensure associated token account exists. + """Ensure associated token account exists, else create it. Args: mint: Token mint @@ -147,14 +146,16 @@ class TokenBuyer(Trader): payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint ) - recent_blockhash: Hash = await self.client.get_latest_blockhash() - create_ata_msg = Message([create_ata_ix], self.wallet.keypair.pubkey()) - create_ata_tx = Transaction( - [self.wallet.keypair], create_ata_msg, recent_blockhash + tx_sig = await self.client.build_and_send_transaction( + [create_ata_ix], + self.wallet.keypair, + skip_preflight=True, + max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + [mint, SystemAddresses.PROGRAM, SystemAddresses.TOKEN_PROGRAM] + ), ) - tx_sig = await self.client.send_transaction(create_ata_tx) - await self.client.confirm_transaction(tx_sig) logger.info( f"Associated token account created: {associated_token_account}" @@ -170,9 +171,7 @@ class TokenBuyer(Trader): async def _send_buy_transaction( self, - mint: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, + token_info: TokenInfo, associated_token_account: Pubkey, token_amount: float, max_amount_lamports: int, @@ -180,9 +179,7 @@ class TokenBuyer(Trader): """Send buy transaction. Args: - mint: Token mint - bonding_curve: Bonding curve address - associated_bonding_curve: Associated bonding curve address + token_info: Token information associated_token_account: User's token account token_amount: Amount of tokens to buy max_amount_lamports: Maximum SOL to spend in lamports @@ -198,10 +195,14 @@ class TokenBuyer(Trader): pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False ), AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), AccountMeta( - pubkey=associated_bonding_curve, is_signer=False, is_writable=True + pubkey=token_info.bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=token_info.associated_bonding_curve, + is_signer=False, + is_writable=True, ), AccountMeta( pubkey=associated_token_account, is_signer=False, is_writable=True @@ -233,16 +234,15 @@ class TokenBuyer(Trader): ) buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) - # Prepare buy transaction data - recent_blockhash: Hash = await self.client.get_latest_blockhash() - buy_message = Message([buy_ix], self.wallet.keypair.pubkey()) - buy_tx = Transaction([self.wallet.keypair], buy_message, recent_blockhash) - try: - return await self.client.send_transaction( - buy_tx, + return await self.client.build_and_send_transaction( + [buy_ix], + self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + self._get_relevant_accounts(token_info) + ), ) except Exception as e: logger.error(f"Buy transaction failed: {str(e)}") diff --git a/src/trading/seller.py b/src/trading/seller.py index a91a7f0..14da81d 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -5,12 +5,10 @@ Sell operations for pump.fun tokens. import struct from typing import Final -from solders.hash import Hash from solders.instruction import AccountMeta, Instruction -from solders.message import Message from solders.pubkey import Pubkey -from solders.transaction import Transaction +from core.priority_fee.manager import PriorityFeeManager from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import ( @@ -37,6 +35,7 @@ class TokenSeller(Trader): client: SolanaClient, wallet: Wallet, curve_manager: BondingCurveManager, + priority_fee_manager: PriorityFeeManager, slippage: float = 0.25, max_retries: int = 5, ): @@ -52,6 +51,7 @@ class TokenSeller(Trader): self.client = client self.wallet = wallet self.curve_manager = curve_manager + self.priority_fee_manager = priority_fee_manager self.slippage = slippage self.max_retries = max_retries @@ -65,13 +65,10 @@ class TokenSeller(Trader): TradeResult with sell outcome """ try: - # Extract token info - mint = token_info.mint - bonding_curve = token_info.bonding_curve - associated_bonding_curve = token_info.associated_bonding_curve - # Get associated token account - associated_token_account = self.wallet.get_associated_token_address(mint) + associated_token_account = self.wallet.get_associated_token_address( + token_info.mint + ) # Get token balance token_balance = await self.client.get_token_account_balance( @@ -86,7 +83,9 @@ class TokenSeller(Trader): return TradeResult(success=False, error_message="No tokens to sell") # Fetch token price - curve_state = await self.curve_manager.get_curve_state(bonding_curve) + curve_state = await self.curve_manager.get_curve_state( + token_info.bonding_curve + ) token_price_sol = curve_state.calculate_price() logger.info(f"Price per Token: {token_price_sol:.8f} SOL") @@ -106,9 +105,7 @@ class TokenSeller(Trader): ) tx_signature = await self._send_sell_transaction( - mint, - bonding_curve, - associated_bonding_curve, + token_info, associated_token_account, amount, min_sol_output, @@ -136,9 +133,7 @@ class TokenSeller(Trader): async def _send_sell_transaction( self, - mint: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, + token_info: TokenInfo, associated_token_account: Pubkey, token_amount: int, min_sol_output: int, @@ -146,9 +141,7 @@ class TokenSeller(Trader): """Send sell transaction. Args: - mint: Token mint - bonding_curve: Bonding curve address - associated_bonding_curve: Associated bonding curve address + mint: Token information associated_token_account: User's token account token_amount: Amount of tokens to sell in raw units min_sol_output: Minimum SOL to receive in lamports @@ -165,10 +158,14 @@ class TokenSeller(Trader): pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False ), AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), AccountMeta( - pubkey=associated_bonding_curve, is_signer=False, is_writable=True + pubkey=token_info.bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=token_info.associated_bonding_curve, + is_signer=False, + is_writable=True, ), AccountMeta( pubkey=associated_token_account, is_signer=False, is_writable=True @@ -201,16 +198,15 @@ class TokenSeller(Trader): ) sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) - # Prepare sell transaction data - recent_blockhash: Hash = await self.client.get_latest_blockhash() - sell_message = Message([sell_ix], self.wallet.keypair.pubkey()) - sell_tx = Transaction([self.wallet.keypair], sell_message, recent_blockhash) - try: - return await self.client.send_transaction( - sell_tx, + return await self.client.build_and_send_transaction( + [sell_ix], + self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + self._get_relevant_accounts(token_info) + ), ) except Exception as e: logger.error(f"Sell transaction failed: {str(e)}") diff --git a/src/trading/trader.py b/src/trading/trader.py index 38fe7e1..55cb9e2 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -9,6 +9,7 @@ import os from datetime import datetime import config +from core.priority_fee.manager import PriorityFeeManager from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import PumpAddresses @@ -50,10 +51,20 @@ class PumpTrader: 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, + ) + self.buyer = TokenBuyer( self.solana_client, self.wallet, self.curve_manager, + self.priority_fee_manager, buy_amount, buy_slippage, max_retries, @@ -63,6 +74,7 @@ class PumpTrader: self.solana_client, self.wallet, self.curve_manager, + self.priority_fee_manager, sell_slippage, max_retries, )