mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-04 11:17:45 +00:00
wip(core): platform aware trading
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
"""
|
||||
Platform factory and registry for managing multiple trading platforms.
|
||||
|
||||
This module provides a centralized way to instantiate and access
|
||||
platform-specific implementations of the trading interfaces.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from core.client import SolanaClient
|
||||
from interfaces.core import (
|
||||
AddressProvider,
|
||||
CurveManager,
|
||||
EventParser,
|
||||
InstructionBuilder,
|
||||
Platform,
|
||||
)
|
||||
|
||||
from . import letsbonk, pumpfun
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformImplementations:
|
||||
"""Container for all platform-specific implementations."""
|
||||
address_provider: AddressProvider
|
||||
instruction_builder: InstructionBuilder
|
||||
curve_manager: CurveManager
|
||||
event_parser: EventParser
|
||||
|
||||
|
||||
class PlatformRegistry:
|
||||
"""Registry for platform implementations."""
|
||||
|
||||
def __init__(self):
|
||||
self._implementations: dict[Platform, dict[str, type]] = {}
|
||||
self._instances: dict[Platform, PlatformImplementations] = {}
|
||||
|
||||
def register_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
) -> None:
|
||||
"""Register platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
instruction_builder_class: InstructionBuilder implementation class
|
||||
curve_manager_class: CurveManager implementation class
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
self._implementations[platform] = {
|
||||
'address_provider': address_provider_class,
|
||||
'instruction_builder': instruction_builder_class,
|
||||
'curve_manager': curve_manager_class,
|
||||
'event_parser': event_parser_class
|
||||
}
|
||||
|
||||
def create_platform_implementations(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**kwargs: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create platform implementation instances.
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**kwargs: Additional arguments for implementation constructors
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
|
||||
Raises:
|
||||
ValueError: If platform is not registered
|
||||
"""
|
||||
if platform not in self._implementations:
|
||||
raise ValueError(f"Platform {platform} is not registered")
|
||||
|
||||
# Check if we already have instances for this platform
|
||||
if platform in self._instances:
|
||||
return self._instances[platform]
|
||||
|
||||
impl_classes = self._implementations[platform]
|
||||
|
||||
# Create instances
|
||||
address_provider = impl_classes['address_provider']()
|
||||
instruction_builder = impl_classes['instruction_builder']()
|
||||
curve_manager = impl_classes['curve_manager'](client)
|
||||
event_parser = impl_classes['event_parser']()
|
||||
|
||||
implementations = PlatformImplementations(
|
||||
address_provider=address_provider,
|
||||
instruction_builder=instruction_builder,
|
||||
curve_manager=curve_manager,
|
||||
event_parser=event_parser
|
||||
)
|
||||
|
||||
# Cache the instances
|
||||
self._instances[platform] = implementations
|
||||
|
||||
return implementations
|
||||
|
||||
def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None:
|
||||
"""Get cached platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
|
||||
Returns:
|
||||
PlatformImplementations if available, None otherwise
|
||||
"""
|
||||
return self._instances.get(platform)
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
Returns:
|
||||
List of registered platforms
|
||||
"""
|
||||
return list(self._implementations.keys())
|
||||
|
||||
def is_platform_supported(self, platform: Platform) -> bool:
|
||||
"""Check if a platform is supported.
|
||||
|
||||
Args:
|
||||
platform: Platform to check
|
||||
|
||||
Returns:
|
||||
True if platform is registered, False otherwise
|
||||
"""
|
||||
return platform in self._implementations
|
||||
|
||||
|
||||
class PlatformFactory:
|
||||
"""Factory for creating platform-specific implementations."""
|
||||
|
||||
def __init__(self):
|
||||
self.registry = PlatformRegistry()
|
||||
self._setup_default_platforms()
|
||||
|
||||
def _setup_default_platforms(self) -> None:
|
||||
"""Setup default platform registrations.
|
||||
|
||||
This will be populated as we implement each platform.
|
||||
"""
|
||||
# Platforms will be registered here as they are implemented
|
||||
# Example:
|
||||
# from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ...
|
||||
# self.registry.register_platform(
|
||||
# Platform.PUMP_FUN,
|
||||
# PumpFunAddressProvider,
|
||||
# PumpFunInstructionBuilder,
|
||||
# PumpFunCurveManager,
|
||||
# PumpFunEventParser
|
||||
# )
|
||||
pass
|
||||
|
||||
def create_for_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**config: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create all implementations for a specific platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**config: Platform-specific configuration
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return self.registry.create_platform_implementations(platform, client, **config)
|
||||
|
||||
def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider:
|
||||
"""Get address provider for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get provider for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
AddressProvider implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.address_provider
|
||||
|
||||
def get_instruction_builder(self, platform: Platform, client: SolanaClient) -> InstructionBuilder:
|
||||
"""Get instruction builder for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get builder for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
InstructionBuilder implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.instruction_builder
|
||||
|
||||
def get_curve_manager(self, platform: Platform, client: SolanaClient) -> CurveManager:
|
||||
"""Get curve manager for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get manager for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
CurveManager implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.curve_manager
|
||||
|
||||
def get_event_parser(self, platform: Platform, client: SolanaClient) -> EventParser:
|
||||
"""Get event parser for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get parser for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
EventParser implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.event_parser
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
Returns:
|
||||
List of supported platforms
|
||||
"""
|
||||
return self.registry.get_supported_platforms()
|
||||
|
||||
|
||||
# Global factory instance
|
||||
platform_factory = PlatformFactory()
|
||||
|
||||
|
||||
def get_platform_implementations(platform: Platform, client: SolanaClient) -> PlatformImplementations:
|
||||
"""Convenience function to get platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return platform_factory.create_for_platform(platform, client)
|
||||
|
||||
|
||||
def register_platform_implementations(
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
) -> None:
|
||||
"""Register platform implementations with the global factory.
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
instruction_builder_class: InstructionBuilder implementation class
|
||||
curve_manager_class: CurveManager implementation class
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
platform_factory.registry.register_platform(
|
||||
platform,
|
||||
address_provider_class,
|
||||
instruction_builder_class,
|
||||
curve_manager_class,
|
||||
event_parser_class
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
LetsBonk platform registration and exports.
|
||||
|
||||
This module registers all LetsBonk implementations with the platform factory
|
||||
and provides convenient imports for the LetsBonk platform.
|
||||
"""
|
||||
|
||||
from interfaces.core import Platform
|
||||
from platforms import register_platform_implementations
|
||||
|
||||
from .address_provider import LetsBonkAddressProvider
|
||||
from .curve_manager import LetsBonkCurveManager
|
||||
from .event_parser import LetsBonkEventParser
|
||||
from .instruction_builder import LetsBonkInstructionBuilder
|
||||
|
||||
# Register LetsBonk platform implementations
|
||||
register_platform_implementations(
|
||||
Platform.LETS_BONK,
|
||||
LetsBonkAddressProvider,
|
||||
LetsBonkInstructionBuilder,
|
||||
LetsBonkCurveManager,
|
||||
LetsBonkEventParser
|
||||
)
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'LetsBonkAddressProvider',
|
||||
'LetsBonkCurveManager',
|
||||
'LetsBonkEventParser',
|
||||
'LetsBonkInstructionBuilder'
|
||||
]
|
||||
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
LetsBonk implementation of AddressProvider interface.
|
||||
|
||||
This module provides all LetsBonk (Raydium LaunchLab) specific addresses and PDA derivations
|
||||
by implementing the AddressProvider interface.
|
||||
"""
|
||||
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
|
||||
|
||||
class LetsBonkAddressProvider(AddressProvider):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface."""
|
||||
|
||||
# Raydium LaunchLab program addresses
|
||||
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
|
||||
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
|
||||
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1")
|
||||
|
||||
# System program addresses
|
||||
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
|
||||
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
|
||||
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for LetsBonk.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
return {
|
||||
# Raydium LaunchLab specific addresses
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
|
||||
# System addresses
|
||||
"system_program": self.SYSTEM_PROGRAM_ID,
|
||||
"token_program": self.TOKEN_PROGRAM_ID,
|
||||
"associated_token_program": self.ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
"rent": self.SYSTEM_RENT_PROGRAM_ID,
|
||||
"wsol_mint": self.WSOL_MINT,
|
||||
}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
"""Derive the pool state address for a token pair.
|
||||
|
||||
For LetsBonk, this derives the pool state PDA using base_mint and WSOL.
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint (defaults to WSOL)
|
||||
|
||||
Returns:
|
||||
Pool state address
|
||||
"""
|
||||
if quote_mint is None:
|
||||
quote_mint = self.WSOL_MINT
|
||||
|
||||
pool_state, _ = Pubkey.find_program_address(
|
||||
[b"pool", bytes(base_mint), bytes(quote_mint)],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return pool_state
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get LetsBonk-specific additional accounts needed for trading.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
# Add pool state if available
|
||||
if token_info.pool_state:
|
||||
accounts["pool_state"] = token_info.pool_state
|
||||
|
||||
# Add vault addresses if available
|
||||
if token_info.base_vault:
|
||||
accounts["base_vault"] = token_info.base_vault
|
||||
if token_info.quote_vault:
|
||||
accounts["quote_vault"] = token_info.quote_vault
|
||||
|
||||
# Derive pool state if not provided
|
||||
if not token_info.pool_state:
|
||||
accounts["pool_state"] = self.derive_pool_address(token_info.mint)
|
||||
|
||||
# Derive authority PDA
|
||||
accounts["authority"] = self.derive_authority_pda()
|
||||
|
||||
# Derive event authority PDA
|
||||
accounts["event_authority"] = self.derive_event_authority_pda()
|
||||
|
||||
return accounts
|
||||
|
||||
def derive_authority_pda(self) -> Pubkey:
|
||||
"""Derive the authority PDA for Raydium LaunchLab.
|
||||
|
||||
This PDA acts as the authority for pool vault operations.
|
||||
|
||||
Returns:
|
||||
Authority PDA address
|
||||
"""
|
||||
AUTH_SEED = b"vault_auth_seed"
|
||||
authority_pda, _ = Pubkey.find_program_address(
|
||||
[AUTH_SEED],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return authority_pda
|
||||
|
||||
def derive_event_authority_pda(self) -> Pubkey:
|
||||
"""Derive the event authority PDA for Raydium LaunchLab.
|
||||
|
||||
This PDA is used for emitting program events during swaps.
|
||||
|
||||
Returns:
|
||||
Event authority PDA address
|
||||
"""
|
||||
EVENT_AUTHORITY_SEED = b"__event_authority"
|
||||
event_authority_pda, _ = Pubkey.find_program_address(
|
||||
[EVENT_AUTHORITY_SEED],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return event_authority_pda
|
||||
|
||||
def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
|
||||
"""Create a WSOL account address using createAccountWithSeed pattern.
|
||||
|
||||
Args:
|
||||
payer: The account that will pay for and own the new account
|
||||
seed: String seed for deterministic account generation
|
||||
|
||||
Returns:
|
||||
New WSOL account address
|
||||
"""
|
||||
return Pubkey.create_with_seed(payer, seed, self.TOKEN_PROGRAM_ID)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"base_vault": additional_accounts.get("base_vault", token_info.base_vault),
|
||||
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault),
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": self.WSOL_MINT,
|
||||
"base_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"quote_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"base_vault": additional_accounts.get("base_vault", token_info.base_vault),
|
||||
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault),
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": self.WSOL_MINT,
|
||||
"base_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"quote_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
}
|
||||
|
||||
def get_wsol_account_creation_accounts(self, user: Pubkey, wsol_account: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get accounts needed for WSOL account creation and initialization.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
wsol_account: WSOL account to be created
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for WSOL operations
|
||||
"""
|
||||
return {
|
||||
"payer": user,
|
||||
"wsol_account": wsol_account,
|
||||
"wsol_mint": self.WSOL_MINT,
|
||||
"owner": user,
|
||||
"system_program": self.SYSTEM_PROGRAM_ID,
|
||||
"token_program": self.TOKEN_PROGRAM_ID,
|
||||
"rent": self.SYSTEM_RENT_PROGRAM_ID,
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
LetsBonk implementation of CurveManager interface.
|
||||
|
||||
This module handles LetsBonk (Raydium LaunchLab) specific pool operations
|
||||
by implementing the CurveManager interface using IDL-based decoding.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
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 platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Pool state discriminator for Raydium LaunchLab
|
||||
POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70])
|
||||
|
||||
|
||||
class LetsBonkCurveManager(CurveManager):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
|
||||
|
||||
def __init__(self, client: SolanaClient):
|
||||
"""Initialize LetsBonk curve manager.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
"""
|
||||
self.client = client
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a LetsBonk pool.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state account
|
||||
|
||||
Returns:
|
||||
Dictionary containing pool state data
|
||||
"""
|
||||
try:
|
||||
account = await self.client.get_account_info(pool_address)
|
||||
if not account.data:
|
||||
raise ValueError(f"No data in pool state account {pool_address}")
|
||||
|
||||
# Decode pool state (simplified - in production you'd use IDL parser)
|
||||
pool_state_data = self._decode_pool_state(account.data)
|
||||
|
||||
return pool_state_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get pool state: {e!s}")
|
||||
raise ValueError(f"Invalid pool state: {e!s}")
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from pool state.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Use virtual reserves for price calculation
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
if virtual_base <= 0 or virtual_quote <= 0:
|
||||
raise ValueError("Invalid reserve state")
|
||||
|
||||
# Price = quote_reserves / base_reserves (how much SOL per token)
|
||||
price_lamports = virtual_quote / virtual_base
|
||||
price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
|
||||
return price_sol
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
# Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
|
||||
numerator = amount_in * virtual_base
|
||||
denominator = virtual_quote + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
# Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in)
|
||||
numerator = amount_in * virtual_quote
|
||||
denominator = virtual_base + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current pool reserves.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Tuple of (base_reserves, quote_reserves) in raw units
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return (pool_state["virtual_base"], pool_state["virtual_quote"])
|
||||
|
||||
def _decode_pool_state(self, data: bytes) -> dict[str, Any]:
|
||||
"""Decode pool state data from raw bytes.
|
||||
|
||||
This is a simplified decoder. In production, you should use the IDL parser.
|
||||
|
||||
Args:
|
||||
data: Raw account data
|
||||
|
||||
Returns:
|
||||
Dictionary with decoded pool state
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Pool state data too short")
|
||||
|
||||
# Skip discriminator
|
||||
offset = 8
|
||||
|
||||
# Based on the PoolState structure from the IDL:
|
||||
# - authority: Pubkey (32 bytes)
|
||||
# - base_mint: Pubkey (32 bytes)
|
||||
# - quote_mint: Pubkey (32 bytes)
|
||||
# - base_vault: Pubkey (32 bytes)
|
||||
# - quote_vault: Pubkey (32 bytes)
|
||||
# - status: u8 (1 byte)
|
||||
# - virtual_base: u64 (8 bytes)
|
||||
# - virtual_quote: u64 (8 bytes)
|
||||
# - real_base: u64 (8 bytes)
|
||||
# - real_quote: u64 (8 bytes)
|
||||
# ... and more fields
|
||||
|
||||
try:
|
||||
# Skip to the fields we need
|
||||
offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults)
|
||||
offset += 1 # Skip status
|
||||
|
||||
# Read virtual reserves
|
||||
virtual_base = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
virtual_quote = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
# Read real reserves
|
||||
real_base = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
real_quote = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
return {
|
||||
"virtual_base": virtual_base,
|
||||
"virtual_quote": virtual_quote,
|
||||
"real_base": real_base,
|
||||
"real_quote": real_quote,
|
||||
"price_per_token": (virtual_quote / virtual_base) * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL if virtual_base > 0 else 0,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decode pool state: {e}")
|
||||
# Return some default values for testing
|
||||
return {
|
||||
"virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals
|
||||
"virtual_quote": 1_000_000_000, # 1 SOL
|
||||
"real_base": 1_000_000_000,
|
||||
"real_quote": 1_000_000_000,
|
||||
"price_per_token": 0.001, # 0.001 SOL per token
|
||||
}
|
||||
|
||||
async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get detailed pool information including status and progress.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Dictionary with pool information
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Calculate additional metrics
|
||||
sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL
|
||||
tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS
|
||||
|
||||
return {
|
||||
"virtual_base_reserves": pool_state["virtual_base"],
|
||||
"virtual_quote_reserves": pool_state["virtual_quote"],
|
||||
"real_base_reserves": pool_state["real_base"],
|
||||
"real_quote_reserves": pool_state["real_quote"],
|
||||
"sol_raised": sol_raised,
|
||||
"tokens_sold": tokens_sold,
|
||||
"current_price": pool_state["price_per_token"],
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
LetsBonk implementation of EventParser interface.
|
||||
|
||||
This module parses LetsBonk-specific token creation events from various sources
|
||||
by implementing the EventParser interface.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from time import monotonic
|
||||
from typing import Any, Final
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from interfaces.core import EventParser, Platform, TokenInfo
|
||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LetsBonkEventParser(EventParser):
|
||||
"""LetsBonk implementation of EventParser interface."""
|
||||
|
||||
# Discriminator for initialize instruction from IDL
|
||||
INITIALIZE_DISCRIMINATOR: Final[bytes] = bytes([175, 175, 109, 31, 13, 152, 155, 237])
|
||||
INITIALIZE_DISCRIMINATOR_INT: Final[int] = struct.unpack("<Q", INITIALIZE_DISCRIMINATOR)[0]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize LetsBonk event parser."""
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from LetsBonk transaction logs.
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
# LetsBonk doesn't emit specific logs for token creation like pump.fun
|
||||
# Token creation is identified through instruction parsing
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from LetsBonk instruction data.
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Helper to get account key
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
return None
|
||||
account_index = accounts[index]
|
||||
if account_index >= len(account_keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data
|
||||
token_data = self._parse_initialize_instruction_data(instruction_data)
|
||||
if not token_data:
|
||||
return None
|
||||
|
||||
# Extract account information based on IDL account order
|
||||
creator = get_account_key(1) # creator account
|
||||
pool_state = get_account_key(5) # pool_state account
|
||||
base_mint = get_account_key(6) # base_mint account
|
||||
base_vault = get_account_key(8) # base_vault account
|
||||
quote_vault = get_account_key(9) # quote_vault account
|
||||
|
||||
if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
|
||||
return None
|
||||
|
||||
return TokenInfo(
|
||||
name=token_data["name"],
|
||||
symbol=token_data["symbol"],
|
||||
uri=token_data["uri"],
|
||||
mint=base_mint,
|
||||
platform=Platform.LETS_BONK,
|
||||
pool_state=pool_state,
|
||||
base_vault=base_vault,
|
||||
quote_vault=quote_vault,
|
||||
user=creator,
|
||||
creator=creator,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
for ix in msg.instructions:
|
||||
# Skip non-LetsBonk program instructions
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
program_id = msg.account_keys[program_idx]
|
||||
if bytes(program_id) != bytes(self.get_program_id()):
|
||||
continue
|
||||
|
||||
# Check if it's the LetsBonk platform config account
|
||||
has_platform_config = False
|
||||
for acc_idx in ix.accounts:
|
||||
if acc_idx < len(msg.account_keys):
|
||||
acc_key = msg.account_keys[acc_idx]
|
||||
if bytes(acc_key) == bytes(self.address_provider.LETSBONK_PLATFORM_CONFIG):
|
||||
has_platform_config = True
|
||||
break
|
||||
|
||||
if not has_platform_config:
|
||||
continue
|
||||
|
||||
# Process instruction data
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the Raydium LaunchLab program ID this parser monitors.
|
||||
|
||||
Returns:
|
||||
Raydium LaunchLab program ID
|
||||
"""
|
||||
return self.address_provider.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self.INITIALIZE_DISCRIMINATOR]
|
||||
|
||||
def _parse_initialize_instruction_data(self, data: bytes) -> dict | None:
|
||||
"""Parse the initialize instruction data from LetsBonk.
|
||||
|
||||
Args:
|
||||
data: Raw instruction data
|
||||
|
||||
Returns:
|
||||
Dictionary of parsed data or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
# Check discriminator
|
||||
discriminator = struct.unpack("<Q", data[:8])[0]
|
||||
if discriminator != self.INITIALIZE_DISCRIMINATOR_INT:
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Helper functions for reading data
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError("Not enough data for string length")
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError("Not enough data for string")
|
||||
value = data[offset:offset + length].decode('utf-8')
|
||||
offset += length
|
||||
return value
|
||||
|
||||
def read_u8():
|
||||
nonlocal offset
|
||||
if offset + 1 > len(data):
|
||||
raise ValueError("Not enough data for u8")
|
||||
value = struct.unpack_from("<B", data, offset)[0]
|
||||
offset += 1
|
||||
return value
|
||||
|
||||
# Parse MintParams struct
|
||||
decimals = read_u8()
|
||||
name = read_string()
|
||||
symbol = read_string()
|
||||
uri = read_string()
|
||||
|
||||
parsed_data["name"] = name
|
||||
parsed_data["symbol"] = symbol
|
||||
parsed_data["uri"] = uri
|
||||
parsed_data["decimals"] = decimals
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> list[TokenInfo]:
|
||||
"""Parse token creations from block data (for block listener).
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
Returns:
|
||||
List of TokenInfo for any token creations found
|
||||
"""
|
||||
tokens = []
|
||||
|
||||
try:
|
||||
if "transactions" not in block_data:
|
||||
return tokens
|
||||
|
||||
for tx in block_data["transactions"]:
|
||||
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||
continue
|
||||
|
||||
# Process transaction (implementation would be similar to pump.fun)
|
||||
# This is a simplified version - full implementation would decode
|
||||
# the transaction and check for LetsBonk initialize instructions
|
||||
pass
|
||||
|
||||
return tokens
|
||||
|
||||
except Exception:
|
||||
return tokens
|
||||
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
LetsBonk implementation of InstructionBuilder interface.
|
||||
|
||||
This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions
|
||||
by implementing the InstructionBuilder interface.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
from solders.instruction import AccountMeta, Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
|
||||
from spl.token.instructions import create_idempotent_associated_token_account
|
||||
|
||||
from core.pubkeys import TOKEN_DECIMALS
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
|
||||
# Instruction discriminators for LetsBonk (from IDL)
|
||||
BUY_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([250, 234, 13, 123, 213, 156, 19, 236])
|
||||
BUY_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([24, 211, 116, 40, 105, 3, 153, 56])
|
||||
SELL_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([149, 39, 222, 155, 211, 124, 152, 26])
|
||||
SELL_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([95, 200, 71, 34, 8, 9, 11, 166])
|
||||
|
||||
|
||||
class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface."""
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for LetsBonk using buy_exact_in.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
minimum_amount_out: Minimum tokens expected (raw token units)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create idempotent ATA for base token
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
address_provider.TOKEN_PROGRAM_ID, # token program
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
# 2. Create WSOL account with seed (temporary account for the transaction)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
# Account creation cost + amount to spend
|
||||
account_creation_lamports = 2_039_280 # Standard account creation cost
|
||||
total_lamports = amount_in + account_creation_lamports
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
to_pubkey=wsol_account,
|
||||
base=user,
|
||||
seed=wsol_seed,
|
||||
lamports=total_lamports,
|
||||
space=165, # Size of a token account
|
||||
owner=address_provider.TOKEN_PROGRAM_ID
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
# 3. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
address_provider.WSOL_MINT,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
# 4. Build buy_exact_in instruction
|
||||
buy_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
BUY_EXACT_IN_DISCRIMINATOR +
|
||||
struct.pack("<Q", amount_in) + # amount_in (u64) - SOL to spend
|
||||
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min tokens
|
||||
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
)
|
||||
|
||||
buy_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=buy_accounts
|
||||
)
|
||||
instructions.append(buy_instruction)
|
||||
|
||||
# 5. Close WSOL account to reclaim SOL
|
||||
close_wsol_ix = self._create_close_account_instruction(
|
||||
wsol_account,
|
||||
user,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(close_wsol_ix)
|
||||
|
||||
return instructions
|
||||
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for LetsBonk using sell_exact_in.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of tokens to sell (raw token units)
|
||||
minimum_amount_out: Minimum SOL expected (in lamports)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create WSOL account with seed (to receive SOL)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
# Minimal account creation cost
|
||||
account_creation_lamports = 2_039_280
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
to_pubkey=wsol_account,
|
||||
base=user,
|
||||
seed=wsol_seed,
|
||||
lamports=account_creation_lamports,
|
||||
space=165, # Size of a token account
|
||||
owner=address_provider.TOKEN_PROGRAM_ID
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
# 2. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
address_provider.WSOL_MINT,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
# 3. Build sell_exact_in instruction
|
||||
sell_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
SELL_EXACT_IN_DISCRIMINATOR +
|
||||
struct.pack("<Q", amount_in) + # amount_in (u64) - tokens to sell
|
||||
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min SOL
|
||||
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
)
|
||||
|
||||
sell_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=sell_accounts
|
||||
)
|
||||
instructions.append(sell_instruction)
|
||||
|
||||
# 4. Close WSOL account to reclaim SOL
|
||||
close_wsol_ix = self._create_close_account_instruction(
|
||||
wsol_account,
|
||||
user,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(close_wsol_ix)
|
||||
|
||||
return instructions
|
||||
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
accounts_info["base_vault"],
|
||||
accounts_info["quote_vault"],
|
||||
accounts_info["base_token_mint"],
|
||||
accounts_info["quote_token_mint"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
accounts_info["base_vault"],
|
||||
accounts_info["quote_vault"],
|
||||
accounts_info["base_token_mint"],
|
||||
accounts_info["quote_token_mint"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def _generate_wsol_seed(self, user: Pubkey) -> str:
|
||||
"""Generate a unique seed for WSOL account creation.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Unique seed string for WSOL account
|
||||
"""
|
||||
# Generate a unique seed based on timestamp and user pubkey
|
||||
seed_data = f"{int(time.time())}{user!s}"
|
||||
return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
|
||||
|
||||
def _create_initialize_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
mint: Pubkey,
|
||||
owner: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> Instruction:
|
||||
"""Create an InitializeAccount instruction for the Token Program.
|
||||
|
||||
Args:
|
||||
account: The account to initialize
|
||||
mint: The token mint
|
||||
owner: The account owner
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Instruction for initializing the account
|
||||
"""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=address_provider.SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
|
||||
data = bytes([1])
|
||||
|
||||
return Instruction(
|
||||
program_id=address_provider.TOKEN_PROGRAM_ID,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
)
|
||||
|
||||
def _create_close_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
destination: Pubkey,
|
||||
owner: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> Instruction:
|
||||
"""Create a CloseAccount instruction for the Token Program.
|
||||
|
||||
Args:
|
||||
account: The account to close
|
||||
destination: Where to send the remaining lamports
|
||||
owner: The account owner (must sign)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Instruction for closing the account
|
||||
"""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
|
||||
]
|
||||
|
||||
# CloseAccount instruction discriminator (instruction 9 in Token Program)
|
||||
data = bytes([9])
|
||||
|
||||
return Instruction(
|
||||
program_id=address_provider.TOKEN_PROGRAM_ID,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
)
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Pump.Fun platform registration and exports.
|
||||
|
||||
This module registers all pump.fun implementations with the platform factory
|
||||
and provides convenient imports for the pump.fun platform.
|
||||
"""
|
||||
|
||||
from interfaces.core import Platform
|
||||
from platforms import register_platform_implementations
|
||||
|
||||
from .address_provider import PumpFunAddressProvider
|
||||
from .curve_manager import PumpFunCurveManager
|
||||
from .event_parser import PumpFunEventParser
|
||||
from .instruction_builder import PumpFunInstructionBuilder
|
||||
|
||||
# Register pump.fun platform implementations
|
||||
register_platform_implementations(
|
||||
Platform.PUMP_FUN,
|
||||
PumpFunAddressProvider,
|
||||
PumpFunInstructionBuilder,
|
||||
PumpFunCurveManager,
|
||||
PumpFunEventParser
|
||||
)
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'PumpFunAddressProvider',
|
||||
'PumpFunCurveManager',
|
||||
'PumpFunEventParser',
|
||||
'PumpFunInstructionBuilder'
|
||||
]
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Pump.Fun implementation of AddressProvider interface.
|
||||
|
||||
This module provides all pump.fun-specific addresses and PDA derivations
|
||||
by implementing the AddressProvider interface.
|
||||
"""
|
||||
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from core.pubkeys import PumpAddresses, SystemAddresses
|
||||
from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
|
||||
|
||||
class PumpFunAddressProvider(AddressProvider):
|
||||
"""Pump.Fun implementation of AddressProvider interface."""
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return PumpAddresses.PROGRAM
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for pump.fun.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
return {
|
||||
# Pump.fun specific addresses
|
||||
"program": PumpAddresses.PROGRAM,
|
||||
"global": PumpAddresses.GLOBAL,
|
||||
"event_authority": PumpAddresses.EVENT_AUTHORITY,
|
||||
"fee": PumpAddresses.FEE,
|
||||
"liquidity_migrator": PumpAddresses.LIQUIDITY_MIGRATOR,
|
||||
|
||||
# System addresses
|
||||
"system_program": SystemAddresses.PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
"rent": SystemAddresses.RENT,
|
||||
"sol_mint": SystemAddresses.SOL,
|
||||
}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
"""Derive the bonding curve address for a token.
|
||||
|
||||
For pump.fun, this is the bonding curve PDA derived from the mint.
|
||||
|
||||
Args:
|
||||
base_mint: Token mint address
|
||||
quote_mint: Not used for pump.fun (SOL is always the quote)
|
||||
|
||||
Returns:
|
||||
Bonding curve address
|
||||
"""
|
||||
bonding_curve, _ = Pubkey.find_program_address(
|
||||
[b"bonding-curve", bytes(base_mint)],
|
||||
PumpAddresses.PROGRAM
|
||||
)
|
||||
return bonding_curve
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get pump.fun-specific additional accounts needed for trading.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
# Add bonding curve if available
|
||||
if token_info.bonding_curve:
|
||||
accounts["bonding_curve"] = token_info.bonding_curve
|
||||
|
||||
# Add associated bonding curve if available
|
||||
if token_info.associated_bonding_curve:
|
||||
accounts["associated_bonding_curve"] = token_info.associated_bonding_curve
|
||||
|
||||
# Add creator vault if available
|
||||
if token_info.creator_vault:
|
||||
accounts["creator_vault"] = token_info.creator_vault
|
||||
|
||||
# Derive associated bonding curve if not provided
|
||||
if not token_info.associated_bonding_curve and token_info.bonding_curve:
|
||||
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
|
||||
token_info.mint, token_info.bonding_curve
|
||||
)
|
||||
|
||||
# Derive creator vault if not provided but creator is available
|
||||
if not token_info.creator_vault and token_info.creator:
|
||||
accounts["creator_vault"] = self.derive_creator_vault(token_info.creator)
|
||||
|
||||
return accounts
|
||||
|
||||
def derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
return get_associated_token_address(bonding_curve, mint)
|
||||
|
||||
def derive_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""Derive the creator vault address.
|
||||
|
||||
Args:
|
||||
creator: Creator address
|
||||
|
||||
Returns:
|
||||
Creator vault address
|
||||
"""
|
||||
creator_vault, _ = Pubkey.find_program_address(
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM
|
||||
)
|
||||
return creator_vault
|
||||
|
||||
def derive_global_volume_accumulator(self) -> Pubkey:
|
||||
"""Derive the global volume accumulator PDA.
|
||||
|
||||
Returns:
|
||||
Global volume accumulator address
|
||||
"""
|
||||
return PumpAddresses.find_global_volume_accumulator()
|
||||
|
||||
def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey:
|
||||
"""Derive the user volume accumulator PDA.
|
||||
|
||||
Args:
|
||||
user: User address
|
||||
|
||||
Returns:
|
||||
User volume accumulator address
|
||||
"""
|
||||
return PumpAddresses.find_user_volume_accumulator(user)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"global": PumpAddresses.GLOBAL,
|
||||
"fee": PumpAddresses.FEE,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
|
||||
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
|
||||
"event_authority": PumpAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpAddresses.PROGRAM,
|
||||
"global_volume_accumulator": self.derive_global_volume_accumulator(),
|
||||
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"global": PumpAddresses.GLOBAL,
|
||||
"fee": PumpAddresses.FEE,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
|
||||
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.PROGRAM,
|
||||
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"event_authority": PumpAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpAddresses.PROGRAM,
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Pump.Fun implementation of CurveManager interface.
|
||||
|
||||
This module handles pump.fun-specific bonding curve operations
|
||||
by implementing the CurveManager interface.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.client import SolanaClient
|
||||
from core.curve import BondingCurveManager
|
||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||
from interfaces.core import CurveManager, Platform
|
||||
|
||||
|
||||
class PumpFunCurveManager(CurveManager):
|
||||
"""Pump.Fun implementation of CurveManager interface."""
|
||||
|
||||
def __init__(self, client: SolanaClient):
|
||||
"""Initialize pump.fun curve manager.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
"""
|
||||
self.client = client
|
||||
self.bonding_curve_manager = BondingCurveManager(client)
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a pump.fun bonding curve.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Dictionary containing bonding curve state data
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
return {
|
||||
"virtual_token_reserves": curve_state.virtual_token_reserves,
|
||||
"virtual_sol_reserves": curve_state.virtual_sol_reserves,
|
||||
"real_token_reserves": curve_state.real_token_reserves,
|
||||
"real_sol_reserves": curve_state.real_sol_reserves,
|
||||
"token_total_supply": curve_state.token_total_supply,
|
||||
"complete": curve_state.complete,
|
||||
"creator": str(curve_state.creator),
|
||||
|
||||
# Calculated fields for convenience
|
||||
"price_per_token": curve_state.calculate_price(),
|
||||
"token_reserves_decimal": curve_state.token_reserves,
|
||||
"sol_reserves_decimal": curve_state.sol_reserves,
|
||||
}
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from bonding curve state.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
return await self.bonding_curve_manager.calculate_price(pool_address)
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
Uses the pump.fun bonding curve formula to calculate token output.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
# Use virtual reserves for bonding curve calculation
|
||||
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
|
||||
numerator = amount_in * curve_state.virtual_token_reserves
|
||||
denominator = curve_state.virtual_sol_reserves + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
Uses the pump.fun bonding curve formula to calculate SOL output.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
# Use virtual reserves for bonding curve calculation
|
||||
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
|
||||
numerator = amount_in * curve_state.virtual_sol_reserves
|
||||
denominator = curve_state.virtual_token_reserves + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current bonding curve reserves.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Tuple of (token_reserves, sol_reserves) in raw units
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves)
|
||||
|
||||
async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float:
|
||||
"""Calculate the expected token amount for a given SOL input.
|
||||
|
||||
This is a convenience method that wraps calculate_buy_amount_out
|
||||
and converts between decimal and raw units.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
sol_amount: Amount of SOL to spend (in decimal SOL)
|
||||
|
||||
Returns:
|
||||
Expected token amount (in decimal tokens)
|
||||
"""
|
||||
sol_lamports = int(sol_amount * LAMPORTS_PER_SOL)
|
||||
tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports)
|
||||
return tokens_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float:
|
||||
"""Calculate the expected SOL amount for a given token input.
|
||||
|
||||
This is a convenience method that wraps calculate_sell_amount_out
|
||||
and converts between decimal and raw units.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
token_amount: Amount of tokens to sell (in decimal tokens)
|
||||
|
||||
Returns:
|
||||
Expected SOL amount (in decimal SOL)
|
||||
"""
|
||||
tokens_raw = int(token_amount * 10**TOKEN_DECIMALS)
|
||||
sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw)
|
||||
return sol_lamports / LAMPORTS_PER_SOL
|
||||
|
||||
async def is_curve_complete(self, pool_address: Pubkey) -> bool:
|
||||
"""Check if the bonding curve is complete (migrated to Raydium).
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
True if curve is complete, False otherwise
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
return curve_state.complete
|
||||
|
||||
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get bonding curve completion progress information.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Dictionary with progress information
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
# Calculate progress based on SOL raised vs target
|
||||
# This is approximate since the exact target isn't stored in the curve state
|
||||
sol_raised = curve_state.real_sol_reserves / LAMPORTS_PER_SOL
|
||||
|
||||
# Estimate progress based on typical pump.fun graduation requirements
|
||||
# (This could be made more accurate with additional on-chain data)
|
||||
estimated_target_sol = 85.0 # Typical pump.fun graduation target
|
||||
progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
|
||||
|
||||
return {
|
||||
"complete": curve_state.complete,
|
||||
"sol_raised": sol_raised,
|
||||
"estimated_target_sol": estimated_target_sol,
|
||||
"progress_percentage": progress_percentage,
|
||||
"tokens_available": curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS,
|
||||
"market_cap_sol": sol_raised, # Approximate market cap
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
Pump.Fun implementation of EventParser interface.
|
||||
|
||||
This module parses pump.fun-specific token creation events from various sources
|
||||
by implementing the EventParser interface.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import struct
|
||||
from time import monotonic
|
||||
from typing import Any, Final
|
||||
|
||||
import base58
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.transaction import VersionedTransaction
|
||||
|
||||
from core.pubkeys import PumpAddresses, SystemAddresses
|
||||
from interfaces.core import EventParser, Platform, TokenInfo
|
||||
|
||||
|
||||
class PumpFunEventParser(EventParser):
|
||||
"""Pump.Fun implementation of EventParser interface."""
|
||||
|
||||
# Discriminators for pump.fun instructions
|
||||
CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728
|
||||
CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack("<Q", CREATE_DISCRIMINATOR)
|
||||
|
||||
# Discriminator for program logs
|
||||
LOGS_CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from pump.fun transaction logs.
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
# Check if this is a token creation
|
||||
if not any("Program log: Instruction: Create" in log for log in logs):
|
||||
return None
|
||||
|
||||
# Skip swaps and other operations
|
||||
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
|
||||
return None
|
||||
|
||||
# Find and process program data
|
||||
for log in logs:
|
||||
if "Program data:" in log:
|
||||
try:
|
||||
encoded_data = log.split(": ")[1]
|
||||
decoded_data = base64.b64decode(encoded_data)
|
||||
return self._parse_create_instruction_data(decoded_data)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from pump.fun instruction data.
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(self.CREATE_DISCRIMINATOR_BYTES):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Helper to get account key
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
return None
|
||||
account_index = accounts[index]
|
||||
if account_index >= len(account_keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data
|
||||
token_data = self._parse_create_instruction_data(instruction_data)
|
||||
if not token_data:
|
||||
return None
|
||||
|
||||
# Extract account information
|
||||
mint = get_account_key(0)
|
||||
bonding_curve = get_account_key(2)
|
||||
associated_bonding_curve = get_account_key(3)
|
||||
user = get_account_key(7)
|
||||
|
||||
if not all([mint, bonding_curve, associated_bonding_curve, user]):
|
||||
return None
|
||||
|
||||
# Create creator vault
|
||||
creator = Pubkey.from_string(token_data["creator"]) if token_data.get("creator") else user
|
||||
creator_vault = self._derive_creator_vault(creator)
|
||||
|
||||
return TokenInfo(
|
||||
name=token_data["name"],
|
||||
symbol=token_data["symbol"],
|
||||
uri=token_data["uri"],
|
||||
mint=mint,
|
||||
platform=Platform.PUMP_FUN,
|
||||
bonding_curve=bonding_curve,
|
||||
associated_bonding_curve=associated_bonding_curve,
|
||||
user=user,
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
for ix in msg.instructions:
|
||||
# Skip non-pump.fun program instructions
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
program_id = msg.account_keys[program_idx]
|
||||
if bytes(program_id) != bytes(self.get_program_id()):
|
||||
continue
|
||||
|
||||
# Process instruction data
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the pump.fun program ID this parser monitors.
|
||||
|
||||
Returns:
|
||||
Pump.fun program ID
|
||||
"""
|
||||
return PumpAddresses.PROGRAM
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self.CREATE_DISCRIMINATOR_BYTES]
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
|
||||
"""Parse token creation from block data (for block listener).
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if "transactions" not in block_data:
|
||||
return None
|
||||
|
||||
for tx in block_data["transactions"]:
|
||||
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||
continue
|
||||
|
||||
# Decode base64 transaction data
|
||||
tx_data_encoded = tx["transaction"][0]
|
||||
tx_data_decoded = base64.b64decode(tx_data_encoded)
|
||||
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
program_id = transaction.message.account_keys[ix.program_id_index]
|
||||
|
||||
# Check if instruction is from pump.fun program
|
||||
if str(program_id) != str(self.get_program_id()):
|
||||
continue
|
||||
|
||||
ix_data = bytes(ix.data)
|
||||
|
||||
# Check for create discriminator
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<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
|
||||
|
||||
# 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
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _parse_create_instruction_data(self, data: bytes) -> dict | None:
|
||||
"""Parse the create instruction data from pump.fun.
|
||||
|
||||
Args:
|
||||
data: Raw instruction data
|
||||
|
||||
Returns:
|
||||
Dictionary of parsed data or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
# Check for the correct instruction discriminator
|
||||
discriminator = struct.unpack("<Q", data[:8])[0]
|
||||
if discriminator not in [self.CREATE_DISCRIMINATOR, self.LOGS_CREATE_DISCRIMINATOR]:
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Parse fields based on CreateEvent structure
|
||||
fields = [
|
||||
("name", "string"),
|
||||
("symbol", "string"),
|
||||
("uri", "string"),
|
||||
]
|
||||
|
||||
# For instruction data, we also have creator info
|
||||
if discriminator == self.CREATE_DISCRIMINATOR:
|
||||
fields.extend([
|
||||
("creator", "publicKey"),
|
||||
])
|
||||
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
if offset + 4 > len(data):
|
||||
return None
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
return None
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
if offset + 32 > len(data):
|
||||
return None
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""Derive the creator vault for a creator.
|
||||
|
||||
Args:
|
||||
creator: Creator address
|
||||
|
||||
Returns:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
def _derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
bytes(bonding_curve),
|
||||
bytes(SystemAddresses.TOKEN_PROGRAM),
|
||||
bytes(mint),
|
||||
],
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Pump.Fun implementation of InstructionBuilder interface.
|
||||
|
||||
This module builds pump.fun-specific buy and sell instructions
|
||||
by implementing the InstructionBuilder interface.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Final
|
||||
|
||||
from solders.instruction import AccountMeta, Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import create_idempotent_associated_token_account
|
||||
|
||||
from core.pubkeys import TOKEN_DECIMALS, SystemAddresses
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
|
||||
# Discriminators for pump.fun instructions
|
||||
BUY_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
|
||||
SELL_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 12502976635542562355)
|
||||
|
||||
|
||||
class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
"""Pump.Fun implementation of InstructionBuilder interface."""
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for pump.fun.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
minimum_amount_out: Minimum tokens expected (raw token units)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create idempotent ATA instruction (won't fail if ATA already exists)
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
# 2. Build buy instruction
|
||||
buy_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_volume_accumulator"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_volume_accumulator"], is_signer=False, is_writable=True),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + token_amount + max_sol_cost
|
||||
instruction_data = (
|
||||
BUY_DISCRIMINATOR +
|
||||
struct.pack("<Q", minimum_amount_out) + # token amount in raw units
|
||||
struct.pack("<Q", amount_in) # max SOL cost in lamports
|
||||
)
|
||||
|
||||
buy_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=buy_accounts
|
||||
)
|
||||
instructions.append(buy_instruction)
|
||||
|
||||
return instructions
|
||||
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for pump.fun.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of tokens to sell (raw token units)
|
||||
minimum_amount_out: Minimum SOL expected (in lamports)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
# Build sell instruction accounts
|
||||
sell_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + token_amount + min_sol_output
|
||||
instruction_data = (
|
||||
SELL_DISCRIMINATOR +
|
||||
struct.pack("<Q", amount_in) + # token amount in raw units
|
||||
struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
|
||||
)
|
||||
|
||||
sell_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=sell_accounts
|
||||
)
|
||||
instructions.append(sell_instruction)
|
||||
|
||||
return instructions
|
||||
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["mint"],
|
||||
accounts_info["bonding_curve"],
|
||||
accounts_info["associated_bonding_curve"],
|
||||
accounts_info["user_token_account"],
|
||||
accounts_info["fee"],
|
||||
accounts_info["creator_vault"],
|
||||
accounts_info["global_volume_accumulator"],
|
||||
accounts_info["user_volume_accumulator"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["mint"],
|
||||
accounts_info["bonding_curve"],
|
||||
accounts_info["associated_bonding_curve"],
|
||||
accounts_info["user_token_account"],
|
||||
accounts_info["fee"],
|
||||
accounts_info["creator_vault"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
Reference in New Issue
Block a user