diff --git a/src/interfaces/core.py b/src/interfaces/core.py index 65513e2..04ecff5 100644 --- a/src/interfaces/core.py +++ b/src/interfaces/core.py @@ -38,6 +38,8 @@ class TokenInfo: pool_state: Pubkey | None = None # LetsBonk specific base_vault: Pubkey | None = None # LetsBonk specific quote_vault: Pubkey | None = None # LetsBonk specific + global_config: Pubkey | None = None # LetsBonk specific + platform_config: Pubkey | None = None # LetsBonk specific # Common fields user: Pubkey | None = None diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index 90f0ff1..a654b98 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -23,11 +23,22 @@ class LetsBonkAddresses: PROGRAM: Final[Pubkey] = Pubkey.from_string( "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj" ) + # NOTE: GLOBAL_CONFIG is NOT constant across all pools! + # Each pool can be initialized with different global_config values. Different global_configs + # may define different program-wide settings, versions, or operational parameters. + # The correct global_config for each pool is extracted during pool initialization + # and stored in TokenInfo.global_config. This value below is a default, used as a fallback. GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string( "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX" ) + # NOTE: PLATFORM_CONFIG is NOT constant across all pools! + # Each pool is initialized with a specific platform_config that defines its fee structure, + # launch restrictions, and other settings. Different pools may use different platform_configs + # (e.g., standard launches vs partner launches). The correct platform_config for each pool + # is extracted during pool initialization and stored in TokenInfo.platform_config. + # This value below is the default/most common platform_config, used as a fallback. PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string( - "FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1" + "5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA" ) @@ -212,6 +223,53 @@ def derive_event_authority_pda(self) -> Pubkey: ) return event_authority_pda + def derive_creator_fee_vault( + self, creator: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: + """Derive the creator fee vault PDA. + + This vault accumulates creator fees from trades. + + Args: + creator: The pool creator's pubkey + quote_mint: The quote token mint (defaults to WSOL) + + Returns: + Creator fee vault address + """ + if quote_mint is None: + quote_mint = SystemAddresses.SOL_MINT + + creator_fee_vault, _ = Pubkey.find_program_address( + [bytes(creator), bytes(quote_mint)], LetsBonkAddresses.PROGRAM + ) + return creator_fee_vault + + def derive_platform_fee_vault( + self, platform_config: Pubkey | None = None, quote_mint: Pubkey | None = None + ) -> Pubkey: + """Derive the platform fee vault PDA. + + This vault accumulates platform fees from trades. + + Args: + platform_config: The platform config account (defaults to LetsBonk config) + quote_mint: The quote token mint (defaults to WSOL) + + Returns: + Platform fee vault address + """ + if platform_config is None: + platform_config = LetsBonkAddresses.PLATFORM_CONFIG + + if quote_mint is None: + quote_mint = SystemAddresses.SOL_MINT + + platform_fee_vault, _ = Pubkey.find_program_address( + [bytes(platform_config), bytes(quote_mint)], LetsBonkAddresses.PROGRAM + ) + return platform_fee_vault + def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey: """Create a WSOL account address using createAccountWithSeed pattern. @@ -238,11 +296,25 @@ def get_buy_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) - return { + # Use global_config from TokenInfo if available, otherwise use default + global_config = ( + token_info.global_config + if token_info.global_config + else LetsBonkAddresses.GLOBAL_CONFIG + ) + + # Use platform_config from TokenInfo if available, otherwise use default + platform_config = ( + token_info.platform_config + if token_info.platform_config + else LetsBonkAddresses.PLATFORM_CONFIG + ) + + accounts = { "payer": user, "authority": additional_accounts["authority"], - "global_config": LetsBonkAddresses.GLOBAL_CONFIG, - "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, + "global_config": global_config, + "platform_config": platform_config, "pool_state": additional_accounts["pool_state"], "user_base_token": self.derive_user_token_account(user, token_info.mint), "base_vault": additional_accounts["base_vault"], @@ -253,8 +325,18 @@ def get_buy_instruction_accounts( "quote_token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, + "system_program": SystemAddresses.SYSTEM_PROGRAM, + "platform_fee_vault": self.derive_platform_fee_vault(platform_config), } + # Add creator fee vault if creator is known + if token_info.creator: + accounts["creator_fee_vault"] = self.derive_creator_fee_vault( + token_info.creator + ) + + return accounts + def get_sell_instruction_accounts( self, token_info: TokenInfo, user: Pubkey ) -> dict[str, Pubkey]: @@ -269,11 +351,25 @@ def get_sell_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) - return { + # Use global_config from TokenInfo if available, otherwise use default + global_config = ( + token_info.global_config + if token_info.global_config + else LetsBonkAddresses.GLOBAL_CONFIG + ) + + # Use platform_config from TokenInfo if available, otherwise use default + platform_config = ( + token_info.platform_config + if token_info.platform_config + else LetsBonkAddresses.PLATFORM_CONFIG + ) + + accounts = { "payer": user, "authority": additional_accounts["authority"], - "global_config": LetsBonkAddresses.GLOBAL_CONFIG, - "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, + "global_config": global_config, + "platform_config": platform_config, "pool_state": additional_accounts["pool_state"], "user_base_token": self.derive_user_token_account(user, token_info.mint), "base_vault": additional_accounts["base_vault"], @@ -284,8 +380,18 @@ def get_sell_instruction_accounts( "quote_token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, + "system_program": SystemAddresses.SYSTEM_PROGRAM, + "platform_fee_vault": self.derive_platform_fee_vault(platform_config), } + # Add creator fee vault if creator is known + if token_info.creator: + accounts["creator_fee_vault"] = self.derive_creator_fee_vault( + token_info.creator + ) + + return accounts + def get_wsol_account_creation_accounts( self, user: Pubkey, wsol_account: Pubkey ) -> dict[str, Pubkey]: diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 4bcf510..b6d3ad4 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -186,6 +186,9 @@ def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]: "real_quote": decoded_pool_state.get("real_quote", 0), "status": decoded_pool_state.get("status", 0), "supply": decoded_pool_state.get("supply", 0), + "creator": decoded_pool_state.get("creator"), # Creator pubkey (as base58 string) + "base_vault": decoded_pool_state.get("base_vault"), # Base vault pubkey + "quote_vault": decoded_pool_state.get("quote_vault"), # Quote vault pubkey } # Calculate additional metrics diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 5010429..566be79 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -33,14 +33,23 @@ def __init__(self, idl_parser: IDLParser): self.address_provider = LetsBonkAddressProvider() self._idl_parser = idl_parser - # Get discriminators from injected IDL parser + # Get all initialize instruction discriminators from injected IDL parser + # LetsBonk has multiple initialize variants: initialize, initialize_v2, initialize_with_token_2022 discriminators = self._idl_parser.get_instruction_discriminators() - self._initialize_discriminator_bytes = discriminators["initialize"] - self._initialize_discriminator = struct.unpack( - " Platform: @@ -76,7 +85,11 @@ def parse_token_creation_from_instruction( Returns: TokenInfo if token creation found, None otherwise """ - if not instruction_data.startswith(self._initialize_discriminator_bytes): + # Check if instruction starts with any of the initialize discriminators + if not any( + instruction_data.startswith(disc_bytes) + for disc_bytes in self._initialize_discriminator_bytes_list + ): return None try: @@ -93,7 +106,12 @@ def get_account_key(index): decoded = self._idl_parser.decode_instruction( instruction_data, account_keys, accounts ) - if not decoded or decoded["instruction_name"] != "initialize": + # Accept any of the initialize instruction variants + if not decoded or decoded["instruction_name"] not in { + "initialize", + "initialize_v2", + "initialize_with_token_2022", + }: return None args = decoded.get("args", {}) @@ -118,15 +136,18 @@ def get_account_key(index): # ... other accounts creator = get_account_key(0) # First signer account (creator) + global_config = get_account_key(2) # global_config account + platform_config = get_account_key(3) # platform_config 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]): + if not all([creator, global_config, platform_config, pool_state, base_mint, base_vault, quote_vault]): logger.debug( - f"Missing required accounts: creator={creator}, pool_state={pool_state}, " - f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}" + f"Missing required accounts: creator={creator}, global_config={global_config}, " + f"platform_config={platform_config}, pool_state={pool_state}, base_mint={base_mint}, " + f"base_vault={base_vault}, quote_vault={quote_vault}" ) return None @@ -139,6 +160,8 @@ def get_account_key(index): pool_state=pool_state, base_vault=base_vault, quote_vault=quote_vault, + global_config=global_config, + platform_config=platform_config, user=creator, creator=creator, creation_timestamp=monotonic(), @@ -219,9 +242,9 @@ def get_instruction_discriminators(self) -> list[bytes]: """Get instruction discriminators for token creation. Returns: - List of discriminator bytes to match + List of discriminator bytes to match (all initialize variants) """ - return [self._initialize_discriminator_bytes] + return self._initialize_discriminator_bytes_list def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: """Parse token creation from block data (for block listener). @@ -259,11 +282,11 @@ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: ix_data = bytes(ix.data) - # Check for initialize discriminator + # Check for any initialize discriminator variant if len(ix_data) >= 8: discriminator = struct.unpack(" TokenInfo | None: if len(ix_data) >= 8: discriminator = struct.unpack(" int: if config_override is not None: return config_override # Buy operations: ATA creation + WSOL creation/init/close + buy instruction - return 100_000 + return 150_000 def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int: """Get the recommended compute unit limit for LetsBonk sell operations. @@ -487,4 +536,4 @@ def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int if config_override is not None: return config_override # Sell operations: WSOL creation/init/close + sell instruction - return 60_000 + return 150_000