From bb35101a8f6b34ce8cd70c0aa5a897d79499aab2 Mon Sep 17 00:00:00 2001
From: smypmsa 0 else 0,
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to decode pool state: {e}")
+ # Return some default values for testing
+ return {
+ "virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals
+ "virtual_quote": 1_000_000_000, # 1 SOL
+ "real_base": 1_000_000_000,
+ "real_quote": 1_000_000_000,
+ "price_per_token": 0.001, # 0.001 SOL per token
+ }
+
+ async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]:
+ """Get detailed pool information including status and progress.
+
+ Args:
+ pool_address: Address of the pool state
+
+ Returns:
+ Dictionary with pool information
+ """
+ pool_state = await self.get_pool_state(pool_address)
+
+ # Calculate additional metrics
+ sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL
+ tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS
+
+ return {
+ "virtual_base_reserves": pool_state["virtual_base"],
+ "virtual_quote_reserves": pool_state["virtual_quote"],
+ "real_base_reserves": pool_state["real_base"],
+ "real_quote_reserves": pool_state["real_quote"],
+ "sol_raised": sol_raised,
+ "tokens_sold": tokens_sold,
+ "current_price": pool_state["price_per_token"],
+ }
\ No newline at end of file
diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py
new file mode 100644
index 0000000..aa18242
--- /dev/null
+++ b/src/platforms/letsbonk/event_parser.py
@@ -0,0 +1,272 @@
+"""
+LetsBonk implementation of EventParser interface.
+
+This module parses LetsBonk-specific token creation events from various sources
+by implementing the EventParser interface.
+"""
+
+import struct
+from time import monotonic
+from typing import Any, Final
+
+from solders.pubkey import Pubkey
+
+from interfaces.core import EventParser, Platform, TokenInfo
+from platforms.letsbonk.address_provider import LetsBonkAddressProvider
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+class LetsBonkEventParser(EventParser):
+ """LetsBonk implementation of EventParser interface."""
+
+ # Discriminator for initialize instruction from IDL
+ INITIALIZE_DISCRIMINATOR: Final[bytes] = bytes([175, 175, 109, 31, 13, 152, 155, 237])
+ INITIALIZE_DISCRIMINATOR_INT: Final[int] = struct.unpack("
Platform:
+ """Get the platform this parser serves."""
+ return Platform.LETS_BONK
+
+ def parse_token_creation_from_logs(
+ self,
+ logs: list[str],
+ signature: str
+ ) -> TokenInfo | None:
+ """Parse token creation from LetsBonk transaction logs.
+
+ Args:
+ logs: List of log strings from transaction
+ signature: Transaction signature
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ # LetsBonk doesn't emit specific logs for token creation like pump.fun
+ # Token creation is identified through instruction parsing
+ return None
+
+ def parse_token_creation_from_instruction(
+ self,
+ instruction_data: bytes,
+ accounts: list[int],
+ account_keys: list[bytes]
+ ) -> TokenInfo | None:
+ """Parse token creation from LetsBonk instruction data.
+
+ Args:
+ instruction_data: Raw instruction data
+ accounts: List of account indices
+ account_keys: List of account public keys
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR):
+ return None
+
+ try:
+ # Helper to get account key
+ def get_account_key(index):
+ if index >= len(accounts):
+ return None
+ account_index = accounts[index]
+ if account_index >= len(account_keys):
+ return None
+ return Pubkey.from_bytes(account_keys[account_index])
+
+ # Parse instruction data
+ token_data = self._parse_initialize_instruction_data(instruction_data)
+ if not token_data:
+ return None
+
+ # Extract account information based on IDL account order
+ creator = get_account_key(1) # creator account
+ pool_state = get_account_key(5) # pool_state account
+ base_mint = get_account_key(6) # base_mint account
+ base_vault = get_account_key(8) # base_vault account
+ quote_vault = get_account_key(9) # quote_vault account
+
+ if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
+ return None
+
+ return TokenInfo(
+ name=token_data["name"],
+ symbol=token_data["symbol"],
+ uri=token_data["uri"],
+ mint=base_mint,
+ platform=Platform.LETS_BONK,
+ pool_state=pool_state,
+ base_vault=base_vault,
+ quote_vault=quote_vault,
+ user=creator,
+ creator=creator,
+ creation_timestamp=monotonic(),
+ )
+
+ except Exception:
+ return None
+
+ def parse_token_creation_from_geyser(
+ self,
+ transaction_info: Any
+ ) -> TokenInfo | None:
+ """Parse token creation from Geyser transaction data.
+
+ Args:
+ transaction_info: Geyser transaction information
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ try:
+ if not hasattr(transaction_info, 'transaction'):
+ return None
+
+ tx = transaction_info.transaction.transaction.transaction
+ msg = getattr(tx, "message", None)
+ if msg is None:
+ return None
+
+ for ix in msg.instructions:
+ # Skip non-LetsBonk program instructions
+ program_idx = ix.program_id_index
+ if program_idx >= len(msg.account_keys):
+ continue
+
+ program_id = msg.account_keys[program_idx]
+ if bytes(program_id) != bytes(self.get_program_id()):
+ continue
+
+ # Check if it's the LetsBonk platform config account
+ has_platform_config = False
+ for acc_idx in ix.accounts:
+ if acc_idx < len(msg.account_keys):
+ acc_key = msg.account_keys[acc_idx]
+ if bytes(acc_key) == bytes(self.address_provider.LETSBONK_PLATFORM_CONFIG):
+ has_platform_config = True
+ break
+
+ if not has_platform_config:
+ continue
+
+ # Process instruction data
+ token_info = self.parse_token_creation_from_instruction(
+ ix.data, ix.accounts, msg.account_keys
+ )
+ if token_info:
+ return token_info
+
+ return None
+
+ except Exception:
+ return None
+
+ def get_program_id(self) -> Pubkey:
+ """Get the Raydium LaunchLab program ID this parser monitors.
+
+ Returns:
+ Raydium LaunchLab program ID
+ """
+ return self.address_provider.RAYDIUM_LAUNCHLAB_PROGRAM_ID
+
+ def get_instruction_discriminators(self) -> list[bytes]:
+ """Get instruction discriminators for token creation.
+
+ Returns:
+ List of discriminator bytes to match
+ """
+ return [self.INITIALIZE_DISCRIMINATOR]
+
+ def _parse_initialize_instruction_data(self, data: bytes) -> dict | None:
+ """Parse the initialize instruction data from LetsBonk.
+
+ Args:
+ data: Raw instruction data
+
+ Returns:
+ Dictionary of parsed data or None if parsing fails
+ """
+ if len(data) < 8:
+ return None
+
+ # Check discriminator
+ discriminator = struct.unpack("
len(data):
+ raise ValueError("Not enough data for string length")
+ length = struct.unpack_from(" len(data):
+ raise ValueError("Not enough data for string")
+ value = data[offset:offset + length].decode('utf-8')
+ offset += length
+ return value
+
+ def read_u8():
+ nonlocal offset
+ if offset + 1 > len(data):
+ raise ValueError("Not enough data for u8")
+ value = struct.unpack_from(" list[TokenInfo]:
+ """Parse token creations from block data (for block listener).
+
+ Args:
+ block_data: Block data from WebSocket
+
+ Returns:
+ List of TokenInfo for any token creations found
+ """
+ tokens = []
+
+ try:
+ if "transactions" not in block_data:
+ return tokens
+
+ for tx in block_data["transactions"]:
+ if not isinstance(tx, dict) or "transaction" not in tx:
+ continue
+
+ # Process transaction (implementation would be similar to pump.fun)
+ # This is a simplified version - full implementation would decode
+ # the transaction and check for LetsBonk initialize instructions
+ pass
+
+ return tokens
+
+ except Exception:
+ return tokens
\ No newline at end of file
diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py
new file mode 100644
index 0000000..b4ff731
--- /dev/null
+++ b/src/platforms/letsbonk/instruction_builder.py
@@ -0,0 +1,402 @@
+"""
+LetsBonk implementation of InstructionBuilder interface.
+
+This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions
+by implementing the InstructionBuilder interface.
+"""
+
+import hashlib
+import struct
+import time
+from typing import Final
+
+from solders.instruction import AccountMeta, Instruction
+from solders.pubkey import Pubkey
+from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
+from spl.token.instructions import create_idempotent_associated_token_account
+
+from core.pubkeys import TOKEN_DECIMALS
+from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
+
+# Instruction discriminators for LetsBonk (from IDL)
+BUY_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([250, 234, 13, 123, 213, 156, 19, 236])
+BUY_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([24, 211, 116, 40, 105, 3, 153, 56])
+SELL_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([149, 39, 222, 155, 211, 124, 152, 26])
+SELL_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([95, 200, 71, 34, 8, 9, 11, 166])
+
+
+class LetsBonkInstructionBuilder(InstructionBuilder):
+ """LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface."""
+
+ @property
+ def platform(self) -> Platform:
+ """Get the platform this builder serves."""
+ return Platform.LETS_BONK
+
+ async def build_buy_instruction(
+ self,
+ token_info: TokenInfo,
+ user: Pubkey,
+ amount_in: int,
+ minimum_amount_out: int,
+ address_provider: AddressProvider
+ ) -> list[Instruction]:
+ """Build buy instruction(s) for LetsBonk using buy_exact_in.
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ amount_in: Amount of SOL to spend (in lamports)
+ minimum_amount_out: Minimum tokens expected (raw token units)
+ address_provider: Platform address provider
+
+ Returns:
+ List of instructions needed for the buy operation
+ """
+ instructions = []
+
+ # Get all required accounts
+ accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
+
+ # 1. Create idempotent ATA for base token
+ ata_instruction = create_idempotent_associated_token_account(
+ user, # payer
+ user, # owner
+ token_info.mint, # mint
+ address_provider.TOKEN_PROGRAM_ID, # token program
+ )
+ instructions.append(ata_instruction)
+
+ # 2. Create WSOL account with seed (temporary account for the transaction)
+ wsol_seed = self._generate_wsol_seed(user)
+ wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
+
+ # Account creation cost + amount to spend
+ account_creation_lamports = 2_039_280 # Standard account creation cost
+ total_lamports = amount_in + account_creation_lamports
+
+ create_wsol_ix = create_account_with_seed(
+ CreateAccountWithSeedParams(
+ from_pubkey=user,
+ to_pubkey=wsol_account,
+ base=user,
+ seed=wsol_seed,
+ lamports=total_lamports,
+ space=165, # Size of a token account
+ owner=address_provider.TOKEN_PROGRAM_ID
+ )
+ )
+ instructions.append(create_wsol_ix)
+
+ # 3. Initialize WSOL account
+ initialize_wsol_ix = self._create_initialize_account_instruction(
+ wsol_account,
+ address_provider.WSOL_MINT,
+ user,
+ address_provider
+ )
+ instructions.append(initialize_wsol_ix)
+
+ # 4. Build buy_exact_in instruction
+ buy_accounts = [
+ AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
+ AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
+ AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
+ ]
+
+ # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
+ SHARE_FEE_RATE = 0 # No sharing fee
+ instruction_data = (
+ BUY_EXACT_IN_DISCRIMINATOR +
+ struct.pack("
list[Instruction]:
+ """Build sell instruction(s) for LetsBonk using sell_exact_in.
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ amount_in: Amount of tokens to sell (raw token units)
+ minimum_amount_out: Minimum SOL expected (in lamports)
+ address_provider: Platform address provider
+
+ Returns:
+ List of instructions needed for the sell operation
+ """
+ instructions = []
+
+ # Get all required accounts
+ accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
+
+ # 1. Create WSOL account with seed (to receive SOL)
+ wsol_seed = self._generate_wsol_seed(user)
+ wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
+
+ # Minimal account creation cost
+ account_creation_lamports = 2_039_280
+
+ create_wsol_ix = create_account_with_seed(
+ CreateAccountWithSeedParams(
+ from_pubkey=user,
+ to_pubkey=wsol_account,
+ base=user,
+ seed=wsol_seed,
+ lamports=account_creation_lamports,
+ space=165, # Size of a token account
+ owner=address_provider.TOKEN_PROGRAM_ID
+ )
+ )
+ instructions.append(create_wsol_ix)
+
+ # 2. Initialize WSOL account
+ initialize_wsol_ix = self._create_initialize_account_instruction(
+ wsol_account,
+ address_provider.WSOL_MINT,
+ user,
+ address_provider
+ )
+ instructions.append(initialize_wsol_ix)
+
+ # 3. Build sell_exact_in instruction
+ sell_accounts = [
+ AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
+ AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
+ AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
+ ]
+
+ # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
+ SHARE_FEE_RATE = 0 # No sharing fee
+ instruction_data = (
+ SELL_EXACT_IN_DISCRIMINATOR +
+ struct.pack("
list[Pubkey]:
+ """Get list of accounts required for buy operation (for priority fee calculation).
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ address_provider: Platform address provider
+
+ Returns:
+ List of account addresses that will be accessed
+ """
+ accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
+
+ return [
+ accounts_info["pool_state"],
+ accounts_info["user_base_token"],
+ accounts_info["base_vault"],
+ accounts_info["quote_vault"],
+ accounts_info["base_token_mint"],
+ accounts_info["quote_token_mint"],
+ accounts_info["program"],
+ ]
+
+ def get_required_accounts_for_sell(
+ self,
+ token_info: TokenInfo,
+ user: Pubkey,
+ address_provider: AddressProvider
+ ) -> list[Pubkey]:
+ """Get list of accounts required for sell operation (for priority fee calculation).
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ address_provider: Platform address provider
+
+ Returns:
+ List of account addresses that will be accessed
+ """
+ accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
+
+ return [
+ accounts_info["pool_state"],
+ accounts_info["user_base_token"],
+ accounts_info["base_vault"],
+ accounts_info["quote_vault"],
+ accounts_info["base_token_mint"],
+ accounts_info["quote_token_mint"],
+ accounts_info["program"],
+ ]
+
+ def _generate_wsol_seed(self, user: Pubkey) -> str:
+ """Generate a unique seed for WSOL account creation.
+
+ Args:
+ user: User's wallet address
+
+ Returns:
+ Unique seed string for WSOL account
+ """
+ # Generate a unique seed based on timestamp and user pubkey
+ seed_data = f"{int(time.time())}{user!s}"
+ return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
+
+ def _create_initialize_account_instruction(
+ self,
+ account: Pubkey,
+ mint: Pubkey,
+ owner: Pubkey,
+ address_provider: AddressProvider
+ ) -> Instruction:
+ """Create an InitializeAccount instruction for the Token Program.
+
+ Args:
+ account: The account to initialize
+ mint: The token mint
+ owner: The account owner
+ address_provider: Platform address provider
+
+ Returns:
+ Instruction for initializing the account
+ """
+ accounts = [
+ AccountMeta(pubkey=account, is_signer=False, is_writable=True),
+ AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
+ AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
+ AccountMeta(pubkey=address_provider.SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
+ ]
+
+ # InitializeAccount instruction discriminator (instruction 1 in Token Program)
+ data = bytes([1])
+
+ return Instruction(
+ program_id=address_provider.TOKEN_PROGRAM_ID,
+ data=data,
+ accounts=accounts
+ )
+
+ def _create_close_account_instruction(
+ self,
+ account: Pubkey,
+ destination: Pubkey,
+ owner: Pubkey,
+ address_provider: AddressProvider
+ ) -> Instruction:
+ """Create a CloseAccount instruction for the Token Program.
+
+ Args:
+ account: The account to close
+ destination: Where to send the remaining lamports
+ owner: The account owner (must sign)
+ address_provider: Platform address provider
+
+ Returns:
+ Instruction for closing the account
+ """
+ accounts = [
+ AccountMeta(pubkey=account, is_signer=False, is_writable=True),
+ AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
+ AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
+ ]
+
+ # CloseAccount instruction discriminator (instruction 9 in Token Program)
+ data = bytes([9])
+
+ return Instruction(
+ program_id=address_provider.TOKEN_PROGRAM_ID,
+ data=data,
+ accounts=accounts
+ )
+
+ def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
+ """Convert decimal token amount to raw token units.
+
+ Args:
+ token_amount_decimal: Token amount in decimal form
+
+ Returns:
+ Token amount in raw units (adjusted for decimals)
+ """
+ return int(token_amount_decimal * 10**TOKEN_DECIMALS)
+
+ def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
+ """Convert raw token amount to decimal form.
+
+ Args:
+ token_amount_raw: Token amount in raw units
+
+ Returns:
+ Token amount in decimal form
+ """
+ return token_amount_raw / 10**TOKEN_DECIMALS
\ No newline at end of file
diff --git a/src/platforms/pumpfun/__init__.py b/src/platforms/pumpfun/__init__.py
new file mode 100644
index 0000000..38c8f53
--- /dev/null
+++ b/src/platforms/pumpfun/__init__.py
@@ -0,0 +1,31 @@
+"""
+Pump.Fun platform registration and exports.
+
+This module registers all pump.fun implementations with the platform factory
+and provides convenient imports for the pump.fun platform.
+"""
+
+from interfaces.core import Platform
+from platforms import register_platform_implementations
+
+from .address_provider import PumpFunAddressProvider
+from .curve_manager import PumpFunCurveManager
+from .event_parser import PumpFunEventParser
+from .instruction_builder import PumpFunInstructionBuilder
+
+# Register pump.fun platform implementations
+register_platform_implementations(
+ Platform.PUMP_FUN,
+ PumpFunAddressProvider,
+ PumpFunInstructionBuilder,
+ PumpFunCurveManager,
+ PumpFunEventParser
+)
+
+# Export implementations for direct use if needed
+__all__ = [
+ 'PumpFunAddressProvider',
+ 'PumpFunCurveManager',
+ 'PumpFunEventParser',
+ 'PumpFunInstructionBuilder'
+]
\ No newline at end of file
diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py
new file mode 100644
index 0000000..cb64349
--- /dev/null
+++ b/src/platforms/pumpfun/address_provider.py
@@ -0,0 +1,216 @@
+"""
+Pump.Fun implementation of AddressProvider interface.
+
+This module provides all pump.fun-specific addresses and PDA derivations
+by implementing the AddressProvider interface.
+"""
+
+
+from solders.pubkey import Pubkey
+from spl.token.instructions import get_associated_token_address
+
+from core.pubkeys import PumpAddresses, SystemAddresses
+from interfaces.core import AddressProvider, Platform, TokenInfo
+
+
+class PumpFunAddressProvider(AddressProvider):
+ """Pump.Fun implementation of AddressProvider interface."""
+
+ @property
+ def platform(self) -> Platform:
+ """Get the platform this provider serves."""
+ return Platform.PUMP_FUN
+
+ @property
+ def program_id(self) -> Pubkey:
+ """Get the main program ID for this platform."""
+ return PumpAddresses.PROGRAM
+
+ def get_system_addresses(self) -> dict[str, Pubkey]:
+ """Get all system addresses required for pump.fun.
+
+ Returns:
+ Dictionary mapping address names to Pubkey objects
+ """
+ return {
+ # Pump.fun specific addresses
+ "program": PumpAddresses.PROGRAM,
+ "global": PumpAddresses.GLOBAL,
+ "event_authority": PumpAddresses.EVENT_AUTHORITY,
+ "fee": PumpAddresses.FEE,
+ "liquidity_migrator": PumpAddresses.LIQUIDITY_MIGRATOR,
+
+ # System addresses
+ "system_program": SystemAddresses.PROGRAM,
+ "token_program": SystemAddresses.TOKEN_PROGRAM,
+ "associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
+ "rent": SystemAddresses.RENT,
+ "sol_mint": SystemAddresses.SOL,
+ }
+
+ def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
+ """Derive the bonding curve address for a token.
+
+ For pump.fun, this is the bonding curve PDA derived from the mint.
+
+ Args:
+ base_mint: Token mint address
+ quote_mint: Not used for pump.fun (SOL is always the quote)
+
+ Returns:
+ Bonding curve address
+ """
+ bonding_curve, _ = Pubkey.find_program_address(
+ [b"bonding-curve", bytes(base_mint)],
+ PumpAddresses.PROGRAM
+ )
+ return bonding_curve
+
+ def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
+ """Derive user's associated token account address.
+
+ Args:
+ user: User's wallet address
+ mint: Token mint address
+
+ Returns:
+ User's associated token account address
+ """
+ return get_associated_token_address(user, mint)
+
+ def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
+ """Get pump.fun-specific additional accounts needed for trading.
+
+ Args:
+ token_info: Token information
+
+ Returns:
+ Dictionary of additional account addresses
+ """
+ accounts = {}
+
+ # Add bonding curve if available
+ if token_info.bonding_curve:
+ accounts["bonding_curve"] = token_info.bonding_curve
+
+ # Add associated bonding curve if available
+ if token_info.associated_bonding_curve:
+ accounts["associated_bonding_curve"] = token_info.associated_bonding_curve
+
+ # Add creator vault if available
+ if token_info.creator_vault:
+ accounts["creator_vault"] = token_info.creator_vault
+
+ # Derive associated bonding curve if not provided
+ if not token_info.associated_bonding_curve and token_info.bonding_curve:
+ accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
+ token_info.mint, token_info.bonding_curve
+ )
+
+ # Derive creator vault if not provided but creator is available
+ if not token_info.creator_vault and token_info.creator:
+ accounts["creator_vault"] = self.derive_creator_vault(token_info.creator)
+
+ return accounts
+
+ def derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
+ """Derive the associated bonding curve (ATA of bonding curve for the token).
+
+ Args:
+ mint: Token mint address
+ bonding_curve: Bonding curve address
+
+ Returns:
+ Associated bonding curve address
+ """
+ return get_associated_token_address(bonding_curve, mint)
+
+ def derive_creator_vault(self, creator: Pubkey) -> Pubkey:
+ """Derive the creator vault address.
+
+ Args:
+ creator: Creator address
+
+ Returns:
+ Creator vault address
+ """
+ creator_vault, _ = Pubkey.find_program_address(
+ [b"creator-vault", bytes(creator)],
+ PumpAddresses.PROGRAM
+ )
+ return creator_vault
+
+ def derive_global_volume_accumulator(self) -> Pubkey:
+ """Derive the global volume accumulator PDA.
+
+ Returns:
+ Global volume accumulator address
+ """
+ return PumpAddresses.find_global_volume_accumulator()
+
+ def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey:
+ """Derive the user volume accumulator PDA.
+
+ Args:
+ user: User address
+
+ Returns:
+ User volume accumulator address
+ """
+ return PumpAddresses.find_user_volume_accumulator(user)
+
+ def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
+ """Get all accounts needed for a buy instruction.
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+
+ Returns:
+ Dictionary of account addresses for buy instruction
+ """
+ additional_accounts = self.get_additional_accounts(token_info)
+
+ return {
+ "global": PumpAddresses.GLOBAL,
+ "fee": PumpAddresses.FEE,
+ "mint": token_info.mint,
+ "bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
+ "associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
+ "user_token_account": self.derive_user_token_account(user, token_info.mint),
+ "user": user,
+ "system_program": SystemAddresses.PROGRAM,
+ "token_program": SystemAddresses.TOKEN_PROGRAM,
+ "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
+ "event_authority": PumpAddresses.EVENT_AUTHORITY,
+ "program": PumpAddresses.PROGRAM,
+ "global_volume_accumulator": self.derive_global_volume_accumulator(),
+ "user_volume_accumulator": self.derive_user_volume_accumulator(user),
+ }
+
+ def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
+ """Get all accounts needed for a sell instruction.
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+
+ Returns:
+ Dictionary of account addresses for sell instruction
+ """
+ additional_accounts = self.get_additional_accounts(token_info)
+
+ return {
+ "global": PumpAddresses.GLOBAL,
+ "fee": PumpAddresses.FEE,
+ "mint": token_info.mint,
+ "bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
+ "associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
+ "user_token_account": self.derive_user_token_account(user, token_info.mint),
+ "user": user,
+ "system_program": SystemAddresses.PROGRAM,
+ "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
+ "token_program": SystemAddresses.TOKEN_PROGRAM,
+ "event_authority": PumpAddresses.EVENT_AUTHORITY,
+ "program": PumpAddresses.PROGRAM,
+ }
\ No newline at end of file
diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py
new file mode 100644
index 0000000..ef909c1
--- /dev/null
+++ b/src/platforms/pumpfun/curve_manager.py
@@ -0,0 +1,215 @@
+"""
+Pump.Fun implementation of CurveManager interface.
+
+This module handles pump.fun-specific bonding curve operations
+by implementing the CurveManager interface.
+"""
+
+from typing import Any
+
+from solders.pubkey import Pubkey
+
+from core.client import SolanaClient
+from core.curve import BondingCurveManager
+from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
+from interfaces.core import CurveManager, Platform
+
+
+class PumpFunCurveManager(CurveManager):
+ """Pump.Fun implementation of CurveManager interface."""
+
+ def __init__(self, client: SolanaClient):
+ """Initialize pump.fun curve manager.
+
+ Args:
+ client: Solana RPC client
+ """
+ self.client = client
+ self.bonding_curve_manager = BondingCurveManager(client)
+
+ @property
+ def platform(self) -> Platform:
+ """Get the platform this manager serves."""
+ return Platform.PUMP_FUN
+
+ async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
+ """Get the current state of a pump.fun bonding curve.
+
+ Args:
+ pool_address: Address of the bonding curve
+
+ Returns:
+ Dictionary containing bonding curve state data
+ """
+ curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+
+ return {
+ "virtual_token_reserves": curve_state.virtual_token_reserves,
+ "virtual_sol_reserves": curve_state.virtual_sol_reserves,
+ "real_token_reserves": curve_state.real_token_reserves,
+ "real_sol_reserves": curve_state.real_sol_reserves,
+ "token_total_supply": curve_state.token_total_supply,
+ "complete": curve_state.complete,
+ "creator": str(curve_state.creator),
+
+ # Calculated fields for convenience
+ "price_per_token": curve_state.calculate_price(),
+ "token_reserves_decimal": curve_state.token_reserves,
+ "sol_reserves_decimal": curve_state.sol_reserves,
+ }
+
+ async def calculate_price(self, pool_address: Pubkey) -> float:
+ """Calculate current token price from bonding curve state.
+
+ Args:
+ pool_address: Address of the bonding curve
+
+ Returns:
+ Current token price in SOL
+ """
+ return await self.bonding_curve_manager.calculate_price(pool_address)
+
+ async def calculate_buy_amount_out(
+ self,
+ pool_address: Pubkey,
+ amount_in: int
+ ) -> int:
+ """Calculate expected tokens received for a buy operation.
+
+ Uses the pump.fun bonding curve formula to calculate token output.
+
+ Args:
+ pool_address: Address of the bonding curve
+ amount_in: Amount of SOL to spend (in lamports)
+
+ Returns:
+ Expected amount of tokens to receive (in raw token units)
+ """
+ curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+
+ # Use virtual reserves for bonding curve calculation
+ # Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
+ numerator = amount_in * curve_state.virtual_token_reserves
+ denominator = curve_state.virtual_sol_reserves + amount_in
+
+ if denominator == 0:
+ return 0
+
+ tokens_out = numerator // denominator
+ return tokens_out
+
+ async def calculate_sell_amount_out(
+ self,
+ pool_address: Pubkey,
+ amount_in: int
+ ) -> int:
+ """Calculate expected SOL received for a sell operation.
+
+ Uses the pump.fun bonding curve formula to calculate SOL output.
+
+ Args:
+ pool_address: Address of the bonding curve
+ amount_in: Amount of tokens to sell (in raw token units)
+
+ Returns:
+ Expected amount of SOL to receive (in lamports)
+ """
+ curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+
+ # Use virtual reserves for bonding curve calculation
+ # Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
+ numerator = amount_in * curve_state.virtual_sol_reserves
+ denominator = curve_state.virtual_token_reserves + amount_in
+
+ if denominator == 0:
+ return 0
+
+ sol_out = numerator // denominator
+ return sol_out
+
+ async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
+ """Get current bonding curve reserves.
+
+ Args:
+ pool_address: Address of the bonding curve
+
+ Returns:
+ Tuple of (token_reserves, sol_reserves) in raw units
+ """
+ curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves)
+
+ async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float:
+ """Calculate the expected token amount for a given SOL input.
+
+ This is a convenience method that wraps calculate_buy_amount_out
+ and converts between decimal and raw units.
+
+ Args:
+ pool_address: Address of the bonding curve
+ sol_amount: Amount of SOL to spend (in decimal SOL)
+
+ Returns:
+ Expected token amount (in decimal tokens)
+ """
+ sol_lamports = int(sol_amount * LAMPORTS_PER_SOL)
+ tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports)
+ return tokens_raw / 10**TOKEN_DECIMALS
+
+ async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float:
+ """Calculate the expected SOL amount for a given token input.
+
+ This is a convenience method that wraps calculate_sell_amount_out
+ and converts between decimal and raw units.
+
+ Args:
+ pool_address: Address of the bonding curve
+ token_amount: Amount of tokens to sell (in decimal tokens)
+
+ Returns:
+ Expected SOL amount (in decimal SOL)
+ """
+ tokens_raw = int(token_amount * 10**TOKEN_DECIMALS)
+ sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw)
+ return sol_lamports / LAMPORTS_PER_SOL
+
+ async def is_curve_complete(self, pool_address: Pubkey) -> bool:
+ """Check if the bonding curve is complete (migrated to Raydium).
+
+ Args:
+ pool_address: Address of the bonding curve
+
+ Returns:
+ True if curve is complete, False otherwise
+ """
+ curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ return curve_state.complete
+
+ async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
+ """Get bonding curve completion progress information.
+
+ Args:
+ pool_address: Address of the bonding curve
+
+ Returns:
+ Dictionary with progress information
+ """
+ curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+
+ # Calculate progress based on SOL raised vs target
+ # This is approximate since the exact target isn't stored in the curve state
+ sol_raised = curve_state.real_sol_reserves / LAMPORTS_PER_SOL
+
+ # Estimate progress based on typical pump.fun graduation requirements
+ # (This could be made more accurate with additional on-chain data)
+ estimated_target_sol = 85.0 # Typical pump.fun graduation target
+ progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
+
+ return {
+ "complete": curve_state.complete,
+ "sol_raised": sol_raised,
+ "estimated_target_sol": estimated_target_sol,
+ "progress_percentage": progress_percentage,
+ "tokens_available": curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS,
+ "market_cap_sol": sol_raised, # Approximate market cap
+ }
\ No newline at end of file
diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py
new file mode 100644
index 0000000..cccade0
--- /dev/null
+++ b/src/platforms/pumpfun/event_parser.py
@@ -0,0 +1,335 @@
+"""
+Pump.Fun implementation of EventParser interface.
+
+This module parses pump.fun-specific token creation events from various sources
+by implementing the EventParser interface.
+"""
+
+import base64
+import struct
+from time import monotonic
+from typing import Any, Final
+
+import base58
+from solders.pubkey import Pubkey
+from solders.transaction import VersionedTransaction
+
+from core.pubkeys import PumpAddresses, SystemAddresses
+from interfaces.core import EventParser, Platform, TokenInfo
+
+
+class PumpFunEventParser(EventParser):
+ """Pump.Fun implementation of EventParser interface."""
+
+ # Discriminators for pump.fun instructions
+ CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728
+ CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack("
Platform:
+ """Get the platform this parser serves."""
+ return Platform.PUMP_FUN
+
+ def parse_token_creation_from_logs(
+ self,
+ logs: list[str],
+ signature: str
+ ) -> TokenInfo | None:
+ """Parse token creation from pump.fun transaction logs.
+
+ Args:
+ logs: List of log strings from transaction
+ signature: Transaction signature
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ # Check if this is a token creation
+ if not any("Program log: Instruction: Create" in log for log in logs):
+ return None
+
+ # Skip swaps and other operations
+ if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
+ return None
+
+ # Find and process program data
+ for log in logs:
+ if "Program data:" in log:
+ try:
+ encoded_data = log.split(": ")[1]
+ decoded_data = base64.b64decode(encoded_data)
+ return self._parse_create_instruction_data(decoded_data)
+ except Exception:
+ continue
+
+ return None
+
+ def parse_token_creation_from_instruction(
+ self,
+ instruction_data: bytes,
+ accounts: list[int],
+ account_keys: list[bytes]
+ ) -> TokenInfo | None:
+ """Parse token creation from pump.fun instruction data.
+
+ Args:
+ instruction_data: Raw instruction data
+ accounts: List of account indices
+ account_keys: List of account public keys
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ if not instruction_data.startswith(self.CREATE_DISCRIMINATOR_BYTES):
+ return None
+
+ try:
+ # Helper to get account key
+ def get_account_key(index):
+ if index >= len(accounts):
+ return None
+ account_index = accounts[index]
+ if account_index >= len(account_keys):
+ return None
+ return Pubkey.from_bytes(account_keys[account_index])
+
+ # Parse instruction data
+ token_data = self._parse_create_instruction_data(instruction_data)
+ if not token_data:
+ return None
+
+ # Extract account information
+ mint = get_account_key(0)
+ bonding_curve = get_account_key(2)
+ associated_bonding_curve = get_account_key(3)
+ user = get_account_key(7)
+
+ if not all([mint, bonding_curve, associated_bonding_curve, user]):
+ return None
+
+ # Create creator vault
+ creator = Pubkey.from_string(token_data["creator"]) if token_data.get("creator") else user
+ creator_vault = self._derive_creator_vault(creator)
+
+ return TokenInfo(
+ name=token_data["name"],
+ symbol=token_data["symbol"],
+ uri=token_data["uri"],
+ mint=mint,
+ platform=Platform.PUMP_FUN,
+ bonding_curve=bonding_curve,
+ associated_bonding_curve=associated_bonding_curve,
+ user=user,
+ creator=creator,
+ creator_vault=creator_vault,
+ creation_timestamp=monotonic(),
+ )
+
+ except Exception:
+ return None
+
+ def parse_token_creation_from_geyser(
+ self,
+ transaction_info: Any
+ ) -> TokenInfo | None:
+ """Parse token creation from Geyser transaction data.
+
+ Args:
+ transaction_info: Geyser transaction information
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ try:
+ if not hasattr(transaction_info, 'transaction'):
+ return None
+
+ tx = transaction_info.transaction.transaction.transaction
+ msg = getattr(tx, "message", None)
+ if msg is None:
+ return None
+
+ for ix in msg.instructions:
+ # Skip non-pump.fun program instructions
+ program_idx = ix.program_id_index
+ if program_idx >= len(msg.account_keys):
+ continue
+
+ program_id = msg.account_keys[program_idx]
+ if bytes(program_id) != bytes(self.get_program_id()):
+ continue
+
+ # Process instruction data
+ token_info = self.parse_token_creation_from_instruction(
+ ix.data, ix.accounts, msg.account_keys
+ )
+ if token_info:
+ return token_info
+
+ return None
+
+ except Exception:
+ return None
+
+ def get_program_id(self) -> Pubkey:
+ """Get the pump.fun program ID this parser monitors.
+
+ Returns:
+ Pump.fun program ID
+ """
+ return PumpAddresses.PROGRAM
+
+ def get_instruction_discriminators(self) -> list[bytes]:
+ """Get instruction discriminators for token creation.
+
+ Returns:
+ List of discriminator bytes to match
+ """
+ return [self.CREATE_DISCRIMINATOR_BYTES]
+
+ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
+ """Parse token creation from block data (for block listener).
+
+ Args:
+ block_data: Block data from WebSocket
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ try:
+ if "transactions" not in block_data:
+ return None
+
+ for tx in block_data["transactions"]:
+ if not isinstance(tx, dict) or "transaction" not in tx:
+ continue
+
+ # Decode base64 transaction data
+ tx_data_encoded = tx["transaction"][0]
+ tx_data_decoded = base64.b64decode(tx_data_encoded)
+ transaction = VersionedTransaction.from_bytes(tx_data_decoded)
+
+ for ix in transaction.message.instructions:
+ program_id = transaction.message.account_keys[ix.program_id_index]
+
+ # Check if instruction is from pump.fun program
+ if str(program_id) != str(self.get_program_id()):
+ continue
+
+ ix_data = bytes(ix.data)
+
+ # Check for create discriminator
+ if len(ix_data) >= 8:
+ discriminator = struct.unpack("
dict | None:
+ """Parse the create instruction data from pump.fun.
+
+ Args:
+ data: Raw instruction data
+
+ Returns:
+ Dictionary of parsed data or None if parsing fails
+ """
+ if len(data) < 8:
+ return None
+
+ # Check for the correct instruction discriminator
+ discriminator = struct.unpack("
len(data):
+ return None
+ length = struct.unpack(" len(data):
+ return None
+ value = data[offset : offset + length].decode("utf-8")
+ offset += length
+ elif field_type == "publicKey":
+ if offset + 32 > len(data):
+ return None
+ value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
+ offset += 32
+
+ parsed_data[field_name] = value
+
+ return parsed_data
+
+ except Exception:
+ return None
+
+ def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
+ """Derive the creator vault for a creator.
+
+ Args:
+ creator: Creator address
+
+ Returns:
+ Creator vault address
+ """
+ derived_address, _ = Pubkey.find_program_address(
+ [b"creator-vault", bytes(creator)],
+ PumpAddresses.PROGRAM,
+ )
+ return derived_address
+
+ def _derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
+ """Derive the associated bonding curve (ATA of bonding curve for the token).
+
+ Args:
+ mint: Token mint address
+ bonding_curve: Bonding curve address
+
+ Returns:
+ Associated bonding curve address
+ """
+ derived_address, _ = Pubkey.find_program_address(
+ [
+ bytes(bonding_curve),
+ bytes(SystemAddresses.TOKEN_PROGRAM),
+ bytes(mint),
+ ],
+ SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
+ )
+ return derived_address
\ No newline at end of file
diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py
new file mode 100644
index 0000000..e495828
--- /dev/null
+++ b/src/platforms/pumpfun/instruction_builder.py
@@ -0,0 +1,234 @@
+"""
+Pump.Fun implementation of InstructionBuilder interface.
+
+This module builds pump.fun-specific buy and sell instructions
+by implementing the InstructionBuilder interface.
+"""
+
+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.pubkeys import TOKEN_DECIMALS, SystemAddresses
+from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
+
+# Discriminators for pump.fun instructions
+BUY_DISCRIMINATOR: Final[bytes] = struct.pack("
Platform:
+ """Get the platform this builder serves."""
+ return Platform.PUMP_FUN
+
+ async def build_buy_instruction(
+ self,
+ token_info: TokenInfo,
+ user: Pubkey,
+ amount_in: int,
+ minimum_amount_out: int,
+ address_provider: AddressProvider
+ ) -> list[Instruction]:
+ """Build buy instruction(s) for pump.fun.
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ amount_in: Amount of SOL to spend (in lamports)
+ minimum_amount_out: Minimum tokens expected (raw token units)
+ address_provider: Platform address provider
+
+ Returns:
+ List of instructions needed for the buy operation
+ """
+ instructions = []
+
+ # Get all required accounts
+ accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
+
+ # 1. Create idempotent ATA instruction (won't fail if ATA already exists)
+ ata_instruction = create_idempotent_associated_token_account(
+ user, # payer
+ user, # owner
+ token_info.mint, # mint
+ SystemAddresses.TOKEN_PROGRAM, # token program
+ )
+ instructions.append(ata_instruction)
+
+ # 2. Build buy instruction
+ buy_accounts = [
+ AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
+ AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["global_volume_accumulator"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user_volume_accumulator"], is_signer=False, is_writable=True),
+ ]
+
+ # Build instruction data: discriminator + token_amount + max_sol_cost
+ instruction_data = (
+ BUY_DISCRIMINATOR +
+ struct.pack("
list[Instruction]:
+ """Build sell instruction(s) for pump.fun.
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ amount_in: Amount of tokens to sell (raw token units)
+ minimum_amount_out: Minimum SOL expected (in lamports)
+ address_provider: Platform address provider
+
+ Returns:
+ List of instructions needed for the sell operation
+ """
+ instructions = []
+
+ # Get all required accounts
+ accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
+
+ # Build sell instruction accounts
+ sell_accounts = [
+ AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
+ AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
+ ]
+
+ # Build instruction data: discriminator + token_amount + min_sol_output
+ instruction_data = (
+ SELL_DISCRIMINATOR +
+ struct.pack("
list[Pubkey]:
+ """Get list of accounts required for buy operation (for priority fee calculation).
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ address_provider: Platform address provider
+
+ Returns:
+ List of account addresses that will be accessed
+ """
+ accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
+
+ return [
+ accounts_info["mint"],
+ accounts_info["bonding_curve"],
+ accounts_info["associated_bonding_curve"],
+ accounts_info["user_token_account"],
+ accounts_info["fee"],
+ accounts_info["creator_vault"],
+ accounts_info["global_volume_accumulator"],
+ accounts_info["user_volume_accumulator"],
+ accounts_info["program"],
+ ]
+
+ def get_required_accounts_for_sell(
+ self,
+ token_info: TokenInfo,
+ user: Pubkey,
+ address_provider: AddressProvider
+ ) -> list[Pubkey]:
+ """Get list of accounts required for sell operation (for priority fee calculation).
+
+ Args:
+ token_info: Token information
+ user: User's wallet address
+ address_provider: Platform address provider
+
+ Returns:
+ List of account addresses that will be accessed
+ """
+ accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
+
+ return [
+ accounts_info["mint"],
+ accounts_info["bonding_curve"],
+ accounts_info["associated_bonding_curve"],
+ accounts_info["user_token_account"],
+ accounts_info["fee"],
+ accounts_info["creator_vault"],
+ accounts_info["program"],
+ ]
+
+ def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
+ """Convert decimal token amount to raw token units.
+
+ Args:
+ token_amount_decimal: Token amount in decimal form
+
+ Returns:
+ Token amount in raw units (adjusted for decimals)
+ """
+ return int(token_amount_decimal * 10**TOKEN_DECIMALS)
+
+ def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
+ """Convert raw token amount to decimal form.
+
+ Args:
+ token_amount_raw: Token amount in raw units
+
+ Returns:
+ Token amount in decimal form
+ """
+ return token_amount_raw / 10**TOKEN_DECIMALS
\ No newline at end of file
diff --git a/src/trading/base.py b/src/trading/base.py
index 25a9862..ee7eb63 100644
--- a/src/trading/base.py
+++ b/src/trading/base.py
@@ -1,5 +1,9 @@
"""
-Base interfaces for trading operations.
+Enhanced base interfaces for trading operations with platform support.
+
+This module provides the complete enhanced base classes that replace the existing
+trading/base.py while maintaining full backward compatibility. It integrates the
+new interface system with the existing trading infrastructure.
"""
from abc import ABC, abstractmethod
@@ -8,13 +12,16 @@ from typing import Any
from solders.pubkey import Pubkey
-from core.pubkeys import PumpAddresses
+from interfaces.core import Platform
+
+# Import the new enhanced TokenInfo and Platform from interfaces
+from interfaces.core import TokenInfo as EnhancedTokenInfo
+# Keep the original TokenInfo structure for backward compatibility
@dataclass
-class TokenInfo:
- """Token information."""
-
+class TokenInfo_Legacy:
+ """Legacy token information structure for backward compatibility."""
name: str
symbol: str
uri: str
@@ -26,14 +33,14 @@ class TokenInfo:
creator_vault: Pubkey
@classmethod
- def from_dict(cls, data: dict[str, Any]) -> "TokenInfo":
+ def from_dict(cls, data: dict[str, Any]) -> "TokenInfo_Legacy":
"""Create TokenInfo from dictionary.
Args:
data: Dictionary with token data
Returns:
- TokenInfo instance
+ TokenInfo_Legacy instance
"""
return cls(
name=data["name"],
@@ -68,40 +75,353 @@ class TokenInfo:
@dataclass
class TradeResult:
- """Result of a trading operation."""
-
+ """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):
- """Base interface for trading operations."""
+ """Enhanced base interface for trading operations with platform support."""
@abstractmethod
- async def execute(self, *args, **kwargs) -> TradeResult:
+ 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
+ TradeResult with operation outcome including platform info
"""
pass
- def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]:
+ 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: Token information for the buy/sell operation.
+ token_info: Enhanced token information
Returns:
- list[Pubkey]: List of relevant accounts.
+ List of relevant accounts (default implementation for pump.fun compatibility)
"""
- 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
- ]
+ # 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.
+
+ This function allows existing code that creates legacy TokenInfo objects
+ to be upgraded to the new enhanced format.
+
+ Args:
+ legacy_token_info: Legacy TokenInfo instance
+
+ Returns:
+ Enhanced TokenInfo with platform information
+ """
+ return TokenInfo(
+ name=legacy_token_info.name,
+ symbol=legacy_token_info.symbol,
+ uri=legacy_token_info.uri,
+ mint=legacy_token_info.mint,
+ platform=Platform.PUMP_FUN, # Default to pump.fun for legacy tokens
+ bonding_curve=legacy_token_info.bonding_curve,
+ associated_bonding_curve=legacy_token_info.associated_bonding_curve,
+ user=legacy_token_info.user,
+ creator=legacy_token_info.creator,
+ creator_vault=legacy_token_info.creator_vault,
+ )
+
+
+def create_legacy_token_info(enhanced_token_info: TokenInfo) -> TokenInfo_Legacy:
+ """Convert enhanced TokenInfo back to legacy TokenInfo if needed.
+
+ This function allows the enhanced TokenInfo to be used with existing
+ code that expects the legacy format.
+
+ Args:
+ enhanced_token_info: Enhanced TokenInfo instance
+
+ Returns:
+ Legacy TokenInfo instance
+
+ Raises:
+ ValueError: If enhanced TokenInfo doesn't have required pump.fun fields
+ """
+ if enhanced_token_info.platform != Platform.PUMP_FUN:
+ raise ValueError("Can only convert pump.fun tokens to legacy format")
+
+ if not all([
+ enhanced_token_info.bonding_curve,
+ enhanced_token_info.associated_bonding_curve,
+ enhanced_token_info.creator_vault
+ ]):
+ raise ValueError("Enhanced TokenInfo missing required pump.fun fields")
+
+ return TokenInfo_Legacy(
+ name=enhanced_token_info.name,
+ symbol=enhanced_token_info.symbol,
+ uri=enhanced_token_info.uri,
+ mint=enhanced_token_info.mint,
+ bonding_curve=enhanced_token_info.bonding_curve,
+ associated_bonding_curve=enhanced_token_info.associated_bonding_curve,
+ user=enhanced_token_info.user or enhanced_token_info.creator,
+ creator=enhanced_token_info.creator or enhanced_token_info.user,
+ creator_vault=enhanced_token_info.creator_vault,
+ )
+
+
+def create_pump_fun_token_info(
+ name: str,
+ symbol: str,
+ uri: str,
+ mint: Pubkey,
+ bonding_curve: Pubkey,
+ associated_bonding_curve: Pubkey,
+ user: Pubkey,
+ creator: Pubkey | None = None,
+ creator_vault: Pubkey | None = None,
+ **kwargs
+) -> TokenInfo:
+ """Convenience function to create pump.fun TokenInfo with proper platform setting.
+
+ Args:
+ name: Token name
+ symbol: Token symbol
+ uri: Token metadata URI
+ mint: Token mint address
+ bonding_curve: Bonding curve address
+ associated_bonding_curve: Associated bonding curve address
+ user: User/trader address
+ creator: Creator address (defaults to user if not provided)
+ creator_vault: Creator vault address (will be derived if not provided)
+ **kwargs: Additional fields for TokenInfo
+
+ 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,
+ )
+
+ return TokenInfo(
+ name=name,
+ symbol=symbol,
+ uri=uri,
+ mint=mint,
+ platform=Platform.PUMP_FUN,
+ bonding_curve=bonding_curve,
+ associated_bonding_curve=associated_bonding_curve,
+ user=user,
+ creator=creator,
+ creator_vault=creator_vault,
+ **kwargs
+ )
+
+
+def create_lets_bonk_token_info(
+ name: str,
+ symbol: str,
+ uri: str,
+ mint: Pubkey,
+ pool_state: Pubkey,
+ base_vault: Pubkey,
+ quote_vault: Pubkey,
+ user: Pubkey,
+ creator: Pubkey | None = None,
+ **kwargs
+) -> TokenInfo:
+ """Convenience function to create LetsBonk TokenInfo with proper platform setting.
+
+ Args:
+ name: Token name
+ symbol: Token symbol
+ uri: Token metadata URI
+ mint: Token mint address
+ pool_state: Pool state address
+ base_vault: Base token vault address
+ quote_vault: Quote token vault address
+ user: User/trader address
+ creator: Creator address (defaults to user if not provided)
+ **kwargs: Additional fields for TokenInfo
+
+ 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,
+ uri=uri,
+ mint=mint,
+ platform=Platform.LETS_BONK,
+ pool_state=pool_state,
+ base_vault=base_vault,
+ quote_vault=quote_vault,
+ user=user,
+ creator=creator,
+ **kwargs
+ )
+
+
+def is_pump_fun_token(token_info: TokenInfo) -> bool:
+ """Check if a TokenInfo is for pump.fun platform.
+
+ Args:
+ token_info: Token information to check
+
+ Returns:
+ True if token is for pump.fun platform
+ """
+ return token_info.platform == Platform.PUMP_FUN
+
+
+def is_lets_bonk_token(token_info: TokenInfo) -> bool:
+ """Check if a TokenInfo is for LetsBonk platform.
+
+ Args:
+ token_info: Token information to check
+
+ Returns:
+ True if token is for LetsBonk platform
+ """
+ return token_info.platform == Platform.LETS_BONK
+
+
+def get_platform_specific_fields(token_info: TokenInfo) -> dict[str, Any]:
+ """Get platform-specific fields from TokenInfo.
+
+ Args:
+ token_info: Token information
+
+ Returns:
+ Dictionary of platform-specific fields
+ """
+ if token_info.platform == Platform.PUMP_FUN:
+ return {
+ "bonding_curve": token_info.bonding_curve,
+ "associated_bonding_curve": token_info.associated_bonding_curve,
+ "creator_vault": token_info.creator_vault,
+ }
+ elif token_info.platform == Platform.LETS_BONK:
+ return {
+ "pool_state": token_info.pool_state,
+ "base_vault": token_info.base_vault,
+ "quote_vault": token_info.quote_vault,
+ }
+ else:
+ return {}
+
+
+def validate_token_info(token_info: TokenInfo) -> bool:
+ """Validate that TokenInfo has required fields for its platform.
+
+ Args:
+ token_info: Token information to validate
+
+ Returns:
+ True if TokenInfo is valid for its platform
+ """
+ # Check common required fields
+ if not all([
+ token_info.name,
+ token_info.symbol,
+ token_info.mint,
+ token_info.platform,
+ ]):
+ return False
+
+ # Check platform-specific required fields
+ if token_info.platform == Platform.PUMP_FUN:
+ return all([
+ token_info.bonding_curve,
+ token_info.associated_bonding_curve,
+ ])
+ elif token_info.platform == Platform.LETS_BONK:
+ return all([
+ token_info.pool_state,
+ token_info.base_vault,
+ token_info.quote_vault,
+ ])
+
+ return True
+
+
+# Backward compatibility exports
+# This allows existing imports to continue working
+__all__ = [
+ 'Platform', # Platform enum
+ 'TokenInfo', # Enhanced TokenInfo (main export)
+ 'TokenInfo_Legacy', # Legacy TokenInfo for compatibility
+ 'TradeResult', # Enhanced TradeResult
+ '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',
+]
\ No newline at end of file
diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py
new file mode 100644
index 0000000..7770991
--- /dev/null
+++ b/src/trading/platform_aware.py
@@ -0,0 +1,315 @@
+"""
+Platform-aware trader implementations that use the interface system.
+
+This module provides new trader classes that work with any platform
+through the interface system, while maintaining compatibility with existing code.
+"""
+
+from solders.pubkey import Pubkey
+
+from core.client import SolanaClient
+from core.priority_fee.manager import PriorityFeeManager
+from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
+from core.wallet import Wallet
+from interfaces.core import AddressProvider, Platform, TokenInfo
+from platforms import get_platform_implementations
+from trading.base import Trader, TradeResult
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+class PlatformAwareBuyer(Trader):
+ """Platform-aware token buyer that works with any supported platform."""
+
+ def __init__(
+ self,
+ client: SolanaClient,
+ wallet: Wallet,
+ 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 platform-aware token buyer.
+
+ Args:
+ client: Solana client for RPC calls
+ wallet: Wallet for signing transactions
+ priority_fee_manager: Priority fee 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 pool state for price estimation
+ """
+ self.client = client
+ self.wallet = wallet
+ 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 using platform-specific implementations.
+
+ Args:
+ token_info: Enhanced token information with platform
+
+ Returns:
+ TradeResult with buy outcome
+ """
+ try:
+ # Get platform-specific implementations
+ implementations = get_platform_implementations(token_info.platform, self.client)
+ address_provider = implementations.address_provider
+ instruction_builder = implementations.instruction_builder
+ curve_manager = implementations.curve_manager
+
+ # 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 if token_amount > 0 else 0
+ else:
+ # Get pool address based on platform
+ pool_address = self._get_pool_address(token_info, address_provider)
+
+ # Regular behavior with RPC call
+ token_price_sol = await curve_manager.calculate_price(pool_address)
+ token_amount = self.amount / token_price_sol if token_price_sol > 0 else 0
+
+ # Calculate minimum token amount with slippage
+ minimum_token_amount = token_amount * (1 - self.slippage)
+ minimum_token_amount_raw = int(minimum_token_amount * 10**TOKEN_DECIMALS)
+
+ # Calculate maximum SOL to spend with slippage
+ max_amount_lamports = int(amount_lamports * (1 + self.slippage))
+
+ # Build buy instructions
+ instructions = await instruction_builder.build_buy_instruction(
+ token_info,
+ self.wallet.pubkey,
+ max_amount_lamports, # amount_in (SOL)
+ minimum_token_amount_raw, # minimum_amount_out (tokens)
+ address_provider
+ )
+
+ # Get accounts for priority fee calculation
+ priority_accounts = instruction_builder.get_required_accounts_for_buy(
+ token_info, self.wallet.pubkey, address_provider
+ )
+
+ 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)"
+ )
+
+ # Send transaction
+ tx_signature = await self.client.build_and_send_transaction(
+ instructions,
+ self.wallet.keypair,
+ skip_preflight=True,
+ max_retries=self.max_retries,
+ priority_fee=await self.priority_fee_manager.calculate_priority_fee(
+ priority_accounts
+ ),
+ )
+
+ success = await self.client.confirm_transaction(tx_signature)
+
+ if success:
+ logger.info(f"Buy transaction confirmed: {tx_signature}")
+ return TradeResult(
+ success=True,
+ platform=token_info.platform,
+ tx_signature=tx_signature,
+ amount=token_amount,
+ price=token_price_sol,
+ )
+ else:
+ return TradeResult(
+ success=False,
+ platform=token_info.platform,
+ 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,
+ platform=token_info.platform,
+ error_message=str(e)
+ )
+
+ def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
+ """Get the pool/curve address for price calculations.
+
+ Args:
+ token_info: Token information
+ address_provider: Platform address provider
+
+ Returns:
+ Pool/curve address
+ """
+ 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)
+
+
+class PlatformAwareSeller(Trader):
+ """Platform-aware token seller that works with any supported platform."""
+
+ def __init__(
+ self,
+ client: SolanaClient,
+ wallet: Wallet,
+ priority_fee_manager: PriorityFeeManager,
+ slippage: float = 0.25,
+ max_retries: int = 5,
+ ):
+ """Initialize platform-aware token seller.
+
+ Args:
+ client: Solana client for RPC calls
+ wallet: Wallet for signing transactions
+ priority_fee_manager: Priority fee manager
+ slippage: Slippage tolerance (0.25 = 25%)
+ max_retries: Maximum number of retry attempts
+ """
+ self.client = client
+ self.wallet = wallet
+ 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 using platform-specific implementations.
+
+ Args:
+ token_info: Enhanced token information with platform
+
+ Returns:
+ TradeResult with sell outcome
+ """
+ try:
+ # Get platform-specific implementations
+ implementations = get_platform_implementations(token_info.platform, self.client)
+ address_provider = implementations.address_provider
+ instruction_builder = implementations.instruction_builder
+ curve_manager = implementations.curve_manager
+
+ # Get user's token account and balance
+ user_token_account = address_provider.derive_user_token_account(
+ self.wallet.pubkey, token_info.mint
+ )
+
+ token_balance = await self.client.get_token_account_balance(user_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,
+ platform=token_info.platform,
+ error_message="No tokens to sell"
+ )
+
+ # Get pool address and current price
+ pool_address = self._get_pool_address(token_info, address_provider)
+ token_price_sol = await curve_manager.calculate_price(pool_address)
+
+ logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
+
+ # Calculate minimum SOL output with slippage
+ expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
+ min_sol_output = int((expected_sol_output * (1 - self.slippage)) * 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"
+ )
+
+ # Build sell instructions
+ instructions = await instruction_builder.build_sell_instruction(
+ token_info,
+ self.wallet.pubkey,
+ token_balance, # amount_in (tokens)
+ min_sol_output, # minimum_amount_out (SOL)
+ address_provider
+ )
+
+ # Get accounts for priority fee calculation
+ priority_accounts = instruction_builder.get_required_accounts_for_sell(
+ token_info, self.wallet.pubkey, address_provider
+ )
+
+ # Send transaction
+ tx_signature = await self.client.build_and_send_transaction(
+ instructions,
+ self.wallet.keypair,
+ skip_preflight=True,
+ max_retries=self.max_retries,
+ priority_fee=await self.priority_fee_manager.calculate_priority_fee(
+ priority_accounts
+ ),
+ )
+
+ success = await self.client.confirm_transaction(tx_signature)
+
+ if success:
+ logger.info(f"Sell transaction confirmed: {tx_signature}")
+ return TradeResult(
+ success=True,
+ platform=token_info.platform,
+ tx_signature=tx_signature,
+ amount=token_balance_decimal,
+ price=token_price_sol,
+ )
+ else:
+ return TradeResult(
+ success=False,
+ platform=token_info.platform,
+ 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,
+ platform=token_info.platform,
+ error_message=str(e)
+ )
+
+ def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
+ """Get the pool/curve address for price calculations.
+
+ Args:
+ token_info: Token information
+ address_provider: Platform address provider
+
+ Returns:
+ Pool/curve address
+ """
+ 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)
\ No newline at end of file
From 9ec9eeb793b141d77a1356956e96653ab16dc652 Mon Sep 17 00:00:00 2001
From: smypmsa
None:
- """Parse bonding curve data.
-
- Args:
- data: Raw account data
-
- Raises:
- ValueError: If data cannot be parsed
- """
- if data[:8] != EXPECTED_DISCRIMINATOR:
- raise ValueError("Invalid curve state discriminator")
-
- parsed = self._STRUCT.parse(data[8:])
- self.__dict__.update(parsed)
-
- # Convert raw bytes to Pubkey for creator field
- if hasattr(self, "creator") and isinstance(self.creator, bytes):
- self.creator = Pubkey.from_bytes(self.creator)
-
- def calculate_price(self) -> float:
- """Calculate token price in SOL.
-
- Returns:
- Token price in SOL
-
- Raises:
- ValueError: If reserve state is invalid
- """
- if self.virtual_token_reserves <= 0 or self.virtual_sol_reserves <= 0:
- raise ValueError("Invalid reserve state")
-
- return (self.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
- self.virtual_token_reserves / 10**TOKEN_DECIMALS
- )
-
- @property
- def token_reserves(self) -> float:
- """Get token reserves in decimal form."""
- return self.virtual_token_reserves / 10**TOKEN_DECIMALS
-
- @property
- def sol_reserves(self) -> float:
- """Get SOL reserves in decimal form."""
- return self.virtual_sol_reserves / LAMPORTS_PER_SOL
-
-
-class BondingCurveManager:
- """Manager for bonding curve operations."""
-
- def __init__(self, client: SolanaClient):
- """Initialize with Solana client.
-
- Args:
- client: Solana client for RPC calls
- """
- self.client = client
-
- async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState:
- """Get the state of a bonding curve.
-
- Args:
- curve_address: Address of the bonding curve account
-
- Returns:
- Bonding curve state
-
- Raises:
- ValueError: If curve data is invalid
- """
- try:
- account = await self.client.get_account_info(curve_address)
- if not account.data:
- raise ValueError(f"No data in bonding curve account {curve_address}")
-
- return BondingCurveState(account.data)
-
- except Exception as e:
- logger.error(f"Failed to get curve state: {e!s}")
- raise ValueError(f"Invalid curve state: {e!s}")
-
- async def calculate_price(self, curve_address: Pubkey) -> float:
- """Calculate the current price of a token.
-
- Args:
- curve_address: Address of the bonding curve account
-
- Returns:
- Token price in SOL
- """
- curve_state = await self.get_curve_state(curve_address)
- return curve_state.calculate_price()
-
- async def calculate_expected_tokens(
- self, curve_address: Pubkey, sol_amount: float
- ) -> float:
- """Calculate the expected token amount for a given SOL input.
-
- Args:
- curve_address: Address of the bonding curve account
- sol_amount: Amount of SOL to spend
-
- Returns:
- Expected token amount
- """
- curve_state = await self.get_curve_state(curve_address)
- price = curve_state.calculate_price()
- return sol_amount / price
diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py
deleted file mode 100644
index c2b3dbf..0000000
--- a/src/interfaces/__init__.py
+++ /dev/null
@@ -1,280 +0,0 @@
-"""
-Platform factory and registry for managing multiple trading platforms.
-
-This module provides a centralized way to instantiate and access
-platform-specific implementations of the trading interfaces.
-"""
-
-from dataclasses import dataclass
-from typing import Any, Dict, Optional, Type
-
-from core.client import SolanaClient
-from interfaces.core import (
- AddressProvider,
- CurveManager,
- EventParser,
- InstructionBuilder,
- Platform,
-)
-
-
-@dataclass
-class PlatformImplementations:
- """Container for all platform-specific implementations."""
- address_provider: AddressProvider
- instruction_builder: InstructionBuilder
- curve_manager: CurveManager
- event_parser: EventParser
-
-
-class PlatformRegistry:
- """Registry for platform implementations."""
-
- def __init__(self):
- self._implementations: dict[Platform, dict[str, type]] = {}
- self._instances: dict[Platform, PlatformImplementations] = {}
-
- def register_platform(
- self,
- platform: Platform,
- address_provider_class: type[AddressProvider],
- instruction_builder_class: type[InstructionBuilder],
- curve_manager_class: type[CurveManager],
- event_parser_class: type[EventParser]
- ) -> None:
- """Register platform implementations.
-
- Args:
- platform: Platform enum value
- address_provider_class: AddressProvider implementation class
- instruction_builder_class: InstructionBuilder implementation class
- curve_manager_class: CurveManager implementation class
- event_parser_class: EventParser implementation class
- """
- self._implementations[platform] = {
- 'address_provider': address_provider_class,
- 'instruction_builder': instruction_builder_class,
- 'curve_manager': curve_manager_class,
- 'event_parser': event_parser_class
- }
-
- def create_platform_implementations(
- self,
- platform: Platform,
- client: SolanaClient,
- **kwargs: Any
- ) -> PlatformImplementations:
- """Create platform implementation instances.
-
- Args:
- platform: Platform to create implementations for
- client: Solana RPC client
- **kwargs: Additional arguments for implementation constructors
-
- Returns:
- PlatformImplementations containing all interface implementations
-
- Raises:
- ValueError: If platform is not registered
- """
- if platform not in self._implementations:
- raise ValueError(f"Platform {platform} is not registered")
-
- # Check if we already have instances for this platform
- if platform in self._instances:
- return self._instances[platform]
-
- impl_classes = self._implementations[platform]
-
- # Create instances
- address_provider = impl_classes['address_provider']()
- instruction_builder = impl_classes['instruction_builder']()
- curve_manager = impl_classes['curve_manager'](client)
- event_parser = impl_classes['event_parser']()
-
- implementations = PlatformImplementations(
- address_provider=address_provider,
- instruction_builder=instruction_builder,
- curve_manager=curve_manager,
- event_parser=event_parser
- )
-
- # Cache the instances
- self._instances[platform] = implementations
-
- return implementations
-
- def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None:
- """Get cached platform implementations.
-
- Args:
- platform: Platform to get implementations for
-
- Returns:
- PlatformImplementations if available, None otherwise
- """
- return self._instances.get(platform)
-
- def get_supported_platforms(self) -> list[Platform]:
- """Get list of supported platforms.
-
- Returns:
- List of registered platforms
- """
- return list(self._implementations.keys())
-
- def is_platform_supported(self, platform: Platform) -> bool:
- """Check if a platform is supported.
-
- Args:
- platform: Platform to check
-
- Returns:
- True if platform is registered, False otherwise
- """
- return platform in self._implementations
-
-
-class PlatformFactory:
- """Factory for creating platform-specific implementations."""
-
- def __init__(self):
- self.registry = PlatformRegistry()
- self._setup_default_platforms()
-
- def _setup_default_platforms(self) -> None:
- """Setup default platform registrations.
-
- This will be populated as we implement each platform.
- """
- # Platforms will be registered here as they are implemented
- # Example:
- # from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ...
- # self.registry.register_platform(
- # Platform.PUMP_FUN,
- # PumpFunAddressProvider,
- # PumpFunInstructionBuilder,
- # PumpFunCurveManager,
- # PumpFunEventParser
- # )
- pass
-
- def create_for_platform(
- self,
- platform: Platform,
- client: SolanaClient,
- **config: Any
- ) -> PlatformImplementations:
- """Create all implementations for a specific platform.
-
- Args:
- platform: Platform to create implementations for
- client: Solana RPC client
- **config: Platform-specific configuration
-
- Returns:
- PlatformImplementations containing all interface implementations
- """
- return self.registry.create_platform_implementations(platform, client, **config)
-
- def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider:
- """Get address provider for platform.
-
- Args:
- platform: Platform to get provider for
- client: Solana RPC client
-
- Returns:
- AddressProvider implementation
- """
- implementations = self.registry.create_platform_implementations(platform, client)
- return implementations.address_provider
-
- def get_instruction_builder(self, platform: Platform, client: SolanaClient) -> InstructionBuilder:
- """Get instruction builder for platform.
-
- Args:
- platform: Platform to get builder for
- client: Solana RPC client
-
- Returns:
- InstructionBuilder implementation
- """
- implementations = self.registry.create_platform_implementations(platform, client)
- return implementations.instruction_builder
-
- def get_curve_manager(self, platform: Platform, client: SolanaClient) -> CurveManager:
- """Get curve manager for platform.
-
- Args:
- platform: Platform to get manager for
- client: Solana RPC client
-
- Returns:
- CurveManager implementation
- """
- implementations = self.registry.create_platform_implementations(platform, client)
- return implementations.curve_manager
-
- def get_event_parser(self, platform: Platform, client: SolanaClient) -> EventParser:
- """Get event parser for platform.
-
- Args:
- platform: Platform to get parser for
- client: Solana RPC client
-
- Returns:
- EventParser implementation
- """
- implementations = self.registry.create_platform_implementations(platform, client)
- return implementations.event_parser
-
- def get_supported_platforms(self) -> list[Platform]:
- """Get list of supported platforms.
-
- Returns:
- List of supported platforms
- """
- return self.registry.get_supported_platforms()
-
-
-# Global factory instance
-platform_factory = PlatformFactory()
-
-
-def get_platform_implementations(platform: Platform, client: SolanaClient) -> PlatformImplementations:
- """Convenience function to get platform implementations.
-
- Args:
- platform: Platform to get implementations for
- client: Solana RPC client
-
- Returns:
- PlatformImplementations containing all interface implementations
- """
- return platform_factory.create_for_platform(platform, client)
-
-
-def register_platform_implementations(
- platform: Platform,
- address_provider_class: type[AddressProvider],
- instruction_builder_class: type[InstructionBuilder],
- curve_manager_class: type[CurveManager],
- event_parser_class: type[EventParser]
-) -> None:
- """Register platform implementations with the global factory.
-
- Args:
- platform: Platform enum value
- address_provider_class: AddressProvider implementation class
- instruction_builder_class: InstructionBuilder implementation class
- curve_manager_class: CurveManager implementation class
- event_parser_class: EventParser implementation class
- """
- platform_factory.registry.register_platform(
- platform,
- address_provider_class,
- instruction_builder_class,
- curve_manager_class,
- event_parser_class
- )
\ No newline at end of file
diff --git a/src/monitoring/base_listener.py b/src/monitoring/base_listener.py
index 0a5e191..10184e2 100644
--- a/src/monitoring/base_listener.py
+++ b/src/monitoring/base_listener.py
@@ -1,15 +1,23 @@
"""
-Base class for WebSocket token listeners.
+Base class for WebSocket token listeners - now platform-agnostic.
"""
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
-from trading.base import TokenInfo
+from interfaces.core import Platform, TokenInfo
class BaseTokenListener(ABC):
- """Base abstract class for token listeners."""
+ """Base abstract class for token listeners - now platform-agnostic."""
+
+ def __init__(self, platform: Platform | None = None):
+ """Initialize the listener with optional platform specification.
+
+ Args:
+ platform: Platform to monitor (if None, monitor all platforms)
+ """
+ self.platform = platform
@abstractmethod
async def listen_for_tokens(
@@ -27,3 +35,16 @@ class BaseTokenListener(ABC):
creator_address: Optional creator address to filter by
"""
pass
+
+ def should_process_token(self, token_info: TokenInfo) -> bool:
+ """Check if a token should be processed based on platform filter.
+
+ Args:
+ token_info: Token information
+
+ Returns:
+ True if token should be processed
+ """
+ if self.platform is None:
+ return True # Process all platforms
+ return token_info.platform == self.platform
\ No newline at end of file
diff --git a/src/monitoring/block_event_processor.py b/src/monitoring/block_event_processor.py
deleted file mode 100644
index a3defbe..0000000
--- a/src/monitoring/block_event_processor.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""
-Event processing for pump.fun tokens.
-"""
-
-import base64
-import json
-import struct
-from typing import Any
-
-import base58
-from solders.pubkey import Pubkey
-from solders.transaction import VersionedTransaction
-
-from core.pubkeys import PumpAddresses
-from trading.base import TokenInfo
-from utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class PumpEventProcessor:
- """Processes events from pump.fun program."""
-
- # Discriminator for create instruction
- CREATE_DISCRIMINATOR = 8576854823835016728
-
- def __init__(self, pump_program: Pubkey):
- """Initialize event processor.
-
- Args:
- pump_program: Pump.fun program address
- """
- self.pump_program = pump_program
- self._idl = self._load_idl()
-
- def _load_idl(self) -> dict[str, Any]:
- """Load IDL from file.
-
- Returns:
- IDL as dictionary
- """
- try:
- with open("idl/pump_fun_idl.json") as f:
- return json.load(f)
- except Exception as e:
- logger.error(f"Failed to load IDL: {e!s}")
- # Create a minimal IDL with just what we need
- return {
- "instructions": [
- {
- "name": "create",
- "args": [
- {"name": "name", "type": "string"},
- {"name": "symbol", "type": "string"},
- {"name": "uri", "type": "string"},
- ],
- }
- ]
- }
-
- def process_transaction(self, tx_data: str) -> TokenInfo | None:
- """Process a transaction and extract token info.
-
- Args:
- tx_data: Base64 encoded transaction data
-
- Returns:
- TokenInfo if a token creation is found, None otherwise
- """
- try:
- tx_data_decoded = base64.b64decode(tx_data)
- transaction = VersionedTransaction.from_bytes(tx_data_decoded)
-
- for ix in transaction.message.instructions:
- # Check if instruction is from pump.fun program
- program_id_index = ix.program_id_index
- if program_id_index >= len(transaction.message.account_keys):
- continue
-
- program_id = transaction.message.account_keys[program_id_index]
-
- if str(program_id) != str(self.pump_program):
- continue
-
- ix_data = bytes(ix.data)
-
- # Check if it's a create instruction
- if len(ix_data) < 8:
- continue
-
- discriminator = struct.unpack("
dict[str, Any]:
- """Decode create instruction data.
-
- Args:
- ix_data: Instruction data bytes
- ix_def: Instruction definition from IDL
- accounts: List of account pubkeys
-
- Returns:
- Decoded instruction arguments
- """
- args = {}
- offset = 8 # Skip 8-byte discriminator
-
- for arg in ix_def["args"]:
- if arg["type"] == "string":
- length = struct.unpack_from(" Pubkey:
- """
- Find the creator vault for a creator.
-
- Args:
- creator: Creator address
-
- Returns:
- Creator vault address
- """
- derived_address, _ = Pubkey.find_program_address(
- [b"creator-vault", bytes(creator)],
- PumpAddresses.PROGRAM,
- )
- return derived_address
diff --git a/src/monitoring/geyser_event_processor.py b/src/monitoring/geyser_event_processor.py
deleted file mode 100644
index 3331c1b..0000000
--- a/src/monitoring/geyser_event_processor.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""
-Event processing for pump.fun tokens using Geyser data.
-"""
-
-import struct
-from typing import Final
-
-import base58
-from solders.pubkey import Pubkey
-
-from core.pubkeys import PumpAddresses
-from trading.base import TokenInfo
-from utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class GeyserEventProcessor:
- """Processes token creation events from Geyser stream."""
-
- CREATE_DISCRIMINATOR: Final[bytes] = struct.pack("
TokenInfo | None:
- """Process transaction data and extract token creation info.
-
- Args:
- instruction_data: Raw instruction data
- accounts: List of account indices
- keys: List of account public keys
-
- Returns:
- TokenInfo if token creation found, None otherwise
- """
- if not instruction_data.startswith(self.CREATE_DISCRIMINATOR):
- return None
-
- try:
- # Skip past the 8-byte discriminator
- offset = 8
-
- # Helper to read strings (prefixed with length)
- def read_string():
- nonlocal offset
- # Get string length (4-byte uint)
- length = struct.unpack_from("= len(accounts):
- return None
- account_index = accounts[index]
- if account_index >= len(keys):
- return None
- return Pubkey.from_bytes(keys[account_index])
-
- name = read_string()
- symbol = read_string()
- uri = read_string()
- creator = read_pubkey()
-
- mint = get_account_key(0)
- bonding_curve = get_account_key(2)
- associated_bonding_curve = get_account_key(3)
- user = get_account_key(7)
-
- creator_vault = self._find_creator_vault(creator)
-
- if not all([mint, bonding_curve, associated_bonding_curve, user]):
- logger.warning("Missing required account keys in token creation")
- return None
-
- return TokenInfo(
- name=name,
- symbol=symbol,
- uri=uri,
- mint=mint,
- bonding_curve=bonding_curve,
- associated_bonding_curve=associated_bonding_curve,
- user=user,
- creator=creator,
- creator_vault=creator_vault,
- )
-
- except Exception as e:
- logger.error(f"Failed to process transaction data: {e}")
- return None
-
- def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
- """
- Find the creator vault for a creator.
-
- Args:
- creator: Creator address
-
- Returns:
- Creator vault address
- """
- derived_address, _ = Pubkey.find_program_address(
- [b"creator-vault", bytes(creator)],
- PumpAddresses.PROGRAM,
- )
- return derived_address
diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py
new file mode 100644
index 0000000..93dca13
--- /dev/null
+++ b/src/monitoring/listener_factory.py
@@ -0,0 +1,137 @@
+"""
+Factory for creating platform-aware token listeners.
+"""
+
+from interfaces.core import Platform
+from monitoring.base_listener import BaseTokenListener
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+class ListenerFactory:
+ """Factory for creating appropriate token listeners based on configuration."""
+
+ @staticmethod
+ def create_listener(
+ listener_type: str,
+ wss_endpoint: str | None = None,
+ 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",
+ platforms: list[Platform] | None = None,
+ **kwargs
+ ) -> BaseTokenListener:
+ """Create a token listener based on the specified type.
+
+ Args:
+ listener_type: Type of listener ('logs', 'blocks', 'geyser', or 'pumpportal')
+ wss_endpoint: WebSocket endpoint URL (for logs/blocks listeners)
+ geyser_endpoint: Geyser gRPC endpoint URL (for geyser listener)
+ geyser_api_token: Geyser API token (for geyser listener)
+ geyser_auth_type: Geyser authentication type
+ pumpportal_url: PumpPortal WebSocket URL (for pumpportal listener)
+ platforms: List of platforms to monitor (if None, monitor all)
+ **kwargs: Additional arguments
+
+ Returns:
+ Configured token listener
+
+ Raises:
+ ValueError: If listener type is invalid or required parameters are missing
+ """
+ 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"
+ )
+
+ from monitoring.universal_geyser_listener import UniversalGeyserListener
+
+ listener = UniversalGeyserListener(
+ geyser_endpoint=geyser_endpoint,
+ geyser_api_token=geyser_api_token,
+ geyser_auth_type=geyser_auth_type,
+ platforms=platforms,
+ )
+ logger.info("Created Universal Geyser listener for token monitoring")
+ return listener
+
+ elif listener_type == "logs":
+ if not wss_endpoint:
+ raise ValueError("WebSocket endpoint is required for logs listener")
+
+ from monitoring.universal_logs_listener import UniversalLogsListener
+
+ listener = UniversalLogsListener(
+ wss_endpoint=wss_endpoint,
+ platforms=platforms,
+ )
+ logger.info("Created Universal Logs listener for token monitoring")
+ return listener
+
+ elif listener_type == "blocks":
+ if not wss_endpoint:
+ raise ValueError("WebSocket endpoint is required for blocks listener")
+
+ from monitoring.universal_block_listener import UniversalBlockListener
+
+ listener = UniversalBlockListener(
+ wss_endpoint=wss_endpoint,
+ platforms=platforms,
+ )
+ logger.info("Created Universal Block listener for token monitoring")
+ return listener
+
+ elif listener_type == "pumpportal":
+ # PumpPortal is pump.fun specific, so filter platforms
+ pumpfun_platforms = [Platform.PUMP_FUN]
+ if platforms:
+ pumpfun_platforms = [p for p in platforms if p == Platform.PUMP_FUN]
+
+ if not pumpfun_platforms:
+ raise ValueError("PumpPortal listener only supports pump.fun platform")
+
+ from monitoring.pumpportal_listener import PumpPortalListener
+
+ listener = PumpPortalListener(
+ pump_program=None, # Will be determined from platform
+ pumpportal_url=pumpportal_url,
+ )
+ logger.info("Created PumpPortal listener for token monitoring")
+ return listener
+
+ else:
+ raise ValueError(
+ f"Invalid listener type '{listener_type}'. "
+ f"Must be one of: 'logs', 'blocks', 'geyser', 'pumpportal'"
+ )
+
+ @staticmethod
+ def get_supported_listener_types() -> list[str]:
+ """Get list of supported listener types.
+
+ Returns:
+ List of supported listener type strings
+ """
+ return ["logs", "blocks", "geyser", "pumpportal"]
+
+ @staticmethod
+ def get_platform_compatible_listeners(platform: Platform) -> list[str]:
+ """Get list of listener types compatible with a specific platform.
+
+ Args:
+ platform: Platform to check compatibility for
+
+ Returns:
+ List of compatible listener types
+ """
+ if platform == Platform.PUMP_FUN:
+ return ["logs", "blocks", "geyser", "pumpportal"]
+ elif platform == Platform.LETS_BONK:
+ return ["logs", "blocks", "geyser"] # PumpPortal is pump.fun only
+ else:
+ return ["logs", "blocks", "geyser"] # Default universal listeners
\ No newline at end of file
diff --git a/src/monitoring/logs_event_processor.py b/src/monitoring/logs_event_processor.py
deleted file mode 100644
index b1bca10..0000000
--- a/src/monitoring/logs_event_processor.py
+++ /dev/null
@@ -1,174 +0,0 @@
-"""
-Event processing for pump.fun tokens using logsSubscribe data.
-"""
-
-import base64
-import struct
-from typing import Final
-
-import base58
-from solders.pubkey import Pubkey
-
-from core.pubkeys import PumpAddresses, SystemAddresses
-from trading.base import TokenInfo
-from utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class LogsEventProcessor:
- """Processes events from pump.fun program logs."""
-
- # Discriminator for create instruction to avoid non-create transactions
- CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891
-
- def __init__(self, pump_program: Pubkey):
- """Initialize event processor.
-
- Args:
- pump_program: Pump.fun program address
- """
- self.pump_program = pump_program
-
- def process_program_logs(self, logs: list[str], signature: str) -> TokenInfo | None:
- """Process program logs and extract token info.
-
- Args:
- logs: List of log strings from the notification
- signature: Transaction signature
-
- Returns:
- TokenInfo if a token creation is found, None otherwise
- """
- # Check if this is a token creation
- if not any("Program log: Instruction: Create" in log for log in logs):
- return None
-
- # Skip swaps as the first condition may pass them
- if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
- return None
-
- # Find and process program data
- for log in logs:
- if "Program data:" in log:
- try:
- encoded_data = log.split(": ")[1]
- decoded_data = base64.b64decode(encoded_data)
- parsed_data = self._parse_create_instruction(decoded_data)
-
- if parsed_data and "name" in parsed_data:
- mint = Pubkey.from_string(parsed_data["mint"])
- bonding_curve = Pubkey.from_string(parsed_data["bondingCurve"])
- associated_curve = self._find_associated_bonding_curve(
- mint, bonding_curve
- )
- creator = Pubkey.from_string(parsed_data["creator"])
- creator_vault = self._find_creator_vault(creator)
-
- return TokenInfo(
- name=parsed_data["name"],
- symbol=parsed_data["symbol"],
- uri=parsed_data["uri"],
- mint=mint,
- bonding_curve=bonding_curve,
- associated_bonding_curve=associated_curve,
- user=Pubkey.from_string(parsed_data["user"]),
- creator=creator,
- creator_vault=creator_vault,
- )
- except Exception as e:
- logger.error(f"Failed to process log data: {e}")
-
- return None
-
- def _parse_create_instruction(self, data: bytes) -> dict | None:
- """Parse the create instruction data.
-
- Args:
- data: Raw instruction data
-
- Returns:
- Dictionary of parsed data or None if parsing fails
- """
- if len(data) < 8:
- return None
-
- # Check for the correct instruction discriminator
- discriminator = struct.unpack("
Pubkey:
- """
- Find the associated bonding curve for a given mint and bonding curve.
- This uses the standard ATA derivation.
-
- Args:
- mint: Token mint address
- bonding_curve: Bonding curve address
-
- Returns:
- Associated bonding curve address
- """
- derived_address, _ = Pubkey.find_program_address(
- [
- bytes(bonding_curve),
- bytes(SystemAddresses.TOKEN_PROGRAM),
- bytes(mint),
- ],
- SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
- )
- return derived_address
-
- def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
- """
- Find the creator vault for a creator.
-
- Args:
- creator: Creator address
-
- Returns:
- Creator vault address
- """
- derived_address, _ = Pubkey.find_program_address(
- [b"creator-vault", bytes(creator)],
- PumpAddresses.PROGRAM,
- )
- return derived_address
diff --git a/src/monitoring/block_listener.py b/src/monitoring/universal_block_listener.py
similarity index 53%
rename from src/monitoring/block_listener.py
rename to src/monitoring/universal_block_listener.py
index 3775e79..fc47448 100644
--- a/src/monitoring/block_listener.py
+++ b/src/monitoring/universal_block_listener.py
@@ -1,5 +1,5 @@
"""
-WebSocket monitoring for pump.fun tokens.
+Universal block listener that works with any platform through the interface system.
"""
import asyncio
@@ -7,30 +7,61 @@ import json
from collections.abc import Awaitable, Callable
import websockets
-from solders.pubkey import Pubkey
+from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
-from monitoring.block_event_processor import PumpEventProcessor
-from trading.base import TokenInfo
+from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
-class BlockListener(BaseTokenListener):
- """WebSocket listener for pump.fun token creation events using blockSubscribe."""
+class UniversalBlockListener(BaseTokenListener):
+ """Universal block listener that works with any platform."""
- def __init__(self, wss_endpoint: str, pump_program: Pubkey):
- """Initialize token listener.
+ def __init__(
+ self,
+ wss_endpoint: str,
+ platforms: list[Platform] | None = None,
+ ):
+ """Initialize universal block listener.
Args:
wss_endpoint: WebSocket endpoint URL
- pump_program: Pump.fun program address
+ platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
+ super().__init__()
self.wss_endpoint = wss_endpoint
- self.pump_program = pump_program
- self.event_processor = PumpEventProcessor(pump_program)
self.ping_interval = 20 # seconds
+
+ # Import platform factory and get supported platforms
+ from platforms import platform_factory
+
+ if platforms is None:
+ # Monitor all supported platforms
+ self.platforms = platform_factory.get_supported_platforms()
+ else:
+ self.platforms = platforms
+
+ # Get event parsers for all platforms
+ self.platform_parsers = {}
+ self.platform_program_ids = []
+
+ for platform in self.platforms:
+ try:
+ # We'll need a dummy client for getting the parser
+ from core.client import SolanaClient
+ dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
+
+ implementations = get_platform_implementations(platform, dummy_client)
+ parser = implementations.event_parser
+ self.platform_parsers[platform] = parser
+ self.platform_program_ids.append(str(parser.get_program_id()))
+
+ logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
+
+ except Exception as e:
+ logger.warning(f"Could not register platform {platform.value}: {e}")
async def listen_for_tokens(
self,
@@ -38,17 +69,21 @@ class BlockListener(BaseTokenListener):
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
- """Listen for new token creations.
+ """Listen for new token creations using blockSubscribe.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
+ if not self.platform_parsers:
+ logger.error("No platform parsers available. Cannot listen for tokens.")
+ return
+
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
- await self._subscribe_to_program(websocket)
+ await self._subscribe_to_programs(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
@@ -58,9 +93,10 @@ class BlockListener(BaseTokenListener):
continue
logger.info(
- f"New token detected: {token_info.name} ({token_info.symbol})"
+ f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
)
+ # Apply filters
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
@@ -70,10 +106,7 @@ class BlockListener(BaseTokenListener):
)
continue
- if (
- creator_address
- and str(token_info.user) != creator_address
- ):
+ if creator_address and str(token_info.user) != creator_address:
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
@@ -90,32 +123,35 @@ class BlockListener(BaseTokenListener):
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
- async def _subscribe_to_program(self, websocket) -> None:
- """Subscribe to blocks mentioning the pump.fun program.
+ async def _subscribe_to_programs(self, websocket) -> None:
+ """Subscribe to blocks mentioning any of the monitored program IDs.
Args:
websocket: Active WebSocket connection
"""
- subscription_message = json.dumps(
- {
- "jsonrpc": "2.0",
- "id": 1,
- "method": "blockSubscribe",
- "params": [
- {"mentionsAccountOrProgram": str(self.pump_program)},
- {
- "commitment": "confirmed",
- "encoding": "base64", # base64 is faster than other encoding options
- "showRewards": False,
- "transactionDetails": "full",
- "maxSupportedTransactionVersion": 0,
- },
- ],
- }
- )
+ # For block subscriptions, we can use mentionsAccountOrProgram to monitor multiple programs
+ # We'll create separate subscriptions for each program to be more specific
+ for i, program_id in enumerate(self.platform_program_ids):
+ subscription_message = json.dumps(
+ {
+ "jsonrpc": "2.0",
+ "id": i + 1,
+ "method": "blockSubscribe",
+ "params": [
+ {"mentionsAccountOrProgram": program_id},
+ {
+ "commitment": "confirmed",
+ "encoding": "base64",
+ "showRewards": False,
+ "transactionDetails": "full",
+ "maxSupportedTransactionVersion": 0,
+ },
+ ],
+ }
+ )
- await websocket.send(subscription_message)
- logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}")
+ await websocket.send(subscription_message)
+ logger.info(f"Subscribed to blocks mentioning program: {program_id}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
@@ -140,7 +176,7 @@ class BlockListener(BaseTokenListener):
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
- """Wait for token creation event.
+ """Wait for token creation event from any platform.
Args:
websocket: Active WebSocket connection
@@ -166,15 +202,21 @@ class BlockListener(BaseTokenListener):
if "transactions" not in block:
return None
+ # Try each platform's event parser on each transaction
for tx in block["transactions"]:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
- token_info = self.event_processor.process_transaction(
- tx["transaction"][0]
- )
- if token_info:
- return token_info
+ for platform, parser in self.platform_parsers.items():
+ # Check if the parser has a block parsing method
+ if hasattr(parser, 'parse_token_creation_from_block'):
+ token_info = parser.parse_token_creation_from_block({
+ "transactions": [tx]
+ })
+ if token_info:
+ return token_info
+
+ return None
except TimeoutError:
logger.debug("No data received for 30 seconds")
@@ -184,4 +226,4 @@ class BlockListener(BaseTokenListener):
except Exception as e:
logger.error(f"Error processing WebSocket message: {e!s}")
- return None
+ return None
\ No newline at end of file
diff --git a/src/monitoring/geyser_listener.py b/src/monitoring/universal_geyser_listener.py
similarity index 60%
rename from src/monitoring/geyser_listener.py
rename to src/monitoring/universal_geyser_listener.py
index b1ba4ff..0bac6f9 100644
--- a/src/monitoring/geyser_listener.py
+++ b/src/monitoring/universal_geyser_listener.py
@@ -1,5 +1,5 @@
"""
-Geyser monitoring for pump.fun tokens.
+Universal Geyser listener that works with any platform through the interface system.
"""
import asyncio
@@ -9,34 +9,36 @@ import grpc
from solders.pubkey import Pubkey
from geyser.generated import geyser_pb2, geyser_pb2_grpc
+from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
-from monitoring.geyser_event_processor import GeyserEventProcessor
-from trading.base import TokenInfo
+from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
-class GeyserListener(BaseTokenListener):
- """Geyser listener for pump.fun token creation events."""
+class UniversalGeyserListener(BaseTokenListener):
+ """Universal Geyser listener that works with any platform."""
def __init__(
self,
geyser_endpoint: str,
geyser_api_token: str,
geyser_auth_type: str,
- pump_program: Pubkey,
+ platforms: list[Platform] | None = None,
):
- """Initialize token listener.
+ """Initialize universal Geyser listener.
Args:
geyser_endpoint: Geyser gRPC endpoint URL
geyser_api_token: API token for authentication
geyser_auth_type: authentication type ('x-token' or 'basic')
- pump_program: Pump.fun program address
+ platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
+ super().__init__()
self.geyser_endpoint = geyser_endpoint
self.geyser_api_token = geyser_api_token
+
valid_auth_types = {"x-token", "basic"}
self.auth_type: str = (geyser_auth_type or "x-token").lower()
if self.auth_type not in valid_auth_types:
@@ -44,8 +46,35 @@ class GeyserListener(BaseTokenListener):
f"Unsupported auth_type={self.auth_type!r}. "
f"Expected one of {valid_auth_types}"
)
- self.pump_program = pump_program
- self.event_processor = GeyserEventProcessor(pump_program)
+
+ # Import platform factory and get supported platforms
+ from platforms import platform_factory
+
+ if platforms is None:
+ # Monitor all supported platforms
+ self.platforms = platform_factory.get_supported_platforms()
+ else:
+ self.platforms = platforms
+
+ # Get event parsers for all platforms
+ self.platform_parsers = {}
+ self.platform_program_ids = set()
+
+ for platform in self.platforms:
+ try:
+ # We'll need a dummy client for getting the parser - this is a design issue we should fix
+ from core.client import SolanaClient
+ dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
+
+ implementations = get_platform_implementations(platform, dummy_client)
+ parser = implementations.event_parser
+ self.platform_parsers[platform] = parser
+ self.platform_program_ids.add(parser.get_program_id())
+
+ logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
+
+ except Exception as e:
+ logger.warning(f"Could not register platform {platform.value}: {e}")
async def _create_geyser_connection(self):
"""Establish a secure connection to the Geyser endpoint."""
@@ -66,12 +95,15 @@ class GeyserListener(BaseTokenListener):
return geyser_pb2_grpc.GeyserStub(channel), channel
def _create_subscription_request(self):
- """Create a subscription request for Pump.fun transactions."""
+ """Create a subscription request for all monitored platforms."""
request = geyser_pb2.SubscribeRequest()
- request.transactions["pump_filter"].account_include.append(
- str(self.pump_program)
- )
- request.transactions["pump_filter"].failed = False
+
+ # Add all platform program IDs to the filter
+ for program_id in self.platform_program_ids:
+ filter_name = f"platform_filter_{program_id}"
+ request.transactions[filter_name].account_include.append(str(program_id))
+ request.transactions[filter_name].failed = False
+
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
return request
@@ -88,15 +120,18 @@ class GeyserListener(BaseTokenListener):
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
+ if not self.platform_parsers:
+ logger.error("No platform parsers available. Cannot listen for tokens.")
+ return
+
while True:
try:
stub, channel = await self._create_geyser_connection()
request = self._create_subscription_request()
logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}")
- logger.info(
- f"Monitoring for transactions involving program: {self.pump_program}"
- )
+ logger.info(f"Monitoring platforms: {[p.value for p in self.platforms]}")
+ logger.info(f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}")
try:
async for update in stub.Subscribe(iter([request])):
@@ -105,9 +140,10 @@ class GeyserListener(BaseTokenListener):
continue
logger.info(
- f"New token detected: {token_info.name} ({token_info.symbol})"
+ f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
)
+ # Apply filters
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
@@ -156,24 +192,25 @@ class GeyserListener(BaseTokenListener):
return None
for ix in msg.instructions:
- # Skip non-Pump.fun program instructions
+ # Check which platform this instruction belongs to
program_idx = ix.program_id_index
if program_idx >= len(msg.account_keys):
continue
- program_id = msg.account_keys[program_idx]
- if bytes(program_id) != bytes(self.pump_program):
- continue
-
- # Process instruction data
- token_info = self.event_processor.process_transaction_data(
- ix.data, ix.accounts, msg.account_keys
- )
- if token_info:
- return token_info
+ program_id = Pubkey.from_bytes(msg.account_keys[program_idx])
+
+ # Find the matching platform parser
+ for platform, parser in self.platform_parsers.items():
+ if program_id == parser.get_program_id():
+ # Use the platform's event parser
+ token_info = parser.parse_token_creation_from_instruction(
+ ix.data, ix.accounts, msg.account_keys
+ )
+ if token_info:
+ return token_info
return None
except Exception as e:
logger.error(f"Error processing Geyser update: {e}")
- return None
+ return None
\ No newline at end of file
diff --git a/src/monitoring/logs_listener.py b/src/monitoring/universal_logs_listener.py
similarity index 51%
rename from src/monitoring/logs_listener.py
rename to src/monitoring/universal_logs_listener.py
index 4af072e..56c5f03 100644
--- a/src/monitoring/logs_listener.py
+++ b/src/monitoring/universal_logs_listener.py
@@ -1,5 +1,5 @@
"""
-WebSocket monitoring for pump.fun tokens using logsSubscribe.
+Universal logs listener that works with any platform through the interface system.
"""
import asyncio
@@ -7,30 +7,61 @@ import json
from collections.abc import Awaitable, Callable
import websockets
-from solders.pubkey import Pubkey
+from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
-from monitoring.logs_event_processor import LogsEventProcessor
-from trading.base import TokenInfo
+from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
-class LogsListener(BaseTokenListener):
- """WebSocket listener for pump.fun token creation events using logsSubscribe."""
+class UniversalLogsListener(BaseTokenListener):
+ """Universal logs listener that works with any platform."""
- def __init__(self, wss_endpoint: str, pump_program: Pubkey):
- """Initialize token listener.
+ def __init__(
+ self,
+ wss_endpoint: str,
+ platforms: list[Platform] | None = None,
+ ):
+ """Initialize universal logs listener.
Args:
wss_endpoint: WebSocket endpoint URL
- pump_program: Pump.fun program address
+ platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
+ super().__init__()
self.wss_endpoint = wss_endpoint
- self.pump_program = pump_program
- self.event_processor = LogsEventProcessor(pump_program)
self.ping_interval = 20 # seconds
+
+ # Import platform factory and get supported platforms
+ from platforms import platform_factory
+
+ if platforms is None:
+ # Monitor all supported platforms
+ self.platforms = platform_factory.get_supported_platforms()
+ else:
+ self.platforms = platforms
+
+ # Get event parsers for all platforms
+ self.platform_parsers = {}
+ self.platform_program_ids = []
+
+ for platform in self.platforms:
+ try:
+ # We'll need a dummy client for getting the parser
+ from core.client import SolanaClient
+ dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
+
+ implementations = get_platform_implementations(platform, dummy_client)
+ parser = implementations.event_parser
+ self.platform_parsers[platform] = parser
+ self.platform_program_ids.append(str(parser.get_program_id()))
+
+ logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
+
+ except Exception as e:
+ logger.warning(f"Could not register platform {platform.value}: {e}")
async def listen_for_tokens(
self,
@@ -45,6 +76,10 @@ class LogsListener(BaseTokenListener):
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
+ if not self.platform_parsers:
+ logger.error("No platform parsers available. Cannot listen for tokens.")
+ return
+
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
@@ -58,9 +93,10 @@ class LogsListener(BaseTokenListener):
continue
logger.info(
- f"New token detected: {token_info.name} ({token_info.symbol})"
+ f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
)
+ # Apply filters
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
@@ -70,10 +106,7 @@ class LogsListener(BaseTokenListener):
)
continue
- if (
- creator_address
- and str(token_info.user) != creator_address
- ):
+ if creator_address and str(token_info.user) != creator_address:
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
@@ -86,38 +119,40 @@ class LogsListener(BaseTokenListener):
ping_task.cancel()
except Exception as e:
- logger.error(f"WebSocket connection error: {str(e)}")
+ logger.error(f"WebSocket connection error: {e!s}")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_logs(self, websocket) -> None:
- """Subscribe to logs mentioning the pump.fun program.
+ """Subscribe to logs mentioning any of the monitored program IDs.
Args:
websocket: Active WebSocket connection
"""
- subscription_message = json.dumps(
- {
- "jsonrpc": "2.0",
- "id": 1,
- "method": "logsSubscribe",
- "params": [
- {"mentions": [str(self.pump_program)]},
- {"commitment": "processed"},
- ],
- }
- )
+ # Subscribe to logs for all monitored platforms
+ for program_id in self.platform_program_ids:
+ subscription_message = json.dumps(
+ {
+ "jsonrpc": "2.0",
+ "id": len(self.platform_program_ids), # Use different IDs
+ "method": "logsSubscribe",
+ "params": [
+ {"mentions": [program_id]},
+ {"commitment": "processed"},
+ ],
+ }
+ )
- await websocket.send(subscription_message)
- logger.info(f"Subscribed to logs mentioning program: {self.pump_program}")
+ await websocket.send(subscription_message)
+ logger.info(f"Subscribed to logs mentioning program: {program_id}")
- # Wait for subscription confirmation
- response = await websocket.recv()
- response_data = json.loads(response)
- if "result" in response_data:
- logger.info(f"Subscription confirmed with ID: {response_data['result']}")
- else:
- logger.warning(f"Unexpected subscription response: {response}")
+ # Wait for subscription confirmation
+ response = await websocket.recv()
+ response_data = json.loads(response)
+ if "result" in response_data:
+ logger.info(f"Subscription confirmed with ID: {response_data['result']}")
+ else:
+ logger.warning(f"Unexpected subscription response: {response}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
@@ -131,7 +166,7 @@ class LogsListener(BaseTokenListener):
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
- except asyncio.TimeoutError:
+ except TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
@@ -139,9 +174,17 @@ class LogsListener(BaseTokenListener):
except asyncio.CancelledError:
pass
except Exception as e:
- logger.error(f"Ping error: {str(e)}")
+ logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
+ """Wait for token creation events from any platform.
+
+ Args:
+ websocket: Active WebSocket connection
+
+ Returns:
+ TokenInfo if a token creation is found, None otherwise
+ """
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
@@ -153,15 +196,20 @@ class LogsListener(BaseTokenListener):
logs = log_data.get("logs", [])
signature = log_data.get("signature", "unknown")
- # Use the processor to extract token info
- return self.event_processor.process_program_logs(logs, signature)
+ # Try each platform's event parser
+ for platform, parser in self.platform_parsers.items():
+ token_info = parser.parse_token_creation_from_logs(logs, signature)
+ if token_info:
+ return token_info
- except asyncio.TimeoutError:
+ return None
+
+ except TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception as e:
- logger.error(f"Error processing WebSocket message: {str(e)}")
+ logger.error(f"Error processing WebSocket message: {e!s}")
- return None
+ return None
\ No newline at end of file
diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py
index ea584fb..9348112 100644
--- a/src/platforms/__init__.py
+++ b/src/platforms/__init__.py
@@ -6,7 +6,7 @@ platform-specific implementations of the trading interfaces.
"""
from dataclasses import dataclass
-from typing import Any, Dict, Optional, Type
+from typing import Any
from core.client import SolanaClient
from interfaces.core import (
@@ -17,8 +17,6 @@ from interfaces.core import (
Platform,
)
-from . import letsbonk, pumpfun
-
@dataclass
class PlatformImplementations:
@@ -145,21 +143,45 @@ class PlatformFactory:
self._setup_default_platforms()
def _setup_default_platforms(self) -> None:
- """Setup default platform registrations.
+ """Setup default platform registrations."""
+ # Import platform implementations dynamically to avoid circular imports
+ try:
+ from platforms.pumpfun import (
+ PumpFunAddressProvider,
+ PumpFunCurveManager,
+ PumpFunEventParser,
+ PumpFunInstructionBuilder,
+ )
+
+ self.registry.register_platform(
+ Platform.PUMP_FUN,
+ PumpFunAddressProvider,
+ PumpFunInstructionBuilder,
+ PumpFunCurveManager,
+ PumpFunEventParser
+ )
+
+ except ImportError as e:
+ print(f"Warning: Could not register pump.fun platform: {e}")
- This will be populated as we implement each platform.
- """
- # Platforms will be registered here as they are implemented
- # Example:
- # from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ...
- # self.registry.register_platform(
- # Platform.PUMP_FUN,
- # PumpFunAddressProvider,
- # PumpFunInstructionBuilder,
- # PumpFunCurveManager,
- # PumpFunEventParser
- # )
- pass
+ try:
+ from platforms.letsbonk import (
+ LetsBonkAddressProvider,
+ LetsBonkCurveManager,
+ LetsBonkEventParser,
+ LetsBonkInstructionBuilder,
+ )
+
+ self.registry.register_platform(
+ Platform.LETS_BONK,
+ LetsBonkAddressProvider,
+ LetsBonkInstructionBuilder,
+ LetsBonkCurveManager,
+ LetsBonkEventParser
+ )
+
+ except ImportError as e:
+ print(f"Warning: Could not register LetsBonk platform: {e}")
def create_for_platform(
self,
diff --git a/src/platforms/letsbonk/__init__.py b/src/platforms/letsbonk/__init__.py
index 8e494e8..60c1cfc 100644
--- a/src/platforms/letsbonk/__init__.py
+++ b/src/platforms/letsbonk/__init__.py
@@ -1,27 +1,15 @@
"""
-LetsBonk platform registration and exports.
+LetsBonk platform exports.
-This module registers all LetsBonk implementations with the platform factory
-and provides convenient imports for the LetsBonk platform.
+This module provides convenient imports for the LetsBonk platform implementations.
+Platform registration is now handled by the main platform factory.
"""
-from interfaces.core import Platform
-from platforms import register_platform_implementations
-
from .address_provider import LetsBonkAddressProvider
from .curve_manager import LetsBonkCurveManager
from .event_parser import LetsBonkEventParser
from .instruction_builder import LetsBonkInstructionBuilder
-# Register LetsBonk platform implementations
-register_platform_implementations(
- Platform.LETS_BONK,
- LetsBonkAddressProvider,
- LetsBonkInstructionBuilder,
- LetsBonkCurveManager,
- LetsBonkEventParser
-)
-
# Export implementations for direct use if needed
__all__ = [
'LetsBonkAddressProvider',
diff --git a/src/platforms/pumpfun/__init__.py b/src/platforms/pumpfun/__init__.py
index 38c8f53..bc31eb3 100644
--- a/src/platforms/pumpfun/__init__.py
+++ b/src/platforms/pumpfun/__init__.py
@@ -1,27 +1,15 @@
"""
-Pump.Fun platform registration and exports.
+Pump.Fun platform exports.
-This module registers all pump.fun implementations with the platform factory
-and provides convenient imports for the pump.fun platform.
+This module provides convenient imports for the pump.fun platform implementations.
+Platform registration is now handled by the main platform factory.
"""
-from interfaces.core import Platform
-from platforms import register_platform_implementations
-
from .address_provider import PumpFunAddressProvider
from .curve_manager import PumpFunCurveManager
from .event_parser import PumpFunEventParser
from .instruction_builder import PumpFunInstructionBuilder
-# Register pump.fun platform implementations
-register_platform_implementations(
- Platform.PUMP_FUN,
- PumpFunAddressProvider,
- PumpFunInstructionBuilder,
- PumpFunCurveManager,
- PumpFunEventParser
-)
-
# Export implementations for direct use if needed
__all__ = [
'PumpFunAddressProvider',
diff --git a/src/trading/base.py b/src/trading/base.py
index ee7eb63..e2cb83d 100644
--- a/src/trading/base.py
+++ b/src/trading/base.py
@@ -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',
]
\ No newline at end of file
diff --git a/src/trading/buyer.py b/src/trading/buyer.py
deleted file mode 100644
index 12dc3f4..0000000
--- a/src/trading/buyer.py
+++ /dev/null
@@ -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("
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("
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
diff --git a/src/trading/seller.py b/src/trading/seller.py
deleted file mode 100644
index ac8b3e5..0000000
--- a/src/trading/seller.py
+++ /dev/null
@@ -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("
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("
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
\ No newline at end of file
From 227ef0b3bdcbb41cc01f740e79af109c1b3a1204 Mon Sep 17 00:00:00 2001
From: smypmsa
None:
- """Listen for new token creations using Geyser subscription.
-
- Args:
- token_callback: Callback function for new tokens
- match_string: Optional string to match in token name/symbol
- creator_address: Optional creator address to filter by
- """
+ """Listen for new token creations using Geyser subscription."""
if not self.platform_parsers:
logger.error("No platform parsers available. Cannot listen for tokens.")
return
@@ -161,8 +148,12 @@ class UniversalGeyserListener(BaseTokenListener):
await token_callback(token_info)
- except grpc.aio.AioRpcError as e:
- logger.error(f"gRPC error: {e.details()}")
+ except Exception as e:
+ import grpc
+ if isinstance(e, grpc.aio.AioRpcError):
+ logger.error(f"gRPC error: {e.details()}")
+ else:
+ logger.error(f"Geyser error: {e}")
await asyncio.sleep(5)
finally:
@@ -174,14 +165,7 @@ class UniversalGeyserListener(BaseTokenListener):
await asyncio.sleep(10)
async def _process_update(self, update) -> TokenInfo | None:
- """Process a Geyser update and extract token creation info.
-
- Args:
- update: Geyser update from the subscription
-
- Returns:
- TokenInfo if a token creation is found, None otherwise
- """
+ """Process a Geyser update and extract token creation info."""
try:
if not update.HasField("transaction"):
return None
@@ -191,6 +175,8 @@ class UniversalGeyserListener(BaseTokenListener):
if msg is None:
return None
+ from solders.pubkey import Pubkey
+
for ix in msg.instructions:
# Check which platform this instruction belongs to
program_idx = ix.program_id_index
diff --git a/src/monitoring/universal_logs_listener.py b/src/monitoring/universal_logs_listener.py
index 56c5f03..5ebb32f 100644
--- a/src/monitoring/universal_logs_listener.py
+++ b/src/monitoring/universal_logs_listener.py
@@ -1,7 +1,6 @@
"""
Universal logs listener that works with any platform through the interface system.
"""
-
import asyncio
import json
from collections.abc import Awaitable, Callable
@@ -10,7 +9,6 @@ import websockets
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
-from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
@@ -47,13 +45,13 @@ class UniversalLogsListener(BaseTokenListener):
self.platform_parsers = {}
self.platform_program_ids = []
+ # Create a temporary client for getting parsers (stateless parsers don't use it)
+ from core.client import SolanaClient
+ temp_client = SolanaClient("http://temp")
+
for platform in self.platforms:
try:
- # We'll need a dummy client for getting the parser
- from core.client import SolanaClient
- dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
-
- implementations = get_platform_implementations(platform, dummy_client)
+ implementations = platform_factory.create_for_platform(platform, temp_client)
parser = implementations.event_parser
self.platform_parsers[platform] = parser
self.platform_program_ids.append(str(parser.get_program_id()))
@@ -130,11 +128,11 @@ class UniversalLogsListener(BaseTokenListener):
websocket: Active WebSocket connection
"""
# Subscribe to logs for all monitored platforms
- for program_id in self.platform_program_ids:
+ for i, program_id in enumerate(self.platform_program_ids):
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
- "id": len(self.platform_program_ids), # Use different IDs
+ "id": i + 1,
"method": "logsSubscribe",
"params": [
{"mentions": [program_id]},
@@ -155,11 +153,7 @@ class UniversalLogsListener(BaseTokenListener):
logger.warning(f"Unexpected subscription response: {response}")
async def _ping_loop(self, websocket) -> None:
- """Keep connection alive with pings.
-
- Args:
- websocket: Active WebSocket connection
- """
+ """Keep connection alive with pings."""
try:
while True:
await asyncio.sleep(self.ping_interval)
@@ -168,7 +162,6 @@ class UniversalLogsListener(BaseTokenListener):
await asyncio.wait_for(pong_waiter, timeout=10)
except TimeoutError:
logger.warning("Ping timeout - server not responding")
- # Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
@@ -177,14 +170,7 @@ class UniversalLogsListener(BaseTokenListener):
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
- """Wait for token creation events from any platform.
-
- Args:
- websocket: Active WebSocket connection
-
- Returns:
- TokenInfo if a token creation is found, None otherwise
- """
+ """Wait for token creation events from any platform."""
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py
index 9348112..7c0db22 100644
--- a/src/platforms/__init__.py
+++ b/src/platforms/__init__.py
@@ -32,7 +32,7 @@ class PlatformRegistry:
def __init__(self):
self._implementations: dict[Platform, dict[str, type]] = {}
- self._instances: dict[Platform, PlatformImplementations] = {}
+ self._instances: dict[tuple[Platform, str], PlatformImplementations] = {}
def register_platform(
self,
@@ -80,13 +80,16 @@ class PlatformRegistry:
if platform not in self._implementations:
raise ValueError(f"Platform {platform} is not registered")
- # Check if we already have instances for this platform
- if platform in self._instances:
- return self._instances[platform]
+ # Use client address as cache key to allow multiple clients
+ cache_key = (platform, str(client.rpc_endpoint))
+
+ # Check if we already have instances for this platform + client combo
+ if cache_key in self._instances:
+ return self._instances[cache_key]
impl_classes = self._implementations[platform]
- # Create instances
+ # Create instances - only curve_manager needs client
address_provider = impl_classes['address_provider']()
instruction_builder = impl_classes['instruction_builder']()
curve_manager = impl_classes['curve_manager'](client)
@@ -100,20 +103,22 @@ class PlatformRegistry:
)
# Cache the instances
- self._instances[platform] = implementations
+ self._instances[cache_key] = implementations
return implementations
- def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None:
+ def get_platform_implementations(self, platform: Platform, client_endpoint: str) -> PlatformImplementations | None:
"""Get cached platform implementations.
Args:
platform: Platform to get implementations for
+ client_endpoint: Client endpoint for cache lookup
Returns:
PlatformImplementations if available, None otherwise
"""
- return self._instances.get(platform)
+ cache_key = (platform, client_endpoint)
+ return self._instances.get(cache_key)
def get_supported_platforms(self) -> list[Platform]:
"""Get list of supported platforms.
@@ -144,7 +149,7 @@ class PlatformFactory:
def _setup_default_platforms(self) -> None:
"""Setup default platform registrations."""
- # Import platform implementations dynamically to avoid circular imports
+ # Import and register pump.fun platform
try:
from platforms.pumpfun import (
PumpFunAddressProvider,
@@ -164,6 +169,7 @@ class PlatformFactory:
except ImportError as e:
print(f"Warning: Could not register pump.fun platform: {e}")
+ # Import and register LetsBonk platform
try:
from platforms.letsbonk import (
LetsBonkAddressProvider,
diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py
index aa18242..8480647 100644
--- a/src/platforms/letsbonk/event_parser.py
+++ b/src/platforms/letsbonk/event_parser.py
@@ -5,11 +5,13 @@ This module parses LetsBonk-specific token creation events from various sources
by implementing the EventParser interface.
"""
+import base64
import struct
from time import monotonic
from typing import Any, Final
from solders.pubkey import Pubkey
+from solders.transaction import VersionedTransaction
from interfaces.core import EventParser, Platform, TokenInfo
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
@@ -184,6 +186,108 @@ class LetsBonkEventParser(EventParser):
"""
return [self.INITIALIZE_DISCRIMINATOR]
+ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
+ """Parse token creation from block data (for block listener).
+
+ Args:
+ block_data: Block data from WebSocket
+
+ Returns:
+ TokenInfo if token creation found, None otherwise
+ """
+ try:
+ if "transactions" not in block_data:
+ return None
+
+ for tx in block_data["transactions"]:
+ if not isinstance(tx, dict) or "transaction" not in tx:
+ continue
+
+ # Decode base64 transaction data if needed
+ tx_data = tx["transaction"]
+ if isinstance(tx_data, list) and len(tx_data) > 0:
+ try:
+ tx_data_encoded = tx_data[0]
+ tx_data_decoded = base64.b64decode(tx_data_encoded)
+ transaction = VersionedTransaction.from_bytes(tx_data_decoded)
+
+ for ix in transaction.message.instructions:
+ program_id = transaction.message.account_keys[ix.program_id_index]
+
+ # Check if instruction is from LetsBonk program
+ if str(program_id) != str(self.get_program_id()):
+ continue
+
+ ix_data = bytes(ix.data)
+
+ # Check for initialize discriminator
+ if len(ix_data) >= 8:
+ discriminator = struct.unpack("
= len(message["accountKeys"]):
+ continue
+
+ program_id_str = message["accountKeys"][program_idx]
+ if program_id_str != str(self.get_program_id()):
+ continue
+
+ # Decode instruction data
+ ix_data = base64.b64decode(ix["data"])
+
+ if len(ix_data) >= 8:
+ discriminator = struct.unpack("
dict | None:
"""Parse the initialize instruction data from LetsBonk.
@@ -240,33 +344,4 @@ class LetsBonkEventParser(EventParser):
return parsed_data
except Exception:
- return None
-
- def parse_token_creation_from_block(self, block_data: dict) -> list[TokenInfo]:
- """Parse token creations from block data (for block listener).
-
- Args:
- block_data: Block data from WebSocket
-
- Returns:
- List of TokenInfo for any token creations found
- """
- tokens = []
-
- try:
- if "transactions" not in block_data:
- return tokens
-
- for tx in block_data["transactions"]:
- if not isinstance(tx, dict) or "transaction" not in tx:
- continue
-
- # Process transaction (implementation would be similar to pump.fun)
- # This is a simplified version - full implementation would decode
- # the transaction and check for LetsBonk initialize instructions
- pass
-
- return tokens
-
- except Exception:
- return tokens
\ No newline at end of file
+ return None
\ No newline at end of file
diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py
index 7770991..ba95bbc 100644
--- a/src/trading/platform_aware.py
+++ b/src/trading/platform_aware.py
@@ -1,8 +1,6 @@
"""
Platform-aware trader implementations that use the interface system.
-
-This module provides new trader classes that work with any platform
-through the interface system, while maintaining compatibility with existing code.
+Final cleanup removing all platform-specific hardcoding.
"""
from solders.pubkey import Pubkey
@@ -33,18 +31,7 @@ class PlatformAwareBuyer(Trader):
extreme_fast_token_amount: int = 0,
extreme_fast_mode: bool = False,
):
- """Initialize platform-aware token buyer.
-
- Args:
- client: Solana client for RPC calls
- wallet: Wallet for signing transactions
- priority_fee_manager: Priority fee 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 pool state for price estimation
- """
+ """Initialize platform-aware token buyer."""
self.client = client
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
@@ -55,14 +42,7 @@ class PlatformAwareBuyer(Trader):
self.extreme_fast_token_amount = extreme_fast_token_amount
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
- """Execute buy operation using platform-specific implementations.
-
- Args:
- token_info: Enhanced token information with platform
-
- Returns:
- TradeResult with buy outcome
- """
+ """Execute buy operation using platform-specific implementations."""
try:
# Get platform-specific implementations
implementations = get_platform_implementations(token_info.platform, self.client)
@@ -78,7 +58,7 @@ class PlatformAwareBuyer(Trader):
token_amount = self.extreme_fast_token_amount
token_price_sol = self.amount / token_amount if token_amount > 0 else 0
else:
- # Get pool address based on platform
+ # Get pool address based on platform using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
# Regular behavior with RPC call
@@ -92,7 +72,7 @@ class PlatformAwareBuyer(Trader):
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + self.slippage))
- # Build buy instructions
+ # Build buy instructions using platform-specific builder
instructions = await instruction_builder.build_buy_instruction(
token_info,
self.wallet.pubkey,
@@ -107,7 +87,7 @@ class PlatformAwareBuyer(Trader):
)
logger.info(
- f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token"
+ f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token on {token_info.platform.value}"
)
logger.info(
f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)"
@@ -151,22 +131,17 @@ class PlatformAwareBuyer(Trader):
)
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
- """Get the pool/curve address for price calculations.
-
- Args:
- token_info: Token information
- address_provider: Platform address provider
-
- Returns:
- Pool/curve address
- """
+ """Get the pool/curve address for price calculations using platform-agnostic method."""
+ # Try to get the address from token_info first, then derive if needed
if token_info.platform == Platform.PUMP_FUN:
- return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
+ if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
+ return token_info.bonding_curve
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)
+ if hasattr(token_info, 'pool_state') and token_info.pool_state:
+ return token_info.pool_state
+
+ # Fallback to deriving the address using platform provider
+ return address_provider.derive_pool_address(token_info.mint)
class PlatformAwareSeller(Trader):
@@ -180,15 +155,7 @@ class PlatformAwareSeller(Trader):
slippage: float = 0.25,
max_retries: int = 5,
):
- """Initialize platform-aware token seller.
-
- Args:
- client: Solana client for RPC calls
- wallet: Wallet for signing transactions
- priority_fee_manager: Priority fee manager
- slippage: Slippage tolerance (0.25 = 25%)
- max_retries: Maximum number of retry attempts
- """
+ """Initialize platform-aware token seller."""
self.client = client
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
@@ -196,14 +163,7 @@ class PlatformAwareSeller(Trader):
self.max_retries = max_retries
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
- """Execute sell operation using platform-specific implementations.
-
- Args:
- token_info: Enhanced token information with platform
-
- Returns:
- TradeResult with sell outcome
- """
+ """Execute sell operation using platform-specific implementations."""
try:
# Get platform-specific implementations
implementations = get_platform_implementations(token_info.platform, self.client)
@@ -229,7 +189,7 @@ class PlatformAwareSeller(Trader):
error_message="No tokens to sell"
)
- # Get pool address and current price
+ # Get pool address and current price using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
token_price_sol = await curve_manager.calculate_price(pool_address)
@@ -239,13 +199,13 @@ class PlatformAwareSeller(Trader):
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
min_sol_output = int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL)
- logger.info(f"Selling {token_balance_decimal} tokens")
+ logger.info(f"Selling {token_balance_decimal} tokens on {token_info.platform.value}")
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"
)
- # Build sell instructions
+ # Build sell instructions using platform-specific builder
instructions = await instruction_builder.build_sell_instruction(
token_info,
self.wallet.pubkey,
@@ -297,19 +257,14 @@ class PlatformAwareSeller(Trader):
)
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
- """Get the pool/curve address for price calculations.
-
- Args:
- token_info: Token information
- address_provider: Platform address provider
-
- Returns:
- Pool/curve address
- """
+ """Get the pool/curve address for price calculations using platform-agnostic method."""
+ # Try to get the address from token_info first, then derive if needed
if token_info.platform == Platform.PUMP_FUN:
- return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
+ if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
+ return token_info.bonding_curve
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)
\ No newline at end of file
+ if hasattr(token_info, 'pool_state') and token_info.pool_state:
+ return token_info.pool_state
+
+ # Fallback to deriving the address using platform provider
+ return address_provider.derive_pool_address(token_info.mint)
\ No newline at end of file
diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py
index 2e10ae4..40c4004 100644
--- a/src/trading/universal_trader.py
+++ b/src/trading/universal_trader.py
@@ -1,6 +1,6 @@
"""
Universal trading coordinator that works with any platform.
-Replaces PumpTrader with platform-agnostic implementation.
+Cleaned up to remove all platform-specific hardcoding.
"""
import asyncio
@@ -84,18 +84,7 @@ class UniversalTrader:
marry_mode: bool = False,
yolo_mode: bool = False,
):
- """Initialize the universal trader.
-
- Args:
- rpc_endpoint: RPC endpoint URL
- wss_endpoint: WebSocket endpoint URL
- private_key: Wallet private key
- buy_amount: Amount of SOL to spend on buys
- buy_slippage: Slippage tolerance for buys
- sell_slippage: Slippage tolerance for sells
- platform: Platform to trade on (Platform enum or string)
- ... (other args same as PumpTrader)
- """
+ """Initialize the universal trader."""
# Core components
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
@@ -116,6 +105,15 @@ class UniversalTrader:
logger.info(f"Initialized Universal Trader for platform: {self.platform.value}")
+ # Validate platform support
+ try:
+ from platforms import platform_factory
+ if not platform_factory.registry.is_platform_supported(self.platform):
+ raise ValueError(f"Platform {self.platform.value} is not supported")
+ except Exception as e:
+ logger.error(f"Platform validation failed: {e}")
+ raise
+
# Get platform-specific implementations
self.platform_implementations = get_platform_implementations(
self.platform, self.solana_client
@@ -141,7 +139,7 @@ class UniversalTrader:
max_retries,
)
- # Initialize the appropriate listener
+ # Initialize the appropriate listener with platform filtering
self.token_listener = ListenerFactory.create_listener(
listener_type=listener_type,
wss_endpoint=wss_endpoint,
@@ -253,11 +251,7 @@ class UniversalTrader:
logger.info("Universal Trader has shut down")
async def _wait_for_token(self) -> TokenInfo | None:
- """Wait for a single token to be detected.
-
- Returns:
- TokenInfo or None if timeout occurs
- """
+ """Wait for a single token to be detected."""
# Create a one-time event to signal when a token is found
token_found = asyncio.Event()
found_token = None
@@ -322,11 +316,7 @@ class UniversalTrader:
await self.solana_client.close()
async def _queue_token(self, token_info: TokenInfo) -> None:
- """Queue a token for processing if not already processed.
-
- Args:
- token_info: Token information to queue
- """
+ """Queue a token for processing if not already processed."""
token_key = str(token_info.mint)
if token_key in self.processed_tokens:
@@ -360,7 +350,6 @@ class UniversalTrader:
await self._handle_token(token_info)
except asyncio.CancelledError:
- # Handle cancellation gracefully
logger.info("Token queue processor was cancelled")
break
except Exception as e:
@@ -369,11 +358,7 @@ class UniversalTrader:
self.token_queue.task_done()
async def _handle_token(self, token_info: TokenInfo) -> None:
- """Handle a new token creation event.
-
- Args:
- token_info: Token information
- """
+ """Handle a new token creation event."""
try:
# Validate that token is for our platform
if token_info.platform != self.platform:
@@ -404,12 +389,7 @@ class UniversalTrader:
logger.error(f"Error handling token {token_info.symbol}: {e!s}")
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
- """
+ """Handle successful token purchase."""
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)
@@ -426,12 +406,7 @@ class UniversalTrader:
logger.info("Marry mode enabled. Skipping sell operation.")
async def _handle_failed_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
- """Handle failed token purchase.
-
- Args:
- token_info: Token information
- buy_result: The result of the buy operation
- """
+ """Handle failed token purchase."""
logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}")
# Close ATA if enabled
await handle_cleanup_after_failure(
@@ -445,12 +420,7 @@ class UniversalTrader:
)
async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
- """Handle take profit/stop loss exit strategy.
-
- Args:
- token_info: Token information
- buy_result: Result from the buy operation
- """
+ """Handle take profit/stop loss exit strategy."""
# Create position
position = Position.create_from_buy_result(
mint=token_info.mint,
@@ -472,11 +442,7 @@ class UniversalTrader:
await self._monitor_position_until_exit(token_info, position)
async def _handle_time_based_exit(self, token_info: TokenInfo) -> None:
- """Handle legacy time-based exit strategy.
-
- Args:
- token_info: Token information
- """
+ """Handle legacy time-based exit strategy."""
logger.info(f"Waiting for {self.wait_time_after_buy} seconds before selling...")
await asyncio.sleep(self.wait_time_after_buy)
@@ -500,15 +466,10 @@ class UniversalTrader:
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:
- """Monitor a position until exit conditions are met.
-
- Args:
- token_info: Token information
- position: Position to monitor
- """
+ """Monitor a position until exit conditions are met."""
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
- # Get pool address for price monitoring
+ # Get pool address for price monitoring using platform-agnostic method
pool_address = self._get_pool_address(token_info)
curve_manager = self.platform_implementations.curve_manager
@@ -570,35 +531,25 @@ class UniversalTrader:
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
- """
+ """Get the pool/curve address for price monitoring using platform-agnostic method."""
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)
+ # Use platform-specific logic to get the appropriate address
+ if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
+ return token_info.bonding_curve
+ elif hasattr(token_info, 'pool_state') and token_info.pool_state:
+ return token_info.pool_state
else:
- # Fallback to deriving the address
+ # Fallback to deriving the address using platform provider
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.
-
- Args:
- token_info: Token information
- """
+ """Save token information to a file."""
try:
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
- # Convert to dictionary for saving
+ # Convert to dictionary for saving - platform-agnostic
token_dict = {
"name": token_info.name,
"symbol": token_info.symbol,
@@ -607,14 +558,22 @@ class UniversalTrader:
"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,
+ "creation_timestamp": token_info.creation_timestamp,
}
+
+ # Add platform-specific fields only if they exist
+ platform_fields = {
+ "bonding_curve": token_info.bonding_curve,
+ "associated_bonding_curve": token_info.associated_bonding_curve,
+ "creator_vault": token_info.creator_vault,
+ "pool_state": token_info.pool_state,
+ "base_vault": token_info.base_vault,
+ "quote_vault": token_info.quote_vault,
+ }
+
+ for field_name, field_value in platform_fields.items():
+ if field_value is not None:
+ token_dict[field_name] = str(field_value)
with open(file_name, "w") as file:
file.write(json.dumps(token_dict, indent=2))
@@ -624,15 +583,7 @@ class UniversalTrader:
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:
- """Log trade information.
-
- Args:
- action: Trade action (buy/sell)
- token_info: Token information
- price: Token price in SOL
- amount: Trade amount in SOL
- tx_hash: Transaction hash
- """
+ """Log trade information."""
try:
os.makedirs("trades", exist_ok=True)
From 8ee6b95d2d7879b1021976c6ad7fe9d8bc86afd6 Mon Sep 17 00:00:00 2001
From: smypmsa
float:
+ """Token reserves in decimal form."""
+ return self.virtual_token_reserves / 10**TOKEN_DECIMALS
+
+ @property
+ def sol_reserves(self) -> float:
+ """SOL reserves in decimal form."""
+ return self.virtual_sol_reserves / LAMPORTS_PER_SOL
+
+ def calculate_price(self) -> float:
+ """Calculate current token price in SOL."""
+ if self.virtual_token_reserves <= 0:
+ return 0.0
+
+ # Price = sol_reserves / token_reserves
+ price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves
+ return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
class PumpFunCurveManager(CurveManager):
@@ -25,7 +64,6 @@ class PumpFunCurveManager(CurveManager):
client: Solana RPC client
"""
self.client = client
- self.bonding_curve_manager = BondingCurveManager(client)
@property
def platform(self) -> Platform:
@@ -41,22 +79,31 @@ class PumpFunCurveManager(CurveManager):
Returns:
Dictionary containing bonding curve state data
"""
- curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
-
- return {
- "virtual_token_reserves": curve_state.virtual_token_reserves,
- "virtual_sol_reserves": curve_state.virtual_sol_reserves,
- "real_token_reserves": curve_state.real_token_reserves,
- "real_sol_reserves": curve_state.real_sol_reserves,
- "token_total_supply": curve_state.token_total_supply,
- "complete": curve_state.complete,
- "creator": str(curve_state.creator),
+ try:
+ account = await self.client.get_account_info(pool_address)
+ if not account.data:
+ raise ValueError(f"No data in bonding curve account {pool_address}")
- # Calculated fields for convenience
- "price_per_token": curve_state.calculate_price(),
- "token_reserves_decimal": curve_state.token_reserves,
- "sol_reserves_decimal": curve_state.sol_reserves,
- }
+ curve_state = self._decode_curve_state(account.data)
+
+ return {
+ "virtual_token_reserves": curve_state.virtual_token_reserves,
+ "virtual_sol_reserves": curve_state.virtual_sol_reserves,
+ "real_token_reserves": curve_state.real_token_reserves,
+ "real_sol_reserves": curve_state.real_sol_reserves,
+ "token_total_supply": curve_state.token_total_supply,
+ "complete": curve_state.complete,
+ "creator": str(curve_state.creator),
+
+ # Calculated fields for convenience
+ "price_per_token": curve_state.calculate_price(),
+ "token_reserves_decimal": curve_state.token_reserves,
+ "sol_reserves_decimal": curve_state.sol_reserves,
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to get curve state: {e!s}")
+ raise ValueError(f"Invalid bonding curve state: {e!s}")
async def calculate_price(self, pool_address: Pubkey) -> float:
"""Calculate current token price from bonding curve state.
@@ -67,7 +114,8 @@ class PumpFunCurveManager(CurveManager):
Returns:
Current token price in SOL
"""
- return await self.bonding_curve_manager.calculate_price(pool_address)
+ curve_state = await self._get_curve_state_object(pool_address)
+ return curve_state.calculate_price()
async def calculate_buy_amount_out(
self,
@@ -85,7 +133,7 @@ class PumpFunCurveManager(CurveManager):
Returns:
Expected amount of tokens to receive (in raw token units)
"""
- curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ curve_state = await self._get_curve_state_object(pool_address)
# Use virtual reserves for bonding curve calculation
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
@@ -114,7 +162,7 @@ class PumpFunCurveManager(CurveManager):
Returns:
Expected amount of SOL to receive (in lamports)
"""
- curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ curve_state = await self._get_curve_state_object(pool_address)
# Use virtual reserves for bonding curve calculation
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
@@ -136,14 +184,106 @@ class PumpFunCurveManager(CurveManager):
Returns:
Tuple of (token_reserves, sol_reserves) in raw units
"""
- curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ curve_state = await self._get_curve_state_object(pool_address)
return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves)
+ async def _get_curve_state_object(self, curve_address: Pubkey) -> BondingCurveState:
+ """Get the current state of a bonding curve as a BondingCurveState object.
+
+ Args:
+ curve_address: Address of the bonding curve
+
+ Returns:
+ BondingCurveState with current curve data
+
+ Raises:
+ ValueError: If curve data is invalid or inaccessible
+ """
+ try:
+ account = await self.client.get_account_info(curve_address)
+ if not account.data:
+ raise ValueError(f"No data in bonding curve account {curve_address}")
+
+ curve_state = self._decode_curve_state(account.data)
+ return curve_state
+
+ except Exception as e:
+ logger.error(f"Failed to get curve state: {e!s}")
+ raise ValueError(f"Invalid bonding curve state: {e!s}")
+
+ def _decode_curve_state(self, data: bytes) -> BondingCurveState:
+ """Decode bonding curve state from raw account data.
+
+ Args:
+ data: Raw account data
+
+ Returns:
+ Decoded BondingCurveState
+
+ Raises:
+ ValueError: If data format is invalid
+ """
+ if len(data) < 8:
+ raise ValueError("Curve data too short")
+
+ # Check discriminator
+ if not data.startswith(CURVE_DISCRIMINATOR):
+ raise ValueError("Invalid curve discriminator")
+
+ offset = 8
+
+ try:
+ # Decode based on pump.fun BondingCurve structure:
+ # - virtual_token_reserves: u64 (8 bytes)
+ # - virtual_sol_reserves: u64 (8 bytes)
+ # - real_token_reserves: u64 (8 bytes)
+ # - real_sol_reserves: u64 (8 bytes)
+ # - token_total_supply: u64 (8 bytes)
+ # - complete: bool (1 byte)
+ # - padding: 7 bytes
+ # - creator: Pubkey (32 bytes)
+
+ virtual_token_reserves = struct.unpack_from("
float:
"""Calculate the expected token amount for a given SOL input.
- This is a convenience method that wraps calculate_buy_amount_out
- and converts between decimal and raw units.
+ This is a convenience method that converts between decimal and raw units.
Args:
pool_address: Address of the bonding curve
@@ -159,8 +299,7 @@ class PumpFunCurveManager(CurveManager):
async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float:
"""Calculate the expected SOL amount for a given token input.
- This is a convenience method that wraps calculate_sell_amount_out
- and converts between decimal and raw units.
+ This is a convenience method that converts between decimal and raw units.
Args:
pool_address: Address of the bonding curve
@@ -182,7 +321,7 @@ class PumpFunCurveManager(CurveManager):
Returns:
True if curve is complete, False otherwise
"""
- curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ curve_state = await self._get_curve_state_object(pool_address)
return curve_state.complete
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
@@ -194,7 +333,7 @@ class PumpFunCurveManager(CurveManager):
Returns:
Dictionary with progress information
"""
- curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
+ curve_state = await self._get_curve_state_object(pool_address)
# Calculate progress based on SOL raised vs target
# This is approximate since the exact target isn't stored in the curve state
From f92f68fc41cd75fc43b7dedc4ddb3d2e5cc0ab57 Mon Sep 17 00:00:00 2001
From: smypmsa
IDLParser:
+ """Load the IDL parser for LetsBonk (Raydium LaunchLab)."""
+ # Get the IDL file path relative to the project root
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ project_root = os.path.join(current_dir, "..", "..", "..")
+ idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json")
+ idl_path = os.path.normpath(idl_path)
+
+ if not os.path.exists(idl_path):
+ raise FileNotFoundError(f"IDL file not found at {idl_path}")
+
+ return IDLParser(idl_path, verbose=False)
@property
def platform(self) -> Platform:
@@ -60,7 +79,7 @@ class LetsBonkEventParser(EventParser):
accounts: list[int],
account_keys: list[bytes]
) -> TokenInfo | None:
- """Parse token creation from LetsBonk instruction data.
+ """Parse token creation from LetsBonk instruction data using IDL.
Args:
instruction_data: Raw instruction data
@@ -70,7 +89,7 @@ class LetsBonkEventParser(EventParser):
Returns:
TokenInfo if token creation found, None otherwise
"""
- if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR):
+ if not instruction_data.startswith(self._initialize_discriminator_bytes):
return None
try:
@@ -83,9 +102,16 @@ class LetsBonkEventParser(EventParser):
return None
return Pubkey.from_bytes(account_keys[account_index])
- # Parse instruction data
- token_data = self._parse_initialize_instruction_data(instruction_data)
- if not token_data:
+ # Parse instruction data using IDL parser
+ decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts)
+ if not decoded or decoded['instruction_name'] != 'initialize':
+ return None
+
+ args = decoded.get('args', {})
+
+ # Extract MintParams from the decoded arguments
+ base_mint_param = args.get('base_mint_param', {})
+ if not base_mint_param:
return None
# Extract account information based on IDL account order
@@ -99,9 +125,9 @@ class LetsBonkEventParser(EventParser):
return None
return TokenInfo(
- name=token_data["name"],
- symbol=token_data["symbol"],
- uri=token_data["uri"],
+ name=base_mint_param.get("name", ""),
+ symbol=base_mint_param.get("symbol", ""),
+ uri=base_mint_param.get("uri", ""),
mint=base_mint,
platform=Platform.LETS_BONK,
pool_state=pool_state,
@@ -112,7 +138,8 @@ class LetsBonkEventParser(EventParser):
creation_timestamp=monotonic(),
)
- except Exception:
+ except Exception as e:
+ logger.debug(f"Failed to parse initialize instruction: {e}")
return None
def parse_token_creation_from_geyser(
@@ -151,7 +178,7 @@ class LetsBonkEventParser(EventParser):
for acc_idx in ix.accounts:
if acc_idx < len(msg.account_keys):
acc_key = msg.account_keys[acc_idx]
- if bytes(acc_key) == bytes(self.address_provider.LETSBONK_PLATFORM_CONFIG):
+ if bytes(acc_key) == bytes(self.address_provider.get_system_addresses()["platform_config"]):
has_platform_config = True
break
@@ -167,7 +194,8 @@ class LetsBonkEventParser(EventParser):
return None
- except Exception:
+ except Exception as e:
+ logger.debug(f"Failed to parse geyser transaction: {e}")
return None
def get_program_id(self) -> Pubkey:
@@ -176,7 +204,7 @@ class LetsBonkEventParser(EventParser):
Returns:
Raydium LaunchLab program ID
"""
- return self.address_provider.RAYDIUM_LAUNCHLAB_PROGRAM_ID
+ return self.address_provider.program_id
def get_instruction_discriminators(self) -> list[bytes]:
"""Get instruction discriminators for token creation.
@@ -184,7 +212,7 @@ class LetsBonkEventParser(EventParser):
Returns:
List of discriminator bytes to match
"""
- return [self.INITIALIZE_DISCRIMINATOR]
+ return [self._initialize_discriminator_bytes]
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
@@ -224,7 +252,7 @@ class LetsBonkEventParser(EventParser):
if len(ix_data) >= 8:
discriminator = struct.unpack("
= 8:
discriminator = struct.unpack("
dict | None:
- """Parse the initialize instruction data from LetsBonk.
-
- Args:
- data: Raw instruction data
-
- Returns:
- Dictionary of parsed data or None if parsing fails
- """
- if len(data) < 8:
- return None
-
- # Check discriminator
- discriminator = struct.unpack("
len(data):
- raise ValueError("Not enough data for string length")
- length = struct.unpack_from(" len(data):
- raise ValueError("Not enough data for string")
- value = data[offset:offset + length].decode('utf-8')
- offset += length
- return value
-
- def read_u8():
- nonlocal offset
- if offset + 1 > len(data):
- raise ValueError("Not enough data for u8")
- value = struct.unpack_from(" IDLParser:
+ """Load the IDL parser for LetsBonk (Raydium LaunchLab)."""
+ # Get the IDL file path relative to the project root
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ project_root = os.path.join(current_dir, "..", "..", "..")
+ idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json")
+ idl_path = os.path.normpath(idl_path)
+
+ if not os.path.exists(idl_path):
+ raise FileNotFoundError(f"IDL file not found at {idl_path}")
+
+ return IDLParser(idl_path, verbose=False)
@property
def platform(self) -> Platform:
@@ -119,7 +141,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = (
- BUY_EXACT_IN_DISCRIMINATOR +
+ self._buy_exact_in_discriminator +
struct.pack("
Date: Sat, 2 Aug 2025 14:38:54 +0000
Subject: [PATCH 08/30] feat(letsbonk): integrate idl parser
---
src/platforms/letsbonk/curve_manager.py | 165 ++++++++++++------------
1 file changed, 84 insertions(+), 81 deletions(-)
diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py
index 243dae1..45540fc 100644
--- a/src/platforms/letsbonk/curve_manager.py
+++ b/src/platforms/letsbonk/curve_manager.py
@@ -5,7 +5,7 @@ This module handles LetsBonk (Raydium LaunchLab) specific pool operations
by implementing the CurveManager interface using IDL-based decoding.
"""
-import struct
+import os
from typing import Any
from solders.pubkey import Pubkey
@@ -14,13 +14,11 @@ from core.client import SolanaClient
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
from interfaces.core import CurveManager, Platform
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
+from utils.idl_parser import IDLParser
from utils.logger import get_logger
logger = get_logger(__name__)
-# Pool state discriminator for Raydium LaunchLab
-POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70])
-
class LetsBonkCurveManager(CurveManager):
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
@@ -33,6 +31,22 @@ class LetsBonkCurveManager(CurveManager):
"""
self.client = client
self.address_provider = LetsBonkAddressProvider()
+ self._idl_parser = self._load_idl_parser()
+
+ logger.info("LetsBonk curve manager initialized with IDL-based account parsing")
+
+ def _load_idl_parser(self) -> IDLParser:
+ """Load the IDL parser for LetsBonk (Raydium LaunchLab)."""
+ # Get the IDL file path relative to the project root
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ project_root = os.path.join(current_dir, "..", "..", "..")
+ idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json")
+ idl_path = os.path.normpath(idl_path)
+
+ if not os.path.exists(idl_path):
+ raise FileNotFoundError(f"IDL file not found at {idl_path}")
+
+ return IDLParser(idl_path, verbose=False)
@property
def platform(self) -> Platform:
@@ -53,8 +67,8 @@ class LetsBonkCurveManager(CurveManager):
if not account.data:
raise ValueError(f"No data in pool state account {pool_address}")
- # Decode pool state (simplified - in production you'd use IDL parser)
- pool_state_data = self._decode_pool_state(account.data)
+ # Decode pool state using IDL parser
+ pool_state_data = self._decode_pool_state_with_idl(account.data)
return pool_state_data
@@ -160,95 +174,84 @@ class LetsBonkCurveManager(CurveManager):
pool_state = await self.get_pool_state(pool_address)
return (pool_state["virtual_base"], pool_state["virtual_quote"])
- def _decode_pool_state(self, data: bytes) -> dict[str, Any]:
- """Decode pool state data from raw bytes.
-
- This is a simplified decoder. In production, you should use the IDL parser.
+ def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]:
+ """Decode pool state data using IDL parser.
Args:
data: Raw account data
Returns:
Dictionary with decoded pool state
+
+ Raises:
+ ValueError: If IDL parsing fails
"""
- if len(data) < 8:
- raise ValueError("Pool state data too short")
+ # Use IDL parser to decode PoolState account data
+ decoded_pool_state = self._idl_parser.decode_account_data(
+ data,
+ "PoolState",
+ skip_discriminator=True
+ )
- # Skip discriminator
- offset = 8
+ if not decoded_pool_state:
+ raise ValueError("Failed to decode pool state with IDL parser")
- # Based on the PoolState structure from the IDL:
- # - authority: Pubkey (32 bytes)
- # - base_mint: Pubkey (32 bytes)
- # - quote_mint: Pubkey (32 bytes)
- # - base_vault: Pubkey (32 bytes)
- # - quote_vault: Pubkey (32 bytes)
- # - status: u8 (1 byte)
- # - virtual_base: u64 (8 bytes)
- # - virtual_quote: u64 (8 bytes)
- # - real_base: u64 (8 bytes)
- # - real_quote: u64 (8 bytes)
- # ... and more fields
+ # Extract the fields we need for trading calculations
+ # Based on the PoolState structure from the IDL
+ pool_data = {
+ "virtual_base": decoded_pool_state.get("virtual_base", 0),
+ "virtual_quote": decoded_pool_state.get("virtual_quote", 0),
+ "real_base": decoded_pool_state.get("real_base", 0),
+ "real_quote": decoded_pool_state.get("real_quote", 0),
+ "status": decoded_pool_state.get("status", 0),
+ "supply": decoded_pool_state.get("supply", 0),
+ }
- try:
- # Skip to the fields we need
- offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults)
- offset += 1 # Skip status
-
- # Read virtual reserves
- virtual_base = struct.unpack_from("
0 else 0,
- }
-
- except Exception as e:
- logger.error(f"Failed to decode pool state: {e}")
- # Return some default values for testing
- return {
- "virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals
- "virtual_quote": 1_000_000_000, # 1 SOL
- "real_base": 1_000_000_000,
- "real_quote": 1_000_000_000,
- "price_per_token": 0.001, # 0.001 SOL per token
- }
+ # Calculate additional metrics
+ if pool_data["virtual_base"] > 0:
+ pool_data["price_per_token"] = (
+ (pool_data["virtual_quote"] / pool_data["virtual_base"])
+ * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
+ )
+ else:
+ pool_data["price_per_token"] = 0
+
+ logger.debug(f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
+ f"virtual_quote={pool_data['virtual_quote']}, "
+ f"price={pool_data['price_per_token']:.8f} SOL")
+
+ return pool_data
- async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]:
- """Get detailed pool information including status and progress.
+ def validate_pool_state_structure(self, pool_address: Pubkey) -> bool:
+ """Validate that the pool state structure matches IDL expectations.
Args:
pool_address: Address of the pool state
Returns:
- Dictionary with pool information
+ True if structure is valid, False otherwise
"""
- pool_state = await self.get_pool_state(pool_address)
-
- # Calculate additional metrics
- sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL
- tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS
-
- return {
- "virtual_base_reserves": pool_state["virtual_base"],
- "virtual_quote_reserves": pool_state["virtual_quote"],
- "real_base_reserves": pool_state["real_base"],
- "real_quote_reserves": pool_state["real_quote"],
- "sol_raised": sol_raised,
- "tokens_sold": tokens_sold,
- "current_price": pool_state["price_per_token"],
- }
\ No newline at end of file
+ try:
+ # This would be used during development/testing to ensure
+ # the IDL parsing is working correctly
+ pool_state = self.get_pool_state(pool_address)
+
+ required_fields = [
+ "virtual_base", "virtual_quote",
+ "real_base", "real_quote"
+ ]
+
+ for field in required_fields:
+ if field not in pool_state:
+ logger.error(f"Missing required field: {field}")
+ return False
+
+ if not isinstance(pool_state[field], int):
+ logger.error(f"Field {field} is not an integer: {type(pool_state[field])}")
+ return False
+
+ return True
+
+ except Exception as e:
+ logger.error(f"Pool state validation failed: {e}")
+ return False
\ No newline at end of file
From 9a09862f0dc79d93049c351e867b617aeaca0e27 Mon Sep 17 00:00:00 2001
From: smypmsa
IDLParser:
- """Load the IDL parser for LetsBonk (Raydium LaunchLab)."""
- # Get the IDL file path relative to the project root
- current_dir = os.path.dirname(os.path.abspath(__file__))
- project_root = os.path.join(current_dir, "..", "..", "..")
- idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json")
- idl_path = os.path.normpath(idl_path)
-
- if not os.path.exists(idl_path):
- raise FileNotFoundError(f"IDL file not found at {idl_path}")
-
- return IDLParser(idl_path, verbose=False)
+ logger.info("LetsBonk event parser initialized with injected IDL parser")
@property
def platform(self) -> Platform:
@@ -79,7 +69,7 @@ class LetsBonkEventParser(EventParser):
accounts: list[int],
account_keys: list[bytes]
) -> TokenInfo | None:
- """Parse token creation from LetsBonk instruction data using IDL.
+ """Parse token creation from LetsBonk instruction data using injected IDL parser.
Args:
instruction_data: Raw instruction data
@@ -102,7 +92,7 @@ class LetsBonkEventParser(EventParser):
return None
return Pubkey.from_bytes(account_keys[account_index])
- # Parse instruction data using IDL parser
+ # Parse instruction data using injected IDL parser
decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts)
if not decoded or decoded['instruction_name'] != 'initialize':
return None
diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py
index 2e3c44b..b16a6bd 100644
--- a/src/platforms/letsbonk/instruction_builder.py
+++ b/src/platforms/letsbonk/instruction_builder.py
@@ -6,7 +6,6 @@ by implementing the InstructionBuilder interface with IDL-based discriminators.
"""
import hashlib
-import os
import struct
import time
@@ -26,29 +25,20 @@ logger = get_logger(__name__)
class LetsBonkInstructionBuilder(InstructionBuilder):
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators."""
- def __init__(self):
- """Initialize LetsBonk instruction builder with IDL support."""
- self._idl_parser = self._load_idl_parser()
+ def __init__(self, idl_parser: IDLParser):
+ """Initialize LetsBonk instruction builder with injected IDL parser.
- # Get discriminators from IDL
+ Args:
+ idl_parser: Pre-loaded IDL parser for LetsBonk platform
+ """
+ self._idl_parser = idl_parser
+
+ # Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators()
self._buy_exact_in_discriminator = discriminators["buy_exact_in"]
self._sell_exact_in_discriminator = discriminators["sell_exact_in"]
- logger.info("LetsBonk instruction builder initialized with IDL-based discriminators")
-
- def _load_idl_parser(self) -> IDLParser:
- """Load the IDL parser for LetsBonk (Raydium LaunchLab)."""
- # Get the IDL file path relative to the project root
- current_dir = os.path.dirname(os.path.abspath(__file__))
- project_root = os.path.join(current_dir, "..", "..", "..")
- idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json")
- idl_path = os.path.normpath(idl_path)
-
- if not os.path.exists(idl_path):
- raise FileNotFoundError(f"IDL file not found at {idl_path}")
-
- return IDLParser(idl_path, verbose=False)
+ logger.info("LetsBonk instruction builder initialized with injected IDL parser")
@property
def platform(self) -> Platform:
diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py
new file mode 100644
index 0000000..bfb208c
--- /dev/null
+++ b/src/utils/idl_manager.py
@@ -0,0 +1,195 @@
+"""
+Centralized IDL management for Solana platforms.
+
+This module provides a single point of IDL loading and management to avoid
+duplicate loading across multiple platform implementation classes.
+"""
+
+import os
+
+from interfaces.core import Platform
+from utils.idl_parser import IDLParser
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+class IDLManager:
+ """Centralized manager for IDL parsers across all platforms."""
+
+ def __init__(self):
+ """Initialize the IDL manager."""
+ self._parsers: dict[Platform, IDLParser] = {}
+ self._idl_paths: dict[Platform, str] = {}
+ self._setup_platform_idl_paths()
+
+ def _setup_platform_idl_paths(self) -> None:
+ """Setup IDL file paths for each platform."""
+ # Get the project root directory (3 levels up from this file)
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ project_root = os.path.join(current_dir, "..", "..")
+ project_root = os.path.normpath(project_root)
+
+ # Define IDL paths for each platform
+ self._idl_paths = {
+ Platform.LETS_BONK: os.path.join(project_root, "idl", "raydium_launchlab_idl.json"),
+ # Platform.PUMP_FUN: os.path.join(project_root, "idl", "pump_fun_idl.json"), # Future
+ }
+
+ def get_parser(self, platform: Platform, verbose: bool = False) -> IDLParser:
+ """Get or create an IDL parser for the specified platform.
+
+ Args:
+ platform: Platform to get parser for
+ verbose: Whether to enable verbose logging in the parser
+
+ Returns:
+ IDLParser instance for the platform
+
+ Raises:
+ ValueError: If platform is not supported or IDL file not found
+ """
+ # Return cached parser if available
+ if platform in self._parsers:
+ return self._parsers[platform]
+
+ # Check if platform has IDL support
+ if platform not in self._idl_paths:
+ raise ValueError(f"Platform {platform.value} does not have IDL support configured")
+
+ idl_path = self._idl_paths[platform]
+
+ # Verify IDL file exists
+ if not os.path.exists(idl_path):
+ raise FileNotFoundError(f"IDL file not found for {platform.value} at {idl_path}")
+
+ # Load and cache the parser
+ logger.info(f"Loading IDL parser for {platform.value} from {idl_path}")
+ parser = IDLParser(idl_path, verbose=verbose)
+ self._parsers[platform] = parser
+
+ logger.info(f"IDL parser loaded for {platform.value} with {len(parser.get_instruction_names())} instructions")
+
+ return parser
+
+ def has_idl_support(self, platform: Platform) -> bool:
+ """Check if a platform has IDL support configured.
+
+ Args:
+ platform: Platform to check
+
+ Returns:
+ True if platform has IDL support
+ """
+ return platform in self._idl_paths
+
+ def get_supported_platforms(self) -> list[Platform]:
+ """Get list of platforms with IDL support.
+
+ Returns:
+ List of platforms that have IDL files configured
+ """
+ return list(self._idl_paths.keys())
+
+ def clear_cache(self, platform: Platform | None = None) -> None:
+ """Clear cached parsers.
+
+ Args:
+ platform: Specific platform to clear, or None to clear all
+ """
+ if platform is None:
+ logger.info("Clearing all cached IDL parsers")
+ self._parsers.clear()
+ elif platform in self._parsers:
+ logger.info(f"Clearing cached IDL parser for {platform.value}")
+ del self._parsers[platform]
+
+ def preload_parser(self, platform: Platform, verbose: bool = False) -> None:
+ """Preload IDL parser for a platform.
+
+ This can be useful for warming up the parser during initialization.
+
+ Args:
+ platform: Platform to preload parser for
+ verbose: Whether to enable verbose logging in the parser
+ """
+ if platform not in self._parsers:
+ logger.info(f"Preloading IDL parser for {platform.value}")
+ self.get_parser(platform, verbose)
+ else:
+ logger.debug(f"IDL parser for {platform.value} already loaded")
+
+ def get_instruction_discriminators(self, platform: Platform) -> dict[str, bytes]:
+ """Get instruction discriminators for a platform.
+
+ Args:
+ platform: Platform to get discriminators for
+
+ Returns:
+ Dictionary mapping instruction names to discriminator bytes
+ """
+ parser = self.get_parser(platform)
+ return parser.get_instruction_discriminators()
+
+ def get_instruction_names(self, platform: Platform) -> list[str]:
+ """Get available instruction names for a platform.
+
+ Args:
+ platform: Platform to get instruction names for
+
+ Returns:
+ List of instruction names
+ """
+ parser = self.get_parser(platform)
+ return parser.get_instruction_names()
+
+
+# Global IDL manager instance
+_idl_manager: IDLManager | None = None
+
+
+def get_idl_manager() -> IDLManager:
+ """Get the global IDL manager instance.
+
+ Returns:
+ Global IDLManager instance
+ """
+ global _idl_manager
+ if _idl_manager is None:
+ _idl_manager = IDLManager()
+ return _idl_manager
+
+
+def get_idl_parser(platform: Platform, verbose: bool = False) -> IDLParser:
+ """Convenience function to get an IDL parser for a platform.
+
+ Args:
+ platform: Platform to get parser for
+ verbose: Whether to enable verbose logging in the parser
+
+ Returns:
+ IDLParser instance for the platform
+ """
+ return get_idl_manager().get_parser(platform, verbose)
+
+
+def has_idl_support(platform: Platform) -> bool:
+ """Check if a platform has IDL support.
+
+ Args:
+ platform: Platform to check
+
+ Returns:
+ True if platform has IDL support
+ """
+ return get_idl_manager().has_idl_support(platform)
+
+
+def preload_platform_idl(platform: Platform, verbose: bool = False) -> None:
+ """Preload IDL parser for a platform.
+
+ Args:
+ platform: Platform to preload parser for
+ verbose: Whether to enable verbose logging
+ """
+ get_idl_manager().preload_parser(platform, verbose)
\ No newline at end of file
From 93332794a3a9cd8ed9d1dcf14d21c7e64068fc23 Mon Sep 17 00:00:00 2001
From: smypmsa
0:
+ curve_data["price_per_token"] = (
+ (curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"])
+ * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
)
-
- except Exception as e:
- raise ValueError(f"Failed to decode curve state: {e}")
+ else:
+ curve_data["price_per_token"] = 0
+
+ # Add convenience decimal fields
+ curve_data["token_reserves_decimal"] = curve_data["virtual_token_reserves"] / 10**TOKEN_DECIMALS
+ curve_data["sol_reserves_decimal"] = curve_data["virtual_sol_reserves"] / LAMPORTS_PER_SOL
+
+ logger.debug(f"Decoded curve state: virtual_token_reserves={curve_data['virtual_token_reserves']}, "
+ f"virtual_sol_reserves={curve_data['virtual_sol_reserves']}, "
+ f"price={curve_data['price_per_token']:.8f} SOL")
+
+ return curve_data
# Additional convenience methods for pump.fun specific operations
async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float:
@@ -321,8 +254,8 @@ class PumpFunCurveManager(CurveManager):
Returns:
True if curve is complete, False otherwise
"""
- curve_state = await self._get_curve_state_object(pool_address)
- return curve_state.complete
+ pool_state = await self.get_pool_state(pool_address)
+ return pool_state.get("complete", False)
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get bonding curve completion progress information.
@@ -333,11 +266,11 @@ class PumpFunCurveManager(CurveManager):
Returns:
Dictionary with progress information
"""
- curve_state = await self._get_curve_state_object(pool_address)
+ pool_state = await self.get_pool_state(pool_address)
# Calculate progress based on SOL raised vs target
# This is approximate since the exact target isn't stored in the curve state
- sol_raised = curve_state.real_sol_reserves / LAMPORTS_PER_SOL
+ sol_raised = pool_state["real_sol_reserves"] / LAMPORTS_PER_SOL
# Estimate progress based on typical pump.fun graduation requirements
# (This could be made more accurate with additional on-chain data)
@@ -345,10 +278,45 @@ class PumpFunCurveManager(CurveManager):
progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
return {
- "complete": curve_state.complete,
+ "complete": pool_state.get("complete", False),
"sol_raised": sol_raised,
"estimated_target_sol": estimated_target_sol,
"progress_percentage": progress_percentage,
- "tokens_available": curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS,
+ "tokens_available": pool_state["virtual_token_reserves"] / 10**TOKEN_DECIMALS,
"market_cap_sol": sol_raised, # Approximate market cap
- }
\ No newline at end of file
+ }
+
+ def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
+ """Validate that the curve state structure matches IDL expectations.
+
+ Args:
+ pool_address: Address of the bonding curve
+
+ Returns:
+ True if structure is valid, False otherwise
+ """
+ try:
+ # This would be used during development/testing to ensure
+ # the IDL parsing is working correctly
+ pool_state = self.get_pool_state(pool_address)
+
+ required_fields = [
+ "virtual_token_reserves", "virtual_sol_reserves",
+ "real_token_reserves", "real_sol_reserves",
+ "token_total_supply", "complete"
+ ]
+
+ for field in required_fields:
+ if field not in pool_state:
+ logger.error(f"Missing required field: {field}")
+ return False
+
+ if field != "complete" and not isinstance(pool_state[field], int):
+ logger.error(f"Field {field} is not an integer: {type(pool_state[field])}")
+ return False
+
+ return True
+
+ except Exception as e:
+ logger.error(f"Curve state validation failed: {e}")
+ return False
\ No newline at end of file
diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py
index cf26f09..ed08873 100644
--- a/src/platforms/pumpfun/event_parser.py
+++ b/src/platforms/pumpfun/event_parser.py
@@ -2,32 +2,43 @@
Pump.Fun implementation of EventParser interface.
This module parses pump.fun-specific token creation events from various sources
-by implementing the EventParser interface.
+by implementing the EventParser interface with IDL-based parsing.
"""
import base64
import struct
from time import monotonic
-from typing import Any, Final
+from typing import Any
-import base58
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from core.pubkeys import SystemAddresses
from interfaces.core import EventParser, Platform, TokenInfo
from platforms.pumpfun.address_provider import PumpFunAddresses
+from utils.idl_parser import IDLParser
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
class PumpFunEventParser(EventParser):
- """Pump.Fun implementation of EventParser interface."""
+ """Pump.Fun implementation of EventParser interface with IDL-based parsing."""
- # Discriminators for pump.fun instructions
- CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728
- CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack("
Platform:
@@ -74,7 +85,7 @@ class PumpFunEventParser(EventParser):
accounts: list[int],
account_keys: list[bytes]
) -> TokenInfo | None:
- """Parse token creation from pump.fun instruction data.
+ """Parse token creation from pump.fun instruction data using injected IDL parser.
Args:
instruction_data: Raw instruction data
@@ -84,7 +95,7 @@ class PumpFunEventParser(EventParser):
Returns:
TokenInfo if token creation found, None otherwise
"""
- if not instruction_data.startswith(self.CREATE_DISCRIMINATOR_BYTES):
+ if not instruction_data.startswith(self._create_discriminator_bytes):
return None
try:
@@ -97,12 +108,14 @@ class PumpFunEventParser(EventParser):
return None
return Pubkey.from_bytes(account_keys[account_index])
- # Parse instruction data
- token_data = self._parse_create_instruction_data(instruction_data)
- if not token_data:
+ # Parse instruction data using injected IDL parser
+ decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts)
+ if not decoded or decoded['instruction_name'] != 'create':
return None
-
- # Extract account information
+
+ args = decoded.get('args', {})
+
+ # Extract account information based on IDL account order
mint = get_account_key(0)
bonding_curve = get_account_key(2)
associated_bonding_curve = get_account_key(3)
@@ -112,13 +125,13 @@ class PumpFunEventParser(EventParser):
return None
# Create creator vault
- creator = Pubkey.from_string(token_data["creator"]) if token_data.get("creator") else user
+ creator = Pubkey.from_string(args.get("creator", str(user))) if args.get("creator") else user
creator_vault = self._derive_creator_vault(creator)
return TokenInfo(
- name=token_data["name"],
- symbol=token_data["symbol"],
- uri=token_data["uri"],
+ name=args.get("name", ""),
+ symbol=args.get("symbol", ""),
+ uri=args.get("uri", ""),
mint=mint,
platform=Platform.PUMP_FUN,
bonding_curve=bonding_curve,
@@ -129,7 +142,8 @@ class PumpFunEventParser(EventParser):
creation_timestamp=monotonic(),
)
- except Exception:
+ except Exception as e:
+ logger.debug(f"Failed to parse create instruction: {e}")
return None
def parse_token_creation_from_geyser(
@@ -172,7 +186,8 @@ class PumpFunEventParser(EventParser):
return None
- except Exception:
+ except Exception as e:
+ logger.debug(f"Failed to parse geyser transaction: {e}")
return None
def get_program_id(self) -> Pubkey:
@@ -189,7 +204,7 @@ class PumpFunEventParser(EventParser):
Returns:
List of discriminator bytes to match
"""
- return [self.CREATE_DISCRIMINATOR_BYTES]
+ return [self._create_discriminator_bytes]
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
@@ -208,43 +223,96 @@ class PumpFunEventParser(EventParser):
if not isinstance(tx, dict) or "transaction" not in tx:
continue
- # Decode base64 transaction data
- tx_data_encoded = tx["transaction"][0]
- tx_data_decoded = base64.b64decode(tx_data_encoded)
- transaction = VersionedTransaction.from_bytes(tx_data_decoded)
-
- for ix in transaction.message.instructions:
- program_id = transaction.message.account_keys[ix.program_id_index]
-
- # Check if instruction is from pump.fun program
- if str(program_id) != str(self.get_program_id()):
- continue
-
- ix_data = bytes(ix.data)
-
- # Check for create discriminator
- if len(ix_data) >= 8:
- discriminator = struct.unpack("
0:
+ try:
+ tx_data_encoded = tx_data[0]
+ tx_data_decoded = base64.b64decode(tx_data_encoded)
+ transaction = VersionedTransaction.from_bytes(tx_data_decoded)
- if discriminator == self.CREATE_DISCRIMINATOR:
- # Token creation should have substantial data and many accounts
- if len(ix_data) <= 8 or len(ix.accounts) < 10:
+ for ix in transaction.message.instructions:
+ program_id = transaction.message.account_keys[ix.program_id_index]
+
+ # Check if instruction is from pump.fun program
+ if str(program_id) != str(self.get_program_id()):
+ continue
+
+ ix_data = bytes(ix.data)
+
+ # Check for create discriminator
+ if len(ix_data) >= 8:
+ discriminator = struct.unpack("
= len(message["accountKeys"]):
+ continue
+
+ program_id_str = message["accountKeys"][program_idx]
+ if program_id_str != str(self.get_program_id()):
continue
- # Parse the instruction
- token_info = self.parse_token_creation_from_instruction(
- ix_data, ix.accounts, transaction.message.account_keys
- )
- if token_info:
- return token_info
+ # Decode instruction data
+ ix_data = base64.b64decode(ix["data"])
+
+ if len(ix_data) >= 8:
+ discriminator = struct.unpack("
dict | None:
- """Parse the create instruction data from pump.fun.
+ """Parse the create instruction data from pump.fun using injected IDL parser.
Args:
data: Raw instruction data
@@ -257,47 +325,30 @@ class PumpFunEventParser(EventParser):
# Check for the correct instruction discriminator
discriminator = struct.unpack("
len(data):
- return None
- length = struct.unpack(" len(data):
- return None
- value = data[offset : offset + length].decode("utf-8")
- offset += length
- elif field_type == "publicKey":
- if offset + 32 > len(data):
- return None
- value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
- offset += 32
-
- parsed_data[field_name] = value
-
- return parsed_data
+ decoded = self._idl_parser.decode_instruction(data, dummy_account_keys, dummy_accounts)
+ if not decoded or decoded['instruction_name'] != 'create':
+ return None
- except Exception:
+ # Extract the arguments
+ args = decoded.get('args', {})
+ return {
+ "name": args.get("name", ""),
+ "symbol": args.get("symbol", ""),
+ "uri": args.get("uri", ""),
+ "creator": args.get("creator", ""),
+ }
+
+ except Exception as e:
+ logger.debug(f"Failed to parse instruction data with IDL: {e}")
return None
def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py
index e495828..474dfdc 100644
--- a/src/platforms/pumpfun/instruction_builder.py
+++ b/src/platforms/pumpfun/instruction_builder.py
@@ -2,11 +2,10 @@
Pump.Fun implementation of InstructionBuilder interface.
This module builds pump.fun-specific buy and sell instructions
-by implementing the InstructionBuilder interface.
+by implementing the InstructionBuilder interface with IDL-based discriminators.
"""
import struct
-from typing import Final
from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
@@ -14,14 +13,29 @@ from spl.token.instructions import create_idempotent_associated_token_account
from core.pubkeys import TOKEN_DECIMALS, SystemAddresses
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
+from utils.idl_parser import IDLParser
+from utils.logger import get_logger
-# Discriminators for pump.fun instructions
-BUY_DISCRIMINATOR: Final[bytes] = struct.pack("
Platform:
@@ -82,7 +96,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Build instruction data: discriminator + token_amount + max_sol_cost
instruction_data = (
- BUY_DISCRIMINATOR +
+ self._buy_discriminator +
struct.pack("
IDLParser:
From 72a3d23e6bf935badb180f676d5390c736ebcdd8 Mon Sep 17 00:00:00 2001
From: smypmsa
list[Instruction]:
- """Build sell instruction(s) for pump.fun.
+ """Build sell instruction(s) for LetsBonk using sell_exact_in.
Args:
token_info: Token information
@@ -135,27 +179,60 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Get all required accounts
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
- # Build sell instruction accounts
+ # 1. Create WSOL account with seed (to receive SOL)
+ wsol_seed = self._generate_wsol_seed(user)
+ wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
+
+ # Minimal account creation cost
+ account_creation_lamports = 2_039_280
+
+ create_wsol_ix = create_account_with_seed(
+ CreateAccountWithSeedParams(
+ from_pubkey=user,
+ to_pubkey=wsol_account,
+ base=user,
+ seed=wsol_seed,
+ lamports=account_creation_lamports,
+ space=165, # Size of a token account
+ owner=SystemAddresses.TOKEN_PROGRAM
+ )
+ )
+ instructions.append(create_wsol_ix)
+
+ # 2. Initialize WSOL account
+ initialize_wsol_ix = self._create_initialize_account_instruction(
+ wsol_account,
+ SystemAddresses.SOL_MINT,
+ user
+ )
+ instructions.append(initialize_wsol_ix)
+
+ # 3. Build sell_exact_in instruction with correct account ordering
sell_accounts = [
- AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
- AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
- AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
- AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
- AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
- AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
- AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
- AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
- AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
- AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
- AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
- AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
+ AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
+ AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
+ AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
+ AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
+ AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token (tokens being sold)
+ AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL received)
+ AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault (receives tokens)
+ AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL)
+ AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
+ AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
+ AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
+ AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
+ AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority
+ AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
]
- # Build instruction data: discriminator + token_amount + min_sol_output
+ # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
+ SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = (
- self._sell_discriminator +
- struct.pack("
str:
+ """Generate a unique seed for WSOL account creation.
+
+ Args:
+ user: User's wallet address
+
+ Returns:
+ Unique seed string for WSOL account
+ """
+ # Generate a unique seed based on timestamp and user pubkey
+ seed_data = f"{int(time.time())}{user!s}"
+ return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
+
+ def _create_initialize_account_instruction(
+ self,
+ account: Pubkey,
+ mint: Pubkey,
+ owner: Pubkey
+ ) -> Instruction:
+ """Create an InitializeAccount instruction for the Token Program.
+
+ Args:
+ account: The account to initialize
+ mint: The token mint
+ owner: The account owner
+
+ Returns:
+ Instruction for initializing the account
+ """
+ accounts = [
+ AccountMeta(pubkey=account, is_signer=False, is_writable=True),
+ AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
+ AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
+ AccountMeta(pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False),
+ ]
+
+ # InitializeAccount instruction discriminator (instruction 1 in Token Program)
+ data = bytes([1])
+
+ return Instruction(
+ program_id=SystemAddresses.TOKEN_PROGRAM,
+ data=data,
+ accounts=accounts
+ )
+
+ def _create_close_account_instruction(
+ self,
+ account: Pubkey,
+ destination: Pubkey,
+ owner: Pubkey
+ ) -> Instruction:
+ """Create a CloseAccount instruction for the Token Program.
+
+ Args:
+ account: The account to close
+ destination: Where to send the remaining lamports
+ owner: The account owner (must sign)
+
+ Returns:
+ Instruction for closing the account
+ """
+ accounts = [
+ AccountMeta(pubkey=account, is_signer=False, is_writable=True),
+ AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
+ AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
+ ]
+
+ # CloseAccount instruction discriminator (instruction 9 in Token Program)
+ data = bytes([9])
+
+ return Instruction(
+ program_id=SystemAddresses.TOKEN_PROGRAM,
+ data=data,
+ accounts=accounts
+ )
+
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
"""Convert decimal token amount to raw token units.
diff --git a/trades/trades.log b/trades/trades.log
index d226794..f1df671 100644
--- a/trades/trades.log
+++ b/trades/trades.log
@@ -1,2 +1,4 @@
{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"}
{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"}
+{"timestamp": "2025-08-02T15:37:04.403139", "action": "buy", "platform": "lets_bonk", "token_address": "7o5FtYXxpX6sqtcJJ3ES4DiWt4C9HnHzKuHrZPpjbonk", "symbol": "pants", "price": 5e-06, "amount": 20, "tx_hash": "26Uu4rZ1PcnzioHwWAh3Reca4MMNZjgiQbmihjisiurpv4xcHFtvSptZoV3zUkS5bdqZ4zpLeGb46J4bWyczu6FY"}
+{"timestamp": "2025-08-02T15:37:21.984944", "action": "sell", "platform": "lets_bonk", "token_address": "7o5FtYXxpX6sqtcJJ3ES4DiWt4C9HnHzKuHrZPpjbonk", "symbol": "pants", "price": 2.7959121193874663e-08, "amount": 3712.914779, "tx_hash": "5zXxjxHZuWXxGih1Aca6HoFkybiyHT4jPghhXCg3NbkyD3nuzzaJtmjg4mnoEQDeTn64b1cUZMawhmYbeq3cwjfj"}
From 26c048c86a175366975fbb4b848c7a1bd42df4bf Mon Sep 17 00:00:00 2001
From: smypmsa
list[Instruction]:
- """Build sell instruction(s) for LetsBonk using sell_exact_in.
+ """Build sell instruction(s) for pump.fun.
Args:
token_info: Token information
@@ -179,60 +135,27 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
# Get all required accounts
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
- # 1. Create WSOL account with seed (to receive SOL)
- wsol_seed = self._generate_wsol_seed(user)
- wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
-
- # Minimal account creation cost
- account_creation_lamports = 2_039_280
-
- create_wsol_ix = create_account_with_seed(
- CreateAccountWithSeedParams(
- from_pubkey=user,
- to_pubkey=wsol_account,
- base=user,
- seed=wsol_seed,
- lamports=account_creation_lamports,
- space=165, # Size of a token account
- owner=SystemAddresses.TOKEN_PROGRAM
- )
- )
- instructions.append(create_wsol_ix)
-
- # 2. Initialize WSOL account
- initialize_wsol_ix = self._create_initialize_account_instruction(
- wsol_account,
- SystemAddresses.SOL_MINT,
- user
- )
- instructions.append(initialize_wsol_ix)
-
- # 3. Build sell_exact_in instruction with correct account ordering
+ # Build sell instruction accounts
sell_accounts = [
- AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
- AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
- AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
- AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
- AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
- AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token (tokens being sold)
- AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL received)
- AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault (receives tokens)
- AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL)
- AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
- AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
- AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
- AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
- AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority
- AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
+ AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
+ AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
+ AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
+ AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
]
- # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
- SHARE_FEE_RATE = 0 # No sharing fee
+ # Build instruction data: discriminator + token_amount + min_sol_output
instruction_data = (
- self._sell_exact_in_discriminator +
- struct.pack("
str:
- """Generate a unique seed for WSOL account creation.
-
- Args:
- user: User's wallet address
-
- Returns:
- Unique seed string for WSOL account
- """
- # Generate a unique seed based on timestamp and user pubkey
- seed_data = f"{int(time.time())}{user!s}"
- return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
-
- def _create_initialize_account_instruction(
- self,
- account: Pubkey,
- mint: Pubkey,
- owner: Pubkey
- ) -> Instruction:
- """Create an InitializeAccount instruction for the Token Program.
-
- Args:
- account: The account to initialize
- mint: The token mint
- owner: The account owner
-
- Returns:
- Instruction for initializing the account
- """
- accounts = [
- AccountMeta(pubkey=account, is_signer=False, is_writable=True),
- AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
- AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
- AccountMeta(pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False),
- ]
-
- # InitializeAccount instruction discriminator (instruction 1 in Token Program)
- data = bytes([1])
-
- return Instruction(
- program_id=SystemAddresses.TOKEN_PROGRAM,
- data=data,
- accounts=accounts
- )
-
- def _create_close_account_instruction(
- self,
- account: Pubkey,
- destination: Pubkey,
- owner: Pubkey
- ) -> Instruction:
- """Create a CloseAccount instruction for the Token Program.
-
- Args:
- account: The account to close
- destination: Where to send the remaining lamports
- owner: The account owner (must sign)
-
- Returns:
- Instruction for closing the account
- """
- accounts = [
- AccountMeta(pubkey=account, is_signer=False, is_writable=True),
- AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
- AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
- ]
-
- # CloseAccount instruction discriminator (instruction 9 in Token Program)
- data = bytes([9])
-
- return Instruction(
- program_id=SystemAddresses.TOKEN_PROGRAM,
- data=data,
- accounts=accounts
- )
-
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
"""Convert decimal token amount to raw token units.
From bae03c3fab26c8fc78165bba24fd3a197dc3b846 Mon Sep 17 00:00:00 2001
From: smypmsa
Platform:
@@ -50,7 +55,7 @@ class PumpFunEventParser(EventParser):
logs: list[str],
signature: str
) -> TokenInfo | None:
- """Parse token creation from pump.fun transaction logs.
+ """Parse token creation from pump.fun transaction logs using IDL instruction parsing.
Args:
logs: List of log strings from transaction
@@ -59,25 +64,97 @@ class PumpFunEventParser(EventParser):
Returns:
TokenInfo if token creation found, None otherwise
"""
- # Check if this is a token creation
+ # Check if this is a token creation transaction
if not any("Program log: Instruction: Create" in log for log in logs):
return None
- # Skip swaps and other operations
+ # Skip swaps as the first condition may pass them
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
return None
- # Find and process program data
- for log in logs:
- if "Program data:" in log:
- try:
- encoded_data = log.split(": ")[1]
- decoded_data = base64.b64decode(encoded_data)
- return self._parse_create_instruction_data(decoded_data)
- except Exception:
- continue
-
- return None
+ # Look for event data in the logs (CreateEvent data!)
+ try:
+ for log in logs:
+ if "Program data:" in log:
+ try:
+ # Extract base64 encoded event data
+ encoded_data = log.split("Program data: ")[1].strip()
+ decoded_data = base64.b64decode(encoded_data)
+
+ # Parse as event data (CreateEvent)
+ # The "Program data:" in logs contains event data, not instruction data
+ if len(decoded_data) < 8:
+ continue
+
+ # Check discriminator from program data
+ discriminator = decoded_data[:8]
+ discriminator_int = struct.unpack("
list[bytes]:
+ """Get event discriminators for token creation.
+
+ Returns:
+ List of event discriminator bytes to match
+ """
+ return [self._create_event_discriminator_bytes]
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
@@ -244,7 +328,7 @@ class PumpFunEventParser(EventParser):
if len(ix_data) >= 8:
discriminator = struct.unpack("
= 8:
discriminator = struct.unpack("
dict | None:
- """Parse the create instruction data from pump.fun using injected IDL parser.
- Args:
- data: Raw instruction data
-
- Returns:
- Dictionary of parsed data or None if parsing fails
- """
- if len(data) < 8:
- return None
-
- # Check for the correct instruction discriminator
- discriminator = struct.unpack("
Pubkey:
"""Derive the creator vault for a creator.
diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py
index c5ed998..ec82af3 100644
--- a/src/utils/idl_manager.py
+++ b/src/utils/idl_manager.py
@@ -6,6 +6,7 @@ duplicate loading across multiple platform implementation classes.
"""
import os
+from typing import Any
from interfaces.core import Platform
from utils.idl_parser import IDLParser
@@ -68,7 +69,9 @@ class IDLManager:
parser = IDLParser(idl_path, verbose=verbose)
self._parsers[platform] = parser
- logger.info(f"IDL parser loaded for {platform.value} with {len(parser.get_instruction_names())} instructions")
+ instruction_count = len(parser.get_instruction_names())
+ event_count = len(parser.get_event_names())
+ logger.info(f"IDL parser loaded for {platform.value} with {instruction_count} instructions and {event_count} events")
return parser
@@ -119,6 +122,10 @@ class IDLManager:
else:
logger.debug(f"IDL parser for {platform.value} already loaded")
+ # --------------------------------------------------------------------------
+ # Instruction-related convenience methods
+ # --------------------------------------------------------------------------
+
def get_instruction_discriminators(self, platform: Platform) -> dict[str, bytes]:
"""Get instruction discriminators for a platform.
@@ -142,6 +149,110 @@ class IDLManager:
"""
parser = self.get_parser(platform)
return parser.get_instruction_names()
+
+ # --------------------------------------------------------------------------
+ # Event-related convenience methods
+ # --------------------------------------------------------------------------
+
+ def get_event_discriminators(self, platform: Platform) -> dict[str, bytes]:
+ """Get event discriminators for a platform.
+
+ Args:
+ platform: Platform to get event discriminators for
+
+ Returns:
+ Dictionary mapping event names to discriminator bytes
+ """
+ parser = self.get_parser(platform)
+ return parser.get_event_discriminators()
+
+ def get_event_names(self, platform: Platform) -> list[str]:
+ """Get available event names for a platform.
+
+ Args:
+ platform: Platform to get event names for
+
+ Returns:
+ List of event names
+ """
+ parser = self.get_parser(platform)
+ return parser.get_event_names()
+
+ def decode_event_from_logs(self, platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None:
+ """Decode event data from transaction logs for a platform.
+
+ Args:
+ platform: Platform to use for decoding
+ logs: List of log strings from transaction
+ event_name: Optional specific event name to look for
+
+ Returns:
+ Decoded event data if found, None otherwise
+ """
+ parser = self.get_parser(platform)
+ return parser.find_event_in_logs(logs, event_name)
+
+ def decode_event_data(self, platform: Platform, event_data: bytes, event_name: str | None = None) -> dict | None:
+ """Decode raw event data for a platform.
+
+ Args:
+ platform: Platform to use for decoding
+ event_data: Raw event data bytes
+ event_name: Optional event name to decode as
+
+ Returns:
+ Decoded event data if successful, None otherwise
+ """
+ parser = self.get_parser(platform)
+ return parser.decode_event_data(event_data, event_name)
+
+ # --------------------------------------------------------------------------
+ # Platform information methods
+ # --------------------------------------------------------------------------
+
+ def get_platform_capabilities(self, platform: Platform) -> dict[str, Any]:
+ """Get comprehensive capability information for a platform.
+
+ Args:
+ platform: Platform to get capabilities for
+
+ Returns:
+ Dictionary with platform capabilities
+ """
+ if not self.has_idl_support(platform):
+ return {
+ "platform": platform.value,
+ "has_idl_support": False,
+ "instructions": [],
+ "events": [],
+ "instruction_count": 0,
+ "event_count": 0,
+ }
+
+ try:
+ parser = self.get_parser(platform)
+ instruction_names = parser.get_instruction_names()
+ event_names = parser.get_event_names()
+
+ return {
+ "platform": platform.value,
+ "has_idl_support": True,
+ "instructions": instruction_names,
+ "events": event_names,
+ "instruction_count": len(instruction_names),
+ "event_count": len(event_names),
+ }
+ except Exception as e:
+ logger.error(f"Failed to get capabilities for {platform.value}: {e}")
+ return {
+ "platform": platform.value,
+ "has_idl_support": False,
+ "error": str(e),
+ "instructions": [],
+ "events": [],
+ "instruction_count": 0,
+ "event_count": 0,
+ }
# Global IDL manager instance
@@ -192,4 +303,46 @@ def preload_platform_idl(platform: Platform, verbose: bool = False) -> None:
platform: Platform to preload parser for
verbose: Whether to enable verbose logging
"""
- get_idl_manager().preload_parser(platform, verbose)
\ No newline at end of file
+ get_idl_manager().preload_parser(platform, verbose)
+
+
+# --------------------------------------------------------------------------
+# Convenience functions for event handling
+# --------------------------------------------------------------------------
+
+def get_event_discriminators(platform: Platform) -> dict[str, bytes]:
+ """Convenience function to get event discriminators for a platform.
+
+ Args:
+ platform: Platform to get event discriminators for
+
+ Returns:
+ Dictionary mapping event names to discriminator bytes
+ """
+ return get_idl_manager().get_event_discriminators(platform)
+
+
+def get_event_names(platform: Platform) -> list[str]:
+ """Convenience function to get event names for a platform.
+
+ Args:
+ platform: Platform to get event names for
+
+ Returns:
+ List of event names
+ """
+ return get_idl_manager().get_event_names(platform)
+
+
+def decode_event_from_logs(platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None:
+ """Convenience function to decode events from logs.
+
+ Args:
+ platform: Platform to use for decoding
+ logs: List of log strings from transaction
+ event_name: Optional specific event name to look for
+
+ Returns:
+ Decoded event data if found, None otherwise
+ """
+ return get_idl_manager().decode_event_from_logs(platform, logs, event_name)
\ No newline at end of file
diff --git a/src/utils/idl_parser.py b/src/utils/idl_parser.py
index 272f580..ffcc272 100644
--- a/src/utils/idl_parser.py
+++ b/src/utils/idl_parser.py
@@ -1,8 +1,9 @@
"""
IDL Parser module for Solana programs.
-Provides functionality to load and parse Anchor IDL files and decode instruction data.
+Provides functionality to load and parse Anchor IDL files and decode instruction data and events.
"""
+import base64
import json
import struct
from typing import Any
@@ -17,7 +18,7 @@ ENUM_DISCRIMINATOR_SIZE = 1
class IDLParser:
- """Parser for automatically decoding instructions using IDL definitions."""
+ """Parser for automatically decoding instructions and events using IDL definitions."""
# A single source of truth for primitive type information, mapping the type name
# to its struct format character and size in bytes.
@@ -48,14 +49,16 @@ class IDLParser:
with open(idl_path) as f:
self.idl = json.load(f)
self.instructions: dict[bytes, dict[str, Any]] = {}
+ self.events: dict[bytes, dict[str, Any]] = {}
self.types: dict[str, dict[str, Any]] = {}
self.instruction_min_sizes: dict[bytes, int] = {}
self._build_instruction_map()
+ self._build_event_map()
self._build_type_map()
self._calculate_instruction_sizes()
# --------------------------------------------------------------------------
- # Public Methods (External API)
+ # Public Methods (External API) - Instructions
# --------------------------------------------------------------------------
def get_instruction_discriminators(self) -> dict[str, bytes]:
@@ -132,6 +135,108 @@ class IDLParser:
'accounts': account_info
}
+ # --------------------------------------------------------------------------
+ # Public Methods (External API) - Events
+ # --------------------------------------------------------------------------
+
+ def get_event_discriminators(self) -> dict[str, bytes]:
+ """Get a mapping of event names to their discriminators."""
+ return {event['name']: disc for disc, event in self.events.items()}
+
+ def get_event_names(self) -> list[str]:
+ """Get a list of all available event names."""
+ return [event['name'] for event in self.events.values()]
+
+ def decode_event_data(self, event_data: bytes, event_name: str | None = None) -> dict[str, Any] | None:
+ """
+ Decode event data using IDL event definitions.
+
+ Args:
+ event_data: Raw event data bytes (typically from base64 decoded log data)
+ event_name: Optional event name to decode as. If None, will try to match discriminator.
+
+ Returns:
+ Decoded event data as a dictionary, or None if decoding fails.
+ """
+ if len(event_data) < DISCRIMINATOR_SIZE:
+ return None
+
+ discriminator = event_data[:DISCRIMINATOR_SIZE]
+
+ # If event_name provided, validate it matches the discriminator
+ if event_name:
+ event_discriminators = self.get_event_discriminators()
+ if event_name not in event_discriminators:
+ if self.verbose:
+ print(f"Unknown event name: {event_name}")
+ return None
+ if event_discriminators[event_name] != discriminator:
+ if self.verbose:
+ print(f"Event discriminator mismatch for {event_name}")
+ return None
+ event_def = self.events[discriminator]
+ else:
+ # Try to find event by discriminator
+ if discriminator not in self.events:
+ if self.verbose:
+ print(f"Unknown event discriminator: {discriminator.hex()}")
+ return None
+ event_def = self.events[discriminator]
+
+ # Decode event fields
+ try:
+ event_fields = {}
+ data_part = event_data[DISCRIMINATOR_SIZE:]
+ decode_offset = 0
+
+ for field in event_def.get('fields', []):
+ value, decode_offset = self._decode_type(data_part, decode_offset, field['type'])
+ event_fields[field['name']] = value
+
+ return {
+ 'event_name': event_def['name'],
+ 'fields': event_fields
+ }
+
+ except Exception as e:
+ if self.verbose:
+ print(f"❌ Error decoding event {event_def['name']}: {e}")
+ return None
+
+ def find_event_in_logs(self, logs: list[str], target_event_name: str | None = None) -> dict[str, Any] | None:
+ """
+ Find and decode event data from transaction logs.
+
+ Args:
+ logs: List of log strings from a transaction
+ target_event_name: Optional specific event name to look for
+
+ Returns:
+ Decoded event data if found, None otherwise
+ """
+ for log in logs:
+ if "Program data:" in log:
+ try:
+ # Extract base64 encoded data
+ encoded_data = log.split("Program data: ")[1].strip()
+ decoded_data = base64.b64decode(encoded_data)
+
+ # Try to decode as event
+ event_data = self.decode_event_data(decoded_data, target_event_name)
+ if event_data:
+ return event_data
+
+ except Exception as e:
+ if self.verbose:
+ print(f"Failed to decode log data: {e}")
+ continue
+
+ return None
+
+ # --------------------------------------------------------------------------
+ # Public Methods (External API) - Account Data
+ # --------------------------------------------------------------------------
+
def decode_account_data(self, account_data: bytes, account_type_name: str, skip_discriminator: bool = True) -> dict[str, Any] | None:
"""
Decode account data using a specific account type from the IDL.
@@ -179,6 +284,15 @@ class IDLParser:
discriminator = bytes(instruction['discriminator'])
self.instructions[discriminator] = instruction
+ def _build_event_map(self):
+ """Build a map of discriminators to event definitions."""
+ for event in self.idl.get('events', []):
+ # The discriminator from the JSON IDL is a list of u8 integers.
+ discriminator = bytes(event['discriminator'])
+ self.events[discriminator] = event
+ if self.verbose:
+ print(f"📅 Loaded event: {event['name']} with discriminator {discriminator.hex()}")
+
def _build_type_map(self):
"""Build a map of type names to their definitions."""
for type_def in self.idl.get('types', []):
From 2c6da58eb7b4b014062e9b6c8922af2b07fe3d25 Mon Sep 17 00:00:00 2001
From: smypmsa
bool:
+ """Check if verbose logging is enabled."""
+ return getattr(self, '_verbose', False)
+
+ @verbose.setter
+ def verbose(self, value: bool) -> None:
+ """Set verbose logging."""
+ self._verbose = value
\ No newline at end of file
diff --git a/src/utils/idl_parser.py b/src/utils/idl_parser.py
index ffcc272..2154b9d 100644
--- a/src/utils/idl_parser.py
+++ b/src/utils/idl_parser.py
@@ -163,44 +163,78 @@ class IDLParser:
discriminator = event_data[:DISCRIMINATOR_SIZE]
- # If event_name provided, validate it matches the discriminator
- if event_name:
- event_discriminators = self.get_event_discriminators()
- if event_name not in event_discriminators:
- if self.verbose:
- print(f"Unknown event name: {event_name}")
- return None
- if event_discriminators[event_name] != discriminator:
- if self.verbose:
- print(f"Event discriminator mismatch for {event_name}")
- return None
- event_def = self.events[discriminator]
- else:
- # Try to find event by discriminator
- if discriminator not in self.events:
- if self.verbose:
- print(f"Unknown event discriminator: {discriminator.hex()}")
- return None
- event_def = self.events[discriminator]
+ # Find event definition by discriminator in events section
+ if discriminator not in self.events:
+ if self.verbose:
+ print(f"Unknown event discriminator: {discriminator.hex()}")
+ return None
+
+ event_def = self.events[discriminator]
+
+ # If event_name provided, validate it matches
+ if event_name and event_def['name'] != event_name:
+ if self.verbose:
+ print(f"Event name mismatch: expected {event_name}, got {event_def['name']}")
+ return None
+ # Get the actual structure definition from types section
+ event_name_actual = event_def['name']
+ if event_name_actual not in self.types:
+ if self.verbose:
+ print(f"Event type {event_name_actual} not found in types section")
+ return None
+
+ type_def = self.types[event_name_actual]
+ event_type = type_def.get('type', {})
+
# Decode event fields
try:
event_fields = {}
data_part = event_data[DISCRIMINATOR_SIZE:]
decode_offset = 0
- for field in event_def.get('fields', []):
- value, decode_offset = self._decode_type(data_part, decode_offset, field['type'])
- event_fields[field['name']] = value
+ if event_type.get('kind') != 'struct':
+ if self.verbose:
+ print(f"Event {event_name_actual} is not a struct type: {event_type.get('kind', 'NO KIND')}")
+ print(f"Available keys in type_def: {list(type_def.keys())}")
+ print(f"Event type structure: {event_type}")
+ return None
+
+ # Decode each field in the struct
+ fields = event_type.get('fields', [])
+ if self.verbose:
+ print(f"Decoding {len(fields)} fields for event {event_name_actual}")
+
+ for field in fields:
+ if self.verbose:
+ print(f"Decoding field: {field['name']} ({field['type']})")
+
+ try:
+ value, decode_offset = self._decode_type(data_part, decode_offset, field['type'])
+ event_fields[field['name']] = value
+
+ if self.verbose:
+ if field['type'] == 'string':
+ print(f" -> '{value}'")
+ elif field['type'] == 'pubkey':
+ print(f" -> {value}")
+ else:
+ print(f" -> {value}")
+
+ except Exception as e:
+ if self.verbose:
+ print(f"Error decoding field {field['name']}: {e}")
+ # Don't return None here, continue with other fields
+ continue
return {
- 'event_name': event_def['name'],
+ 'event_name': event_name_actual,
'fields': event_fields
}
except Exception as e:
if self.verbose:
- print(f"❌ Error decoding event {event_def['name']}: {e}")
+ print(f"❌ Error decoding event {event_name_actual}: {e}")
return None
def find_event_in_logs(self, logs: list[str], target_event_name: str | None = None) -> dict[str, Any] | None:
From bf7fbaa8aba694821ea78762ebfc6cbf0355a39d Mon Sep 17 00:00:00 2001
From: smypmsa