From ac3d463d200e96c2ce92805c0cc70f88561a52fb Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 12 Mar 2025 22:11:31 +0000 Subject: [PATCH] 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, )