fix(letsbonk): buy/sell instructions

This commit is contained in:
smypmsa
2025-08-02 15:38:53 +00:00
parent 72a3d23e6b
commit e7107751f5
7 changed files with 306 additions and 131 deletions
+2 -2
View File
@@ -8,11 +8,11 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}" private_key: "${SOLANA_PRIVATE_KEY}"
enabled: false # You can turn off the bot w/o removing its config enabled: true # You can turn off the bot w/o removing its config
separate_process: true separate_process: true
# Options: "pump_fun" (default), "lets_bonk" # Options: "pump_fun" (default), "lets_bonk"
platform: "pump_fun" platform: "lets_bonk"
# Geyser configuration (fastest method for getting updates) # Geyser configuration (fastest method for getting updates)
geyser: geyser:
+1 -1
View File
@@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}" private_key: "${SOLANA_PRIVATE_KEY}"
enabled: true # You can turn off the bot w/o removing its config enabled: false # You can turn off the bot w/o removing its config
separate_process: true separate_process: true
# Options: "pump_fun" (default), "lets_bonk" # Options: "pump_fun" (default), "lets_bonk"
+18 -12
View File
@@ -102,20 +102,16 @@ class LetsBonkAddressProvider(AddressProvider):
""" """
accounts = {} accounts = {}
# Add pool state if available # Add pool state - must be present in token_info
if token_info.pool_state: if token_info.pool_state:
accounts["pool_state"] = token_info.pool_state accounts["pool_state"] = token_info.pool_state
# Add vault addresses if available # Add vault addresses - must be present in token_info
if token_info.base_vault: if token_info.base_vault:
accounts["base_vault"] = token_info.base_vault accounts["base_vault"] = token_info.base_vault
if token_info.quote_vault: if token_info.quote_vault:
accounts["quote_vault"] = token_info.quote_vault accounts["quote_vault"] = token_info.quote_vault
# Derive pool state if not provided
if not token_info.pool_state:
accounts["pool_state"] = self.derive_pool_address(token_info.mint)
# Derive authority PDA # Derive authority PDA
accounts["authority"] = self.derive_authority_pda() accounts["authority"] = self.derive_authority_pda()
@@ -178,15 +174,20 @@ class LetsBonkAddressProvider(AddressProvider):
""" """
additional_accounts = self.get_additional_accounts(token_info) additional_accounts = self.get_additional_accounts(token_info)
# Vault addresses must be present in token_info
if not token_info.base_vault or not token_info.quote_vault:
raise ValueError(f"Missing required vault addresses for token {token_info.mint}. "
f"base_vault: {token_info.base_vault}, quote_vault: {token_info.quote_vault}")
return { return {
"payer": user, "payer": user,
"authority": additional_accounts["authority"], "authority": additional_accounts["authority"],
"global_config": LetsBonkAddresses.GLOBAL_CONFIG, "global_config": LetsBonkAddresses.GLOBAL_CONFIG,
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
"pool_state": additional_accounts["pool_state"], "pool_state": token_info.pool_state,
"user_base_token": self.derive_user_token_account(user, token_info.mint), "user_base_token": self.derive_user_token_account(user, token_info.mint),
"base_vault": additional_accounts.get("base_vault", token_info.base_vault), "base_vault": token_info.base_vault,
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), "quote_vault": token_info.quote_vault,
"base_token_mint": token_info.mint, "base_token_mint": token_info.mint,
"quote_token_mint": SystemAddresses.SOL_MINT, "quote_token_mint": SystemAddresses.SOL_MINT,
"base_token_program": SystemAddresses.TOKEN_PROGRAM, "base_token_program": SystemAddresses.TOKEN_PROGRAM,
@@ -207,15 +208,20 @@ class LetsBonkAddressProvider(AddressProvider):
""" """
additional_accounts = self.get_additional_accounts(token_info) additional_accounts = self.get_additional_accounts(token_info)
# Vault addresses must be present in token_info
if not token_info.base_vault or not token_info.quote_vault:
raise ValueError(f"Missing required vault addresses for token {token_info.mint}. "
f"base_vault: {token_info.base_vault}, quote_vault: {token_info.quote_vault}")
return { return {
"payer": user, "payer": user,
"authority": additional_accounts["authority"], "authority": additional_accounts["authority"],
"global_config": LetsBonkAddresses.GLOBAL_CONFIG, "global_config": LetsBonkAddresses.GLOBAL_CONFIG,
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
"pool_state": additional_accounts["pool_state"], "pool_state": token_info.pool_state,
"user_base_token": self.derive_user_token_account(user, token_info.mint), "user_base_token": self.derive_user_token_account(user, token_info.mint),
"base_vault": additional_accounts.get("base_vault", token_info.base_vault), "base_vault": token_info.base_vault,
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), "quote_vault": token_info.quote_vault,
"base_token_mint": token_info.mint, "base_token_mint": token_info.mint,
"quote_token_mint": SystemAddresses.SOL_MINT, "quote_token_mint": SystemAddresses.SOL_MINT,
"base_token_program": SystemAddresses.TOKEN_PROGRAM, "base_token_program": SystemAddresses.TOKEN_PROGRAM,
+17 -2
View File
@@ -104,14 +104,29 @@ class LetsBonkEventParser(EventParser):
if not base_mint_param: if not base_mint_param:
return None return None
# Extract account information based on IDL account order # Extract account information based on IDL account order for initialize instruction
creator = get_account_key(1) # creator account # From the manual example, the account order is:
# 0: creator (signer)
# 1: creator_ata (not needed for TokenInfo)
# 2: global_config
# 3: platform_config
# 4: creator
# 5: pool_state
# 6: base_mint
# 7: quote_mint (WSOL)
# 8: base_vault
# 9: quote_vault
# ... other accounts
creator = get_account_key(0) # First signer account (creator)
pool_state = get_account_key(5) # pool_state account pool_state = get_account_key(5) # pool_state account
base_mint = get_account_key(6) # base_mint account base_mint = get_account_key(6) # base_mint account
base_vault = get_account_key(8) # base_vault account base_vault = get_account_key(8) # base_vault account
quote_vault = get_account_key(9) # quote_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, 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}")
return None return None
return TokenInfo( return TokenInfo(
+43 -50
View File
@@ -104,28 +104,28 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
initialize_wsol_ix = self._create_initialize_account_instruction( initialize_wsol_ix = self._create_initialize_account_instruction(
wsol_account, wsol_account,
SystemAddresses.SOL_MINT, SystemAddresses.SOL_MINT,
user, user
address_provider
) )
instructions.append(initialize_wsol_ix) instructions.append(initialize_wsol_ix)
# 4. Build buy_exact_in instruction # 4. Build buy_exact_in instruction with correct account ordering
# Based on the IDL and manual examples, the account order should be:
buy_accounts = [ buy_accounts = [
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True), AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), 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), 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), 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), 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 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), 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), AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False), AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False), 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), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), 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), AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
] ]
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
@@ -148,8 +148,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
close_wsol_ix = self._create_close_account_instruction( close_wsol_ix = self._create_close_account_instruction(
wsol_account, wsol_account,
user, user,
user, user
address_provider
) )
instructions.append(close_wsol_ix) instructions.append(close_wsol_ix)
@@ -204,28 +203,27 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
initialize_wsol_ix = self._create_initialize_account_instruction( initialize_wsol_ix = self._create_initialize_account_instruction(
wsol_account, wsol_account,
SystemAddresses.SOL_MINT, SystemAddresses.SOL_MINT,
user, user
address_provider
) )
instructions.append(initialize_wsol_ix) instructions.append(initialize_wsol_ix)
# 3. Build sell_exact_in instruction # 3. Build sell_exact_in instruction with correct account ordering
sell_accounts = [ sell_accounts = [
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True), AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), 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), 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), 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), 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 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), 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), AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL)
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False), AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False), 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), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), 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), AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
] ]
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
@@ -248,8 +246,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
close_wsol_ix = self._create_close_account_instruction( close_wsol_ix = self._create_close_account_instruction(
wsol_account, wsol_account,
user, user,
user, user
address_provider
) )
instructions.append(close_wsol_ix) instructions.append(close_wsol_ix)
@@ -278,8 +275,8 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
accounts_info["user_base_token"], accounts_info["user_base_token"],
accounts_info["base_vault"], accounts_info["base_vault"],
accounts_info["quote_vault"], accounts_info["quote_vault"],
accounts_info["base_token_mint"], token_info.mint,
accounts_info["quote_token_mint"], SystemAddresses.SOL_MINT,
accounts_info["program"], accounts_info["program"],
] ]
@@ -306,8 +303,8 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
accounts_info["user_base_token"], accounts_info["user_base_token"],
accounts_info["base_vault"], accounts_info["base_vault"],
accounts_info["quote_vault"], accounts_info["quote_vault"],
accounts_info["base_token_mint"], token_info.mint,
accounts_info["quote_token_mint"], SystemAddresses.SOL_MINT,
accounts_info["program"], accounts_info["program"],
] ]
@@ -328,8 +325,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
self, self,
account: Pubkey, account: Pubkey,
mint: Pubkey, mint: Pubkey,
owner: Pubkey, owner: Pubkey
address_provider: AddressProvider
) -> Instruction: ) -> Instruction:
"""Create an InitializeAccount instruction for the Token Program. """Create an InitializeAccount instruction for the Token Program.
@@ -337,7 +333,6 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
account: The account to initialize account: The account to initialize
mint: The token mint mint: The token mint
owner: The account owner owner: The account owner
address_provider: Platform address provider
Returns: Returns:
Instruction for initializing the account Instruction for initializing the account
@@ -362,8 +357,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
self, self,
account: Pubkey, account: Pubkey,
destination: Pubkey, destination: Pubkey,
owner: Pubkey, owner: Pubkey
address_provider: AddressProvider
) -> Instruction: ) -> Instruction:
"""Create a CloseAccount instruction for the Token Program. """Create a CloseAccount instruction for the Token Program.
@@ -371,7 +365,6 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
account: The account to close account: The account to close
destination: Where to send the remaining lamports destination: Where to send the remaining lamports
owner: The account owner (must sign) owner: The account owner (must sign)
address_provider: Platform address provider
Returns: Returns:
Instruction for closing the account Instruction for closing the account
+223 -64
View File
@@ -1,14 +1,17 @@
""" """
Pump.Fun implementation of InstructionBuilder interface. LetsBonk implementation of InstructionBuilder interface.
This module builds pump.fun-specific buy and sell instructions This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions
by implementing the InstructionBuilder interface with IDL-based discriminators. by implementing the InstructionBuilder interface with IDL-based discriminators.
""" """
import hashlib
import struct import struct
import time
from solders.instruction import AccountMeta, Instruction from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey 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 from spl.token.instructions import create_idempotent_associated_token_account
from core.pubkeys import TOKEN_DECIMALS, SystemAddresses from core.pubkeys import TOKEN_DECIMALS, SystemAddresses
@@ -19,28 +22,28 @@ from utils.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
class PumpFunInstructionBuilder(InstructionBuilder): class LetsBonkInstructionBuilder(InstructionBuilder):
"""Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators.""" """LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators."""
def __init__(self, idl_parser: IDLParser): def __init__(self, idl_parser: IDLParser):
"""Initialize pump.fun instruction builder with injected IDL parser. """Initialize LetsBonk instruction builder with injected IDL parser.
Args: Args:
idl_parser: Pre-loaded IDL parser for pump.fun platform idl_parser: Pre-loaded IDL parser for LetsBonk platform
""" """
self._idl_parser = idl_parser self._idl_parser = idl_parser
# Get discriminators from injected IDL parser # Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators() discriminators = self._idl_parser.get_instruction_discriminators()
self._buy_discriminator = discriminators["buy"] self._buy_exact_in_discriminator = discriminators["buy_exact_in"]
self._sell_discriminator = discriminators["sell"] self._sell_exact_in_discriminator = discriminators["sell_exact_in"]
logger.info("Pump.Fun instruction builder initialized with injected IDL parser") logger.info("LetsBonk instruction builder initialized with injected IDL parser")
@property @property
def platform(self) -> Platform: def platform(self) -> Platform:
"""Get the platform this builder serves.""" """Get the platform this builder serves."""
return Platform.PUMP_FUN return Platform.LETS_BONK
async def build_buy_instruction( async def build_buy_instruction(
self, self,
@@ -50,7 +53,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
minimum_amount_out: int, minimum_amount_out: int,
address_provider: AddressProvider address_provider: AddressProvider
) -> list[Instruction]: ) -> list[Instruction]:
"""Build buy instruction(s) for pump.fun. """Build buy instruction(s) for LetsBonk using buy_exact_in.
Args: Args:
token_info: Token information token_info: Token information
@@ -67,38 +70,71 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Get all required accounts # Get all required accounts
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
# 1. Create idempotent ATA instruction (won't fail if ATA already exists) # 1. Create idempotent ATA for base token
ata_instruction = create_idempotent_associated_token_account( ata_instruction = create_idempotent_associated_token_account(
user, # payer user, # payer
user, # owner user, # owner
token_info.mint, # mint token_info.mint, # mint
SystemAddresses.TOKEN_PROGRAM, # token program SystemAddresses.TOKEN_PROGRAM, # token program
) )
instructions.append(ata_instruction) instructions.append(ata_instruction)
# 2. Build buy instruction # 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)
# Account creation cost + amount to spend
account_creation_lamports = 2_039_280 # Standard account creation cost
total_lamports = amount_in + account_creation_lamports
create_wsol_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=user,
to_pubkey=wsol_account,
base=user,
seed=wsol_seed,
lamports=total_lamports,
space=165, # Size of a token account
owner=SystemAddresses.TOKEN_PROGRAM
)
)
instructions.append(create_wsol_ix)
# 3. Initialize WSOL account
initialize_wsol_ix = self._create_initialize_account_instruction(
wsol_account,
SystemAddresses.SOL_MINT,
user
)
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 = [ buy_accounts = [
AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False), AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token
AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True), AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL account)
AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault
AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault
AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True), AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
AccountMeta(pubkey=accounts_info["global_volume_accumulator"], is_signer=False, is_writable=True), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
AccountMeta(pubkey=accounts_info["user_volume_accumulator"], is_signer=False, is_writable=True), 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
] ]
# Build instruction data: discriminator + token_amount + max_sol_cost # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = ( instruction_data = (
self._buy_discriminator + self._buy_exact_in_discriminator +
struct.pack("<Q", minimum_amount_out) + # token amount in raw units struct.pack("<Q", amount_in) + # amount_in (u64) - SOL to spend
struct.pack("<Q", amount_in) # max SOL cost in lamports struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min tokens
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
) )
buy_instruction = Instruction( buy_instruction = Instruction(
@@ -108,6 +144,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
) )
instructions.append(buy_instruction) instructions.append(buy_instruction)
# 5. Close WSOL account to reclaim SOL
close_wsol_ix = self._create_close_account_instruction(
wsol_account,
user,
user
)
instructions.append(close_wsol_ix)
return instructions return instructions
async def build_sell_instruction( async def build_sell_instruction(
@@ -118,7 +162,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
minimum_amount_out: int, minimum_amount_out: int,
address_provider: AddressProvider address_provider: AddressProvider
) -> list[Instruction]: ) -> list[Instruction]:
"""Build sell instruction(s) for pump.fun. """Build sell instruction(s) for LetsBonk using sell_exact_in.
Args: Args:
token_info: Token information token_info: Token information
@@ -135,27 +179,60 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Get all required accounts # Get all required accounts
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
# Build sell instruction accounts # 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)
# Minimal account creation cost
account_creation_lamports = 2_039_280
create_wsol_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=user,
to_pubkey=wsol_account,
base=user,
seed=wsol_seed,
lamports=account_creation_lamports,
space=165, # Size of a token account
owner=SystemAddresses.TOKEN_PROGRAM
)
)
instructions.append(create_wsol_ix)
# 2. Initialize WSOL account
initialize_wsol_ix = self._create_initialize_account_instruction(
wsol_account,
SystemAddresses.SOL_MINT,
user
)
instructions.append(initialize_wsol_ix)
# 3. Build sell_exact_in instruction with correct account ordering
sell_accounts = [ sell_accounts = [
AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False), AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token (tokens being sold)
AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True), AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL received)
AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False), AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault (receives tokens)
AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True), AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL)
AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False), AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # 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
] ]
# Build instruction data: discriminator + token_amount + min_sol_output # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = ( instruction_data = (
self._sell_discriminator + self._sell_exact_in_discriminator +
struct.pack("<Q", amount_in) + # token amount in raw units struct.pack("<Q", amount_in) + # amount_in (u64) - tokens to sell
struct.pack("<Q", minimum_amount_out) # min SOL output in lamports struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min SOL
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
) )
sell_instruction = Instruction( sell_instruction = Instruction(
@@ -165,6 +242,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
) )
instructions.append(sell_instruction) instructions.append(sell_instruction)
# 4. Close WSOL account to reclaim SOL
close_wsol_ix = self._create_close_account_instruction(
wsol_account,
user,
user
)
instructions.append(close_wsol_ix)
return instructions return instructions
def get_required_accounts_for_buy( def get_required_accounts_for_buy(
@@ -186,14 +271,12 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
return [ return [
accounts_info["mint"], accounts_info["pool_state"],
accounts_info["bonding_curve"], accounts_info["user_base_token"],
accounts_info["associated_bonding_curve"], accounts_info["base_vault"],
accounts_info["user_token_account"], accounts_info["quote_vault"],
accounts_info["fee"], token_info.mint,
accounts_info["creator_vault"], SystemAddresses.SOL_MINT,
accounts_info["global_volume_accumulator"],
accounts_info["user_volume_accumulator"],
accounts_info["program"], accounts_info["program"],
] ]
@@ -216,15 +299,91 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
return [ return [
accounts_info["mint"], accounts_info["pool_state"],
accounts_info["bonding_curve"], accounts_info["user_base_token"],
accounts_info["associated_bonding_curve"], accounts_info["base_vault"],
accounts_info["user_token_account"], accounts_info["quote_vault"],
accounts_info["fee"], token_info.mint,
accounts_info["creator_vault"], SystemAddresses.SOL_MINT,
accounts_info["program"], accounts_info["program"],
] ]
def _generate_wsol_seed(self, user: Pubkey) -> str:
"""Generate a unique seed for WSOL account creation.
Args:
user: User's wallet address
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]
def _create_initialize_account_instruction(
self,
account: Pubkey,
mint: Pubkey,
owner: Pubkey
) -> Instruction:
"""Create an InitializeAccount instruction for the Token Program.
Args:
account: The account to initialize
mint: The token mint
owner: The account owner
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),
AccountMeta(pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False),
]
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
data = bytes([1])
return Instruction(
program_id=SystemAddresses.TOKEN_PROGRAM,
data=data,
accounts=accounts
)
def _create_close_account_instruction(
self,
account: Pubkey,
destination: Pubkey,
owner: Pubkey
) -> Instruction:
"""Create a CloseAccount instruction for the Token Program.
Args:
account: The account to close
destination: Where to send the remaining lamports
owner: The account owner (must sign)
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),
]
# CloseAccount instruction discriminator (instruction 9 in Token Program)
data = bytes([9])
return Instruction(
program_id=SystemAddresses.TOKEN_PROGRAM,
data=data,
accounts=accounts
)
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
"""Convert decimal token amount to raw token units. """Convert decimal token amount to raw token units.
+2
View File
@@ -1,2 +1,4 @@
{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"} {"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"}
{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"} {"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"}
{"timestamp": "2025-08-02T15:37:04.403139", "action": "buy", "platform": "lets_bonk", "token_address": "7o5FtYXxpX6sqtcJJ3ES4DiWt4C9HnHzKuHrZPpjbonk", "symbol": "pants", "price": 5e-06, "amount": 20, "tx_hash": "26Uu4rZ1PcnzioHwWAh3Reca4MMNZjgiQbmihjisiurpv4xcHFtvSptZoV3zUkS5bdqZ4zpLeGb46J4bWyczu6FY"}
{"timestamp": "2025-08-02T15:37:21.984944", "action": "sell", "platform": "lets_bonk", "token_address": "7o5FtYXxpX6sqtcJJ3ES4DiWt4C9HnHzKuHrZPpjbonk", "symbol": "pants", "price": 2.7959121193874663e-08, "amount": 3712.914779, "tx_hash": "5zXxjxHZuWXxGih1Aca6HoFkybiyHT4jPghhXCg3NbkyD3nuzzaJtmjg4mnoEQDeTn64b1cUZMawhmYbeq3cwjfj"}