Merge pull request #64 from chainstacklabs/feature/dynamic-prior-fees

Add dynamic/fixed priority fee
This commit is contained in:
Anton
2025-03-15 08:22:12 +00:00
committed by GitHub
13 changed files with 415 additions and 100 deletions
+1
View File
@@ -1,5 +1,6 @@
trades/* trades/*
.vscode
.pylintrc .pylintrc
.ruff_cache .ruff_cache
+2 -6
View File
@@ -62,12 +62,8 @@ async def main() -> None:
args = parse_args() args = parse_args()
# Get configuration values, preferring command line args over config.py # Get configuration values, preferring command line args over config.py
rpc_endpoint: str | None = ( rpc_endpoint: str | None = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
os.environ.get("SOLANA_NODE_RPC_ENDPOINT") or config.PUBLIC_RPC_ENDPOINT wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
)
wss_endpoint: str | None = (
os.environ.get("SOLANA_NODE_WSS_ENDPOINT") or config.PUBLIC_WSS_ENDPOINT
)
private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY") private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY")
# Validate configuration values # Validate configuration values
+56 -15
View File
@@ -3,25 +3,66 @@ Configuration for the pump.fun trading bot.
""" """
# Trading parameters # Trading parameters
BUY_AMOUNT = 0.000001 # Amount of SOL to spend when buying BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying
BUY_SLIPPAGE = 0.4 # 40% slippage tolerance for buying BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying
SELL_SLIPPAGE = 0.4 # 40% slippage tolerance for selling SELL_SLIPPAGE: float = 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
# 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 # Retries and timeouts
MAX_RETRIES: int = 2 MAX_RETRIES: int = 10 # Number of retries for transaction sending
WAIT_TIME_AFTER_BUY: int = 15 # TODO: waiting times will be replaced with retries to shorten delays
WAIT_TIME_BEFORE_NEW_TOKEN: int = 30 WAIT_TIME_AFTER_CREATION: int | float = (
WAIT_TIME_AFTER_CREATION: int = 15 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. # 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 # This threshold is checked before processing starts - tokens older than this are skipped
# since they likely contain outdated information from the websocket stream # 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
MAX_RPS = 25 # TODO: not implemented. Max RPS to avoid rate limit errors # Tested with Chainstack nodes (https://console.chainstack.com), but you can use any node provider
PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" # You can get a trader node https://docs.chainstack.com/docs/solana-trader-nodes
PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" 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()
+1
View File
@@ -6,3 +6,4 @@ solana==0.36.6
solders>=0.26.0 solders>=0.26.0
websockets>=15.0 websockets>=15.0
python-dotenv>=1.0.1 python-dotenv>=1.0.1
aiohttp==3.11.13
+60 -12
View File
@@ -3,12 +3,18 @@ Solana client abstraction for blockchain operations.
""" """
import asyncio import asyncio
import json
from typing import Any from typing import Any
import aiohttp
from solana.rpc.async_api import AsyncClient from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts 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.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.pubkey import Pubkey
from solders.transaction import Transaction from solders.transaction import Transaction
@@ -88,34 +94,50 @@ class SolanaClient:
response = await client.get_latest_blockhash() response = await client.get_latest_blockhash()
return response.value.blockhash return response.value.blockhash
async def send_transaction( async def build_and_send_transaction(
self, self,
transaction: Transaction, instructions: list[Instruction],
signer_keypair: Keypair,
skip_preflight: bool = True, skip_preflight: bool = True,
max_retries: int = 3, max_retries: int = 3,
priority_fee: int | None = None,
) -> str: ) -> str:
"""Send a transaction to the network. """
Send a transaction with optional priority fee.
Args: Args:
transaction: Prepared transaction instructions: List of instructions to include in the transaction.
skip_preflight: Whether to skip preflight checks skip_preflight: Whether to skip preflight checks.
max_retries: Maximum number of sending attempts max_retries: Maximum number of retry attempts.
priority_fee: Optional priority fee in microlamports.
Returns: Returns:
Transaction signature Transaction signature.
Raises:
Exception: If transaction fails after all retries
""" """
client = await self.get_client() 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): for attempt in range(max_retries):
try: try:
tx_opts = TxOpts( tx_opts = TxOpts(
skip_preflight=skip_preflight, preflight_commitment=Confirmed 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 return response.value
except Exception as e: except Exception as e:
@@ -150,3 +172,29 @@ class SolanaClient:
except Exception as e: except Exception as e:
logger.error(f"Failed to confirm transaction {signature}: {str(e)}") logger.error(f"Failed to confirm transaction {signature}: {str(e)}")
return False 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
+15
View File
@@ -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
+69
View File
@@ -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
+25
View File
@@ -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
+92
View File
@@ -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
+19
View File
@@ -8,6 +8,8 @@ from typing import Any
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from core.pubkeys import PumpAddresses
@dataclass @dataclass
class TokenInfo: class TokenInfo:
@@ -80,3 +82,20 @@ class Trader(ABC):
TradeResult with operation outcome TradeResult with operation outcome
""" """
pass 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
]
+37 -37
View File
@@ -5,13 +5,11 @@ Buy operations for pump.fun tokens.
import struct import struct
from typing import Final from typing import Final
from solders.hash import Hash
from solders.instruction import AccountMeta, Instruction from solders.instruction import AccountMeta, Instruction
from solders.message import Message
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from solders.transaction import Transaction
from spl.token.instructions import create_associated_token_account 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.client import SolanaClient
from src.core.curve import BondingCurveManager from src.core.curve import BondingCurveManager
from src.core.pubkeys import ( from src.core.pubkeys import (
@@ -38,6 +36,7 @@ class TokenBuyer(Trader):
client: SolanaClient, client: SolanaClient,
wallet: Wallet, wallet: Wallet,
curve_manager: BondingCurveManager, curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
amount: float, amount: float,
slippage: float = 0.01, slippage: float = 0.01,
max_retries: int = 5, max_retries: int = 5,
@@ -55,6 +54,7 @@ class TokenBuyer(Trader):
self.client = client self.client = client
self.wallet = wallet self.wallet = wallet
self.curve_manager = curve_manager self.curve_manager = curve_manager
self.priority_fee_manager = priority_fee_manager
self.amount = amount self.amount = amount
self.slippage = slippage self.slippage = slippage
self.max_retries = max_retries self.max_retries = max_retries
@@ -69,16 +69,13 @@ class TokenBuyer(Trader):
TradeResult with buy outcome TradeResult with buy outcome
""" """
try: 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 # Convert amount to lamports
amount_lamports = int(self.amount * LAMPORTS_PER_SOL) amount_lamports = int(self.amount * LAMPORTS_PER_SOL)
# Fetch token price # 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_price_sol = curve_state.calculate_price()
token_amount = self.amount / token_price_sol 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)" 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( tx_signature = await self._send_buy_transaction(
mint, token_info,
bonding_curve,
associated_bonding_curve,
associated_token_account, associated_token_account,
token_amount, token_amount,
max_amount_lamports, max_amount_lamports,
@@ -128,7 +127,7 @@ class TokenBuyer(Trader):
async def _ensure_associated_token_account( async def _ensure_associated_token_account(
self, mint: Pubkey, associated_token_account: Pubkey self, mint: Pubkey, associated_token_account: Pubkey
) -> None: ) -> None:
"""Ensure associated token account exists. """Ensure associated token account exists, else create it.
Args: Args:
mint: Token mint mint: Token mint
@@ -147,14 +146,16 @@ class TokenBuyer(Trader):
payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint
) )
recent_blockhash: Hash = await self.client.get_latest_blockhash() tx_sig = await self.client.build_and_send_transaction(
create_ata_msg = Message([create_ata_ix], self.wallet.keypair.pubkey()) [create_ata_ix],
create_ata_tx = Transaction( self.wallet.keypair,
[self.wallet.keypair], create_ata_msg, recent_blockhash 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) await self.client.confirm_transaction(tx_sig)
logger.info( logger.info(
f"Associated token account created: {associated_token_account}" f"Associated token account created: {associated_token_account}"
@@ -170,9 +171,7 @@ class TokenBuyer(Trader):
async def _send_buy_transaction( async def _send_buy_transaction(
self, self,
mint: Pubkey, token_info: TokenInfo,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
associated_token_account: Pubkey, associated_token_account: Pubkey,
token_amount: float, token_amount: float,
max_amount_lamports: int, max_amount_lamports: int,
@@ -180,9 +179,7 @@ class TokenBuyer(Trader):
"""Send buy transaction. """Send buy transaction.
Args: Args:
mint: Token mint token_info: Token information
bonding_curve: Bonding curve address
associated_bonding_curve: Associated bonding curve address
associated_token_account: User's token account associated_token_account: User's token account
token_amount: Amount of tokens to buy token_amount: Amount of tokens to buy
max_amount_lamports: Maximum SOL to spend in lamports 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 pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
), ),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta( 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( AccountMeta(
pubkey=associated_token_account, is_signer=False, is_writable=True pubkey=associated_token_account, is_signer=False, is_writable=True
@@ -233,16 +234,15 @@ class TokenBuyer(Trader):
) )
buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) 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: try:
return await self.client.send_transaction( return await self.client.build_and_send_transaction(
buy_tx, [buy_ix],
self.wallet.keypair,
skip_preflight=True, skip_preflight=True,
max_retries=self.max_retries, 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: except Exception as e:
logger.error(f"Buy transaction failed: {str(e)}") logger.error(f"Buy transaction failed: {str(e)}")
+25 -29
View File
@@ -5,12 +5,10 @@ Sell operations for pump.fun tokens.
import struct import struct
from typing import Final from typing import Final
from solders.hash import Hash
from solders.instruction import AccountMeta, Instruction from solders.instruction import AccountMeta, Instruction
from solders.message import Message
from solders.pubkey import Pubkey 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.client import SolanaClient
from src.core.curve import BondingCurveManager from src.core.curve import BondingCurveManager
from src.core.pubkeys import ( from src.core.pubkeys import (
@@ -37,6 +35,7 @@ class TokenSeller(Trader):
client: SolanaClient, client: SolanaClient,
wallet: Wallet, wallet: Wallet,
curve_manager: BondingCurveManager, curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
slippage: float = 0.25, slippage: float = 0.25,
max_retries: int = 5, max_retries: int = 5,
): ):
@@ -52,6 +51,7 @@ class TokenSeller(Trader):
self.client = client self.client = client
self.wallet = wallet self.wallet = wallet
self.curve_manager = curve_manager self.curve_manager = curve_manager
self.priority_fee_manager = priority_fee_manager
self.slippage = slippage self.slippage = slippage
self.max_retries = max_retries self.max_retries = max_retries
@@ -65,13 +65,10 @@ class TokenSeller(Trader):
TradeResult with sell outcome TradeResult with sell outcome
""" """
try: 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 # 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 # Get token balance
token_balance = await self.client.get_token_account_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") return TradeResult(success=False, error_message="No tokens to sell")
# Fetch token price # 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_price_sol = curve_state.calculate_price()
logger.info(f"Price per Token: {token_price_sol:.8f} SOL") 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( tx_signature = await self._send_sell_transaction(
mint, token_info,
bonding_curve,
associated_bonding_curve,
associated_token_account, associated_token_account,
amount, amount,
min_sol_output, min_sol_output,
@@ -136,9 +133,7 @@ class TokenSeller(Trader):
async def _send_sell_transaction( async def _send_sell_transaction(
self, self,
mint: Pubkey, token_info: TokenInfo,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
associated_token_account: Pubkey, associated_token_account: Pubkey,
token_amount: int, token_amount: int,
min_sol_output: int, min_sol_output: int,
@@ -146,9 +141,7 @@ class TokenSeller(Trader):
"""Send sell transaction. """Send sell transaction.
Args: Args:
mint: Token mint mint: Token information
bonding_curve: Bonding curve address
associated_bonding_curve: Associated bonding curve address
associated_token_account: User's token account associated_token_account: User's token account
token_amount: Amount of tokens to sell in raw units token_amount: Amount of tokens to sell in raw units
min_sol_output: Minimum SOL to receive in lamports 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 pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
), ),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta( 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( AccountMeta(
pubkey=associated_token_account, is_signer=False, is_writable=True pubkey=associated_token_account, is_signer=False, is_writable=True
@@ -201,16 +198,15 @@ class TokenSeller(Trader):
) )
sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) 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: try:
return await self.client.send_transaction( return await self.client.build_and_send_transaction(
sell_tx, [sell_ix],
self.wallet.keypair,
skip_preflight=True, skip_preflight=True,
max_retries=self.max_retries, 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: except Exception as e:
logger.error(f"Sell transaction failed: {str(e)}") logger.error(f"Sell transaction failed: {str(e)}")
+12
View File
@@ -9,6 +9,7 @@ import os
from datetime import datetime from datetime import datetime
import config import config
from core.priority_fee.manager import PriorityFeeManager
from src.core.client import SolanaClient from src.core.client import SolanaClient
from src.core.curve import BondingCurveManager from src.core.curve import BondingCurveManager
from src.core.pubkeys import PumpAddresses from src.core.pubkeys import PumpAddresses
@@ -50,10 +51,20 @@ class PumpTrader:
self.wallet = Wallet(private_key) self.wallet = Wallet(private_key)
self.curve_manager = BondingCurveManager(self.solana_client) 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.buyer = TokenBuyer(
self.solana_client, self.solana_client,
self.wallet, self.wallet,
self.curve_manager, self.curve_manager,
self.priority_fee_manager,
buy_amount, buy_amount,
buy_slippage, buy_slippage,
max_retries, max_retries,
@@ -63,6 +74,7 @@ class PumpTrader:
self.solana_client, self.solana_client,
self.wallet, self.wallet,
self.curve_manager, self.curve_manager,
self.priority_fee_manager,
sell_slippage, sell_slippage,
max_retries, max_retries,
) )