From 9ad6e5316546166e990a1c07e2201223fb414658 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sun, 26 Oct 2025 14:42:41 +0000 Subject: [PATCH 1/3] feat(core): support multiple initialize instruction variants in LetsBonkEventParser --- src/platforms/letsbonk/event_parser.py | 46 ++++++++++++++++++-------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 50104299..46066b64 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", {}) @@ -219,9 +237,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 +277,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(" Date: Sun, 26 Oct 2025 14:43:05 +0000 Subject: [PATCH 2/3] feat(core): add creator and platform fee vault derivation methods and update account handling in buy/sell instructions --- src/platforms/letsbonk/address_provider.py | 73 ++++++++++++++++++- src/platforms/letsbonk/curve_manager.py | 3 + src/platforms/letsbonk/instruction_builder.py | 59 +++++++++++++-- 3 files changed, 127 insertions(+), 8 deletions(-) diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index 90f0ff1a..676d8f94 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -27,7 +27,7 @@ class LetsBonkAddresses: "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX" ) PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string( - "FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1" + "5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA" ) @@ -212,6 +212,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,7 +285,7 @@ def get_buy_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) - return { + accounts = { "payer": user, "authority": additional_accounts["authority"], "global_config": LetsBonkAddresses.GLOBAL_CONFIG, @@ -253,8 +300,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(), } + # 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,7 +326,7 @@ def get_sell_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) - return { + accounts = { "payer": user, "authority": additional_accounts["authority"], "global_config": LetsBonkAddresses.GLOBAL_CONFIG, @@ -284,8 +341,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(), } + # 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 4bcf510f..b6d3ad42 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/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py index 01179623..b47a8331 100644 --- a/src/platforms/letsbonk/instruction_builder.py +++ b/src/platforms/letsbonk/instruction_builder.py @@ -112,9 +112,8 @@ async def build_buy_instruction( instructions.append(initialize_wsol_ix) # 4. Build buy_exact_in instruction with correct account ordering - # Based on the IDL and manual examples, the account order should be: buy_accounts = [ - AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer + AccountMeta(pubkey=user, is_signer=True, is_writable=False), # payer AccountMeta( pubkey=accounts_info["authority"], is_signer=False, is_writable=False ), # authority @@ -167,6 +166,31 @@ async def build_buy_instruction( ), # program ] + # Add remaining accounts (required by the program for fee collection) + # These are not explicitly listed in IDL but required by the program + buy_accounts.append( + AccountMeta( + pubkey=accounts_info["system_program"], + is_signer=False, + is_writable=False, + ) + ) # #16: System Program + buy_accounts.append( + AccountMeta( + pubkey=accounts_info["platform_fee_vault"], + is_signer=False, + is_writable=True, + ) + ) # #17: Platform fee vault + if "creator_fee_vault" in accounts_info: + buy_accounts.append( + AccountMeta( + pubkey=accounts_info["creator_fee_vault"], + is_signer=False, + is_writable=True, + ) + ) # #18: Creator fee vault + # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( @@ -244,7 +268,7 @@ async def build_sell_instruction( # 3. Build sell_exact_in instruction with correct account ordering sell_accounts = [ - AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer + AccountMeta(pubkey=user, is_signer=True, is_writable=False), # payer AccountMeta( pubkey=accounts_info["authority"], is_signer=False, is_writable=False ), # authority @@ -297,6 +321,31 @@ async def build_sell_instruction( ), # program ] + # Add remaining accounts (required by the program for fee collection) + # These are not explicitly listed in IDL but required by the program + sell_accounts.append( + AccountMeta( + pubkey=accounts_info["system_program"], + is_signer=False, + is_writable=False, + ) + ) # #16: System Program + sell_accounts.append( + AccountMeta( + pubkey=accounts_info["platform_fee_vault"], + is_signer=False, + is_writable=True, + ) + ) # #17: Platform fee vault + if "creator_fee_vault" in accounts_info: + sell_accounts.append( + AccountMeta( + pubkey=accounts_info["creator_fee_vault"], + is_signer=False, + is_writable=True, + ) + ) # #18: Creator fee vault + # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( @@ -473,7 +522,7 @@ def get_buy_compute_unit_limit(self, config_override: int | None = None) -> 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 From 9da52a3d0c872cbac1fe2c271285299a3d7a5cff Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sun, 26 Oct 2025 15:17:42 +0000 Subject: [PATCH 3/3] feat(letsbonk): add global_config and platform_config to TokenInfo and update address provider logic --- src/interfaces/core.py | 2 + src/platforms/letsbonk/address_provider.py | 51 +++++++++++++++++++--- src/platforms/letsbonk/event_parser.py | 11 +++-- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/interfaces/core.py b/src/interfaces/core.py index 65513e2a..04ecff50 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 676d8f94..a654b980 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -23,9 +23,20 @@ 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( "5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA" ) @@ -285,11 +296,25 @@ def get_buy_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) + # 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"], @@ -301,7 +326,7 @@ def get_buy_instruction_accounts( "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, "system_program": SystemAddresses.SYSTEM_PROGRAM, - "platform_fee_vault": self.derive_platform_fee_vault(), + "platform_fee_vault": self.derive_platform_fee_vault(platform_config), } # Add creator fee vault if creator is known @@ -326,11 +351,25 @@ def get_sell_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) + # 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"], @@ -342,7 +381,7 @@ def get_sell_instruction_accounts( "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, "system_program": SystemAddresses.SYSTEM_PROGRAM, - "platform_fee_vault": self.derive_platform_fee_vault(), + "platform_fee_vault": self.derive_platform_fee_vault(platform_config), } # Add creator fee vault if creator is known diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 46066b64..566be79e 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -136,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 @@ -157,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(),