From ac3d463d200e96c2ce92805c0cc70f88561a52fb Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 12 Mar 2025 22:11:31 +0000 Subject: [PATCH 1/3] feat: add priority fee support with dynamic/fixed options, hard cap, and extra fee --- .gitignore | 1 + config.py | 37 ++++++++++-- src/core/client.py | 40 +++++++++---- src/core/priority_fee/__init__.py | 15 +++++ src/core/priority_fee/dynamic_fee.py | 38 +++++++++++++ src/core/priority_fee/fixed_fee.py | 25 +++++++++ src/core/priority_fee/manager.py | 84 ++++++++++++++++++++++++++++ src/trading/buyer.py | 36 +++++++----- src/trading/seller.py | 18 +++--- src/trading/trader.py | 12 ++++ 10 files changed, 266 insertions(+), 40 deletions(-) create mode 100644 src/core/priority_fee/__init__.py create mode 100644 src/core/priority_fee/dynamic_fee.py create mode 100644 src/core/priority_fee/fixed_fee.py create mode 100644 src/core/priority_fee/manager.py 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/config.py b/config.py index 54a316c..32dc0b3 100644 --- a/config.py +++ b/config.py @@ -3,11 +3,18 @@ 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: float = 0.000001 # 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) +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) # Retries and timeouts MAX_RETRIES: int = 2 @@ -22,6 +29,24 @@ MAX_TOKEN_AGE: 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 +MAX_RPS: int = 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" + + +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/src/core/client.py b/src/core/client.py index ab9a485..8a2da66 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -8,7 +8,11 @@ from typing import Any 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 +92,46 @@ 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 lamports. Returns: - Transaction signature - - Raises: - Exception: If transaction fails after all retries + Transaction signature. """ client = await self.get_client() - # Attempt to send with retries + # 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_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: 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..02804ab --- /dev/null +++ b/src/core/priority_fee/dynamic_fee.py @@ -0,0 +1,38 @@ +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.""" + + 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) -> int | None: + """ + Fetch the recent priority fee from the Solana network. + + Returns: + Optional[int]: Recent priority fee in lamports, 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 + except Exception as e: + logger.error(f"Failed to fetch recent priority fee: {str(e)}") + 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..48cfc27 --- /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 lamports. + """ + 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 lamports, 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..641d05d --- /dev/null +++ b/src/core/priority_fee/manager.py @@ -0,0 +1,84 @@ +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 lamports. + extra_fee: Percentage increase to apply to the base fee. + hard_cap: Maximum allowed priority fee in lamports. + """ + 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) -> int | None: + """ + Calculate the priority fee based on the configuration. + + Returns: + Optional[int]: Calculated priority fee in lamports, or None if no fee should be applied. + """ + base_fee = await self._get_base_fee() + 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) -> 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. + """ + # Prefer dynamic fee if both are enabled + if self.enable_dynamic_fee: + dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee() + 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 fee if both are disabled or return None + return None diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 1ddc99a..c1f6a69 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 @@ -147,13 +147,19 @@ 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 - ) + # 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.send_transaction(create_ata_tx) + 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 + ) await self.client.confirm_transaction(tx_sig) logger.info( @@ -234,15 +240,17 @@ 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) + # 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(), # Get priority fee from manager ) 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..244221d 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 @@ -202,15 +202,17 @@ 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) + # 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(), # Get priority fee from manager ) 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, ) From a1d5966f2995e560f69a3f00dd3ce3c05208cce2 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 13 Mar 2025 21:01:26 +0000 Subject: [PATCH 2/3] 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)}") From f8d05ae825ab7239651381de51ced4f472922b18 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 14 Mar 2025 18:25:56 +0000 Subject: [PATCH 3/3] chore(config): update comments and remove default nodes --- cli.py | 8 ++------ config.py | 42 ++++++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 20 deletions(-) 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 3f075d8..4e499be 100644 --- a/config.py +++ b/config.py @@ -3,37 +3,51 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT: 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 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 = False # Enable fixed priority fee -FIXED_PRIORITY_FEE: int = 50_000 # Fixed priority fee in microlamports +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.1 # Percentage increase applied to priority fee (0.1 = 10%) + 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 + +# 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 -PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" -PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" def validate_priority_fee_config() -> None: