mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-18 01:38:04 +00:00
feat(pump): integrate idl parser manager
This commit is contained in:
@@ -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']()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user