From a1d5966f2995e560f69a3f00dd3ce3c05208cce2 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 13 Mar 2025 21:01:26 +0000 Subject: [PATCH] refactor: tx building, getting dynamic prior fee --- config.py | 10 +++-- requirements.txt | 3 +- src/core/client.py | 36 ++++++++++++++++- src/core/priority_fee/dynamic_fee.py | 59 ++++++++++++++++++++------- src/core/priority_fee/fixed_fee.py | 4 +- src/core/priority_fee/manager.py | 26 +++++++----- src/trading/base.py | 19 +++++++++ src/trading/buyer.py | 60 ++++++++++++---------------- src/trading/seller.py | 44 +++++++++----------- 9 files changed, 170 insertions(+), 91 deletions(-) diff --git a/config.py b/config.py index 32dc0b3..3f075d8 100644 --- a/config.py +++ b/config.py @@ -3,18 +3,20 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT: float = 0.000001 # Amount of SOL to spend when buying +BUY_AMOUNT: 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 = True # Enable dynamic priority fee calculation -ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee -FIXED_PRIORITY_FEE: int = 200000 # Fixed priority fee in lamports (0 means no fee) +ENABLE_FIXED_PRIORITY_FEE: bool = False # Enable fixed priority fee +FIXED_PRIORITY_FEE: int = 50_000 # Fixed priority fee in microlamports EXTRA_PRIORITY_FEE: float = ( 0.1 # Percentage increase applied to priority fee (0.1 = 10%) ) -HARD_CAP_PRIOR_FEE: int = 1000000 # Maximum allowed priority fee in lamports (hard cap) +HARD_CAP_PRIOR_FEE: int = ( + 200_000 # Maximum allowed priority fee in microlamports (hard cap) +) # Retries and timeouts MAX_RETRIES: int = 2 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 8a2da66..a6d4b3b 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -3,8 +3,10 @@ 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 @@ -107,17 +109,21 @@ class SolanaClient: 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 lamports. + priority_fee: Optional priority fee in microlamports. Returns: Transaction signature. """ client = await self.get_client() + 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(200_000), # Default compute unit limit + set_compute_unit_limit(100_000), # Default compute unit limit set_compute_unit_price(priority_fee), ] instructions = fee_instructions + instructions @@ -166,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/dynamic_fee.py b/src/core/priority_fee/dynamic_fee.py index 02804ab..b8a1ff8 100644 --- a/src/core/priority_fee/dynamic_fee.py +++ b/src/core/priority_fee/dynamic_fee.py @@ -1,13 +1,16 @@ +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 -from . import PriorityFeePlugin - logger = get_logger(__name__) class DynamicPriorityFee(PriorityFeePlugin): - """Default dynamic priority fee plugin using getRecentPriorityFee.""" + """Dynamic priority fee plugin using getRecentPrioritizationFees.""" def __init__(self, client: SolanaClient): """ @@ -18,21 +21,49 @@ class DynamicPriorityFee(PriorityFeePlugin): """ self.client = client - async def get_priority_fee(self) -> int | None: + async def get_priority_fee( + self, accounts: list[Pubkey] | None = None + ) -> int | None: """ - Fetch the recent priority fee from the Solana network. + 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]: Recent priority fee in lamports, or None if the request fails. + Optional[int]: Median priority fee in microlamports, or None if the request fails. """ try: - client = await self.client.get_client() - response = await client.get_recent_prioritization_fees() - if response and response.value: - return response.value[ - 0 - ].prioritization_fee # Use the first fee from the list - return None + 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)}") + 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 index 48cfc27..64102bc 100644 --- a/src/core/priority_fee/fixed_fee.py +++ b/src/core/priority_fee/fixed_fee.py @@ -9,7 +9,7 @@ class FixedPriorityFee(PriorityFeePlugin): Initialize the fixed fee plugin. Args: - fixed_fee: Fixed priority fee in lamports. + fixed_fee: Fixed priority fee in microlamports. """ self.fixed_fee = fixed_fee @@ -18,7 +18,7 @@ class FixedPriorityFee(PriorityFeePlugin): Return the fixed priority fee. Returns: - Optional[int]: Fixed priority fee in lamports, or None if fixed_fee is 0. + Optional[int]: Fixed priority fee in microlamports, or None if fixed_fee is 0. """ if self.fixed_fee == 0: return None diff --git a/src/core/priority_fee/manager.py b/src/core/priority_fee/manager.py index 641d05d..db5b12d 100644 --- a/src/core/priority_fee/manager.py +++ b/src/core/priority_fee/manager.py @@ -1,3 +1,5 @@ +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 @@ -25,9 +27,9 @@ class PriorityFeeManager: 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 lamports. + fixed_fee: Fixed priority fee in microlamports. extra_fee: Percentage increase to apply to the base fee. - hard_cap: Maximum allowed priority fee in lamports. + hard_cap: Maximum allowed priority fee in microlamports. """ self.client = client self.enable_dynamic_fee = enable_dynamic_fee @@ -40,14 +42,20 @@ class PriorityFeeManager: self.dynamic_fee_plugin = DynamicPriorityFee(client) self.fixed_fee_plugin = FixedPriorityFee(fixed_fee) - async def calculate_priority_fee(self) -> int | None: + 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 lamports, or None if no fee should be applied. + Optional[int]: Calculated priority fee in microlamports, or None if no fee should be applied. """ - base_fee = await self._get_base_fee() + base_fee = await self._get_base_fee(accounts) if base_fee is None: return None @@ -63,16 +71,16 @@ class PriorityFeeManager: return final_fee - async def _get_base_fee(self) -> int | None: + 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 lamports, or None if no fee should be applied. + 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() + dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee(accounts) if dynamic_fee is not None: return dynamic_fee @@ -80,5 +88,5 @@ class PriorityFeeManager: if self.enable_fixed_fee: return await self.fixed_fee_plugin.get_priority_fee() - # No fee if both are disabled or return None + # 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 c1f6a69..c38dfe7 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -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,18 +146,14 @@ 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(), # Get priority fee from manager + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + [mint, SystemAddresses.PROGRAM, SystemAddresses.TOKEN_PROGRAM] + ), ) await self.client.confirm_transaction(tx_sig) @@ -176,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, @@ -186,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 @@ -204,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 @@ -239,18 +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.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(), # Get priority fee from manager + 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 244221d..14da81d 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -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,18 +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.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(), # Get priority fee from manager + 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)}")