Files
pumpfun-bonkfun-bot/src/platforms/letsbonk/instruction_builder.py
T

547 lines
20 KiB
Python
Raw Normal View History

2025-08-02 09:25:39 +00:00
"""
LetsBonk implementation of InstructionBuilder interface.
This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions
by implementing the InstructionBuilder interface with IDL-based discriminators.
2025-08-02 09:25:39 +00:00
"""
import hashlib
import struct
import time
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
2025-08-05 20:19:00 +00:00
from core.pubkeys import (
TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE,
TOKEN_ACCOUNT_SIZE,
TOKEN_DECIMALS,
SystemAddresses,
)
2025-08-02 09:25:39 +00:00
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
from utils.idl_parser import IDLParser
from utils.logger import get_logger
2025-08-02 09:25:39 +00:00
logger = get_logger(__name__)
2025-08-02 09:25:39 +00:00
class LetsBonkInstructionBuilder(InstructionBuilder):
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators."""
2025-08-11 05:35:25 +00:00
2025-08-02 14:52:40 +00:00
def __init__(self, idl_parser: IDLParser):
"""Initialize LetsBonk instruction builder with injected IDL parser.
2025-08-11 05:35:25 +00:00
2025-08-02 14:52:40 +00:00
Args:
idl_parser: Pre-loaded IDL parser for LetsBonk platform
"""
self._idl_parser = idl_parser
2025-08-11 05:35:25 +00:00
2025-08-02 14:52:40 +00:00
# Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators()
self._buy_exact_in_discriminator = discriminators["buy_exact_in"]
self._sell_exact_in_discriminator = discriminators["sell_exact_in"]
2025-08-11 05:35:25 +00:00
2025-08-02 14:52:40 +00:00
logger.info("LetsBonk instruction builder initialized with injected IDL parser")
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
@property
def platform(self) -> Platform:
"""Get the platform this builder serves."""
return Platform.LETS_BONK
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
async def build_buy_instruction(
self,
token_info: TokenInfo,
user: Pubkey,
amount_in: int,
minimum_amount_out: int,
2025-08-11 05:35:25 +00:00
address_provider: AddressProvider,
2025-08-02 09:25:39 +00:00
) -> list[Instruction]:
"""Build buy instruction(s) for LetsBonk using buy_exact_in.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
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
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Returns:
List of instructions needed for the buy operation
"""
instructions = []
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# Get all required accounts
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
2025-08-11 05:35:25 +00:00
# Determine token program to use
token_program_id = (
token_info.token_program_id
if token_info.token_program_id
else SystemAddresses.TOKEN_2022_PROGRAM
)
2025-08-02 09:25:39 +00:00
# 1. Create idempotent ATA for base token
ata_instruction = create_idempotent_associated_token_account(
user, # payer
2025-08-11 05:35:25 +00:00
user, # owner
2025-08-02 09:25:39 +00:00
token_info.mint, # mint
token_program_id, # token program (dynamic for token2022 support)
2025-08-02 09:25:39 +00:00
)
instructions.append(ata_instruction)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# 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)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# Account creation cost + amount to spend
2025-08-05 20:19:00 +00:00
account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE
2025-08-02 09:25:39 +00:00
total_lamports = amount_in + account_creation_lamports
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
create_wsol_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=user,
to_pubkey=wsol_account,
base=user,
seed=wsol_seed,
lamports=total_lamports,
2025-08-05 20:19:00 +00:00
space=TOKEN_ACCOUNT_SIZE,
2025-08-11 05:35:25 +00:00
owner=SystemAddresses.TOKEN_PROGRAM,
2025-08-02 09:25:39 +00:00
)
)
instructions.append(create_wsol_ix)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# 3. Initialize WSOL account
initialize_wsol_ix = self._create_initialize_account_instruction(
2025-08-11 05:35:25 +00:00
wsol_account, SystemAddresses.SOL_MINT, user
2025-08-02 09:25:39 +00:00
)
instructions.append(initialize_wsol_ix)
2025-08-11 05:35:25 +00:00
2025-08-02 15:38:53 +00:00
# 4. Build buy_exact_in instruction with correct account ordering
2025-08-02 09:25:39 +00:00
buy_accounts = [
AccountMeta(pubkey=user, is_signer=True, is_writable=False), # payer
2025-08-11 05:35:25 +00:00
AccountMeta(
pubkey=accounts_info["authority"], is_signer=False, is_writable=False
), # authority
AccountMeta(
pubkey=accounts_info["global_config"],
is_signer=False,
is_writable=False,
), # global_config
AccountMeta(
pubkey=accounts_info["platform_config"],
is_signer=False,
is_writable=False,
), # platform_config
AccountMeta(
pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True
), # pool_state
AccountMeta(
pubkey=accounts_info["user_base_token"],
is_signer=False,
is_writable=True,
), # user_base_token
AccountMeta(
pubkey=wsol_account, is_signer=False, is_writable=True
), # user_quote_token (WSOL account)
AccountMeta(
pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True
), # base_vault
AccountMeta(
pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True
), # quote_vault
AccountMeta(
pubkey=token_info.mint, is_signer=False, is_writable=False
), # base_token_mint
AccountMeta(
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
2025-08-11 05:35:25 +00:00
), # base_token_program
AccountMeta(
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
2025-08-11 05:35:25 +00:00
), # quote_token_program
AccountMeta(
pubkey=accounts_info["event_authority"],
is_signer=False,
is_writable=False,
), # event_authority
AccountMeta(
pubkey=accounts_info["program"], is_signer=False, is_writable=False
), # program
2025-08-02 09:25:39 +00:00
]
2025-08-11 05:35:25 +00:00
# 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
2025-08-02 09:25:39 +00:00
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = (
2025-08-11 05:35:25 +00:00
self._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
2025-08-02 09:25:39 +00:00
)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
buy_instruction = Instruction(
program_id=accounts_info["program"],
data=instruction_data,
2025-08-11 05:35:25 +00:00
accounts=buy_accounts,
2025-08-02 09:25:39 +00:00
)
instructions.append(buy_instruction)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# 5. Close WSOL account to reclaim SOL
2025-08-11 05:35:25 +00:00
close_wsol_ix = self._create_close_account_instruction(wsol_account, user, user)
2025-08-02 09:25:39 +00:00
instructions.append(close_wsol_ix)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
return instructions
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
async def build_sell_instruction(
self,
token_info: TokenInfo,
user: Pubkey,
amount_in: int,
minimum_amount_out: int,
2025-08-11 05:35:25 +00:00
address_provider: AddressProvider,
2025-08-02 09:25:39 +00:00
) -> list[Instruction]:
"""Build sell instruction(s) for LetsBonk using sell_exact_in.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
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
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Returns:
List of instructions needed for the sell operation
"""
instructions = []
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# Get all required accounts
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# 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)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# Minimal account creation cost
2025-08-05 20:19:00 +00:00
account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
create_wsol_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=user,
to_pubkey=wsol_account,
base=user,
seed=wsol_seed,
lamports=account_creation_lamports,
2025-08-05 20:19:00 +00:00
space=TOKEN_ACCOUNT_SIZE,
2025-08-11 05:35:25 +00:00
owner=SystemAddresses.TOKEN_PROGRAM,
2025-08-02 09:25:39 +00:00
)
)
instructions.append(create_wsol_ix)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# 2. Initialize WSOL account
initialize_wsol_ix = self._create_initialize_account_instruction(
2025-08-11 05:35:25 +00:00
wsol_account, SystemAddresses.SOL_MINT, user
2025-08-02 09:25:39 +00:00
)
instructions.append(initialize_wsol_ix)
2025-08-11 05:35:25 +00:00
2025-08-02 15:38:53 +00:00
# 3. Build sell_exact_in instruction with correct account ordering
2025-08-02 09:25:39 +00:00
sell_accounts = [
AccountMeta(pubkey=user, is_signer=True, is_writable=False), # payer
2025-08-11 05:35:25 +00:00
AccountMeta(
pubkey=accounts_info["authority"], is_signer=False, is_writable=False
), # authority
AccountMeta(
pubkey=accounts_info["global_config"],
is_signer=False,
is_writable=False,
), # global_config
AccountMeta(
pubkey=accounts_info["platform_config"],
is_signer=False,
is_writable=False,
), # platform_config
AccountMeta(
pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True
), # pool_state
AccountMeta(
pubkey=accounts_info["user_base_token"],
is_signer=False,
is_writable=True,
), # user_base_token (tokens being sold)
AccountMeta(
pubkey=wsol_account, is_signer=False, is_writable=True
), # user_quote_token (WSOL received)
AccountMeta(
pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True
), # base_vault (receives tokens)
AccountMeta(
pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True
), # quote_vault (sends WSOL)
AccountMeta(
pubkey=token_info.mint, is_signer=False, is_writable=False
), # base_token_mint
AccountMeta(
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
2025-08-11 05:35:25 +00:00
), # base_token_program
AccountMeta(
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
2025-08-11 05:35:25 +00:00
), # quote_token_program
AccountMeta(
pubkey=accounts_info["event_authority"],
is_signer=False,
is_writable=False,
), # event_authority
AccountMeta(
pubkey=accounts_info["program"], is_signer=False, is_writable=False
), # program
2025-08-02 09:25:39 +00:00
]
2025-08-11 05:35:25 +00:00
# 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
2025-08-02 09:25:39 +00:00
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = (
2025-08-11 05:35:25 +00:00
self._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
2025-08-02 09:25:39 +00:00
)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
sell_instruction = Instruction(
program_id=accounts_info["program"],
data=instruction_data,
2025-08-11 05:35:25 +00:00
accounts=sell_accounts,
2025-08-02 09:25:39 +00:00
)
instructions.append(sell_instruction)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# 4. Close WSOL account to reclaim SOL
2025-08-11 05:35:25 +00:00
close_wsol_ix = self._create_close_account_instruction(wsol_account, user, user)
2025-08-02 09:25:39 +00:00
instructions.append(close_wsol_ix)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
return instructions
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def get_required_accounts_for_buy(
2025-08-11 05:35:25 +00:00
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
2025-08-02 09:25:39 +00:00
) -> list[Pubkey]:
"""Get list of accounts required for buy operation (for priority fee calculation).
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
token_info: Token information
user: User's wallet address
address_provider: Platform address provider
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Returns:
List of account addresses that will be accessed
"""
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
return [
accounts_info["pool_state"],
accounts_info["user_base_token"],
accounts_info["base_vault"],
accounts_info["quote_vault"],
2025-08-02 15:38:53 +00:00
token_info.mint,
SystemAddresses.SOL_MINT,
2025-08-02 09:25:39 +00:00
accounts_info["program"],
]
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def get_required_accounts_for_sell(
2025-08-11 05:35:25 +00:00
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
2025-08-02 09:25:39 +00:00
) -> list[Pubkey]:
"""Get list of accounts required for sell operation (for priority fee calculation).
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
token_info: Token information
user: User's wallet address
address_provider: Platform address provider
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Returns:
List of account addresses that will be accessed
"""
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
return [
accounts_info["pool_state"],
accounts_info["user_base_token"],
accounts_info["base_vault"],
accounts_info["quote_vault"],
2025-08-02 15:38:53 +00:00
token_info.mint,
SystemAddresses.SOL_MINT,
2025-08-02 09:25:39 +00:00
accounts_info["program"],
]
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def _generate_wsol_seed(self, user: Pubkey) -> str:
"""Generate a unique seed for WSOL account creation.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
user: User's wallet address
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
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]
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def _create_initialize_account_instruction(
2025-08-11 05:35:25 +00:00
self, account: Pubkey, mint: Pubkey, owner: Pubkey
2025-08-02 09:25:39 +00:00
) -> Instruction:
"""Create an InitializeAccount instruction for the Token Program.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
account: The account to initialize
mint: The token mint
owner: The account owner
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
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),
2025-08-11 05:35:25 +00:00
AccountMeta(
pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False
),
2025-08-02 09:25:39 +00:00
]
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
data = bytes([1])
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
return Instruction(
2025-08-11 05:35:25 +00:00
program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts
2025-08-02 09:25:39 +00:00
)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def _create_close_account_instruction(
2025-08-11 05:35:25 +00:00
self, account: Pubkey, destination: Pubkey, owner: Pubkey
2025-08-02 09:25:39 +00:00
) -> Instruction:
"""Create a CloseAccount instruction for the Token Program.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
account: The account to close
destination: Where to send the remaining lamports
owner: The account owner (must sign)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
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),
]
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
# CloseAccount instruction discriminator (instruction 9 in Token Program)
data = bytes([9])
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
return Instruction(
2025-08-11 05:35:25 +00:00
program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts
2025-08-02 09:25:39 +00:00
)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
"""Convert decimal token amount to raw token units.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
token_amount_decimal: Token amount in decimal form
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Returns:
Token amount in raw units (adjusted for decimals)
"""
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
"""Convert raw token amount to decimal form.
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Args:
token_amount_raw: Token amount in raw units
2025-08-11 05:35:25 +00:00
2025-08-02 09:25:39 +00:00
Returns:
Token amount in decimal form
"""
2025-08-11 05:35:25 +00:00
return token_amount_raw / 10**TOKEN_DECIMALS
2025-09-03 15:05:12 +00:00
def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int:
"""Get the recommended compute unit limit for LetsBonk buy operations.
Args:
config_override: Optional override from configuration
Returns:
Compute unit limit appropriate for buy operations
"""
if config_override is not None:
return config_override
# Buy operations: ATA creation + WSOL creation/init/close + buy instruction
return 150_000
2025-09-03 15:05:12 +00:00
def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int:
"""Get the recommended compute unit limit for LetsBonk sell operations.
Args:
config_override: Optional override from configuration
Returns:
Compute unit limit appropriate for sell operations
"""
if config_override is not None:
return config_override
# Sell operations: WSOL creation/init/close + sell instruction
return 150_000