mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
* fix(cookbook,tools): read GEYSER_AUTH_TYPE and reject an unknown scheme
Four Geyser clients advertised the variable and ignored it, sending Basic for
anything that was not exactly "x-token". Validate against {x-token, basic}.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): detect blockSubscribe creations from logs alone
The envelope fallback cannot see a create reached by CPI, so it covered a
subset of the log route. Envelope decoding stays in the decode/ script.
Also skip meta.err: logs emitted before a failure are kept, and that mint
does not exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): raise when the graduation baseline cannot be read
The watchers fell back to a hardcoded 793,100,000 and still printed it as
"(from Global)". Every progress figure is measured against that baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): report an unsent create flag as unknown, not false
Truncated create_v2 args and legacy create were filled in as False, reading
as a coin the program said is not in mayhem mode. Leave them out, print N/A.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(tools): build Geyser credentials before the reconnect loop
The loop catches every exception and sleeps, so an unusable auth type
reconnected forever instead of reporting a misconfiguration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): treat a short Global buffer as an unreadable baseline
The guard only caught a missing account, so a short read raised struct.error
past the documented ValueError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(tools): surface a listener's exception instead of an idle market
A listener that raised on connect returned nothing, exactly like a quiet
market, in the harness whose job is to certify listeners. Take the listener
list from the factory too, so one added there is not silently skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(tools): simulate the transaction the bot built
The hook rebuilt the transaction itself and dropped the account data size
limit, so the simulation was more permissive than the send. Hook the RPC
client instead, build the client before detection so its blockhash cache is
warm, and report the compute budget read back off the wire.
A listener that raised on connect also read as a quiet market here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(tools): skip failed migrations on err, not on the word "Error"
The notification carries err beside the logs. Scanning the text dropped a
migration whose logs merely said "Error" and kept ones that truly failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): refuse a letsbonk buy that accepts any output
The only one of the four without the guard. Near graduation the floor
division reaches zero, and the buy submits with no slippage protection.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): require the pool's own platform_config
The fallback was a partner-specific address that reverts with
ConstraintAddress (2012). A pool that omits it cannot be traded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): raise when curve data ends before a flag
is_mayhem_mode picks the fee recipient for a buy, and the wrong one reverts
with NotAuthorized (6000), so defaulting it only moves the failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): trade the coin's own PumpSwap pool, not an arbitrary one
Anyone can open a pool against any mint and getProgramAccounts has no defined
order, so [0] was a stranger's pool. Select on coin_creator, which migration
copies from the bonding curve and nothing else sets, and refuse rather than
guess when there is no single match. --pool trades a specific one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
735 lines
26 KiB
Python
735 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")
|
|
|
|
# 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: partner launches use their own, and
|
|
# the wrong one fails the trade with ConstraintAddress (2012). There is
|
|
# no value to fall back to, so a pool that omits it cannot be traded.
|
|
if not pool_state_data.get("platform_config"):
|
|
print("Pool state carries no platform_config; cannot build the trade")
|
|
return None
|
|
platform_config = Pubkey.from_string(pool_state_data["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()
|