Files
pumpfun-bonkfun-bot_github/cookbook/letsbonk/letsbonk_buy_token_exact_in.py
T
ae937fd477 fix(cookbook,tools): fail on an unreadable value instead of substituting a default (#225)
* 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>
2026-09-24 17:49:12 +02:00

722 lines
25 KiB
Python

"""Buy a letsbonk.fun coin with buy_exact_in, spending a fixed amount of SOL.
WARNING: this submits a real transaction and spends real funds.
Usage:
uv run cookbook/letsbonk/letsbonk_buy_token_exact_in.py <MINT> [SOL_TO_SPEND] [--slippage 0.25]
letsbonk.fun runs on Raydium LaunchLab, which quotes in two directions:
*exact in* fixes what you spend, *exact out* fixes what you receive. Slippage
protection is `minimum_amount_out`. WSOL is wrapped and unwrapped around the
trade, and the base-token ATA is created idempotently.
"""
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.001
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 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.
"""
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_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.
Instruction order: compute unit price and limit, idempotent base-token ATA,
WSOL account creation and initialization, the buy, then closing WSOL.
`buy_exact_in` takes 18 accounts: the 15 the IDL lists, plus three 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()
# 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_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
)
# expected_output is a floor division by the quote reserve, so once the
# pool nears graduation an ordinary amount_in floors to zero and the
# buy would submit accepting any output at all.
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} 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=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 = 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 buy_exact_in: {e}")
import traceback
traceback.print_exc()
return None
async def run(token_mint: Pubkey, amount: float, slippage: float) -> None:
"""Execute the buy_exact_in example.
Args:
token_mint: The coin to trade
amount: SOL to spend
slippage: Slippage tolerance
"""
try:
print(f"Starting buy_exact_in for token: {token_mint}")
print(f"Amount to spend: {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")
if (
balance_sol < amount + 0.001
): # Include some buffer for fees
print("Insufficient SOL balance!")
return
tx_signature = await buy_exact_in(
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="Buy a letsbonk.fun coin by spending 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 spend (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()