Fix/update letsbonk fun instructions in the bot (#142)

* feat(core): support multiple initialize instruction variants in LetsBonkEventParser

* feat(core): add creator and platform fee vault derivation methods and update account handling in buy/sell instructions

* feat(letsbonk): add global_config and platform_config to TokenInfo and update address provider logic
This commit is contained in:
Anton
2025-10-26 16:21:58 +01:00
committed by GitHub
parent 7377e1757e
commit 68cac87aad
5 changed files with 211 additions and 28 deletions
+2
View File
@@ -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
+113 -7
View File
@@ -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 @@ class LetsBonkAddressProvider(AddressProvider):
)
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 @@ class LetsBonkAddressProvider(AddressProvider):
"""
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 @@ class LetsBonkAddressProvider(AddressProvider):
"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 @@ class LetsBonkAddressProvider(AddressProvider):
"""
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 @@ class LetsBonkAddressProvider(AddressProvider):
"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]:
+3
View File
@@ -186,6 +186,9 @@ class LetsBonkCurveManager(CurveManager):
"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
+39 -16
View File
@@ -33,14 +33,23 @@ class LetsBonkEventParser(EventParser):
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(
"<Q", self._initialize_discriminator_bytes
)[0]
self._initialize_discriminator_bytes_list = [
discriminators["initialize"],
discriminators["initialize_v2"],
discriminators["initialize_with_token_2022"],
]
self._initialize_discriminators = {
struct.unpack("<Q", disc_bytes)[0]
for disc_bytes in self._initialize_discriminator_bytes_list
}
logger.info("LetsBonk event parser initialized with injected IDL parser")
logger.info(
f"LetsBonk event parser initialized with {len(self._initialize_discriminators)} "
f"initialize instruction variants"
)
@property
def platform(self) -> Platform:
@@ -76,7 +85,11 @@ class LetsBonkEventParser(EventParser):
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 @@ class LetsBonkEventParser(EventParser):
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 @@ class LetsBonkEventParser(EventParser):
# ... 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 @@ class LetsBonkEventParser(EventParser):
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 @@ class LetsBonkEventParser(EventParser):
"""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 @@ class LetsBonkEventParser(EventParser):
ix_data = bytes(ix.data)
# Check for initialize discriminator
# Check for any initialize discriminator variant
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if discriminator == self._initialize_discriminator:
if discriminator in self._initialize_discriminators:
# Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10:
continue
@@ -318,7 +341,7 @@ class LetsBonkEventParser(EventParser):
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if discriminator == self._initialize_discriminator:
if discriminator in self._initialize_discriminators:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue
+54 -5
View File
@@ -112,9 +112,8 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
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 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
), # 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 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
# 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 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
), # 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 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
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 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
if config_override is not None:
return config_override
# Sell operations: WSOL creation/init/close + sell instruction
return 60_000
return 150_000