mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user