Feat/letsbonk examples (#140)

* feat: update letsbonk idl

* feat: add letsbonk examples
This commit is contained in:
Anton
2025-10-22 00:20:30 +02:00
committed by GitHub
parent e97e2f2d35
commit 25c376dd02
6 changed files with 4893 additions and 41 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,391 @@
"""
IDL Parser module for Solana programs.
Provides functionality to load and parse Anchor IDL files and decode instruction data.
"""
import json
import struct
from typing import Any
import base58
# Constants for Anchor data layout
DISCRIMINATOR_SIZE = 8
PUBLIC_KEY_SIZE = 32
STRING_LENGTH_PREFIX_SIZE = 4
ENUM_DISCRIMINATOR_SIZE = 1
class IDLParser:
"""Parser for automatically decoding instructions using IDL definitions."""
# A single source of truth for primitive type information, mapping the type name
# to its struct format character and size in bytes.
_PRIMITIVE_TYPE_INFO = {
# type_name: (format_char, size_in_bytes)
"u8": ("<B", 1),
"u16": ("<H", 2),
"u32": ("<I", 4),
"u64": ("<Q", 8),
"i8": ("<b", 1),
"i16": ("<h", 2),
"i32": ("<i", 4),
"i64": ("<q", 8),
"bool": ("<?", 1),
"pubkey": (None, PUBLIC_KEY_SIZE),
"string": (
None,
STRING_LENGTH_PREFIX_SIZE,
), # Min size is for the length prefix
}
def __init__(self, idl_path: str, verbose: bool = False):
"""
Initialize the IDL parser.
Args:
idl_path: Path to the IDL JSON file
verbose: Whether to print debug information during initialization
"""
self.verbose = verbose
with open(idl_path) as f:
self.idl = json.load(f)
self.instructions: dict[bytes, dict[str, Any]] = {}
self.types: dict[str, dict[str, Any]] = {}
self.instruction_min_sizes: dict[bytes, int] = {}
self._build_instruction_map()
self._build_type_map()
self._calculate_instruction_sizes()
# --------------------------------------------------------------------------
# Public Methods (External API)
# --------------------------------------------------------------------------
def get_instruction_discriminators(self) -> dict[str, bytes]:
"""Get a mapping of instruction names to their discriminators."""
return {instr["name"]: disc for disc, instr in self.instructions.items()}
def get_instruction_names(self) -> list[str]:
"""Get a list of all available instruction names."""
return [instr["name"] for instr in self.instructions.values()]
def validate_instruction_data_length(
self, ix_data: bytes, discriminator: bytes
) -> bool:
"""Validate that instruction data meets minimum length requirements."""
if discriminator not in self.instruction_min_sizes:
return True # Allow if we don't know the expected size
expected_min_size = self.instruction_min_sizes[discriminator]
actual_size = len(ix_data)
if actual_size < expected_min_size:
instruction_name = self.instructions[discriminator]["name"]
if self.verbose:
print(
f"⚠️ Instruction data for '{instruction_name}' is shorter than the expected minimum "
f"({actual_size}/{expected_min_size} bytes)."
)
return False
return True
def decode_instruction(
self, ix_data: bytes, keys: list[bytes], accounts: list[int]
) -> dict[str, Any] | None:
"""Decode instruction data using IDL definitions."""
if len(ix_data) < DISCRIMINATOR_SIZE:
return None
discriminator = ix_data[:DISCRIMINATOR_SIZE]
if discriminator not in self.instructions:
return None
if not self.validate_instruction_data_length(ix_data, discriminator):
return None
instruction = self.instructions[discriminator]
data_args = ix_data[DISCRIMINATOR_SIZE:]
# Decode instruction arguments
args = {}
decode_offset = 0
for arg in instruction.get("args", []):
try:
value, decode_offset = self._decode_type(
data_args, decode_offset, arg["type"]
)
args[arg["name"]] = value
except Exception as e:
if self.verbose:
print(f"❌ Decode error in argument '{arg['name']}': {e}")
return None
# Helper to safely retrieve account public keys
def get_account_key(index: int) -> str | None:
if index < len(accounts):
account_index = accounts[index]
if account_index < len(keys):
return base58.b58encode(keys[account_index]).decode("utf-8")
return None # Return None for invalid indices
# Build account info based on instruction definition
account_info = {}
instruction_accounts = instruction.get("accounts", [])
for i, account_def in enumerate(instruction_accounts):
account_info[account_def["name"]] = get_account_key(i)
return {
"instruction_name": instruction["name"],
"args": args,
"accounts": account_info,
}
def decode_account_data(
self,
account_data: bytes,
account_type_name: str,
skip_discriminator: bool = True,
) -> dict[str, Any] | None:
"""
Decode account data using a specific account type from the IDL.
Args:
account_data: Raw account data bytes.
account_type_name: Name of the account type in the IDL (e.g., "MyAccount").
skip_discriminator: Whether to skip the first 8 bytes, which Anchor uses as a
type discriminator for account data. Set to False if your
data does not have this prefix.
Returns:
Decoded account data as a dictionary, or None if decoding fails.
"""
try:
if account_type_name not in self.types:
if self.verbose:
print(f"Account type '{account_type_name}' not found in IDL")
return None
data = account_data
if skip_discriminator:
if len(account_data) < DISCRIMINATOR_SIZE:
if self.verbose:
print(
f"Account data too short to contain a discriminator: {len(account_data)} bytes"
)
return None
data = account_data[DISCRIMINATOR_SIZE:]
decoded_data, _ = self._decode_defined_type(data, 0, account_type_name)
return decoded_data
except Exception as e:
if self.verbose:
print(f"Error decoding account data for {account_type_name}: {e}")
return None
# --------------------------------------------------------------------------
# Internal Helper Methods
# --------------------------------------------------------------------------
def _build_instruction_map(self):
"""Build a map of discriminators to instruction definitions."""
for instruction in self.idl.get("instructions", []):
# The discriminator from the JSON IDL is a list of u8 integers.
discriminator = bytes(instruction["discriminator"])
self.instructions[discriminator] = instruction
def _build_type_map(self):
"""Build a map of type names to their definitions."""
for type_def in self.idl.get("types", []):
self.types[type_def["name"]] = type_def
def _calculate_instruction_sizes(self):
"""Calculate minimum data sizes for each instruction."""
for discriminator, instruction in self.instructions.items():
try:
min_size = DISCRIMINATOR_SIZE
for arg in instruction.get("args", []):
min_size += self._calculate_type_min_size(arg["type"])
self.instruction_min_sizes[discriminator] = min_size
if self.verbose and instruction["name"] == "initialize":
print(f"📏 Initialize instruction min size: {min_size} bytes")
except Exception as e:
if self.verbose:
print(f"⚠️ Could not calculate size for {instruction['name']}: {e}")
self.instruction_min_sizes[discriminator] = DISCRIMINATOR_SIZE
def _calculate_type_min_size(self, type_def: str | dict) -> int:
"""Calculate minimum size in bytes for a type definition."""
if isinstance(type_def, str):
return self._get_primitive_size(type_def)
if isinstance(type_def, dict):
if "defined" in type_def:
type_name = self._get_defined_type_name(type_def)
return self._calculate_defined_type_min_size(type_name)
if "array" in type_def:
element_type, array_length = type_def["array"]
element_size = self._calculate_type_min_size(element_type)
return element_size * array_length
raise ValueError(
f"Invalid or unknown type definition for size calculation: {type_def}"
)
def _get_primitive_size(self, type_name: str) -> int:
"""Get size in bytes for primitive types from the central map."""
info = self._PRIMITIVE_TYPE_INFO.get(type_name)
return info[1] if info else 0
def _get_defined_type_name(self, type_def: dict[str, Any]) -> str:
"""Extracts the type name from a 'defined' type, handling old and new IDL formats."""
defined_value = type_def["defined"]
# New format: {'defined': {'name': 'MyType'}}
# Old format: {'defined': 'MyType'}
return (
defined_value["name"] if isinstance(defined_value, dict) else defined_value
)
def _calculate_defined_type_min_size(self, type_name: str) -> int:
"""Calculate minimum size for user-defined types (structs and enums)."""
if type_name not in self.types:
raise ValueError(f"Unknown defined type: {type_name}")
type_def = self.types[type_name]["type"]
if type_def["kind"] == "struct":
return sum(
self._calculate_type_min_size(field["type"])
for field in type_def["fields"]
)
if type_def["kind"] == "enum":
# The size of an enum is its discriminator plus the size of its LARGEST variant,
# as the data layout must accommodate any possible variant.
max_variant_size = 0
for variant in type_def["variants"]:
variant_size = 0
for field in variant.get("fields", []):
# A field can be a type string/dict (tuple variant) or a dict with a 'type' key (struct variant)
field_type = field["type"] if isinstance(field, dict) else field
variant_size += self._calculate_type_min_size(field_type)
max_variant_size = max(max_variant_size, variant_size)
return ENUM_DISCRIMINATOR_SIZE + max_variant_size
raise ValueError(
f"Unsupported type kind for size calculation: {type_def['kind']}"
)
def _decode_type(
self, data: bytes, offset: int, type_def: str | dict
) -> tuple[Any, int]:
"""Decode a value based on its type definition."""
if isinstance(type_def, str):
return self._decode_primitive(data, offset, type_def)
if isinstance(type_def, dict):
if "defined" in type_def:
type_name = self._get_defined_type_name(type_def)
return self._decode_defined_type(data, offset, type_name)
if "array" in type_def:
return self._decode_array(data, offset, type_def["array"])
raise ValueError(f"Invalid or unknown type definition for decoding: {type_def}")
def _decode_array(
self, data: bytes, offset: int, array_def: list
) -> tuple[list[Any], int]:
"""Decode fixed-size array types."""
element_type, array_length = array_def
array_data = []
for _ in range(array_length):
value, offset = self._decode_type(data, offset, element_type)
array_data.append(value)
return array_data, offset
def _decode_primitive(
self, data: bytes, offset: int, type_name: str
) -> tuple[Any, int]:
"""Decode primitive types."""
if type_name not in self._PRIMITIVE_TYPE_INFO:
raise ValueError(f"Unknown primitive type: {type_name}")
if type_name == "string":
length = struct.unpack_from("<I", data, offset)[0]
offset += STRING_LENGTH_PREFIX_SIZE
value = data[offset : offset + length].decode("utf-8")
return value, offset + length
if type_name == "pubkey":
end = offset + PUBLIC_KEY_SIZE
value = base58.b58encode(data[offset:end]).decode("utf-8")
return value, end
# Handle all numeric and bool types from the map
fmt, size = self._PRIMITIVE_TYPE_INFO[type_name]
value = struct.unpack_from(fmt, data, offset)[0]
return value, offset + size
def _decode_defined_type(
self, data: bytes, offset: int, type_name: str
) -> tuple[dict[str, Any], int]:
"""Decode user-defined types (structs and enums)."""
if type_name not in self.types:
raise ValueError(f"Unknown defined type: {type_name}")
type_def = self.types[type_name]["type"]
if type_def["kind"] == "struct":
struct_data = {}
for field in type_def["fields"]:
value, offset = self._decode_type(data, offset, field["type"])
struct_data[field["name"]] = value
return struct_data, offset
if type_def["kind"] == "enum":
variant_index = struct.unpack_from("<B", data, offset)[0]
offset += ENUM_DISCRIMINATOR_SIZE
variants = type_def["variants"]
if variant_index >= len(variants):
raise ValueError(
f"Invalid enum variant index {variant_index} for type {type_name}"
)
variant = variants[variant_index]
result = {"variant": variant["name"]}
variant_fields = variant.get("fields", [])
if variant_fields:
# Check if it's a struct variant (fields are dicts) or tuple variant (fields are strings/dicts)
if isinstance(variant_fields[0], dict):
struct_data = {}
for field in variant_fields:
value, offset = self._decode_type(data, offset, field["type"])
struct_data[field["name"]] = value
result["data"] = struct_data
else: # Tuple variant
tuple_data = []
for field_type in variant_fields:
value, offset = self._decode_type(data, offset, field_type)
tuple_data.append(value)
result["data"] = tuple_data
return result, offset
raise ValueError(f"Unsupported type kind for decoding: {type_def['kind']}")
def load_idl_parser(idl_path: str, verbose: bool = False) -> IDLParser:
"""
Convenience function to load an IDL parser.
Args:
idl_path: Path to the IDL JSON file
verbose: Whether to print debug information
Returns:
Initialized IDLParser instance
"""
return IDLParser(idl_path, verbose)
@@ -0,0 +1,734 @@
"""
Manual Buy Exact In Example for Raydium LaunchLab
This script demonstrates how to buy tokens using the buy_exact_in instruction
from the Raydium LaunchLab program. It follows the IDL structure.
Key features:
- Uses buy_exact_in instruction
- Implements proper account ordering as per IDL
- Includes slippage protection with minimum_amount_out
- Handles WSOL wrapping/unwrapping automatically
- Follows the exact transaction structure from the Solscan example
- User configurable SOL amount and slippage
- Uses idempotent ATA creation
"""
import asyncio
import os
import struct
import sys
import base58
from dotenv import load_dotenv
from idl_parser import load_idl_parser
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
from solders.transaction import VersionedTransaction
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True)
load_dotenv()
TOKEN_MINT_ADDRESS = Pubkey.from_string(
"MYcq5mUyoAtCfyDWYAWvioou3cgnYjnCvFd7U6fspot"
) # Replace with actual token mint address
# Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
# User configurable parameters
SOL_AMOUNT_TO_SPEND = float(os.environ.get("SOL_AMOUNT", "0.001"))
SLIPPAGE_TOLERANCE = float(os.environ.get("SLIPPAGE", "0.25"))
# Transaction parameters
SHARE_FEE_RATE = 0
# Program IDs and addresses from Raydium LaunchLab
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string(
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
)
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string(
"5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA"
)
# Token program and system addresses
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
COMPUTE_BUDGET_PROGRAM_ID = Pubkey.from_string(
"ComputeBudget111111111111111111111111111111"
)
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string(
"SysvarRent111111111111111111111111111111111"
)
# Instruction discriminator for buy_exact_in (from IDL)
BUY_EXACT_IN_DISCRIMINATOR = bytes([250, 234, 13, 123, 213, 156, 19, 236])
# Compute budget settings
COMPUTE_UNIT_LIMIT = 150_000
COMPUTE_UNIT_PRICE = 1_000
LAMPORTS_PER_SOL = 1_000_000_000
def derive_authority_pda() -> Pubkey:
"""
Derive the authority PDA for the Raydium LaunchLab program.
This PDA acts as the authority for pool vault operations and is generated
using the AUTH_SEED as specified in the IDL.
Returns:
Pubkey: The derived authority PDA
"""
AUTH_SEED = b"vault_auth_seed"
authority_pda, _ = Pubkey.find_program_address(
[AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return authority_pda
def derive_event_authority_pda() -> Pubkey:
"""
Derive the event authority PDA for the Raydium LaunchLab program.
This PDA is used for emitting program events during swaps.
Returns:
Pubkey: The derived event authority PDA
"""
EVENT_AUTHORITY_SEED = b"__event_authority"
event_authority_pda, _ = Pubkey.find_program_address(
[EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return event_authority_pda
def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None:
"""
Derive the pool state account for a given base token mint.
Args:
base_token_mint: The token mint address to search for
Returns:
Pubkey of the pool state account, or None if not found
"""
seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)]
pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID)
return pool_state_pda
def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> 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 (WSOL)
Returns:
Pubkey of the creator fee vault
"""
seeds = [bytes(creator), bytes(quote_mint)]
creator_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return creator_fee_vault_pda
def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey:
"""
Derive the platform fee vault PDA.
This vault accumulates platform fees from trades.
Args:
platform_config: The platform config account
quote_mint: The quote token mint (WSOL)
Returns:
Pubkey of the platform fee vault
"""
seeds = [bytes(platform_config), bytes(quote_mint)]
platform_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return platform_fee_vault_pda
def decode_pool_state(account_data: bytes) -> dict | None:
"""
Decode pool state account data using the IDL parser.
Args:
account_data: Raw account data from the pool state account
Returns:
Dictionary containing decoded pool state data, or None if decoding fails
"""
try:
result = IDL_PARSER.decode_account_data(
account_data, "PoolState", skip_discriminator=True
)
if result:
return result
return None
except Exception as e:
print(f"Error decoding pool state: {e}")
import traceback
traceback.print_exc()
return None
async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None:
"""
Get and decode the pool state account data.
Args:
client: Solana RPC client
pool_state: The pool state account address
Returns:
Dictionary containing decoded pool state data, or None if error
"""
try:
account_info = await client.get_account_info(pool_state)
if not account_info.value:
print("Pool state account not found")
return None
return decode_pool_state(account_info.value.data)
except Exception as e:
print(f"Error getting pool state data: {e}")
return None
def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey:
"""
Calculate the associated token account address for a given owner and mint.
This manually implements the ATA derivation without requiring the spl-token package.
Args:
owner: The wallet that owns the token account
mint: The token mint address
Returns:
Pubkey of the associated token account
"""
ata_address, _ = Pubkey.find_program_address(
[bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)],
ASSOCIATED_TOKEN_PROGRAM_ID,
)
return ata_address
def create_associated_token_account_idempotent_instruction(
payer: Pubkey, owner: Pubkey, mint: Pubkey
) -> Instruction:
"""
Create an idempotent instruction to create an Associated Token Account.
This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists.
Args:
payer: The account that will pay for the creation
owner: The owner of the new token account
mint: The token mint
Returns:
Instruction for creating the ATA idempotently
"""
ata_address = get_associated_token_address(owner, mint)
accounts = [
AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account
AccountMeta(
pubkey=ata_address, is_signer=False, is_writable=True
), # Associated token account
AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address
AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint
AccountMeta(
pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False
), # System program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # Token program
]
data = bytes([1])
return Instruction(
program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts
)
def create_initialize_account_instruction(
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=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
]
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
data = bytes([1])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_close_account_instruction(
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),
]
data = bytes([9])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_wsol_account_with_seed(
payer: Pubkey, seed: str, lamports: int
) -> tuple[Pubkey, Instruction, Instruction]:
"""
Create a WSOL account using createAccountWithSeed and initialize it.
This replicates the exact pattern from the Solscan example where a new account
is created with a seed and then initialized as a token account.
Args:
payer: The account that will pay for and own the new account
seed: String seed for deterministic account generation
lamports: Amount of lamports to transfer to the new account
Returns:
Tuple of (new_account_pubkey, create_instruction, initialize_instruction)
"""
new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID)
create_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=payer,
to_pubkey=new_account,
base=payer,
seed=seed,
lamports=lamports,
space=165, # Size of a token account
owner=TOKEN_PROGRAM_ID,
)
)
initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer)
return new_account, create_ix, initialize_ix
def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey:
"""
Get the user's associated token account for the base token.
In a real implementation, this should check if the account exists and create it if needed.
For this example, we'll derive the standard ATA address.
Args:
payer: The user's wallet address
base_mint: The base token mint address
Returns:
Pubkey of the user's base token account
"""
return get_associated_token_address(payer, base_mint)
def calculate_minimum_amount_out_from_pool_state(
pool_state_data: dict, amount_in: int, slippage_tolerance: float
) -> int:
"""
Calculate the minimum amount out based on pool state data and slippage tolerance.
Uses the actual pool reserves to calculate expected output using constant product formula.
Args:
pool_state_data: Decoded pool state data containing reserves
amount_in: Amount of quote tokens being swapped in (in lamports)
slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%)
Returns:
Minimum amount of base tokens to receive
"""
try:
# Extract pool reserves from decoded state
virtual_base = pool_state_data["virtual_base"]
virtual_quote = pool_state_data["virtual_quote"]
real_base = pool_state_data["real_base"]
real_quote = pool_state_data["real_quote"]
print("Pool State:")
print(f" Virtual Base: {virtual_base:,}")
print(f" Virtual Quote: {virtual_quote:,}")
print(f" Real Base: {real_base:,}")
print(f" Real Quote: {real_quote:,}")
# Use virtual reserves for bonding curve calculation
# This follows the constant product AMM formula: x * y = k
# amount_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
# Calculate expected output using constant product formula
numerator = amount_in * virtual_base
denominator = virtual_quote + amount_in
expected_output = numerator // denominator
# Apply slippage tolerance
minimum_with_slippage = int(expected_output * (1 - slippage_tolerance))
print(f"Amount in: {amount_in:,} lamports")
print(f"Expected output: {expected_output:,} tokens")
print(
f"Minimum with {slippage_tolerance * 100}% slippage: {minimum_with_slippage:,} tokens"
)
return minimum_with_slippage
except Exception as e:
print(f"Error calculating minimum amount out from pool state: {e}")
return None
async def buy_exact_in(
client: AsyncClient,
base_token_mint: Pubkey,
amount_in_sol: float,
slippage_tolerance: float,
) -> str | None:
"""
Execute a buy_exact_in transaction on Raydium LaunchLab.
This function implements the exact transaction flow from the Solscan example:
1. SetComputeUnitPrice
2. SetComputeUnitLimit
3. Create Associated Token Account for base token (idempotent)
4. Create WSOL account with seed
5. Initialize WSOL account
6. Execute buy_exact_in instruction (15 main accounts + 3 remaining accounts)
7. Close WSOL account
The buy_exact_in instruction requires 18 total accounts:
- 15 main accounts (as per IDL)
- 3 remaining accounts: System Program, Creator Fee Vault, Platform Fee Vault
Args:
client: Solana RPC client
base_token_mint: Address of the token to buy
amount_in_sol: Amount of SOL to spend
slippage_tolerance: Slippage tolerance as decimal
Returns:
Transaction signature if successful, None otherwise
"""
try:
print(f"Finding pool state for token: {base_token_mint}")
pool_state = derive_pool_state_for_token(base_token_mint)
if not pool_state:
print("Pool state not found for this token")
return None
# Get and decode pool state data using IDL parser
pool_state_data = await get_pool_state_data(client, pool_state)
if not pool_state_data:
print("Failed to decode pool state data")
return None
# Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects)
base_vault = Pubkey.from_string(pool_state_data["base_vault"])
quote_vault = Pubkey.from_string(pool_state_data["quote_vault"])
creator = Pubkey.from_string(pool_state_data["creator"])
print(f"Found pool state: {pool_state}")
print(f"Base vault: {base_vault}")
print(f"Quote vault: {quote_vault}")
print(f"Creator: {creator}")
print(f"Pool status: {pool_state_data['status']}")
# Derive necessary PDAs
authority = derive_authority_pda()
event_authority = derive_event_authority_pda()
creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT)
platform_fee_vault = derive_platform_fee_vault(
LETSBONK_PLATFORM_CONFIG, WSOL_MINT
)
print(f"Creator fee vault: {creator_fee_vault}")
print(f"Platform fee vault: {platform_fee_vault}")
# Calculate amounts using pool state data
amount_in = int(amount_in_sol * LAMPORTS_PER_SOL)
minimum_amount_out = calculate_minimum_amount_out_from_pool_state(
pool_state_data, amount_in, slippage_tolerance
)
print(f"Amount in: {amount_in} lamports ({amount_in_sol} SOL)")
print(f"Minimum amount out: {minimum_amount_out}")
# Step 1: Create Associated Token Account for base token (idempotent)
user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint)
create_ata_ix = create_associated_token_account_idempotent_instruction(
PAYER.pubkey(), PAYER.pubkey(), base_token_mint
)
# Step 2: Create WSOL account with seed
import hashlib
import time
# Generate a unique seed based on timestamp and user pubkey
seed_data = f"{int(time.time())}{PAYER.pubkey()!s}"
wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32]
# Calculate required lamports (amount + small buffer for account creation)
account_creation_lamports = 2_039_280 # Standard account creation cost
total_lamports = amount_in + account_creation_lamports
user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed(
PAYER.pubkey(), wsol_seed, total_lamports
)
print(f"User base token account: {user_base_token}")
print(f"User quote token account: {user_quote_token}")
# Step 3: Build the buy_exact_in instruction
accounts = [
AccountMeta(
pubkey=PAYER.pubkey(), is_signer=True, is_writable=False
), # payer
AccountMeta(
pubkey=authority, is_signer=False, is_writable=False
), # authority
AccountMeta(
pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False
), # global_config
AccountMeta(
pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False
), # platform_config
AccountMeta(
pubkey=pool_state, is_signer=False, is_writable=True
), # pool_state
AccountMeta(
pubkey=user_base_token, is_signer=False, is_writable=True
), # user_base_token
AccountMeta(
pubkey=user_quote_token, is_signer=False, is_writable=True
), # user_quote_token
AccountMeta(
pubkey=base_vault, is_signer=False, is_writable=True
), # base_vault
AccountMeta(
pubkey=quote_vault, is_signer=False, is_writable=True
), # quote_vault
AccountMeta(
pubkey=base_token_mint, is_signer=False, is_writable=False
), # base_token_mint
AccountMeta(
pubkey=WSOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # base_token_program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # quote_token_program
AccountMeta(
pubkey=event_authority, is_signer=False, is_writable=False
), # event_authority
AccountMeta(
pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False
), # program
]
# Add remaining accounts (not explicitly listed in IDL but required by the program)
# These accounts are used for fee collection during swaps
accounts.append(
AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False)
) # #16: System Program
accounts.append(
AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True)
) # #17: Platform fee vault
accounts.append(
AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True)
) # #18: Creator fee vault
# Instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
instruction_data = (
BUY_EXACT_IN_DISCRIMINATOR
+ struct.pack("<Q", amount_in) # amount_in (u64)
+ struct.pack("<Q", minimum_amount_out) # minimum_amount_out (u64)
+ struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
)
buy_exact_in_ix = Instruction(
program_id=RAYDIUM_LAUNCHLAB_PROGRAM_ID,
data=instruction_data,
accounts=accounts,
)
# Step 4: Create close WSOL account instruction
close_wsol_ix = create_close_account_instruction(
user_quote_token, PAYER.pubkey(), PAYER.pubkey()
)
# Step 5: Build complete transaction
instructions = [
set_compute_unit_price(COMPUTE_UNIT_PRICE),
set_compute_unit_limit(COMPUTE_UNIT_LIMIT),
# Instruction #3: Create Associated Token Account for base token (idempotent)
create_ata_ix,
# Instruction #4: Create WSOL account with seed
create_wsol_ix,
# Instruction #5: Initialize WSOL account
init_wsol_ix,
# Instruction #6: Execute buy_exact_in
buy_exact_in_ix,
# Instruction #7: Close WSOL account
close_wsol_ix,
]
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
message = Message.new_with_blockhash(
instructions, PAYER.pubkey(), recent_blockhash
)
transaction = VersionedTransaction(message, [PAYER])
print("Simulating transaction...")
simulation = await client.simulate_transaction(transaction)
if simulation.value.err:
print(f"Simulation failed: {simulation.value.err}")
return None
print(
f"Simulation successful. Compute units consumed: {simulation.value.units_consumed}"
)
print("Sending transaction...")
result = await client.send_transaction(
transaction,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
tx_signature = result.value
print(f"Transaction sent: https://solscan.io/tx/{tx_signature}")
print("Waiting for confirmation...")
await client.confirm_transaction(tx_signature, commitment="confirmed")
print("Transaction confirmed!")
return tx_signature
except Exception as e:
print(f"Error executing buy_exact_in: {e}")
import traceback
traceback.print_exc()
return None
async def main():
"""
Main function to execute the buy_exact_in example.
Takes configuration from environment variables or uses defaults.
"""
try:
print(f"Starting buy_exact_in for token: {TOKEN_MINT_ADDRESS}")
print(f"Amount to spend: {SOL_AMOUNT_TO_SPEND} SOL")
print(f"Slippage tolerance: {SLIPPAGE_TOLERANCE * 100}%")
print(f"Using RPC endpoint: {RPC_ENDPOINT}")
print()
async with AsyncClient(RPC_ENDPOINT) as client:
balance_resp = await client.get_balance(PAYER.pubkey())
balance_sol = balance_resp.value / LAMPORTS_PER_SOL
print(f"Wallet balance: {balance_sol:.6f} SOL")
if (
balance_sol < SOL_AMOUNT_TO_SPEND + 0.001
): # Include some buffer for fees
print("Insufficient SOL balance!")
return
tx_signature = await buy_exact_in(
client, TOKEN_MINT_ADDRESS, SOL_AMOUNT_TO_SPEND, SLIPPAGE_TOLERANCE
)
if tx_signature:
print(f"\n✅ Success! Transaction: {tx_signature}")
print(f"🔗 View on Solscan: https://solscan.io/tx/{tx_signature}")
else:
print("\n❌ Transaction failed!")
except ValueError as e:
print(f"Invalid token mint address: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,739 @@
"""
Manual Buy Exact Out Example for Raydium LaunchLab
This script demonstrates how to buy tokens using the buy_exact_out instruction
from the Raydium LaunchLab program. It follows the IDL structure.
Key features:
- Uses buy_exact_out instruction
- Implements proper account ordering as per IDL
- Includes slippage protection with maximum_amount_in
- Handles WSOL wrapping/unwrapping automatically
- Follows the exact transaction structure from the buy_exact_in example
- User configurable token amount and slippage
- Uses idempotent ATA creation
"""
import asyncio
import os
import struct
import sys
import base58
from dotenv import load_dotenv
from idl_parser import load_idl_parser
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
from solders.transaction import VersionedTransaction
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True)
load_dotenv()
TOKEN_MINT_ADDRESS = Pubkey.from_string(
"MYcq5mUyoAtCfyDWYAWvioou3cgnYjnCvFd7U6fspot"
) # Replace with actual token mint address
# Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
# User configurable parameters
TOKEN_AMOUNT_TO_RECEIVE = int(
os.environ.get("TOKEN_AMOUNT", "1000000")
) # Amount of tokens to receive (in base units)
SLIPPAGE_TOLERANCE = float(os.environ.get("SLIPPAGE", "0.25"))
# Transaction parameters
SHARE_FEE_RATE = 0
# Program IDs and addresses from Raydium LaunchLab
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string(
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
)
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string(
"5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA"
)
# Token program and system addresses
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
COMPUTE_BUDGET_PROGRAM_ID = Pubkey.from_string(
"ComputeBudget111111111111111111111111111111"
)
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string(
"SysvarRent111111111111111111111111111111111"
)
# Instruction discriminator for buy_exact_out (from IDL)
BUY_EXACT_OUT_DISCRIMINATOR = bytes([24, 211, 116, 40, 105, 3, 153, 56])
# Compute budget settings
COMPUTE_UNIT_LIMIT = 150_000
COMPUTE_UNIT_PRICE = 1_000
LAMPORTS_PER_SOL = 1_000_000_000
def derive_authority_pda() -> Pubkey:
"""
Derive the authority PDA for the Raydium LaunchLab program.
This PDA acts as the authority for pool vault operations and is generated
using the AUTH_SEED as specified in the IDL.
Returns:
Pubkey: The derived authority PDA
"""
AUTH_SEED = b"vault_auth_seed"
authority_pda, _ = Pubkey.find_program_address(
[AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return authority_pda
def derive_event_authority_pda() -> Pubkey:
"""
Derive the event authority PDA for the Raydium LaunchLab program.
This PDA is used for emitting program events during swaps.
Returns:
Pubkey: The derived event authority PDA
"""
EVENT_AUTHORITY_SEED = b"__event_authority"
event_authority_pda, _ = Pubkey.find_program_address(
[EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return event_authority_pda
def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None:
"""
Derive the pool state account for a given base token mint.
Args:
base_token_mint: The token mint address to search for
Returns:
Pubkey of the pool state account, or None if not found
"""
seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)]
pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID)
return pool_state_pda
def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> 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 (WSOL)
Returns:
Pubkey of the creator fee vault
"""
seeds = [bytes(creator), bytes(quote_mint)]
creator_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return creator_fee_vault_pda
def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey:
"""
Derive the platform fee vault PDA.
This vault accumulates platform fees from trades.
Args:
platform_config: The platform config account
quote_mint: The quote token mint (WSOL)
Returns:
Pubkey of the platform fee vault
"""
seeds = [bytes(platform_config), bytes(quote_mint)]
platform_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return platform_fee_vault_pda
def decode_pool_state(account_data: bytes) -> dict | None:
"""
Decode pool state account data using the IDL parser.
Args:
account_data: Raw account data from the pool state account
Returns:
Dictionary containing decoded pool state data, or None if decoding fails
"""
try:
result = IDL_PARSER.decode_account_data(
account_data, "PoolState", skip_discriminator=True
)
if result:
return result
return None
except Exception as e:
print(f"Error decoding pool state: {e}")
import traceback
traceback.print_exc()
return None
async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None:
"""
Get and decode the pool state account data.
Args:
client: Solana RPC client
pool_state: The pool state account address
Returns:
Dictionary containing decoded pool state data, or None if error
"""
try:
account_info = await client.get_account_info(pool_state)
if not account_info.value:
print("Pool state account not found")
return None
return decode_pool_state(account_info.value.data)
except Exception as e:
print(f"Error getting pool state data: {e}")
return None
def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey:
"""
Calculate the associated token account address for a given owner and mint.
This manually implements the ATA derivation without requiring the spl-token package.
Args:
owner: The wallet that owns the token account
mint: The token mint address
Returns:
Pubkey of the associated token account
"""
ata_address, _ = Pubkey.find_program_address(
[bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)],
ASSOCIATED_TOKEN_PROGRAM_ID,
)
return ata_address
def create_associated_token_account_idempotent_instruction(
payer: Pubkey, owner: Pubkey, mint: Pubkey
) -> Instruction:
"""
Create an idempotent instruction to create an Associated Token Account.
This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists.
Args:
payer: The account that will pay for the creation
owner: The owner of the new token account
mint: The token mint
Returns:
Instruction for creating the ATA idempotently
"""
ata_address = get_associated_token_address(owner, mint)
accounts = [
AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account
AccountMeta(
pubkey=ata_address, is_signer=False, is_writable=True
), # Associated token account
AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address
AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint
AccountMeta(
pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False
), # System program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # Token program
]
data = bytes([1])
return Instruction(
program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts
)
def create_initialize_account_instruction(
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=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
]
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
data = bytes([1])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_close_account_instruction(
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),
]
data = bytes([9])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_wsol_account_with_seed(
payer: Pubkey, seed: str, lamports: int
) -> tuple[Pubkey, Instruction, Instruction]:
"""
Create a WSOL account using createAccountWithSeed and initialize it.
This replicates the exact pattern from the Solscan example where a new account
is created with a seed and then initialized as a token account.
Args:
payer: The account that will pay for and own the new account
seed: String seed for deterministic account generation
lamports: Amount of lamports to transfer to the new account
Returns:
Tuple of (new_account_pubkey, create_instruction, initialize_instruction)
"""
new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID)
create_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=payer,
to_pubkey=new_account,
base=payer,
seed=seed,
lamports=lamports,
space=165, # Size of a token account
owner=TOKEN_PROGRAM_ID,
)
)
initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer)
return new_account, create_ix, initialize_ix
def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey:
"""
Get the user's associated token account for the base token.
In a real implementation, this should check if the account exists and create it if needed.
For this example, we'll derive the standard ATA address.
Args:
payer: The user's wallet address
base_mint: The base token mint address
Returns:
Pubkey of the user's base token account
"""
return get_associated_token_address(payer, base_mint)
def calculate_maximum_amount_in_from_pool_state(
pool_state_data: dict, amount_out: int, slippage_tolerance: float
) -> int:
"""
Calculate the maximum amount in based on pool state data and slippage tolerance.
Uses the actual pool reserves to calculate expected input using constant product formula.
For buy_exact_out, we know the output amount and need to calculate the required input.
Args:
pool_state_data: Decoded pool state data containing reserves
amount_out: Amount of base tokens to receive (exact output)
slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%)
Returns:
Maximum amount of quote tokens to spend
"""
try:
# Extract pool reserves from decoded state
virtual_base = pool_state_data["virtual_base"]
virtual_quote = pool_state_data["virtual_quote"]
real_base = pool_state_data["real_base"]
real_quote = pool_state_data["real_quote"]
print("Pool State:")
print(f" Virtual Base: {virtual_base:,}")
print(f" Virtual Quote: {virtual_quote:,}")
print(f" Real Base: {real_base:,}")
print(f" Real Quote: {real_quote:,}")
# Use virtual reserves for bonding curve calculation
# For exact output, we need to solve: amount_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
# Rearranging: amount_in = (amount_out * virtual_quote) / (virtual_base - amount_out)
if virtual_base <= amount_out:
raise ValueError(
f"Amount out ({amount_out}) cannot be >= virtual base reserves ({virtual_base})"
)
# Calculate required input using rearranged constant product formula
numerator = amount_out * virtual_quote
denominator = virtual_base - amount_out
expected_input = numerator // denominator
# Apply slippage tolerance (add buffer for price movement)
maximum_with_slippage = int(expected_input * (1 + slippage_tolerance))
print(f"Amount out: {amount_out:,} tokens")
print(
f"Expected input: {expected_input:,} lamports ({expected_input / LAMPORTS_PER_SOL:.6f} SOL)"
)
print(
f"Maximum with {slippage_tolerance * 100}% slippage: {maximum_with_slippage:,} lamports ({maximum_with_slippage / LAMPORTS_PER_SOL:.6f} SOL)"
)
return maximum_with_slippage
except Exception as e:
print(f"Error calculating maximum amount in from pool state: {e}")
return None
async def buy_exact_out(
client: AsyncClient,
base_token_mint: Pubkey,
amount_out: int,
slippage_tolerance: float,
) -> str | None:
"""
Execute a buy_exact_out transaction on Raydium LaunchLab.
This function implements the exact transaction flow similar to buy_exact_in:
1. SetComputeUnitPrice
2. SetComputeUnitLimit
3. Create Associated Token Account for base token (idempotent)
4. Create WSOL account with seed
5. Initialize WSOL account
6. Execute buy_exact_out instruction
7. Close WSOL account
Args:
client: Solana RPC client
base_token_mint: Address of the token to buy
amount_out: Exact amount of tokens to receive
slippage_tolerance: Slippage tolerance as decimal
Returns:
Transaction signature if successful, None otherwise
"""
try:
print(f"Finding pool state for token: {base_token_mint}")
pool_state = derive_pool_state_for_token(base_token_mint)
if not pool_state:
print("Pool state not found for this token")
return None
# Get and decode pool state data using IDL parser
pool_state_data = await get_pool_state_data(client, pool_state)
if not pool_state_data:
print("Failed to decode pool state data")
return None
# Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects)
base_vault = Pubkey.from_string(pool_state_data["base_vault"])
quote_vault = Pubkey.from_string(pool_state_data["quote_vault"])
creator = Pubkey.from_string(pool_state_data["creator"])
print(f"Found pool state: {pool_state}")
print(f"Base vault: {base_vault}")
print(f"Quote vault: {quote_vault}")
print(f"Creator: {creator}")
print(f"Pool status: {pool_state_data['status']}")
# Derive necessary PDAs
authority = derive_authority_pda()
event_authority = derive_event_authority_pda()
creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT)
platform_fee_vault = derive_platform_fee_vault(
LETSBONK_PLATFORM_CONFIG, WSOL_MINT
)
print(f"Creator fee vault: {creator_fee_vault}")
print(f"Platform fee vault: {platform_fee_vault}")
# Calculate amounts using pool state data
maximum_amount_in = calculate_maximum_amount_in_from_pool_state(
pool_state_data, amount_out, slippage_tolerance
)
if maximum_amount_in is None:
print("Failed to calculate maximum amount in")
return None
print(f"Amount out: {amount_out} tokens")
print(
f"Maximum amount in: {maximum_amount_in} lamports ({maximum_amount_in / LAMPORTS_PER_SOL:.6f} SOL)"
)
# Step 1: Create Associated Token Account for base token (idempotent)
user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint)
create_ata_ix = create_associated_token_account_idempotent_instruction(
PAYER.pubkey(), PAYER.pubkey(), base_token_mint
)
# Step 2: Create WSOL account with seed
import hashlib
import time
# Generate a unique seed based on timestamp and user pubkey
seed_data = f"{int(time.time())}{PAYER.pubkey()!s}"
wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32]
# Calculate required lamports (maximum_amount_in + small buffer for account creation)
account_creation_lamports = 2_039_280 # Standard account creation cost
total_lamports = maximum_amount_in + account_creation_lamports
user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed(
PAYER.pubkey(), wsol_seed, total_lamports
)
print(f"User base token account: {user_base_token}")
print(f"User quote token account: {user_quote_token}")
# Step 3: Build the buy_exact_out instruction
accounts = [
AccountMeta(
pubkey=PAYER.pubkey(), is_signer=True, is_writable=False
), # payer
AccountMeta(
pubkey=authority, is_signer=False, is_writable=False
), # authority
AccountMeta(
pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False
), # global_config
AccountMeta(
pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False
), # platform_config
AccountMeta(
pubkey=pool_state, is_signer=False, is_writable=True
), # pool_state
AccountMeta(
pubkey=user_base_token, is_signer=False, is_writable=True
), # user_base_token
AccountMeta(
pubkey=user_quote_token, is_signer=False, is_writable=True
), # user_quote_token
AccountMeta(
pubkey=base_vault, is_signer=False, is_writable=True
), # base_vault
AccountMeta(
pubkey=quote_vault, is_signer=False, is_writable=True
), # quote_vault
AccountMeta(
pubkey=base_token_mint, is_signer=False, is_writable=False
), # base_token_mint
AccountMeta(
pubkey=WSOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # base_token_program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # quote_token_program
AccountMeta(
pubkey=event_authority, is_signer=False, is_writable=False
), # event_authority
AccountMeta(
pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False
), # program
]
# Add remaining accounts (not explicitly listed in IDL but required by the program)
# These accounts are used for fee collection during swaps
accounts.append(
AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False)
) # #16: System Program
accounts.append(
AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True)
) # #17: Platform fee vault
accounts.append(
AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True)
) # #18: Creator fee vault
# Instruction data: discriminator + amount_out + maximum_amount_in + share_fee_rate
instruction_data = (
BUY_EXACT_OUT_DISCRIMINATOR
+ struct.pack("<Q", amount_out) # amount_out (u64)
+ struct.pack("<Q", maximum_amount_in) # maximum_amount_in (u64)
+ struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
)
buy_exact_out_ix = Instruction(
program_id=RAYDIUM_LAUNCHLAB_PROGRAM_ID,
data=instruction_data,
accounts=accounts,
)
# Step 4: Create close WSOL account instruction
close_wsol_ix = create_close_account_instruction(
user_quote_token, PAYER.pubkey(), PAYER.pubkey()
)
# Step 5: Build complete transaction
instructions = [
set_compute_unit_price(COMPUTE_UNIT_PRICE),
set_compute_unit_limit(COMPUTE_UNIT_LIMIT),
# Instruction #3: Create Associated Token Account for base token (idempotent)
create_ata_ix,
# Instruction #4: Create WSOL account with seed
create_wsol_ix,
# Instruction #5: Initialize WSOL account
init_wsol_ix,
# Instruction #6: Execute buy_exact_out
buy_exact_out_ix,
# Instruction #7: Close WSOL account
close_wsol_ix,
]
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
message = Message.new_with_blockhash(
instructions, PAYER.pubkey(), recent_blockhash
)
transaction = VersionedTransaction(message, [PAYER])
print("Simulating transaction...")
simulation = await client.simulate_transaction(transaction)
if simulation.value.err:
print(f"Simulation failed: {simulation.value.err}")
return None
print(
f"Simulation successful. Compute units consumed: {simulation.value.units_consumed}"
)
print("Sending transaction...")
result = await client.send_transaction(
transaction,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
tx_signature = result.value
print(f"Transaction sent: https://solscan.io/tx/{tx_signature}")
print("Waiting for confirmation...")
await client.confirm_transaction(tx_signature, commitment="confirmed")
print("Transaction confirmed!")
return tx_signature
except Exception as e:
print(f"Error executing buy_exact_out: {e}")
import traceback
traceback.print_exc()
return None
async def main():
"""
Main function to execute the buy_exact_out example.
Takes configuration from environment variables or uses defaults.
"""
try:
print(f"Starting buy_exact_out for token: {TOKEN_MINT_ADDRESS}")
print(f"Amount to receive: {TOKEN_AMOUNT_TO_RECEIVE:,} tokens")
print(f"Slippage tolerance: {SLIPPAGE_TOLERANCE * 100}%")
print(f"Using RPC endpoint: {RPC_ENDPOINT}")
print()
async with AsyncClient(RPC_ENDPOINT) as client:
balance_resp = await client.get_balance(PAYER.pubkey())
balance_sol = balance_resp.value / LAMPORTS_PER_SOL
print(f"Wallet balance: {balance_sol:.6f} SOL")
tx_signature = await buy_exact_out(
client, TOKEN_MINT_ADDRESS, TOKEN_AMOUNT_TO_RECEIVE, SLIPPAGE_TOLERANCE
)
if tx_signature:
print(f"\n✅ Success! Transaction: {tx_signature}")
print(f"🔗 View on Solscan: https://solscan.io/tx/{tx_signature}")
else:
print("\n❌ Transaction failed!")
except ValueError as e:
print(f"Invalid token mint address: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,754 @@
"""
Manual Sell Exact In Example for Raydium LaunchLab
This script demonstrates how to sell tokens using the sell_exact_in instruction
from the Raydium LaunchLab program. It follows the IDL structure.
Key features:
- Uses sell_exact_in instruction
- Implements proper account ordering as per IDL
- Includes slippage protection with minimum_amount_out
- Handles WSOL wrapping/unwrapping automatically
- Follows the exact transaction structure from the buy_exact_in example
- User configurable token amount and slippage
- Uses idempotent ATA creation
"""
import asyncio
import os
import struct
import sys
import base58
from dotenv import load_dotenv
from idl_parser import load_idl_parser
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
from solders.transaction import VersionedTransaction
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True)
load_dotenv()
TOKEN_MINT_ADDRESS = Pubkey.from_string(
"MYcq5mUyoAtCfyDWYAWvioou3cgnYjnCvFd7U6fspot"
) # Replace with actual token mint address
# Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
# User configurable parameters
TOKEN_AMOUNT_TO_SELL = int(
os.environ.get("TOKEN_AMOUNT", "1000000")
) # Amount of tokens to sell (in base units)
SLIPPAGE_TOLERANCE = float(os.environ.get("SLIPPAGE", "0.25"))
# Transaction parameters
SHARE_FEE_RATE = 0
# Program IDs and addresses from Raydium LaunchLab
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string(
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
)
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string(
"5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA"
)
# Token program and system addresses
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
COMPUTE_BUDGET_PROGRAM_ID = Pubkey.from_string(
"ComputeBudget111111111111111111111111111111"
)
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string(
"SysvarRent111111111111111111111111111111111"
)
# Instruction discriminator for sell_exact_in (from IDL)
SELL_EXACT_IN_DISCRIMINATOR = bytes([149, 39, 222, 155, 211, 124, 152, 26])
# Compute budget settings
COMPUTE_UNIT_LIMIT = 150_000
COMPUTE_UNIT_PRICE = 1_000
LAMPORTS_PER_SOL = 1_000_000_000
def derive_authority_pda() -> Pubkey:
"""
Derive the authority PDA for the Raydium LaunchLab program.
This PDA acts as the authority for pool vault operations and is generated
using the AUTH_SEED as specified in the IDL.
Returns:
Pubkey: The derived authority PDA
"""
AUTH_SEED = b"vault_auth_seed"
authority_pda, _ = Pubkey.find_program_address(
[AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return authority_pda
def derive_event_authority_pda() -> Pubkey:
"""
Derive the event authority PDA for the Raydium LaunchLab program.
This PDA is used for emitting program events during swaps.
Returns:
Pubkey: The derived event authority PDA
"""
EVENT_AUTHORITY_SEED = b"__event_authority"
event_authority_pda, _ = Pubkey.find_program_address(
[EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return event_authority_pda
def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None:
"""
Derive the pool state account for a given base token mint.
Args:
base_token_mint: The token mint address to search for
Returns:
Pubkey of the pool state account, or None if not found
"""
seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)]
pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID)
return pool_state_pda
def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> 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 (WSOL)
Returns:
Pubkey of the creator fee vault
"""
seeds = [bytes(creator), bytes(quote_mint)]
creator_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return creator_fee_vault_pda
def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey:
"""
Derive the platform fee vault PDA.
This vault accumulates platform fees from trades.
Args:
platform_config: The platform config account
quote_mint: The quote token mint (WSOL)
Returns:
Pubkey of the platform fee vault
"""
seeds = [bytes(platform_config), bytes(quote_mint)]
platform_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return platform_fee_vault_pda
def decode_pool_state(account_data: bytes) -> dict | None:
"""
Decode pool state account data using the IDL parser.
Args:
account_data: Raw account data from the pool state account
Returns:
Dictionary containing decoded pool state data, or None if decoding fails
"""
try:
result = IDL_PARSER.decode_account_data(
account_data, "PoolState", skip_discriminator=True
)
if result:
return result
return None
except Exception as e:
print(f"Error decoding pool state: {e}")
import traceback
traceback.print_exc()
return None
async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None:
"""
Get and decode the pool state account data.
Args:
client: Solana RPC client
pool_state: The pool state account address
Returns:
Dictionary containing decoded pool state data, or None if error
"""
try:
account_info = await client.get_account_info(pool_state)
if not account_info.value:
print("Pool state account not found")
return None
return decode_pool_state(account_info.value.data)
except Exception as e:
print(f"Error getting pool state data: {e}")
return None
def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey:
"""
Calculate the associated token account address for a given owner and mint.
This manually implements the ATA derivation without requiring the spl-token package.
Args:
owner: The wallet that owns the token account
mint: The token mint address
Returns:
Pubkey of the associated token account
"""
ata_address, _ = Pubkey.find_program_address(
[bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)],
ASSOCIATED_TOKEN_PROGRAM_ID,
)
return ata_address
def create_associated_token_account_idempotent_instruction(
payer: Pubkey, owner: Pubkey, mint: Pubkey
) -> Instruction:
"""
Create an idempotent instruction to create an Associated Token Account.
This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists.
Args:
payer: The account that will pay for the creation
owner: The owner of the new token account
mint: The token mint
Returns:
Instruction for creating the ATA idempotently
"""
ata_address = get_associated_token_address(owner, mint)
accounts = [
AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account
AccountMeta(
pubkey=ata_address, is_signer=False, is_writable=True
), # Associated token account
AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address
AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint
AccountMeta(
pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False
), # System program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # Token program
]
data = bytes([1])
return Instruction(
program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts
)
def create_initialize_account_instruction(
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=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
]
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
data = bytes([1])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_close_account_instruction(
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),
]
data = bytes([9])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_wsol_account_with_seed(
payer: Pubkey, seed: str, lamports: int
) -> tuple[Pubkey, Instruction, Instruction]:
"""
Create a WSOL account using createAccountWithSeed and initialize it.
This replicates the exact pattern from the Solscan example where a new account
is created with a seed and then initialized as a token account.
Args:
payer: The account that will pay for and own the new account
seed: String seed for deterministic account generation
lamports: Amount of lamports to transfer to the new account
Returns:
Tuple of (new_account_pubkey, create_instruction, initialize_instruction)
"""
new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID)
create_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=payer,
to_pubkey=new_account,
base=payer,
seed=seed,
lamports=lamports,
space=165, # Size of a token account
owner=TOKEN_PROGRAM_ID,
)
)
initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer)
return new_account, create_ix, initialize_ix
def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey:
"""
Get the user's associated token account for the base token.
In a real implementation, this should check if the account exists and create it if needed.
For this example, we'll derive the standard ATA address.
Args:
payer: The user's wallet address
base_mint: The base token mint address
Returns:
Pubkey of the user's base token account
"""
return get_associated_token_address(payer, base_mint)
def calculate_minimum_amount_out_from_pool_state(
pool_state_data: dict, amount_in: int, slippage_tolerance: float
) -> int:
"""
Calculate the minimum amount out based on pool state data and slippage tolerance.
Uses the actual pool reserves to calculate expected output using constant product formula.
This is for selling base tokens to get quote tokens (WSOL).
Args:
pool_state_data: Decoded pool state data containing reserves
amount_in: Amount of base tokens being sold
slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%)
Returns:
Minimum amount of quote tokens (WSOL) to receive
"""
try:
# Extract pool reserves from decoded state
virtual_base = pool_state_data["virtual_base"]
virtual_quote = pool_state_data["virtual_quote"]
real_base = pool_state_data["real_base"]
real_quote = pool_state_data["real_quote"]
print("Pool State:")
print(f" Virtual Base: {virtual_base:,}")
print(f" Virtual Quote: {virtual_quote:,}")
print(f" Real Base: {real_base:,}")
print(f" Real Quote: {real_quote:,}")
# Use virtual reserves for bonding curve calculation
# For selling base tokens: amount_out = (amount_in * virtual_quote) / (virtual_base + amount_in)
# Calculate expected output using constant product formula
# Note: The program deducts fees before calculating output, so we need to account for that
# Trade fee is typically around 0.25% - 1%
# For safety, we'll calculate without fee adjustment and let slippage handle it
numerator = amount_in * virtual_quote
denominator = virtual_base + amount_in
expected_output = numerator // denominator
# Apply slippage tolerance (be more conservative for small amounts)
minimum_with_slippage = int(expected_output * (1 - slippage_tolerance))
print(f"Amount in: {amount_in:,} tokens")
print(
f"Expected output: {expected_output:,} lamports ({expected_output / LAMPORTS_PER_SOL:.6f} SOL)"
)
print(
f"Minimum with {slippage_tolerance * 100}% slippage: {minimum_with_slippage:,} lamports ({minimum_with_slippage / LAMPORTS_PER_SOL:.6f} SOL)"
)
return minimum_with_slippage
except Exception as e:
print(f"Error calculating minimum amount out from pool state: {e}")
return None
async def sell_exact_in(
client: AsyncClient,
base_token_mint: Pubkey,
amount_in_tokens: int,
slippage_tolerance: float,
) -> str | None:
"""
Execute a sell_exact_in transaction on Raydium LaunchLab.
This function implements the exact transaction flow similar to buy_exact_in:
1. SetComputeUnitPrice
2. SetComputeUnitLimit
3. Create WSOL account with seed
4. Initialize WSOL account
5. Execute sell_exact_in instruction
6. Close WSOL account
7. Optional: Transfer remaining SOL (as seen in the example)
Args:
client: Solana RPC client
base_token_mint: Address of the token to sell
amount_in_tokens: Amount of tokens to sell
slippage_tolerance: Slippage tolerance as decimal
Returns:
Transaction signature if successful, None otherwise
"""
try:
print(f"Finding pool state for token: {base_token_mint}")
pool_state = derive_pool_state_for_token(base_token_mint)
if not pool_state:
print("Pool state not found for this token")
return None
# Get and decode pool state data using IDL parser
pool_state_data = await get_pool_state_data(client, pool_state)
if not pool_state_data:
print("Failed to decode pool state data")
return None
# Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects)
base_vault = Pubkey.from_string(pool_state_data["base_vault"])
quote_vault = Pubkey.from_string(pool_state_data["quote_vault"])
creator = Pubkey.from_string(pool_state_data["creator"])
print(f"Found pool state: {pool_state}")
print(f"Base vault: {base_vault}")
print(f"Quote vault: {quote_vault}")
print(f"Creator: {creator}")
print(f"Pool status: {pool_state_data['status']}")
# Derive necessary PDAs
authority = derive_authority_pda()
event_authority = derive_event_authority_pda()
creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT)
platform_fee_vault = derive_platform_fee_vault(
LETSBONK_PLATFORM_CONFIG, WSOL_MINT
)
print(f"Creator fee vault: {creator_fee_vault}")
print(f"Platform fee vault: {platform_fee_vault}")
# Calculate amounts using pool state data
minimum_amount_out = calculate_minimum_amount_out_from_pool_state(
pool_state_data, amount_in_tokens, slippage_tolerance
)
if minimum_amount_out is None or minimum_amount_out == 0:
print("Failed to calculate minimum amount out or amount is too small")
return None
print(f"Amount in: {amount_in_tokens:,} tokens")
print(
f"Minimum amount out: {minimum_amount_out:,} lamports ({minimum_amount_out / LAMPORTS_PER_SOL:.6f} SOL)"
)
# Get user's base token account (where tokens will be debited from)
user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint)
# Step 1: Create WSOL account with seed (where WSOL will be received)
import hashlib
import time
# Generate a unique seed based on timestamp and user pubkey
seed_data = f"{int(time.time())}{PAYER.pubkey()!s}"
wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32]
# Calculate required lamports (minimal amount for account creation)
account_creation_lamports = 2_039_280 # Standard account creation cost
user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed(
PAYER.pubkey(), wsol_seed, account_creation_lamports
)
print(f"User base token account: {user_base_token}")
print(f"User quote token account: {user_quote_token}")
# Step 2: Build the sell_exact_in instruction
accounts = [
AccountMeta(
pubkey=PAYER.pubkey(), is_signer=True, is_writable=False
), # payer
AccountMeta(
pubkey=authority, is_signer=False, is_writable=False
), # authority
AccountMeta(
pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False
), # global_config
AccountMeta(
pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False
), # platform_config
AccountMeta(
pubkey=pool_state, is_signer=False, is_writable=True
), # pool_state
AccountMeta(
pubkey=user_base_token, is_signer=False, is_writable=True
), # user_base_token (tokens being sold)
AccountMeta(
pubkey=user_quote_token, is_signer=False, is_writable=True
), # user_quote_token (WSOL received)
AccountMeta(
pubkey=base_vault, is_signer=False, is_writable=True
), # base_vault (receives tokens)
AccountMeta(
pubkey=quote_vault, is_signer=False, is_writable=True
), # quote_vault (sends WSOL)
AccountMeta(
pubkey=base_token_mint, is_signer=False, is_writable=False
), # base_token_mint
AccountMeta(
pubkey=WSOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # base_token_program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # quote_token_program
AccountMeta(
pubkey=event_authority, is_signer=False, is_writable=False
), # event_authority
AccountMeta(
pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False
), # program
]
# Add remaining accounts (not explicitly listed in IDL but required by the program)
# These accounts are used for fee collection during swaps
accounts.append(
AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False)
) # #16: System Program
accounts.append(
AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True)
) # #17: Platform fee vault
accounts.append(
AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True)
) # #18: Creator fee vault
# Instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
instruction_data = (
SELL_EXACT_IN_DISCRIMINATOR
+ struct.pack("<Q", amount_in_tokens) # amount_in (u64)
+ struct.pack("<Q", minimum_amount_out) # minimum_amount_out (u64)
+ struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
)
sell_exact_in_ix = Instruction(
program_id=RAYDIUM_LAUNCHLAB_PROGRAM_ID,
data=instruction_data,
accounts=accounts,
)
# Step 3: Create close WSOL account instruction
close_wsol_ix = create_close_account_instruction(
user_quote_token, PAYER.pubkey(), PAYER.pubkey()
)
# Step 4: Build complete transaction
instructions = [
set_compute_unit_price(COMPUTE_UNIT_PRICE),
set_compute_unit_limit(COMPUTE_UNIT_LIMIT),
# Instruction #3: Create WSOL account with seed
create_wsol_ix,
# Instruction #4: Initialize WSOL account
init_wsol_ix,
# Instruction #5: Execute sell_exact_in
sell_exact_in_ix,
# Instruction #6: Close WSOL account
close_wsol_ix,
]
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
message = Message.new_with_blockhash(
instructions, PAYER.pubkey(), recent_blockhash
)
transaction = VersionedTransaction(message, [PAYER])
print("Simulating transaction...")
simulation = await client.simulate_transaction(transaction)
if simulation.value.err:
print(f"Simulation failed: {simulation.value.err}")
return None
print(
f"Simulation successful. Compute units consumed: {simulation.value.units_consumed}"
)
print("Sending transaction...")
result = await client.send_transaction(
transaction,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
tx_signature = result.value
print(f"Transaction sent: https://solscan.io/tx/{tx_signature}")
print("Waiting for confirmation...")
await client.confirm_transaction(tx_signature, commitment="confirmed")
print("Transaction confirmed!")
return tx_signature
except Exception as e:
print(f"Error executing sell_exact_in: {e}")
import traceback
traceback.print_exc()
return None
async def main():
"""
Main function to execute the sell_exact_in example.
Takes configuration from environment variables or uses defaults.
"""
try:
print(f"Starting sell_exact_in for token: {TOKEN_MINT_ADDRESS}")
print(f"Amount to sell: {TOKEN_AMOUNT_TO_SELL:,} tokens")
print(f"Slippage tolerance: {SLIPPAGE_TOLERANCE * 100}%")
print(f"Using RPC endpoint: {RPC_ENDPOINT}")
print()
async with AsyncClient(RPC_ENDPOINT) as client:
balance_resp = await client.get_balance(PAYER.pubkey())
balance_sol = balance_resp.value / LAMPORTS_PER_SOL
print(f"Wallet balance: {balance_sol:.6f} SOL")
# Check if user has the base token account and sufficient balance
user_base_token = get_associated_token_address(
PAYER.pubkey(), TOKEN_MINT_ADDRESS
)
try:
token_account_info = await client.get_token_account_balance(
user_base_token
)
if token_account_info.value:
token_balance = int(token_account_info.value.amount)
print(f"Token balance: {token_balance:,} tokens")
if token_balance < TOKEN_AMOUNT_TO_SELL:
print(
f"Insufficient token balance! You have {token_balance:,} tokens but want to sell {TOKEN_AMOUNT_TO_SELL:,}"
)
return
else:
print("Token account not found or has no balance!")
return
except Exception as e:
print(f"Error checking token balance: {e}")
print("Continuing anyway...")
tx_signature = await sell_exact_in(
client, TOKEN_MINT_ADDRESS, TOKEN_AMOUNT_TO_SELL, SLIPPAGE_TOLERANCE
)
if tx_signature:
print(f"\n✅ Success! Transaction: {tx_signature}")
print(f"🔗 View on Solscan: https://solscan.io/tx/{tx_signature}")
else:
print("\n❌ Transaction failed!")
except ValueError as e:
print(f"Invalid token mint address: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,755 @@
"""
Manual Sell Exact Out Example for Raydium LaunchLab
This script demonstrates how to sell tokens using the sell_exact_out instruction
from the Raydium LaunchLab program. It follows the IDL structure.
Key features:
- Uses sell_exact_out instruction
- Implements proper account ordering as per IDL
- Includes slippage protection with maximum_amount_in
- Handles WSOL wrapping/unwrapping automatically
- Follows the exact transaction structure from the sell_exact_in example
- User configurable SOL amount to receive and slippage
- Uses idempotent ATA creation
"""
import asyncio
import os
import struct
import sys
import base58
from dotenv import load_dotenv
from idl_parser import load_idl_parser
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
from solders.transaction import VersionedTransaction
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True)
load_dotenv()
TOKEN_MINT_ADDRESS = Pubkey.from_string(
"MYcq5mUyoAtCfyDWYAWvioou3cgnYjnCvFd7U6fspot"
) # Replace with actual token mint address
# Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
# User configurable parameters
SOL_AMOUNT_TO_RECEIVE = float(
os.environ.get("SOL_AMOUNT", "0.0001")
) # Amount of SOL to receive
SLIPPAGE_TOLERANCE = float(os.environ.get("SLIPPAGE", "0.25"))
# Transaction parameters
SHARE_FEE_RATE = 0
# Program IDs and addresses from Raydium LaunchLab
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string(
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
)
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string(
"5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA"
)
# Token program and system addresses
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
COMPUTE_BUDGET_PROGRAM_ID = Pubkey.from_string(
"ComputeBudget111111111111111111111111111111"
)
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string(
"SysvarRent111111111111111111111111111111111"
)
# Instruction discriminator for sell_exact_out (from IDL)
SELL_EXACT_OUT_DISCRIMINATOR = bytes([95, 200, 71, 34, 8, 9, 11, 166])
# Compute budget settings
COMPUTE_UNIT_LIMIT = 150_000
COMPUTE_UNIT_PRICE = 1_000
LAMPORTS_PER_SOL = 1_000_000_000
def derive_authority_pda() -> Pubkey:
"""
Derive the authority PDA for the Raydium LaunchLab program.
This PDA acts as the authority for pool vault operations and is generated
using the AUTH_SEED as specified in the IDL.
Returns:
Pubkey: The derived authority PDA
"""
AUTH_SEED = b"vault_auth_seed"
authority_pda, _ = Pubkey.find_program_address(
[AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return authority_pda
def derive_event_authority_pda() -> Pubkey:
"""
Derive the event authority PDA for the Raydium LaunchLab program.
This PDA is used for emitting program events during swaps.
Returns:
Pubkey: The derived event authority PDA
"""
EVENT_AUTHORITY_SEED = b"__event_authority"
event_authority_pda, _ = Pubkey.find_program_address(
[EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return event_authority_pda
def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None:
"""
Derive the pool state account for a given base token mint.
Args:
base_token_mint: The token mint address to search for
Returns:
Pubkey of the pool state account, or None if not found
"""
seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)]
pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID)
return pool_state_pda
def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> 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 (WSOL)
Returns:
Pubkey of the creator fee vault
"""
seeds = [bytes(creator), bytes(quote_mint)]
creator_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return creator_fee_vault_pda
def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey:
"""
Derive the platform fee vault PDA.
This vault accumulates platform fees from trades.
Args:
platform_config: The platform config account
quote_mint: The quote token mint (WSOL)
Returns:
Pubkey of the platform fee vault
"""
seeds = [bytes(platform_config), bytes(quote_mint)]
platform_fee_vault_pda, _ = Pubkey.find_program_address(
seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID
)
return platform_fee_vault_pda
def decode_pool_state(account_data: bytes) -> dict | None:
"""
Decode pool state account data using the IDL parser.
Args:
account_data: Raw account data from the pool state account
Returns:
Dictionary containing decoded pool state data, or None if decoding fails
"""
try:
result = IDL_PARSER.decode_account_data(
account_data, "PoolState", skip_discriminator=True
)
if result:
return result
return None
except Exception as e:
print(f"Error decoding pool state: {e}")
import traceback
traceback.print_exc()
return None
async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None:
"""
Get and decode the pool state account data.
Args:
client: Solana RPC client
pool_state: The pool state account address
Returns:
Dictionary containing decoded pool state data, or None if error
"""
try:
account_info = await client.get_account_info(pool_state)
if not account_info.value:
print("Pool state account not found")
return None
return decode_pool_state(account_info.value.data)
except Exception as e:
print(f"Error getting pool state data: {e}")
return None
def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey:
"""
Calculate the associated token account address for a given owner and mint.
This manually implements the ATA derivation without requiring the spl-token package.
Args:
owner: The wallet that owns the token account
mint: The token mint address
Returns:
Pubkey of the associated token account
"""
ata_address, _ = Pubkey.find_program_address(
[bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)],
ASSOCIATED_TOKEN_PROGRAM_ID,
)
return ata_address
def create_associated_token_account_idempotent_instruction(
payer: Pubkey, owner: Pubkey, mint: Pubkey
) -> Instruction:
"""
Create an idempotent instruction to create an Associated Token Account.
This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists.
Args:
payer: The account that will pay for the creation
owner: The owner of the new token account
mint: The token mint
Returns:
Instruction for creating the ATA idempotently
"""
ata_address = get_associated_token_address(owner, mint)
accounts = [
AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account
AccountMeta(
pubkey=ata_address, is_signer=False, is_writable=True
), # Associated token account
AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address
AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint
AccountMeta(
pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False
), # System program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # Token program
]
data = bytes([1])
return Instruction(
program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts
)
def create_initialize_account_instruction(
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=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
]
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
data = bytes([1])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_close_account_instruction(
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),
]
data = bytes([9])
return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts)
def create_wsol_account_with_seed(
payer: Pubkey, seed: str, lamports: int
) -> tuple[Pubkey, Instruction, Instruction]:
"""
Create a WSOL account using createAccountWithSeed and initialize it.
This replicates the exact pattern from the Solscan example where a new account
is created with a seed and then initialized as a token account.
Args:
payer: The account that will pay for and own the new account
seed: String seed for deterministic account generation
lamports: Amount of lamports to transfer to the new account
Returns:
Tuple of (new_account_pubkey, create_instruction, initialize_instruction)
"""
new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID)
create_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=payer,
to_pubkey=new_account,
base=payer,
seed=seed,
lamports=lamports,
space=165, # Size of a token account
owner=TOKEN_PROGRAM_ID,
)
)
initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer)
return new_account, create_ix, initialize_ix
def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey:
"""
Get the user's associated token account for the base token.
In a real implementation, this should check if the account exists and create it if needed.
For this example, we'll derive the standard ATA address.
Args:
payer: The user's wallet address
base_mint: The base token mint address
Returns:
Pubkey of the user's base token account
"""
return get_associated_token_address(payer, base_mint)
def calculate_maximum_amount_in_from_pool_state(
pool_state_data: dict, amount_out: int, slippage_tolerance: float
) -> int:
"""
Calculate the maximum amount in based on pool state data and slippage tolerance.
Uses the actual pool reserves to calculate required input using constant product formula.
This is for selling base tokens to get an exact amount of quote tokens (WSOL).
Args:
pool_state_data: Decoded pool state data containing reserves
amount_out: Amount of quote tokens (WSOL) desired to receive
slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%)
Returns:
Maximum amount of base tokens to sell
"""
try:
# Extract pool reserves from decoded state
virtual_base = pool_state_data["virtual_base"]
virtual_quote = pool_state_data["virtual_quote"]
real_base = pool_state_data["real_base"]
real_quote = pool_state_data["real_quote"]
print("Pool State:")
print(f" Virtual Base: {virtual_base:,}")
print(f" Virtual Quote: {virtual_quote:,}")
print(f" Real Base: {real_base:,}")
print(f" Real Quote: {real_quote:,}")
# Use virtual reserves for bonding curve calculation
# For selling base tokens to get exact quote: amount_in = (amount_out * virtual_base) / (virtual_quote - amount_out)
# This is the inverse of the sell formula
# Calculate required input using constant product formula
numerator = amount_out * virtual_base
denominator = virtual_quote - amount_out
if denominator <= 0:
print("Error: Amount out is too large for current pool state")
return None
expected_input = numerator // denominator
# Apply slippage tolerance (allow selling more tokens than expected)
maximum_with_slippage = int(expected_input * (1 + slippage_tolerance))
print(
f"Amount out: {amount_out:,} lamports ({amount_out / LAMPORTS_PER_SOL:.6f} SOL)"
)
print(f"Expected input: {expected_input:,} tokens")
print(
f"Maximum with {slippage_tolerance * 100}% slippage: {maximum_with_slippage:,} tokens"
)
return maximum_with_slippage
except Exception as e:
print(f"Error calculating maximum amount in from pool state: {e}")
return None
async def sell_exact_out(
client: AsyncClient,
base_token_mint: Pubkey,
amount_out_sol: float,
slippage_tolerance: float,
) -> str | None:
"""
Execute a sell_exact_out transaction on Raydium LaunchLab.
This function implements the exact transaction flow similar to sell_exact_in:
1. SetComputeUnitPrice
2. SetComputeUnitLimit
3. Create WSOL account with seed
4. Initialize WSOL account
5. Execute sell_exact_out instruction
6. Close WSOL account
7. Optional: Transfer remaining SOL (as seen in the example)
Args:
client: Solana RPC client
base_token_mint: Address of the token to sell
amount_out_sol: Exact amount of SOL to receive
slippage_tolerance: Slippage tolerance as decimal
Returns:
Transaction signature if successful, None otherwise
"""
try:
print(f"Finding pool state for token: {base_token_mint}")
pool_state = derive_pool_state_for_token(base_token_mint)
if not pool_state:
print("Pool state not found for this token")
return None
# Get and decode pool state data using IDL parser
pool_state_data = await get_pool_state_data(client, pool_state)
if not pool_state_data:
print("Failed to decode pool state data")
return None
# Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects)
base_vault = Pubkey.from_string(pool_state_data["base_vault"])
quote_vault = Pubkey.from_string(pool_state_data["quote_vault"])
creator = Pubkey.from_string(pool_state_data["creator"])
print(f"Found pool state: {pool_state}")
print(f"Base vault: {base_vault}")
print(f"Quote vault: {quote_vault}")
print(f"Creator: {creator}")
print(f"Pool status: {pool_state_data['status']}")
# Derive necessary PDAs
authority = derive_authority_pda()
event_authority = derive_event_authority_pda()
creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT)
platform_fee_vault = derive_platform_fee_vault(
LETSBONK_PLATFORM_CONFIG, WSOL_MINT
)
print(f"Creator fee vault: {creator_fee_vault}")
print(f"Platform fee vault: {platform_fee_vault}")
# Calculate amounts using pool state data
amount_out = int(amount_out_sol * LAMPORTS_PER_SOL)
maximum_amount_in = calculate_maximum_amount_in_from_pool_state(
pool_state_data, amount_out, slippage_tolerance
)
if maximum_amount_in is None:
print("Failed to calculate maximum amount in")
return None
print(f"Amount out: {amount_out:,} lamports ({amount_out_sol} SOL)")
print(f"Maximum amount in: {maximum_amount_in:,} tokens")
# Get user's base token account (where tokens will be debited from)
user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint)
# Step 1: Create WSOL account with seed (where WSOL will be received)
import hashlib
import time
# Generate a unique seed based on timestamp and user pubkey
seed_data = f"{int(time.time())}{PAYER.pubkey()!s}"
wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32]
# Calculate required lamports (minimal amount for account creation)
account_creation_lamports = 2_039_280 # Standard account creation cost
user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed(
PAYER.pubkey(), wsol_seed, account_creation_lamports
)
print(f"User base token account: {user_base_token}")
print(f"User quote token account: {user_quote_token}")
# Step 2: Build the sell_exact_out instruction
accounts = [
AccountMeta(
pubkey=PAYER.pubkey(), is_signer=True, is_writable=False
), # payer
AccountMeta(
pubkey=authority, is_signer=False, is_writable=False
), # authority
AccountMeta(
pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False
), # global_config
AccountMeta(
pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False
), # platform_config
AccountMeta(
pubkey=pool_state, is_signer=False, is_writable=True
), # pool_state
AccountMeta(
pubkey=user_base_token, is_signer=False, is_writable=True
), # user_base_token (tokens being sold)
AccountMeta(
pubkey=user_quote_token, is_signer=False, is_writable=True
), # user_quote_token (WSOL received)
AccountMeta(
pubkey=base_vault, is_signer=False, is_writable=True
), # base_vault (receives tokens)
AccountMeta(
pubkey=quote_vault, is_signer=False, is_writable=True
), # quote_vault (sends WSOL)
AccountMeta(
pubkey=base_token_mint, is_signer=False, is_writable=False
), # base_token_mint
AccountMeta(
pubkey=WSOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # base_token_program
AccountMeta(
pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False
), # quote_token_program
AccountMeta(
pubkey=event_authority, is_signer=False, is_writable=False
), # event_authority
AccountMeta(
pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False
), # program
]
# Add remaining accounts (not explicitly listed in IDL but required by the program)
# These accounts are used for fee collection during swaps
accounts.append(
AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False)
) # #16: System Program
accounts.append(
AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True)
) # #17: Platform fee vault
accounts.append(
AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True)
) # #18: Creator fee vault
# Instruction data: discriminator + amount_out + maximum_amount_in + share_fee_rate
instruction_data = (
SELL_EXACT_OUT_DISCRIMINATOR
+ struct.pack("<Q", amount_out) # amount_out (u64)
+ struct.pack("<Q", maximum_amount_in) # maximum_amount_in (u64)
+ struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
)
sell_exact_out_ix = Instruction(
program_id=RAYDIUM_LAUNCHLAB_PROGRAM_ID,
data=instruction_data,
accounts=accounts,
)
# Step 3: Create close WSOL account instruction
close_wsol_ix = create_close_account_instruction(
user_quote_token, PAYER.pubkey(), PAYER.pubkey()
)
# Step 4: Build complete transaction
instructions = [
set_compute_unit_price(COMPUTE_UNIT_PRICE),
set_compute_unit_limit(COMPUTE_UNIT_LIMIT),
# Instruction #3: Create WSOL account with seed
create_wsol_ix,
# Instruction #4: Initialize WSOL account
init_wsol_ix,
# Instruction #5: Execute sell_exact_out
sell_exact_out_ix,
# Instruction #6: Close WSOL account
close_wsol_ix,
]
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
message = Message.new_with_blockhash(
instructions, PAYER.pubkey(), recent_blockhash
)
transaction = VersionedTransaction(message, [PAYER])
print("Simulating transaction...")
simulation = await client.simulate_transaction(transaction)
if simulation.value.err:
print(f"Simulation failed: {simulation.value.err}")
return None
print(
f"Simulation successful. Compute units consumed: {simulation.value.units_consumed}"
)
print("Sending transaction...")
result = await client.send_transaction(
transaction,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
tx_signature = result.value
print(f"Transaction sent: https://solscan.io/tx/{tx_signature}")
print("Waiting for confirmation...")
await client.confirm_transaction(tx_signature, commitment="confirmed")
print("Transaction confirmed!")
return tx_signature
except Exception as e:
print(f"Error executing sell_exact_out: {e}")
import traceback
traceback.print_exc()
return None
async def main():
"""
Main function to execute the sell_exact_out example.
Takes configuration from environment variables or uses defaults.
"""
try:
print(f"Starting sell_exact_out for token: {TOKEN_MINT_ADDRESS}")
print(f"Amount to receive: {SOL_AMOUNT_TO_RECEIVE} SOL")
print(f"Slippage tolerance: {SLIPPAGE_TOLERANCE * 100}%")
print(f"Using RPC endpoint: {RPC_ENDPOINT}")
print()
async with AsyncClient(RPC_ENDPOINT) as client:
balance_resp = await client.get_balance(PAYER.pubkey())
balance_sol = balance_resp.value / LAMPORTS_PER_SOL
print(f"Wallet balance: {balance_sol:.6f} SOL")
# Check if user has the base token account and sufficient balance
user_base_token = get_associated_token_address(
PAYER.pubkey(), TOKEN_MINT_ADDRESS
)
try:
token_account_info = await client.get_token_account_balance(
user_base_token
)
if token_account_info.value:
token_balance = int(token_account_info.value.amount)
print(f"Token balance: {token_balance:,} tokens")
# We don't know exactly how many tokens will be needed until we calculate from pool state
# So we'll just warn if balance is very low
if token_balance < 1000:
print(f"Warning: Low token balance ({token_balance:,} tokens)")
else:
print("Token account not found or has no balance!")
return
except Exception as e:
print(f"Error checking token balance: {e}")
print("Continuing anyway...")
tx_signature = await sell_exact_out(
client, TOKEN_MINT_ADDRESS, SOL_AMOUNT_TO_RECEIVE, SLIPPAGE_TOLERANCE
)
if tx_signature:
print(f"\n✅ Success! Transaction: {tx_signature}")
print(f"🔗 View on Solscan: https://solscan.io/tx/{tx_signature}")
else:
print("\n❌ Transaction failed!")
except ValueError as e:
print(f"Invalid token mint address: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())