wip(core): platform aware trading

This commit is contained in:
smypmsa
2025-08-02 11:30:30 +00:00
parent bb35101a8f
commit 9ec9eeb793
19 changed files with 726 additions and 2038 deletions
+75 -107
View File
@@ -12,13 +12,76 @@ from typing import Any
from solders.pubkey import Pubkey
from interfaces.core import Platform
# Import the new enhanced TokenInfo and Platform from interfaces
from interfaces.core import TokenInfo as EnhancedTokenInfo
# Import from interfaces to avoid duplication
from interfaces.core import Platform, TokenInfo
# Keep the original TokenInfo structure for backward compatibility
@dataclass
class TradeResult:
"""Enhanced result of a trading operation with platform support."""
success: bool
platform: Platform = Platform.PUMP_FUN # Add platform tracking
tx_signature: str | None = None
error_message: str | None = None
amount: float | None = None
price: float | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for logging/serialization.
Returns:
Dictionary representation of the trade result
"""
return {
"success": self.success,
"platform": self.platform.value,
"tx_signature": self.tx_signature,
"error_message": self.error_message,
"amount": self.amount,
"price": self.price,
}
class Trader(ABC):
"""Enhanced base interface for trading operations with platform support."""
@abstractmethod
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute trading operation.
Args:
token_info: Enhanced token information with platform support
Returns:
TradeResult with operation outcome including platform info
"""
pass
def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]:
"""
Get the list of accounts relevant for calculating the priority fee.
This is now platform-agnostic and should be overridden by platform-specific traders.
Args:
token_info: Enhanced token information
Returns:
List of relevant accounts (basic implementation)
"""
# Basic implementation - platform-specific traders should override this
accounts = [token_info.mint]
if token_info.bonding_curve:
accounts.append(token_info.bonding_curve)
if token_info.pool_state: # For other platforms
accounts.append(token_info.pool_state)
return accounts
# Legacy TokenInfo for backward compatibility (keep pump.fun specific)
@dataclass
class TokenInfo_Legacy:
"""Legacy token information structure for backward compatibility."""
@@ -73,86 +136,6 @@ class TokenInfo_Legacy:
}
@dataclass
class TradeResult:
"""Enhanced result of a trading operation with platform support."""
success: bool
platform: Platform = Platform.PUMP_FUN # Add platform tracking
tx_signature: str | None = None
error_message: str | None = None
amount: float | None = None
price: float | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for logging/serialization.
Returns:
Dictionary representation of the trade result
"""
return {
"success": self.success,
"platform": self.platform.value,
"tx_signature": self.tx_signature,
"error_message": self.error_message,
"amount": self.amount,
"price": self.price,
}
class Trader(ABC):
"""Enhanced base interface for trading operations with platform support."""
@abstractmethod
async def execute(self, token_info: "TokenInfo", *args, **kwargs) -> TradeResult:
"""Execute trading operation.
Args:
token_info: Enhanced token information with platform support
Returns:
TradeResult with operation outcome including platform info
"""
pass
def _get_relevant_accounts(self, token_info: "TokenInfo") -> list[Pubkey]:
"""
Get the list of accounts relevant for calculating the priority fee.
This is now platform-agnostic and should be overridden by platform-specific traders.
Args:
token_info: Enhanced token information
Returns:
List of relevant accounts (default implementation for pump.fun compatibility)
"""
# Default implementation maintains pump.fun compatibility
from core.pubkeys import PumpAddresses
accounts = [token_info.mint]
if token_info.bonding_curve:
accounts.append(token_info.bonding_curve)
if token_info.pool_state: # For other platforms
accounts.append(token_info.pool_state)
# Add platform program
if token_info.platform == Platform.PUMP_FUN:
accounts.extend([
PumpAddresses.PROGRAM,
PumpAddresses.FEE,
])
# Other platforms would add their specific accounts here
return accounts
# Use the enhanced TokenInfo as the main TokenInfo class
# This provides the new functionality while maintaining the same import path
TokenInfo = EnhancedTokenInfo
def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo:
"""Convert legacy TokenInfo to enhanced TokenInfo.
@@ -246,18 +229,11 @@ def create_pump_fun_token_info(
Returns:
Enhanced TokenInfo configured for pump.fun
"""
from core.pubkeys import PumpAddresses
# Default creator to user if not provided
if creator is None:
creator = user
# Derive creator vault if not provided
if creator_vault is None:
creator_vault, _ = Pubkey.find_program_address(
[b"creator-vault", bytes(creator)],
PumpAddresses.PROGRAM,
)
# Derive creator vault if not provided (import here to avoid circular imports)
if creator_vault is None and creator:
# We can't import PumpAddresses here, so this will need to be handled elsewhere
# For now, leave it as None and let the platform implementation handle it
pass
return TokenInfo(
name=name,
@@ -268,7 +244,7 @@ def create_pump_fun_token_info(
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
user=user,
creator=creator,
creator=creator or user,
creator_vault=creator_vault,
**kwargs
)
@@ -303,10 +279,6 @@ def create_lets_bonk_token_info(
Returns:
Enhanced TokenInfo configured for LetsBonk
"""
# Default creator to user if not provided
if creator is None:
creator = user
return TokenInfo(
name=name,
symbol=symbol,
@@ -317,7 +289,7 @@ def create_lets_bonk_token_info(
base_vault=base_vault,
quote_vault=quote_vault,
user=user,
creator=creator,
creator=creator or user,
**kwargs
)
@@ -406,7 +378,6 @@ def validate_token_info(token_info: TokenInfo) -> bool:
# Backward compatibility exports
# This allows existing imports to continue working
__all__ = [
'Platform', # Platform enum
'TokenInfo', # Enhanced TokenInfo (main export)
@@ -415,13 +386,10 @@ __all__ = [
'Trader', # Enhanced Trader base class
'create_legacy_token_info',
'create_lets_bonk_token_info',
# Convenience functions
'create_pump_fun_token_info',
'get_platform_specific_fields',
'is_lets_bonk_token',
# Utility functions
'is_pump_fun_token',
# Conversion functions
'upgrade_token_info',
'validate_token_info',
]
-416
View File
@@ -1,416 +0,0 @@
"""
Buy operations for pump.fun tokens.
"""
import struct
from typing import Final
from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
from spl.token.instructions import create_idempotent_associated_token_account
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS,
PumpAddresses,
SystemAddresses,
)
from core.wallet import Wallet
from trading.base import TokenInfo, Trader, TradeResult
from utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the buy instruction
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
class TokenBuyer(Trader):
"""Handles buying tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
amount: float,
slippage: float = 0.01,
max_retries: int = 5,
extreme_fast_token_amount: int = 0,
extreme_fast_mode: bool = False,
):
"""Initialize token buyer.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
amount: Amount of SOL to spend
slippage: Slippage tolerance (0.01 = 1%)
max_retries: Maximum number of retry attempts
extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled
extreme_fast_mode: If enabled, avoid fetching associated bonding curve state
"""
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
self.extreme_fast_mode = extreme_fast_mode
self.extreme_fast_token_amount = extreme_fast_token_amount
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute buy operation.
Args:
token_info: Token information
Returns:
TradeResult with buy outcome
"""
try:
# Convert amount to lamports
amount_lamports = int(self.amount * LAMPORTS_PER_SOL)
if self.extreme_fast_mode:
# Skip the wait and directly calculate the amount
token_amount = self.extreme_fast_token_amount
token_price_sol = self.amount / token_amount
# logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.")
else:
# Regular behavior with RPC call
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
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + self.slippage))
associated_token_account = self.wallet.get_associated_token_address(
token_info.mint
)
tx_signature = await self._send_buy_transaction(
token_info,
associated_token_account,
token_amount,
max_amount_lamports,
)
logger.info(
f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token"
)
logger.info(
f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)"
)
success = await self.client.confirm_transaction(tx_signature)
if success:
# Get actual execution data from bonding curve balance changes
actual_price, actual_tokens = await self._get_actual_execution_price(
tx_signature, token_info
)
logger.info(f"Buy transaction confirmed: {tx_signature}")
logger.info(
f"Actual price paid to bonding curve: {actual_price:.8f} SOL per token"
)
return TradeResult(
success=True,
tx_signature=tx_signature,
amount=actual_tokens, # Actual tokens received
price=actual_price, # Actual price based on bonding curve SOL flow
)
else:
return TradeResult(
success=False,
error_message=f"Transaction failed to confirm: {tx_signature}",
)
except Exception as e:
logger.error(f"Buy operation failed: {e!s}")
return TradeResult(success=False, error_message=str(e))
async def _send_buy_transaction(
self,
token_info: TokenInfo,
associated_token_account: Pubkey,
token_amount: float,
max_amount_lamports: int,
) -> str:
"""Send buy transaction.
Args:
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
Returns:
Transaction signature
Raises:
Exception: If transaction fails after all retries
"""
accounts = [
AccountMeta(
pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False),
AccountMeta(
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
),
AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True),
AccountMeta(
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=token_info.creator_vault, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.find_global_volume_accumulator(), is_signer=False, is_writable=True
),
AccountMeta(
pubkey=PumpAddresses.find_user_volume_accumulator(self.wallet.pubkey), is_signer=False, is_writable=True
),
]
# Prepare idempotent create ATA instruction: it will not fail if ATA already exists
idempotent_ata_ix = create_idempotent_associated_token_account(
self.wallet.pubkey,
self.wallet.pubkey,
token_info.mint,
SystemAddresses.TOKEN_PROGRAM,
)
# Prepare buy instruction data
token_amount_raw = int(token_amount * 10**TOKEN_DECIMALS)
data = (
EXPECTED_DISCRIMINATOR
+ struct.pack("<Q", token_amount_raw)
+ struct.pack("<Q", max_amount_lamports)
)
buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
try:
return await self.client.build_and_send_transaction(
[idempotent_ata_ix, buy_ix],
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
self._get_relevant_accounts(token_info)
),
)
except Exception as e:
logger.error(f"Buy transaction failed: {e!s}")
raise
async def _get_actual_execution_price(
self, tx_signature: str, token_info: TokenInfo
) -> tuple[float, float]:
"""Get actual execution price from bonding curve SOL balance changes."""
try:
client = await self.client.get_client()
tx_response = await client.get_transaction(
tx_signature,
encoding="jsonParsed",
commitment="confirmed",
max_supported_transaction_version=0,
)
if not tx_response.value or not tx_response.value.transaction:
raise ValueError("Transaction not found")
meta = tx_response.value.transaction.meta
if not meta or not meta.pre_balances or not meta.post_balances:
raise ValueError("Transaction balance data not found")
# Get accounts - they're ParsedAccountTxStatus objects, need to extract pubkey
accounts = tx_response.value.transaction.transaction.message.account_keys
# Find bonding curve account index in the transaction
bonding_curve_index = None
for i, account in enumerate(accounts):
# Extract pubkey from ParsedAccountTxStatus object
account_pubkey = (
str(account.pubkey) if hasattr(account, "pubkey") else str(account)
)
if account_pubkey == str(token_info.bonding_curve):
bonding_curve_index = i
break
if bonding_curve_index is None:
raise ValueError("Bonding curve not found in transaction accounts")
pre_balance_lamports = meta.pre_balances[bonding_curve_index]
post_balance_lamports = meta.post_balances[bonding_curve_index]
sol_sent_to_curve = (
post_balance_lamports - pre_balance_lamports
) / LAMPORTS_PER_SOL
if sol_sent_to_curve <= 0:
raise ValueError(f"No SOL sent to bonding curve: {sol_sent_to_curve}")
tokens_received = await self._get_tokens_received_from_tx(
tx_response, token_info
)
if tokens_received == 0:
raise ValueError("Cannot compute execution price: zero tokens received")
actual_price = sol_sent_to_curve / tokens_received
logger.info(f"Bonding curve received: {sol_sent_to_curve:.6f} SOL")
logger.info(f"We received: {tokens_received:.6f} tokens")
logger.info(f"Actual execution price: {actual_price:.8f} SOL per token")
return actual_price, tokens_received
except Exception as e:
logger.warning(
f"Failed to get actual execution price from bonding curve: {e}"
)
# Fallback to EXTREME_FAST estimate
tokens_received = (
self.extreme_fast_token_amount
if self.extreme_fast_mode
else self.amount
/ await self.curve_manager.calculate_price(token_info.bonding_curve)
)
if tokens_received == 0:
logger.error("Fallback failed unable to determine tokens received")
return 0.0, 0.0
return self.amount / tokens_received, tokens_received
async def _get_tokens_received_from_tx(
self, tx_response, token_info: TokenInfo
) -> float:
"""Extract tokens received from transaction token balance changes."""
meta = tx_response.value.transaction.meta
pre_token_balance = 0
post_token_balance = 0
wallet_str = str(self.wallet.pubkey)
mint_str = str(token_info.mint)
if meta.pre_token_balances:
for balance in meta.pre_token_balances:
# Convert to string for comparison
balance_owner = (
str(balance.owner)
if hasattr(balance, "owner")
else str(getattr(balance, "owner", ""))
)
balance_mint = (
str(balance.mint)
if hasattr(balance, "mint")
else str(getattr(balance, "mint", ""))
)
if balance_owner == wallet_str and balance_mint == mint_str:
try:
# Try multiple ways to get the amount
if hasattr(balance, "ui_token_amount"):
amount_obj = balance.ui_token_amount
if (
hasattr(amount_obj, "amount")
and amount_obj.amount is not None
):
pre_token_balance = int(amount_obj.amount)
elif (
hasattr(amount_obj, "ui_amount")
and amount_obj.ui_amount is not None
):
pre_token_balance = int(
float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS)
)
except (ValueError, TypeError) as e:
logger.warning(f"Error parsing pre-token balance: {e}")
break
# Check post-token balances
if meta.post_token_balances:
for balance in meta.post_token_balances:
# Convert to string for comparison
balance_owner = (
str(balance.owner)
if hasattr(balance, "owner")
else str(getattr(balance, "owner", ""))
)
balance_mint = (
str(balance.mint)
if hasattr(balance, "mint")
else str(getattr(balance, "mint", ""))
)
if balance_owner == wallet_str and balance_mint == mint_str:
try:
# Try multiple ways to get the amount
if hasattr(balance, "ui_token_amount"):
amount_obj = balance.ui_token_amount
if (
hasattr(amount_obj, "amount")
and amount_obj.amount is not None
):
post_token_balance = int(amount_obj.amount)
elif (
hasattr(amount_obj, "ui_amount")
and amount_obj.ui_amount is not None
):
post_token_balance = int(
float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS)
)
except (ValueError, TypeError) as e:
logger.warning(f"Error parsing post-token balance: {e}")
break
# Calculate tokens received
if pre_token_balance == 0 and post_token_balance > 0:
tokens_received_raw = post_token_balance
else:
tokens_received_raw = post_token_balance - pre_token_balance
if tokens_received_raw <= 0:
logger.warning(
"Token balance search failed. Using fallback from EXTREME_FAST estimate."
)
# Fallback: use the amount we know we bought
if self.extreme_fast_mode and self.extreme_fast_token_amount > 0:
return self.extreme_fast_token_amount
else:
logger.error("Cannot determine tokens received from transaction")
return 0.0
return tokens_received_raw / 10**TOKEN_DECIMALS
-213
View File
@@ -1,213 +0,0 @@
"""
Sell operations for pump.fun tokens.
"""
import struct
from typing import Final
from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS,
PumpAddresses,
SystemAddresses,
)
from core.wallet import Wallet
from trading.base import TokenInfo, Trader, TradeResult
from utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the sell instruction
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 12502976635542562355)
class TokenSeller(Trader):
"""Handles selling tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
slippage: float = 0.25,
max_retries: int = 5,
):
"""Initialize token seller.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
slippage: Slippage tolerance (0.25 = 25%)
max_retries: Maximum number of retry attempts
"""
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
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute sell operation.
Args:
token_info: Token information
Returns:
TradeResult with sell outcome
"""
try:
# Get associated token account
associated_token_account = self.wallet.get_associated_token_address(
token_info.mint
)
# Get token balance
token_balance = await self.client.get_token_account_balance(
associated_token_account
)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
logger.info(f"Token balance: {token_balance_decimal}")
if token_balance == 0:
logger.info("No tokens to sell.")
return TradeResult(success=False, error_message="No tokens to sell")
# Fetch token price
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")
# Calculate minimum SOL output with slippage
amount = token_balance
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - self.slippage
min_sol_output = int(
(expected_sol_output * slippage_factor) * LAMPORTS_PER_SOL
)
logger.info(f"Selling {token_balance_decimal} tokens")
logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL")
logger.info(
f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
)
tx_signature = await self._send_sell_transaction(
token_info,
associated_token_account,
amount,
min_sol_output,
)
success = await self.client.confirm_transaction(tx_signature)
if success:
logger.info(f"Sell transaction confirmed: {tx_signature}")
return TradeResult(
success=True,
tx_signature=tx_signature,
amount=token_balance_decimal,
price=token_price_sol,
)
else:
return TradeResult(
success=False,
error_message=f"Transaction failed to confirm: {tx_signature}",
)
except Exception as e:
logger.error(f"Sell operation failed: {e!s}")
return TradeResult(success=False, error_message=str(e))
async def _send_sell_transaction(
self,
token_info: TokenInfo,
associated_token_account: Pubkey,
token_amount: int,
min_sol_output: int,
) -> str:
"""Send sell transaction.
Args:
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
Returns:
Transaction signature
Raises:
Exception: If transaction fails after all retries
"""
# Prepare sell instruction accounts
accounts = [
AccountMeta(
pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False),
AccountMeta(
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
),
AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True),
AccountMeta(
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=token_info.creator_vault,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False
),
]
# Prepare sell instruction data
data = (
EXPECTED_DISCRIMINATOR
+ struct.pack("<Q", token_amount)
+ struct.pack("<Q", min_sol_output)
)
sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
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(
self._get_relevant_accounts(token_info)
),
)
except Exception as e:
logger.error(f"Sell transaction failed: {e!s}")
raise
@@ -1,6 +1,6 @@
"""
Main trading coordinator for pump.fun tokens.
Refactored PumpTrader to only process fresh tokens from WebSocket.
Universal trading coordinator that works with any platform.
Replaces PumpTrader with platform-agnostic implementation.
"""
import asyncio
@@ -18,18 +18,14 @@ from cleanup.modes import (
handle_cleanup_post_session,
)
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import PumpAddresses
from core.wallet import Wallet
from monitoring.block_listener import BlockListener
from monitoring.geyser_listener import GeyserListener
from monitoring.logs_listener import LogsListener
from monitoring.pumpportal_listener import PumpPortalListener
from trading.base import TokenInfo, TradeResult
from trading.buyer import TokenBuyer
from interfaces.core import Platform, TokenInfo
from monitoring.listener_factory import ListenerFactory
from platforms import get_platform_implementations
from trading.base import TradeResult
from trading.platform_aware import PlatformAwareBuyer, PlatformAwareSeller
from trading.position import Position
from trading.seller import TokenSeller
from utils.logger import get_logger
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
@@ -37,8 +33,8 @@ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
logger = get_logger(__name__)
class PumpTrader:
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
class UniversalTrader:
"""Universal trading coordinator that works with any supported platform."""
def __init__(
self,
@@ -48,11 +44,15 @@ class PumpTrader:
buy_amount: float,
buy_slippage: float,
sell_slippage: float,
# Platform configuration
platform: Platform | str = Platform.PUMP_FUN,
# Listener configuration
listener_type: str = "logs",
geyser_endpoint: str | None = None,
geyser_api_token: str | None = None,
geyser_auth_type: str = "x-token",
pumpportal_url: str = "wss://pumpportal.fun/api/data",
# Trading configuration
extreme_fast_mode: bool = False,
extreme_fast_token_amount: int = 30,
# Exit strategy configuration
@@ -69,7 +69,7 @@ class PumpTrader:
hard_cap_prior_fee: int = 200_000,
# Retry and timeout settings
max_retries: int = 3,
wait_time_after_creation: int = 15, # here and further - seconds
wait_time_after_creation: int = 15,
wait_time_after_buy: int = 15,
wait_time_before_new_token: int = 15,
max_token_age: int | float = 0.001,
@@ -84,7 +84,8 @@ class PumpTrader:
marry_mode: bool = False,
yolo_mode: bool = False,
):
"""Initialize the pump trader.
"""Initialize the universal trader.
Args:
rpc_endpoint: RPC endpoint URL
wss_endpoint: WebSocket endpoint URL
@@ -92,47 +93,12 @@ class PumpTrader:
buy_amount: Amount of SOL to spend on buys
buy_slippage: Slippage tolerance for buys
sell_slippage: Slippage tolerance for sells
listener_type: Type of listener to use ('logs', 'blocks', 'geyser', or 'pumpportal')
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
geyser_api_token: Geyser API token (required for geyser listener)
geyser_auth_type: Geyser authentication type ('x-token' or 'basic')
pumpportal_url: PumpPortal WebSocket URL (default: wss://pumpportal.fun/api/data)
extreme_fast_mode: Whether to enable extreme fast mode
extreme_fast_token_amount: Maximum token amount for extreme fast mode
exit_strategy: Exit strategy ("time_based", "tp_sl", or "manual")
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
max_hold_time: Maximum hold time in seconds
price_check_interval: How often to check price for TP/SL (seconds)
enable_dynamic_priority_fee: Whether to enable dynamic priority fees
enable_fixed_priority_fee: Whether to enable fixed priority fees
fixed_priority_fee: Fixed priority fee amount
extra_priority_fee: Extra percentage for priority fees
hard_cap_prior_fee: Hard cap for priority fees
max_retries: Maximum number of retry attempts
wait_time_after_creation: Time to wait after token creation (seconds)
wait_time_after_buy: Time to wait after buying a token (seconds)
wait_time_before_new_token: Time to wait before processing a new token (seconds)
max_token_age: Maximum age of token to process (seconds)
token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds)
cleanup_mode: Cleanup mode ("disabled", "auto", or "manual")
cleanup_force_close_with_burn: Whether to force close with burn during cleanup
cleanup_with_priority_fee: Whether to use priority fees during cleanup
match_string: Optional string to match in token name/symbol
bro_address: Optional creator address to filter by
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, trade continuously
platform: Platform to trade on (Platform enum or string)
... (other args same as PumpTrader)
"""
# Core components
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
self.curve_manager = BondingCurveManager(self.solana_client)
self.priority_fee_manager = PriorityFeeManager(
client=self.solana_client,
enable_dynamic_fee=enable_dynamic_priority_fee,
@@ -141,10 +107,24 @@ class PumpTrader:
extra_fee=extra_priority_fee,
hard_cap=hard_cap_prior_fee,
)
self.buyer = TokenBuyer(
# Platform setup
if isinstance(platform, str):
self.platform = Platform(platform)
else:
self.platform = platform
logger.info(f"Initialized Universal Trader for platform: {self.platform.value}")
# Get platform-specific implementations
self.platform_implementations = get_platform_implementations(
self.platform, self.solana_client
)
# Create platform-aware traders
self.buyer = PlatformAwareBuyer(
self.solana_client,
self.wallet,
self.curve_manager,
self.priority_fee_manager,
buy_amount,
buy_slippage,
@@ -152,41 +132,25 @@ class PumpTrader:
extreme_fast_token_amount,
extreme_fast_mode,
)
self.seller = TokenSeller(
self.seller = PlatformAwareSeller(
self.solana_client,
self.wallet,
self.curve_manager,
self.priority_fee_manager,
sell_slippage,
max_retries,
)
# Initialize the appropriate listener type
listener_type = listener_type.lower()
if listener_type == "geyser":
if not geyser_endpoint or not geyser_api_token:
raise ValueError(
"Geyser endpoint and API token are required for geyser listener"
)
self.token_listener = GeyserListener(
geyser_endpoint,
geyser_api_token,
geyser_auth_type,
PumpAddresses.PROGRAM,
)
logger.info("Using Geyser listener for token monitoring")
elif listener_type == "logs":
self.token_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using logsSubscribe listener for token monitoring")
elif listener_type == "pumpportal":
self.token_listener = PumpPortalListener(
PumpAddresses.PROGRAM, pumpportal_url
)
logger.info("Using PumpPortal listener for token monitoring")
else:
self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using blockSubscribe listener for token monitoring")
# Initialize the appropriate listener
self.token_listener = ListenerFactory.create_listener(
listener_type=listener_type,
wss_endpoint=wss_endpoint,
geyser_endpoint=geyser_endpoint,
geyser_api_token=geyser_api_token,
geyser_auth_type=geyser_auth_type,
pumpportal_url=pumpportal_url,
platforms=[self.platform], # Only listen for our platform
)
# Trading parameters
self.buy_amount = buy_amount
@@ -230,26 +194,18 @@ class PumpTrader:
async def start(self) -> None:
"""Start the trading bot and listen for new tokens."""
logger.info("Starting pump.fun trader")
logger.info(
f"Match filter: {self.match_string if self.match_string else 'None'}"
)
logger.info(
f"Creator filter: {self.bro_address if self.bro_address else 'None'}"
)
logger.info(f"Starting Universal Trader for {self.platform.value}")
logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}")
logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}")
logger.info(f"Marry mode: {self.marry_mode}")
logger.info(f"YOLO mode: {self.yolo_mode}")
logger.info(f"Exit strategy: {self.exit_strategy}")
if self.exit_strategy == "tp_sl":
logger.info(
f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%"
)
logger.info(
f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%"
)
logger.info(
f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds"
)
logger.info(f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%")
logger.info(f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%")
logger.info(f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds")
logger.info(f"Max token age: {self.max_token_age} seconds")
try:
@@ -262,22 +218,16 @@ class PumpTrader:
# Choose operating mode based on yolo_mode
if not self.yolo_mode:
# Single token mode: process one token and exit
logger.info(
"Running in single token mode - will process one token and exit"
)
logger.info("Running in single token mode - will process one token and exit")
token_info = await self._wait_for_token()
if token_info:
await self._handle_token(token_info)
logger.info("Finished processing single token. Exiting...")
else:
logger.info(
f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting..."
)
logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...")
else:
# Continuous mode: process tokens until interrupted
logger.info(
"Running in continuous mode - will process tokens until interrupted"
)
logger.info("Running in continuous mode - will process tokens until interrupted")
processor_task = asyncio.create_task(self._process_token_queue())
try:
@@ -300,7 +250,7 @@ class PumpTrader:
finally:
await self._cleanup_resources()
logger.info("Pump trader has shut down")
logger.info("Universal Trader has shut down")
async def _wait_for_token(self) -> TokenInfo | None:
"""Wait for a single token to be detected.
@@ -334,16 +284,12 @@ class PumpTrader:
# Wait for a token with a timeout
try:
logger.info(
f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)..."
)
logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...")
await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout)
logger.info(f"Found token: {found_token.symbol} ({found_token.mint})")
return found_token
except TimeoutError:
logger.info(
f"Timed out after waiting {self.token_wait_timeout}s for a token"
)
logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token")
return None
finally:
listener_task.cancel()
@@ -391,7 +337,7 @@ class PumpTrader:
self.token_timestamps[token_key] = monotonic()
await self.token_queue.put(token_info)
logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})")
logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint}) on {token_info.platform.value}")
async def _process_token_queue(self) -> None:
"""Continuously process tokens from the queue, only if they're fresh."""
@@ -402,21 +348,15 @@ class PumpTrader:
# Check if token is still "fresh"
current_time = monotonic()
token_age = current_time - self.token_timestamps.get(
token_key, current_time
)
token_age = current_time - self.token_timestamps.get(token_key, current_time)
if token_age > self.max_token_age:
logger.info(
f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)"
)
logger.info(f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)")
continue
self.processed_tokens.add(token_key)
logger.info(
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
)
logger.info(f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)")
await self._handle_token(token_info)
except asyncio.CancelledError:
@@ -435,19 +375,19 @@ class PumpTrader:
token_info: Token information
"""
try:
# Wait for bonding curve to stabilize (unless in extreme fast mode)
# Validate that token is for our platform
if token_info.platform != self.platform:
logger.warning(f"Token platform mismatch: expected {self.platform.value}, got {token_info.platform.value}")
return
# Wait for pool/curve to stabilize (unless in extreme fast mode)
if not self.extreme_fast_mode:
# Save token info to file
# await self._save_token_info(token_info)
logger.info(
f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..."
)
await self._save_token_info(token_info)
logger.info(f"Waiting for {self.wait_time_after_creation} seconds for the pool/curve to stabilize...")
await asyncio.sleep(self.wait_time_after_creation)
# Buy token
logger.info(
f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..."
)
logger.info(f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol} on {token_info.platform.value}...")
buy_result: TradeResult = await self.buyer.execute(token_info)
if buy_result.success:
@@ -457,31 +397,21 @@ class PumpTrader:
# Only wait for next token in yolo mode
if self.yolo_mode:
logger.info(
f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..."
)
logger.info(f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token...")
await asyncio.sleep(self.wait_time_before_new_token)
except Exception as e:
logger.error(f"Error handling token {token_info.symbol}: {e!s}")
async def _handle_successful_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle successful token purchase.
Args:
token_info: Token information
buy_result: The result of the buy operation
"""
logger.info(f"Successfully bought {token_info.symbol}")
self._log_trade(
"buy",
token_info,
buy_result.price, # type: ignore
buy_result.amount, # type: ignore
buy_result.tx_signature,
)
logger.info(f"Successfully bought {token_info.symbol} on {token_info.platform.value}")
self._log_trade("buy", token_info, buy_result.price, buy_result.amount, buy_result.tx_signature)
self.traded_mints.add(token_info.mint)
# Choose exit strategy
@@ -495,9 +425,7 @@ class PumpTrader:
else:
logger.info("Marry mode enabled. Skipping sell operation.")
async def _handle_failed_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
async def _handle_failed_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle failed token purchase.
Args:
@@ -516,9 +444,7 @@ class PumpTrader:
self.cleanup_force_close_with_burn,
)
async def _handle_tp_sl_exit(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle take profit/stop loss exit strategy.
Args:
@@ -529,8 +455,8 @@ class PumpTrader:
position = Position.create_from_buy_result(
mint=token_info.mint,
symbol=token_info.symbol,
entry_price=buy_result.price, # type: ignore
quantity=buy_result.amount, # type: ignore
entry_price=buy_result.price,
quantity=buy_result.amount,
take_profit_percentage=self.take_profit_percentage,
stop_loss_percentage=self.stop_loss_percentage,
max_hold_time=self.max_hold_time,
@@ -559,13 +485,7 @@ class PumpTrader:
if sell_result.success:
logger.info(f"Successfully sold {token_info.symbol}")
self._log_trade(
"sell",
token_info,
sell_result.price, # type: ignore
sell_result.amount, # type: ignore
sell_result.tx_signature,
)
self._log_trade("sell", token_info, sell_result.price, sell_result.amount, sell_result.tx_signature)
# Close ATA if enabled
await handle_cleanup_after_sell(
self.solana_client,
@@ -577,29 +497,25 @@ class PumpTrader:
self.cleanup_force_close_with_burn,
)
else:
logger.error(
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
)
logger.error(f"Failed to sell {token_info.symbol}: {sell_result.error_message}")
async def _monitor_position_until_exit(
self, token_info: TokenInfo, position: Position
) -> None:
async def _monitor_position_until_exit(self, token_info: TokenInfo, position: Position) -> None:
"""Monitor a position until exit conditions are met.
Args:
token_info: Token information
position: Position to monitor
"""
logger.info(
f"Starting position monitoring (check interval: {self.price_check_interval}s)"
)
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
# Get pool address for price monitoring
pool_address = self._get_pool_address(token_info)
curve_manager = self.platform_implementations.curve_manager
while position.is_active:
try:
# Get current price from bonding curve
current_price = await self.curve_manager.calculate_price(
token_info.bonding_curve
)
# Get current price from pool/curve
current_price = await curve_manager.calculate_price(pool_address)
# Check if position should be exited
should_exit, exit_reason = position.should_exit(current_price)
@@ -610,33 +526,21 @@ class PumpTrader:
# Log PnL before exit
pnl = position.get_pnl(current_price)
logger.info(
f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)"
)
logger.info(f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)")
# Execute sell
sell_result = await self.seller.execute(token_info)
if sell_result.success:
# Close position with actual exit price
position.close_position(sell_result.price, exit_reason) # type: ignore
position.close_position(sell_result.price, exit_reason)
logger.info(
f"Successfully exited position: {exit_reason.value}"
)
self._log_trade(
"sell",
token_info,
sell_result.price, # type: ignore
sell_result.amount, # type: ignore
sell_result.tx_signature,
)
logger.info(f"Successfully exited position: {exit_reason.value}")
self._log_trade("sell", token_info, sell_result.price, sell_result.amount, sell_result.tx_signature)
# Log final PnL
final_pnl = position.get_pnl()
logger.info(
f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)"
)
logger.info(f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)")
# Close ATA if enabled
await handle_cleanup_after_sell(
@@ -649,27 +553,40 @@ class PumpTrader:
self.cleanup_force_close_with_burn,
)
else:
logger.error(
f"Failed to exit position: {sell_result.error_message}"
)
logger.error(f"Failed to exit position: {sell_result.error_message}")
# Keep monitoring in case sell can be retried
break
else:
# Log current status
pnl = position.get_pnl(current_price)
logger.debug(
f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)"
)
logger.debug(f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)")
# Wait before next price check
await asyncio.sleep(self.price_check_interval)
except Exception as e:
logger.error(f"Error monitoring position: {e}")
await asyncio.sleep(
self.price_check_interval
) # Continue monitoring despite errors
await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors
def _get_pool_address(self, token_info: TokenInfo) -> Pubkey:
"""Get the pool/curve address for price monitoring.
Args:
token_info: Token information
Returns:
Pool/curve address
"""
address_provider = self.platform_implementations.address_provider
if token_info.platform == Platform.PUMP_FUN:
return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
elif token_info.platform == Platform.LETS_BONK:
return token_info.pool_state or address_provider.derive_pool_address(token_info.mint)
else:
# Fallback to deriving the address
return address_provider.derive_pool_address(token_info.mint)
async def _save_token_info(self, token_info: TokenInfo) -> None:
"""Save token information to a file.
@@ -681,21 +598,32 @@ class PumpTrader:
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
# Convert to dictionary for saving
token_dict = {
"name": token_info.name,
"symbol": token_info.symbol,
"uri": token_info.uri,
"mint": str(token_info.mint),
"platform": token_info.platform.value,
"user": str(token_info.user) if token_info.user else None,
"creator": str(token_info.creator) if token_info.creator else None,
# Platform-specific fields
"bonding_curve": str(token_info.bonding_curve) if token_info.bonding_curve else None,
"associated_bonding_curve": str(token_info.associated_bonding_curve) if token_info.associated_bonding_curve else None,
"creator_vault": str(token_info.creator_vault) if token_info.creator_vault else None,
"pool_state": str(token_info.pool_state) if token_info.pool_state else None,
"base_vault": str(token_info.base_vault) if token_info.base_vault else None,
"quote_vault": str(token_info.quote_vault) if token_info.quote_vault else None,
}
with open(file_name, "w") as file:
file.write(json.dumps(token_info.to_dict(), indent=2))
file.write(json.dumps(token_dict, indent=2))
logger.info(f"Token information saved to {file_name}")
except Exception as e:
logger.error(f"Failed to save token information: {e!s}")
def _log_trade(
self,
action: str,
token_info: TokenInfo,
price: float,
amount: float,
tx_hash: str | None,
) -> None:
def _log_trade(self, action: str, token_info: TokenInfo, price: float, amount: float, tx_hash: str | None) -> None:
"""Log trade information.
Args:
@@ -711,6 +639,7 @@ class PumpTrader:
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"action": action,
"platform": token_info.platform.value,
"token_address": str(token_info.mint),
"symbol": token_info.symbol,
"price": price,
@@ -722,3 +651,7 @@ class PumpTrader:
log_file.write(json.dumps(log_entry) + "\n")
except Exception as e:
logger.error(f"Failed to log trade information: {e!s}")
# Backward compatibility alias
PumpTrader = UniversalTrader # Legacy name for backward compatibility