mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
741 lines
26 KiB
Python
741 lines
26 KiB
Python
"""Sell a letsbonk.fun coin with sell_exact_out, for a fixed amount received.
|
|
|
|
WARNING: this submits a real transaction and spends real funds.
|
|
|
|
Usage:
|
|
uv run cookbook/letsbonk/letsbonk_sell_token_exact_out.py <MINT> [SOL_TO_RECEIVE] [--slippage 0.25]
|
|
|
|
letsbonk.fun runs on Raydium LaunchLab, which quotes in two directions:
|
|
*exact in* fixes what you sell, *exact out* fixes what you receive. Slippage
|
|
protection is `maximum_amount_in`. WSOL is wrapped and unwrapped around the
|
|
trade.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import os
|
|
import struct
|
|
import sys
|
|
from urllib.parse import urlsplit
|
|
|
|
import base58
|
|
from dotenv import load_dotenv
|
|
from letsbonk_idl_parser import load_idl_parser
|
|
from solana.rpc.async_api import AsyncClient
|
|
from solana.rpc.commitment import Confirmed
|
|
from solana.rpc.core import TxOptsModel
|
|
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 MessageV0
|
|
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.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "solana")
|
|
)
|
|
|
|
import solana_transaction_status as tx_status
|
|
|
|
# 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()
|
|
|
|
# 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)
|
|
|
|
# Defaults for the command line below, not fixed settings.
|
|
DEFAULT_AMOUNT = 0.0001
|
|
DEFAULT_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")
|
|
# Fallback only. platform_config is NOT the same for every LaunchLab pool:
|
|
# partner launches use their own, and passing the wrong one fails the buy/sell
|
|
# with ConstraintAddress (2012). The live value is read from the pool state
|
|
# below; this constant is only used if the pool omits it.
|
|
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.
|
|
"""
|
|
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.
|
|
"""
|
|
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, which accumulates creator fees.
|
|
|
|
Args:
|
|
creator: The pool creator's pubkey
|
|
quote_mint: The quote token mint (WSOL)
|
|
"""
|
|
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, which accumulates platform fees.
|
|
|
|
Args:
|
|
quote_mint: The quote token mint (WSOL)
|
|
"""
|
|
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
|
|
|
|
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:
|
|
"""Derive an associated token account address, without spl-token.
|
|
|
|
Args:
|
|
owner: The wallet that owns the token account
|
|
mint: The token mint address
|
|
"""
|
|
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:
|
|
"""Build a CreateIdempotent instruction, which succeeds if the ATA 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
|
|
|
|
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:
|
|
"""Derive the user's associated token account for the base token.
|
|
|
|
Args:
|
|
payer: The user's wallet address
|
|
base_mint: The base token mint address
|
|
"""
|
|
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.
|
|
|
|
Instruction order: compute unit price and limit, WSOL account creation and
|
|
initialization, the sell, then closing WSOL.
|
|
|
|
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()
|
|
# platform_config varies per pool — take the pool's own value.
|
|
platform_config = (
|
|
Pubkey.from_string(pool_state_data["platform_config"])
|
|
if pool_state_data.get("platform_config")
|
|
else LETSBONK_PLATFORM_CONFIG
|
|
)
|
|
creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT)
|
|
platform_fee_vault = derive_platform_fee_vault(
|
|
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=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 = MessageV0.try_compile(
|
|
PAYER.pubkey(), instructions, [], 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}")
|
|
# The error code alone does not say which account or
|
|
# constraint failed; the program logs do.
|
|
for line in simulation.value.logs or []:
|
|
print(f" {line}")
|
|
return None
|
|
|
|
print(
|
|
f"Simulation successful. Compute units consumed: {simulation.value.units_consumed}"
|
|
)
|
|
|
|
print("Sending transaction...")
|
|
result = await client.send_transaction(
|
|
transaction,
|
|
opts=TxOptsModel(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 tx_status.confirm_and_assert(client, tx_signature)
|
|
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 run(token_mint: Pubkey, amount: float, slippage: float) -> None:
|
|
"""Execute the sell_exact_out example.
|
|
|
|
Args:
|
|
token_mint: The coin to trade
|
|
amount: SOL to receive
|
|
slippage: Slippage tolerance
|
|
"""
|
|
try:
|
|
print(f"Starting sell_exact_out for token: {token_mint}")
|
|
print(f"Amount to receive: {amount} SOL")
|
|
print(f"Slippage tolerance: {slippage * 100}%")
|
|
# Endpoint carries an API key. hostname, not netloc: netloc keeps any
|
|
# user:pass@ userinfo, which would leak the credential anyway.
|
|
print(f"Using RPC endpoint: {urlsplit(RPC_ENDPOINT).hostname or '<unset>'}")
|
|
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
|
|
)
|
|
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, amount, slippage
|
|
)
|
|
|
|
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)
|
|
|
|
|
|
def main() -> None:
|
|
"""Parse the command line and run the trade."""
|
|
parser = argparse.ArgumentParser(description="Sell a letsbonk.fun coin for a fixed amount of SOL")
|
|
parser.add_argument("mint", help="The coin's mint address")
|
|
parser.add_argument(
|
|
"amount",
|
|
nargs="?",
|
|
type=float,
|
|
default=DEFAULT_AMOUNT,
|
|
help=f"SOL to receive (default {DEFAULT_AMOUNT})",
|
|
)
|
|
parser.add_argument(
|
|
"--slippage",
|
|
type=float,
|
|
default=DEFAULT_SLIPPAGE,
|
|
help=f"Slippage tolerance (default {DEFAULT_SLIPPAGE})",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
asyncio.run(run(Pubkey.from_string(args.mint), args.amount, args.slippage))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|