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