feat(pump): integrate idl parser manager

This commit is contained in:
smypmsa
2025-08-02 15:10:24 +00:00
parent 9a09862f0d
commit 93332794a3
5 changed files with 284 additions and 251 deletions
+2 -2
View File
@@ -107,12 +107,12 @@ class PlatformRegistry:
address_provider = impl_classes['address_provider']() address_provider = impl_classes['address_provider']()
# For platforms with IDL support, pass the parser to relevant classes # 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) instruction_builder = impl_classes['instruction_builder'](idl_parser=idl_parser)
curve_manager = impl_classes['curve_manager'](client, 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) event_parser = impl_classes['event_parser'](idl_parser=idl_parser)
else: else:
# Fallback for platforms without IDL support (like pump.fun) # Fallback for platforms without IDL support
instruction_builder = impl_classes['instruction_builder']() instruction_builder = impl_classes['instruction_builder']()
curve_manager = impl_classes['curve_manager'](client) curve_manager = impl_classes['curve_manager'](client)
event_parser = impl_classes['event_parser']() event_parser = impl_classes['event_parser']()
+118 -150
View File
@@ -2,11 +2,9 @@
Pump.Fun implementation of CurveManager interface. Pump.Fun implementation of CurveManager interface.
This module handles pump.fun-specific bonding curve operations 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 typing import Any
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
@@ -14,56 +12,26 @@ from solders.pubkey import Pubkey
from core.client import SolanaClient from core.client import SolanaClient
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
from interfaces.core import CurveManager, Platform from interfaces.core import CurveManager, Platform
from utils.idl_parser import IDLParser
from utils.logger import get_logger from utils.logger import get_logger
logger = get_logger(__name__) 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): class PumpFunCurveManager(CurveManager):
"""Pump.Fun implementation of CurveManager interface.""" """Pump.Fun implementation of CurveManager interface using IDL-based decoding."""
def __init__(self, client: SolanaClient): def __init__(self, client: SolanaClient, idl_parser: IDLParser):
"""Initialize pump.fun curve manager. """Initialize pump.fun curve manager with injected IDL parser.
Args: Args:
client: Solana RPC client client: Solana RPC client
idl_parser: Pre-loaded IDL parser for pump.fun platform
""" """
self.client = client self.client = client
self._idl_parser = idl_parser
logger.info("Pump.Fun curve manager initialized with injected IDL parser")
@property @property
def platform(self) -> Platform: def platform(self) -> Platform:
@@ -84,22 +52,10 @@ class PumpFunCurveManager(CurveManager):
if not account.data: if not account.data:
raise ValueError(f"No data in bonding curve account {pool_address}") 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 { return curve_state_data
"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: except Exception as e:
logger.error(f"Failed to get curve state: {e!s}") logger.error(f"Failed to get curve state: {e!s}")
@@ -114,8 +70,18 @@ class PumpFunCurveManager(CurveManager):
Returns: Returns:
Current token price in SOL Current token price in SOL
""" """
curve_state = await self._get_curve_state_object(pool_address) pool_state = await self.get_pool_state(pool_address)
return curve_state.calculate_price()
# 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( async def calculate_buy_amount_out(
self, self,
@@ -133,12 +99,15 @@ class PumpFunCurveManager(CurveManager):
Returns: Returns:
Expected amount of tokens to receive (in raw token units) 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 # Use virtual reserves for bonding curve calculation
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in) # Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
numerator = amount_in * curve_state.virtual_token_reserves numerator = amount_in * virtual_token_reserves
denominator = curve_state.virtual_sol_reserves + amount_in denominator = virtual_sol_reserves + amount_in
if denominator == 0: if denominator == 0:
return 0 return 0
@@ -162,12 +131,15 @@ class PumpFunCurveManager(CurveManager):
Returns: Returns:
Expected amount of SOL to receive (in lamports) 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 # Use virtual reserves for bonding curve calculation
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in) # Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
numerator = amount_in * curve_state.virtual_sol_reserves numerator = amount_in * virtual_sol_reserves
denominator = curve_state.virtual_token_reserves + amount_in denominator = virtual_token_reserves + amount_in
if denominator == 0: if denominator == 0:
return 0 return 0
@@ -184,100 +156,61 @@ class PumpFunCurveManager(CurveManager):
Returns: Returns:
Tuple of (token_reserves, sol_reserves) in raw units Tuple of (token_reserves, sol_reserves) in raw units
""" """
curve_state = await self._get_curve_state_object(pool_address) pool_state = await self.get_pool_state(pool_address)
return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves) return (pool_state["virtual_token_reserves"], pool_state["virtual_sol_reserves"])
async def _get_curve_state_object(self, curve_address: Pubkey) -> BondingCurveState: def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]:
"""Get the current state of a bonding curve as a BondingCurveState object. """Decode bonding curve state data using injected IDL parser.
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: Args:
data: Raw account data data: Raw account data
Returns: Returns:
Decoded BondingCurveState Dictionary with decoded bonding curve state
Raises: Raises:
ValueError: If data format is invalid ValueError: If IDL parsing fails
""" """
if len(data) < 8: # Use injected IDL parser to decode BondingCurve account data
raise ValueError("Curve data too short") decoded_curve_state = self._idl_parser.decode_account_data(
data,
"BondingCurve",
skip_discriminator=True
)
# Check discriminator if not decoded_curve_state:
if not data.startswith(CURVE_DISCRIMINATOR): raise ValueError("Failed to decode bonding curve state with IDL parser")
raise ValueError("Invalid curve discriminator")
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: # Calculate additional metrics
# Decode based on pump.fun BondingCurve structure: if curve_data["virtual_token_reserves"] > 0:
# - virtual_token_reserves: u64 (8 bytes) curve_data["price_per_token"] = (
# - virtual_sol_reserves: u64 (8 bytes) (curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"])
# - real_token_reserves: u64 (8 bytes) * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
# - 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("<Q", data, offset)[0]
offset += 8
virtual_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
real_token_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
real_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
token_total_supply = struct.unpack_from("<Q", data, offset)[0]
offset += 8
complete = bool(struct.unpack_from("<B", data, offset)[0])
offset += 1
# Skip padding
offset += 7
creator = Pubkey.from_bytes(data[offset:offset + 32])
return BondingCurveState(
virtual_token_reserves=virtual_token_reserves,
virtual_sol_reserves=virtual_sol_reserves,
real_token_reserves=real_token_reserves,
real_sol_reserves=real_sol_reserves,
token_total_supply=token_total_supply,
complete=complete,
creator=creator,
) )
else:
curve_data["price_per_token"] = 0
except Exception as e: # Add convenience decimal fields
raise ValueError(f"Failed to decode curve state: {e}") 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 # Additional convenience methods for pump.fun specific operations
async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float: async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float:
@@ -321,8 +254,8 @@ class PumpFunCurveManager(CurveManager):
Returns: Returns:
True if curve is complete, False otherwise True if curve is complete, False otherwise
""" """
curve_state = await self._get_curve_state_object(pool_address) pool_state = await self.get_pool_state(pool_address)
return curve_state.complete return pool_state.get("complete", False)
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]: async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get bonding curve completion progress information. """Get bonding curve completion progress information.
@@ -333,11 +266,11 @@ class PumpFunCurveManager(CurveManager):
Returns: Returns:
Dictionary with progress information 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 # Calculate progress based on SOL raised vs target
# This is approximate since the exact target isn't stored in the curve state # 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 # Estimate progress based on typical pump.fun graduation requirements
# (This could be made more accurate with additional on-chain data) # (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) progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
return { return {
"complete": curve_state.complete, "complete": pool_state.get("complete", False),
"sol_raised": sol_raised, "sol_raised": sol_raised,
"estimated_target_sol": estimated_target_sol, "estimated_target_sol": estimated_target_sol,
"progress_percentage": progress_percentage, "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 "market_cap_sol": sol_raised, # Approximate market cap
} }
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
+135 -84
View File
@@ -2,32 +2,43 @@
Pump.Fun implementation of EventParser interface. Pump.Fun implementation of EventParser interface.
This module parses pump.fun-specific token creation events from various sources 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 base64
import struct import struct
from time import monotonic from time import monotonic
from typing import Any, Final from typing import Any
import base58
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction from solders.transaction import VersionedTransaction
from core.pubkeys import SystemAddresses from core.pubkeys import SystemAddresses
from interfaces.core import EventParser, Platform, TokenInfo from interfaces.core import EventParser, Platform, TokenInfo
from platforms.pumpfun.address_provider import PumpFunAddresses 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): class PumpFunEventParser(EventParser):
"""Pump.Fun implementation of EventParser interface.""" """Pump.Fun implementation of EventParser interface with IDL-based parsing."""
# Discriminators for pump.fun instructions def __init__(self, idl_parser: IDLParser):
CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728 """Initialize pump.fun event parser with injected IDL parser.
CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack("<Q", CREATE_DISCRIMINATOR)
# Discriminator for program logs Args:
LOGS_CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891 idl_parser: Pre-loaded IDL parser for pump.fun platform
"""
self._idl_parser = idl_parser
# Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators()
self._create_discriminator_bytes = discriminators["create"]
self._create_discriminator = struct.unpack("<Q", self._create_discriminator_bytes)[0]
logger.info("Pump.Fun event parser initialized with injected IDL parser")
@property @property
def platform(self) -> Platform: def platform(self) -> Platform:
@@ -74,7 +85,7 @@ class PumpFunEventParser(EventParser):
accounts: list[int], accounts: list[int],
account_keys: list[bytes] account_keys: list[bytes]
) -> TokenInfo | None: ) -> TokenInfo | None:
"""Parse token creation from pump.fun instruction data. """Parse token creation from pump.fun instruction data using injected IDL parser.
Args: Args:
instruction_data: Raw instruction data instruction_data: Raw instruction data
@@ -84,7 +95,7 @@ class PumpFunEventParser(EventParser):
Returns: Returns:
TokenInfo if token creation found, None otherwise 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 return None
try: try:
@@ -97,12 +108,14 @@ class PumpFunEventParser(EventParser):
return None return None
return Pubkey.from_bytes(account_keys[account_index]) return Pubkey.from_bytes(account_keys[account_index])
# Parse instruction data # Parse instruction data using injected IDL parser
token_data = self._parse_create_instruction_data(instruction_data) decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts)
if not token_data: if not decoded or decoded['instruction_name'] != 'create':
return None return None
# Extract account information args = decoded.get('args', {})
# Extract account information based on IDL account order
mint = get_account_key(0) mint = get_account_key(0)
bonding_curve = get_account_key(2) bonding_curve = get_account_key(2)
associated_bonding_curve = get_account_key(3) associated_bonding_curve = get_account_key(3)
@@ -112,13 +125,13 @@ class PumpFunEventParser(EventParser):
return None return None
# Create creator vault # 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) creator_vault = self._derive_creator_vault(creator)
return TokenInfo( return TokenInfo(
name=token_data["name"], name=args.get("name", ""),
symbol=token_data["symbol"], symbol=args.get("symbol", ""),
uri=token_data["uri"], uri=args.get("uri", ""),
mint=mint, mint=mint,
platform=Platform.PUMP_FUN, platform=Platform.PUMP_FUN,
bonding_curve=bonding_curve, bonding_curve=bonding_curve,
@@ -129,7 +142,8 @@ class PumpFunEventParser(EventParser):
creation_timestamp=monotonic(), creation_timestamp=monotonic(),
) )
except Exception: except Exception as e:
logger.debug(f"Failed to parse create instruction: {e}")
return None return None
def parse_token_creation_from_geyser( def parse_token_creation_from_geyser(
@@ -172,7 +186,8 @@ class PumpFunEventParser(EventParser):
return None return None
except Exception: except Exception as e:
logger.debug(f"Failed to parse geyser transaction: {e}")
return None return None
def get_program_id(self) -> Pubkey: def get_program_id(self) -> Pubkey:
@@ -189,7 +204,7 @@ class PumpFunEventParser(EventParser):
Returns: Returns:
List of discriminator bytes to match 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: def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener). """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: if not isinstance(tx, dict) or "transaction" not in tx:
continue continue
# Decode base64 transaction data # Decode base64 transaction data if needed
tx_data_encoded = tx["transaction"][0] tx_data = tx["transaction"]
tx_data_decoded = base64.b64decode(tx_data_encoded) if isinstance(tx_data, list) and len(tx_data) > 0:
transaction = VersionedTransaction.from_bytes(tx_data_decoded) 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: for ix in transaction.message.instructions:
program_id = transaction.message.account_keys[ix.program_id_index] program_id = transaction.message.account_keys[ix.program_id_index]
# Check if instruction is from pump.fun program # Check if instruction is from pump.fun program
if str(program_id) != str(self.get_program_id()): 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("<Q", ix_data[:8])[0]
if discriminator == self.CREATE_DISCRIMINATOR:
# Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10:
continue continue
# Parse the instruction ix_data = bytes(ix.data)
token_info = self.parse_token_creation_from_instruction(
ix_data, ix.accounts, transaction.message.account_keys # Check for create discriminator
) if len(ix_data) >= 8:
if token_info: discriminator = struct.unpack("<Q", ix_data[:8])[0]
return token_info
if discriminator == self._create_discriminator:
# Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10:
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
except Exception as e:
logger.debug(f"Failed to parse block transaction: {e}")
continue
# Handle already decoded transaction data
elif isinstance(tx_data, dict) and "message" in tx_data:
try:
message = tx_data["message"]
if "instructions" not in message or "accountKeys" not in message:
continue
for ix in message["instructions"]:
if "programIdIndex" not in ix or "accounts" not in ix or "data" not in ix:
continue
program_idx = ix["programIdIndex"]
if program_idx >= 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("<Q", ix_data[:8])[0]
if discriminator == self._create_discriminator:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue
# Convert account keys to bytes for parsing
account_keys_bytes = [
Pubkey.from_string(key).to_bytes()
for key in message["accountKeys"]
]
token_info = self.parse_token_creation_from_instruction(
ix_data, ix["accounts"], account_keys_bytes
)
if token_info:
return token_info
except Exception as e:
logger.debug(f"Failed to parse decoded block transaction: {e}")
continue
return None return None
except Exception: except Exception as e:
logger.debug(f"Failed to parse block data: {e}")
return None return None
def _parse_create_instruction_data(self, data: bytes) -> dict | None: def _parse_create_instruction_data(self, data: bytes) -> dict | None:
"""Parse the create instruction data from pump.fun. """Parse the create instruction data from pump.fun using injected IDL parser.
Args: Args:
data: Raw instruction data data: Raw instruction data
@@ -257,47 +325,30 @@ class PumpFunEventParser(EventParser):
# Check for the correct instruction discriminator # Check for the correct instruction discriminator
discriminator = struct.unpack("<Q", data[:8])[0] discriminator = struct.unpack("<Q", data[:8])[0]
if discriminator not in [self.CREATE_DISCRIMINATOR, self.LOGS_CREATE_DISCRIMINATOR]: if discriminator != self._create_discriminator:
return None return None
offset = 8
parsed_data = {}
try: try:
# Parse fields based on CreateEvent structure # Use IDL parser to decode the instruction data
fields = [ # For log data parsing, we need to create dummy account info
("name", "string"), dummy_accounts = list(range(20)) # Assume enough accounts
("symbol", "string"), dummy_account_keys = [b'\x00' * 32] * 20 # Dummy keys
("uri", "string"),
]
# For instruction data, we also have creator info decoded = self._idl_parser.decode_instruction(data, dummy_account_keys, dummy_accounts)
if discriminator == self.CREATE_DISCRIMINATOR: if not decoded or decoded['instruction_name'] != 'create':
fields.extend([ return None
("creator", "publicKey"),
])
for field_name, field_type in fields: # Extract the arguments
if field_type == "string": args = decoded.get('args', {})
if offset + 4 > len(data): return {
return None "name": args.get("name", ""),
length = struct.unpack("<I", data[offset : offset + 4])[0] "symbol": args.get("symbol", ""),
offset += 4 "uri": args.get("uri", ""),
if offset + length > len(data): "creator": args.get("creator", ""),
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 except Exception as e:
logger.debug(f"Failed to parse instruction data with IDL: {e}")
return parsed_data
except Exception:
return None return None
def _derive_creator_vault(self, creator: Pubkey) -> Pubkey: def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
+22 -8
View File
@@ -2,11 +2,10 @@
Pump.Fun implementation of InstructionBuilder interface. Pump.Fun implementation of InstructionBuilder interface.
This module builds pump.fun-specific buy and sell instructions 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 import struct
from typing import Final
from solders.instruction import AccountMeta, Instruction from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey 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 core.pubkeys import TOKEN_DECIMALS, SystemAddresses
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo 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 logger = get_logger(__name__)
BUY_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
SELL_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 12502976635542562355)
class PumpFunInstructionBuilder(InstructionBuilder): class PumpFunInstructionBuilder(InstructionBuilder):
"""Pump.Fun implementation of InstructionBuilder interface.""" """Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators."""
def __init__(self, idl_parser: IDLParser):
"""Initialize pump.fun instruction builder with injected IDL parser.
Args:
idl_parser: Pre-loaded IDL parser for pump.fun platform
"""
self._idl_parser = idl_parser
# Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators()
self._buy_discriminator = discriminators["buy"]
self._sell_discriminator = discriminators["sell"]
logger.info("Pump.Fun instruction builder initialized with injected IDL parser")
@property @property
def platform(self) -> Platform: def platform(self) -> Platform:
@@ -82,7 +96,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Build instruction data: discriminator + token_amount + max_sol_cost # Build instruction data: discriminator + token_amount + max_sol_cost
instruction_data = ( instruction_data = (
BUY_DISCRIMINATOR + self._buy_discriminator +
struct.pack("<Q", minimum_amount_out) + # token amount in raw units struct.pack("<Q", minimum_amount_out) + # token amount in raw units
struct.pack("<Q", amount_in) # max SOL cost in lamports struct.pack("<Q", amount_in) # max SOL cost in lamports
) )
@@ -139,7 +153,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Build instruction data: discriminator + token_amount + min_sol_output # Build instruction data: discriminator + token_amount + min_sol_output
instruction_data = ( instruction_data = (
SELL_DISCRIMINATOR + self._sell_discriminator +
struct.pack("<Q", amount_in) + # token amount in raw units struct.pack("<Q", amount_in) + # token amount in raw units
struct.pack("<Q", minimum_amount_out) # min SOL output in lamports struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
) )
+1 -1
View File
@@ -33,7 +33,7 @@ class IDLManager:
# Define IDL paths for each platform # Define IDL paths for each platform
self._idl_paths = { self._idl_paths = {
Platform.LETS_BONK: os.path.join(project_root, "idl", "raydium_launchlab_idl.json"), 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 Platform.PUMP_FUN: os.path.join(project_root, "idl", "pump_fun_idl.json"),
} }
def get_parser(self, platform: Platform, verbose: bool = False) -> IDLParser: def get_parser(self, platform: Platform, verbose: bool = False) -> IDLParser: