From 93332794a3a9cd8ed9d1dcf14d21c7e64068fc23 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 15:10:24 +0000 Subject: [PATCH] feat(pump): integrate idl parser manager --- src/platforms/__init__.py | 4 +- src/platforms/pumpfun/curve_manager.py | 272 ++++++++----------- src/platforms/pumpfun/event_parser.py | 227 ++++++++++------ src/platforms/pumpfun/instruction_builder.py | 30 +- src/utils/idl_manager.py | 2 +- 5 files changed, 284 insertions(+), 251 deletions(-) diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index feb53f0..f7dd9b5 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -107,12 +107,12 @@ class PlatformRegistry: address_provider = impl_classes['address_provider']() # For platforms with IDL support, pass the parser to relevant classes - if idl_parser and platform in [Platform.LETS_BONK]: # Add other IDL-supported platforms here + if idl_parser and platform in [Platform.LETS_BONK, Platform.PUMP_FUN]: instruction_builder = impl_classes['instruction_builder'](idl_parser=idl_parser) curve_manager = impl_classes['curve_manager'](client, idl_parser=idl_parser) event_parser = impl_classes['event_parser'](idl_parser=idl_parser) else: - # Fallback for platforms without IDL support (like pump.fun) + # Fallback for platforms without IDL support instruction_builder = impl_classes['instruction_builder']() curve_manager = impl_classes['curve_manager'](client) event_parser = impl_classes['event_parser']() diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py index 74220fb..b113c3c 100644 --- a/src/platforms/pumpfun/curve_manager.py +++ b/src/platforms/pumpfun/curve_manager.py @@ -2,11 +2,9 @@ Pump.Fun implementation of CurveManager interface. This module handles pump.fun-specific bonding curve operations -by implementing the CurveManager interface with all curve logic self-contained. +by implementing the CurveManager interface using IDL-based decoding. """ -import struct -from dataclasses import dataclass from typing import Any from solders.pubkey import Pubkey @@ -14,56 +12,26 @@ from solders.pubkey import Pubkey from core.client import SolanaClient from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS from interfaces.core import CurveManager, Platform +from utils.idl_parser import IDLParser from utils.logger import get_logger logger = get_logger(__name__) -# Bonding curve discriminator for pump.fun -CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3]) - - -@dataclass -class BondingCurveState: - """Represents the state of a pump.fun bonding curve.""" - - virtual_token_reserves: int - virtual_sol_reserves: int - real_token_reserves: int - real_sol_reserves: int - token_total_supply: int - complete: bool - creator: Pubkey - - @property - def token_reserves(self) -> 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): - """Pump.Fun implementation of CurveManager interface.""" + """Pump.Fun implementation of CurveManager interface using IDL-based decoding.""" - def __init__(self, client: SolanaClient): - """Initialize pump.fun curve manager. + def __init__(self, client: SolanaClient, idl_parser: IDLParser): + """Initialize pump.fun curve manager with injected IDL parser. Args: client: Solana RPC client + idl_parser: Pre-loaded IDL parser for pump.fun platform """ self.client = client + self._idl_parser = idl_parser + + logger.info("Pump.Fun curve manager initialized with injected IDL parser") @property def platform(self) -> Platform: @@ -84,22 +52,10 @@ class PumpFunCurveManager(CurveManager): if not account.data: raise ValueError(f"No data in bonding curve account {pool_address}") - curve_state = self._decode_curve_state(account.data) + # Decode bonding curve state using injected IDL parser + curve_state_data = self._decode_curve_state_with_idl(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, - } + return curve_state_data except Exception as e: logger.error(f"Failed to get curve state: {e!s}") @@ -114,8 +70,18 @@ class PumpFunCurveManager(CurveManager): Returns: Current token price in SOL """ - curve_state = await self._get_curve_state_object(pool_address) - return curve_state.calculate_price() + pool_state = await self.get_pool_state(pool_address) + + # Use virtual reserves for price calculation + virtual_token_reserves = pool_state["virtual_token_reserves"] + virtual_sol_reserves = pool_state["virtual_sol_reserves"] + + if virtual_token_reserves <= 0: + return 0.0 + + # Price = sol_reserves / token_reserves + price_lamports = virtual_sol_reserves / virtual_token_reserves + return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL async def calculate_buy_amount_out( self, @@ -133,12 +99,15 @@ class PumpFunCurveManager(CurveManager): Returns: Expected amount of tokens to receive (in raw token units) """ - curve_state = await self._get_curve_state_object(pool_address) + pool_state = await self.get_pool_state(pool_address) + + virtual_token_reserves = pool_state["virtual_token_reserves"] + virtual_sol_reserves = pool_state["virtual_sol_reserves"] # 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 + numerator = amount_in * virtual_token_reserves + denominator = virtual_sol_reserves + amount_in if denominator == 0: return 0 @@ -162,12 +131,15 @@ class PumpFunCurveManager(CurveManager): Returns: Expected amount of SOL to receive (in lamports) """ - curve_state = await self._get_curve_state_object(pool_address) + pool_state = await self.get_pool_state(pool_address) + + virtual_token_reserves = pool_state["virtual_token_reserves"] + virtual_sol_reserves = pool_state["virtual_sol_reserves"] # 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 + numerator = amount_in * virtual_sol_reserves + denominator = virtual_token_reserves + amount_in if denominator == 0: return 0 @@ -184,100 +156,61 @@ class PumpFunCurveManager(CurveManager): Returns: Tuple of (token_reserves, sol_reserves) in raw units """ - curve_state = await self._get_curve_state_object(pool_address) - return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves) + pool_state = await self.get_pool_state(pool_address) + return (pool_state["virtual_token_reserves"], pool_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. + def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]: + """Decode bonding curve state data using injected IDL parser. Args: data: Raw account data Returns: - Decoded BondingCurveState + Dictionary with decoded bonding curve state Raises: - ValueError: If data format is invalid + ValueError: If IDL parsing fails """ - if len(data) < 8: - raise ValueError("Curve data too short") + # Use injected IDL parser to decode BondingCurve account data + decoded_curve_state = self._idl_parser.decode_account_data( + data, + "BondingCurve", + skip_discriminator=True + ) - # Check discriminator - if not data.startswith(CURVE_DISCRIMINATOR): - raise ValueError("Invalid curve discriminator") + if not decoded_curve_state: + raise ValueError("Failed to decode bonding curve state with IDL parser") - offset = 8 + # Extract the fields we need for trading calculations + # Based on the BondingCurve structure from the IDL + curve_data = { + "virtual_token_reserves": decoded_curve_state.get("virtual_token_reserves", 0), + "virtual_sol_reserves": decoded_curve_state.get("virtual_sol_reserves", 0), + "real_token_reserves": decoded_curve_state.get("real_token_reserves", 0), + "real_sol_reserves": decoded_curve_state.get("real_sol_reserves", 0), + "token_total_supply": decoded_curve_state.get("token_total_supply", 0), + "complete": decoded_curve_state.get("complete", False), + "creator": decoded_curve_state.get("creator", ""), + } - 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(" 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: