""" 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( "YOUR_TOKEN_MINT_ADDRESS_HERE" ) # 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("