From d8a0ff7fe5cb9a5a38f3b9ddaca52edafa56fb1b Mon Sep 17 00:00:00 2001
From: Anton Sauchyk <43762166+smypmsa@users.noreply.github.com>
Date: Sun, 4 Oct 2026 22:29:07 +0200
Subject: [PATCH] feat(platforms,cookbook): add StonkFun on a shared Raydium
LaunchLab base (#235)
letsbonk.fun and StonkFun now share one LaunchLab implementation that prices
from real reserves, trades any quote mint, spends exactly the buy amount, and
accounts for the curve fee and Token-2022 transfer fees. Cleanup harvests
withheld fees before closing an account.
Co-authored-by: Claude Opus 5.5
None: + """Parse the command line and read the pool.""" + parser = argparse.ArgumentParser(description="Read a StonkFun coin's pool state") + parser.add_argument("mint", help="The coin's mint address") + args = parser.parse_args() + asyncio.run(read_pool(Pubkey.from_string(args.mint))) + + +if __name__ == "__main__": + main() diff --git a/cookbook/stonkfun/trade/stonkfun_buy_token.py b/cookbook/stonkfun/trade/stonkfun_buy_token.py new file mode 100644 index 0000000..354304a --- /dev/null +++ b/cookbook/stonkfun/trade/stonkfun_buy_token.py @@ -0,0 +1,400 @@ +"""Spend an exact amount of a StonkFun coin's quote asset on it, using buy_exact_in. + +WARNING: this submits a real transaction and spends real funds. + +Usage: + uv run cookbook/stonkfun/trade/stonkfun_buy_token.py+ uv run cookbook/stonkfun/trade/stonkfun_buy_token.py 0.001 --dry-run + +StonkFun coins are Raydium LaunchLab pools, and each one is paired with the +quote asset its launcher picked. AMOUNT is in that asset: 0.001 means 0.001 SOL +on a SOL-paired coin and 0.001 SPCXX on a SpaceX-paired one. For SOL the script +wraps the amount into a temporary account and closes it afterwards. For anything +else you must already hold the asset. + +`buy_exact_in` spends exactly AMOUNT. The curve fee comes out of it first, and on +a *reward* coin the Token-2022 transfer fee then comes out of the tokens on their +way to you. The expected amount below accounts for both, so `--slippage` only +has to cover the price moving. +""" + +import argparse +import asyncio +import os +import secrets +import struct +import sys +from pathlib import Path + +# solana_transaction_status.py lives in cookbook/solana/. +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "solana")) + +import base58 +import solana_transaction_status as tx_status +from dotenv import load_dotenv +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.core import DataSliceOpts, MemcmpOpts, TxOptsModel +from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.message import MessageV0 +from solders.pubkey import Pubkey +from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed +from solders.transaction import VersionedTransaction +from spl.token.instructions import ( + close_account, + create_idempotent_associated_token_account, + get_associated_token_address, + initialize_account, +) +from spl.token.models import CloseAccountParams, InitializeAccountParams + +load_dotenv() + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +PRIVATE_KEY = os.environ.get("SOLANA_PRIVATE_KEY") + +DEFAULT_SLIPPAGE = 0.1 +PRIORITY_FEE_MICROLAMPORTS = 1_000 +COMPUTE_UNIT_LIMIT = 200_000 + +LAUNCHLAB_PROGRAM = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") +STONKFUN_PLATFORM_CONFIGS = { + Pubkey.from_string("4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7"), + Pubkey.from_string("6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt"), +} +TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") +SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") +WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112") + +BUY_EXACT_IN_DISCRIMINATOR = bytes([250, 234, 13, 123, 213, 156, 19, 236]) +# A front end may charge its own share fee on top; this script charges none. +SHARE_FEE_RATE = 0 + +# Rent-exempt minimum for a 165-byte token account. +TOKEN_ACCOUNT_SIZE = 165 +TOKEN_ACCOUNT_RENT = 2_039_280 + +POOL_STATE_SIZE = 429 +POOL_PUBKEYS_OFFSET = 8 + 8 + 5 + 10 * 8 + 5 * 8 +POOL_BASE_MINT_OFFSET = POOL_PUBKEYS_OFFSET + 2 * 32 +FEE_RATE_DENOMINATOR = 1_000_000 +GLOBAL_TRADE_FEE_RATE_OFFSET = 8 + 8 + 1 + 2 + 8 +PLATFORM_FEE_RATE_OFFSET = 8 + 8 + 32 + 32 + 3 * 8 +PLATFORM_CREATOR_FEE_RATE_OFFSET = PLATFORM_FEE_RATE_OFFSET + 8 + 64 + 256 + 256 + 32 +TOKEN_2022_EXTENSIONS_OFFSET = 166 +TRANSFER_FEE_CONFIG_EXTENSION = 1 + + +def decode_pool(data: bytes) -> dict: + """Decode the PoolState fields a trade needs.""" + u64s = struct.unpack_from("<10Q", data, 8 + 8 + 5) + pubkeys = [ + Pubkey.from_bytes( + data[POOL_PUBKEYS_OFFSET + 32 * i : POOL_PUBKEYS_OFFSET + 32 * (i + 1)] + ) + for i in range(7) + ] + return { + "status": data[17], + "base_decimals": data[18], + "quote_decimals": data[19], + "virtual_base": u64s[2], + "virtual_quote": u64s[3], + "real_base": u64s[4], + "real_quote": u64s[5], + "global_config": pubkeys[0], + "platform_config": pubkeys[1], + "base_mint": pubkeys[2], + "quote_mint": pubkeys[3], + "base_vault": pubkeys[4], + "quote_vault": pubkeys[5], + "creator": pubkeys[6], + } + + +def transfer_fee(mint_data: bytes, epoch: int) -> tuple[int, int]: + """The transfer fee in force for `epoch`, as (basis points, max fee raw).""" + offset = TOKEN_2022_EXTENSIONS_OFFSET + while offset + 4 <= len(mint_data): + kind, length = struct.unpack_from(" = newer[0] else older + return active[2], active[1] + if kind == 0 and length == 0: + break + offset += 4 + length + return 0, 0 + + +def expected_tokens_out(pool: dict, amount_in: int, fee_rate: int, tax: tuple) -> int: + """Raw tokens that arrive in your account for `amount_in` raw quote. + + The curve fee is taken from the input, rounded up. The rest trades against + the constant-product curve, then the transfer fee is withheld on delivery. + """ + fee = -(-amount_in * fee_rate // FEE_RATE_DENOMINATOR) + net_in = amount_in - fee + base_reserve = pool["virtual_base"] - pool["real_base"] + quote_reserve = pool["virtual_quote"] + pool["real_quote"] + out = base_reserve * net_in // (quote_reserve + net_in) + bps, max_fee = tax + withheld = min(-(-out * bps // 10_000), max_fee) if bps else 0 + return out - withheld + + +def pda(seeds: list[bytes]) -> Pubkey: + """A LaunchLab program-derived address.""" + return Pubkey.find_program_address(seeds, LAUNCHLAB_PROGRAM)[0] + + +async def find_pool(client: AsyncClient, mint: Pubkey) -> Pubkey: + """Find a coin's pool by its base mint, whatever it is quoted in.""" + response = await client.get_program_accounts( + LAUNCHLAB_PROGRAM, + encoding="base64", + data_slice=DataSliceOpts(offset=0, length=0), + filters=[ + POOL_STATE_SIZE, + MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=str(mint)), + ], + ) + if not response.value: + raise ValueError(f"No LaunchLab pool found for {mint}") + return response.value[0].pubkey + + +def build_buy_exact_in( + pool_address: Pubkey, + pool: dict, + payer: Pubkey, + user_base_account: Pubkey, + user_quote_account: Pubkey, + quote_token_program: Pubkey, + amount_in: int, + minimum_amount_out: int, +) -> Instruction: + """The 15 accounts the IDL names, then three the program reads positionally. + + The fee vaults are keyed by quote mint, so a coin paired with anything but + SOL needs them derived with its own quote, not with wrapped SOL. + """ + quote_mint = pool["quote_mint"] + accounts = [ + AccountMeta(payer, is_signer=True, is_writable=True), + AccountMeta(pda([b"vault_auth_seed"]), is_signer=False, is_writable=False), + AccountMeta(pool["global_config"], is_signer=False, is_writable=False), + AccountMeta(pool["platform_config"], is_signer=False, is_writable=False), + AccountMeta(pool_address, is_signer=False, is_writable=True), + AccountMeta(user_base_account, is_signer=False, is_writable=True), + AccountMeta(user_quote_account, is_signer=False, is_writable=True), + AccountMeta(pool["base_vault"], is_signer=False, is_writable=True), + AccountMeta(pool["quote_vault"], is_signer=False, is_writable=True), + AccountMeta(pool["base_mint"], is_signer=False, is_writable=False), + AccountMeta(quote_mint, is_signer=False, is_writable=False), + AccountMeta(TOKEN_2022_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(quote_token_program, is_signer=False, is_writable=False), + AccountMeta(pda([b"__event_authority"]), is_signer=False, is_writable=False), + AccountMeta(LAUNCHLAB_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pda([bytes(pool["platform_config"]), bytes(quote_mint)]), + is_signer=False, + is_writable=True, + ), + AccountMeta( + pda([bytes(pool["creator"]), bytes(quote_mint)]), + is_signer=False, + is_writable=True, + ), + ] + data = BUY_EXACT_IN_DISCRIMINATOR + struct.pack( + " None: + """Spend exactly `amount` of the coin's quote asset on it.""" + payer = Keypair.from_bytes(base58.b58decode(PRIVATE_KEY)) + user = payer.pubkey() + + async with AsyncClient(RPC_ENDPOINT, commitment=Confirmed) as client: + pool_address = await find_pool(client, mint) + pool = decode_pool((await client.get_account_info(pool_address)).value.data) + if pool["platform_config"] not in STONKFUN_PLATFORM_CONFIGS: + raise ValueError(f"{mint} is a LaunchLab coin, but not a StonkFun one") + if pool["status"] != 0: + raise ValueError(f"{mint} has left the curve (status {pool['status']})") + + quote_mint = pool["quote_mint"] + global_info, platform_info, mint_info, quote_info = ( + await client.get_multiple_accounts( + [pool["global_config"], pool["platform_config"], mint, quote_mint] + ) + ).value + fee_rate = ( + struct.unpack_from(" = {minimum_out / base_unit:,.6f} tokens") + + user_base_account = get_associated_token_address(user, mint, TOKEN_2022_PROGRAM) + instructions = [ + set_compute_unit_limit(COMPUTE_UNIT_LIMIT), + set_compute_unit_price(PRIORITY_FEE_MICROLAMPORTS), + create_idempotent_associated_token_account( + user, user, mint, token_program_id=TOKEN_2022_PROGRAM + ), + ] + + is_sol = quote_mint == WSOL_MINT + if is_sol: + # A throwaway account at a fresh seed, funded with the spend and + # closed in the same transaction. It leaves any WSOL you already + # hold alone. + seed = secrets.token_hex(16) + user_quote_account = Pubkey.create_with_seed(user, seed, TOKEN_PROGRAM) + instructions += [ + create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=user_quote_account, + base=user, + seed=seed, + lamports=amount_in + TOKEN_ACCOUNT_RENT, + space=TOKEN_ACCOUNT_SIZE, + owner=TOKEN_PROGRAM, + ) + ), + initialize_account( + InitializeAccountParams( + program_id=TOKEN_PROGRAM, + account=user_quote_account, + mint=WSOL_MINT, + owner=user, + ) + ), + ] + else: + user_quote_account = get_associated_token_address( + user, quote_mint, quote_token_program + ) + held = 0 + if (await client.get_account_info(user_quote_account)).value is not None: + balance = await client.get_token_account_balance(user_quote_account) + held = int(balance.value.amount) + if held < amount_in: + raise ValueError( + f"Need {amount_in} raw units of {quote_mint}, hold {held}" + ) + + instructions.append( + build_buy_exact_in( + pool_address, + pool, + user, + user_base_account, + user_quote_account, + quote_token_program, + amount_in, + minimum_out, + ) + ) + if is_sol: + instructions.append( + close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM, + account=user_quote_account, + dest=user, + owner=user, + ) + ) + ) + + blockhash = (await client.get_latest_blockhash()).value.blockhash + transaction = VersionedTransaction( + MessageV0.try_compile(user, instructions, [], blockhash), [payer] + ) + + if dry_run: + result = await client.simulate_transaction(transaction) + print(f"\nSimulated: error={result.value.err}") + print(f"Compute units: {result.value.units_consumed}") + if result.value.err: + for line in result.value.logs or []: + print(f" {line}") + return + + signature = ( + await client.send_transaction( + transaction, + opts=TxOptsModel(skip_preflight=True, preflight_commitment=Confirmed), + ) + ).value + print(f"Sent: https://explorer.solana.com/tx/{signature}") + await tx_status.confirm_and_assert(client, signature) + print("Confirmed") + + +def main() -> None: + """Parse the command line and run the buy.""" + parser = argparse.ArgumentParser( + description="Spend an exact amount of a StonkFun coin's quote asset on it" + ) + parser.add_argument("mint", help="The coin's mint address") + parser.add_argument( + "amount", type=float, help="Amount to spend, in the quote asset" + ) + parser.add_argument( + "--slippage", + type=float, + default=DEFAULT_SLIPPAGE, + help=f"How far below the expected token count to accept (default {DEFAULT_SLIPPAGE})", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Simulate the buy instead of submitting it — spends nothing", + ) + args = parser.parse_args() + asyncio.run( + buy( + Pubkey.from_string(args.mint), + args.amount, + args.slippage, + dry_run=args.dry_run, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/cookbook/stonkfun/trade/stonkfun_sell_token.py b/cookbook/stonkfun/trade/stonkfun_sell_token.py new file mode 100644 index 0000000..7331f38 --- /dev/null +++ b/cookbook/stonkfun/trade/stonkfun_sell_token.py @@ -0,0 +1,444 @@ +"""Sell a StonkFun coin back into its pool with sell_exact_in. + +WARNING: this submits a real transaction and spends real funds. + +Usage: + uv run cookbook/stonkfun/trade/stonkfun_sell_token.py+ uv run cookbook/stonkfun/trade/stonkfun_sell_token.py --close + uv run cookbook/stonkfun/trade/stonkfun_sell_token.py --amount 1000 --dry-run + +Sells your whole balance unless `--amount` says otherwise, and pays out in the +pool's quote asset: SOL for a SOL-paired coin, received into a temporary account +and unwrapped in the same transaction; anything else lands in your account for +that asset. + +On a *reward* coin the Token-2022 transfer fee is withheld from the tokens on +their way into the pool, so the pool trades on fewer than you sent, and the +curve fee then comes out of the quote side. The expected payout accounts for +both. + +The same fee was withheld inside *your* account when you bought, and an account +holding withheld fees cannot be closed. `--close` first harvests them into the +mint — an instruction anyone may call — then closes the account and returns its +rent. Without it, a sold-out reward-coin account keeps its rent locked. +""" + +import argparse +import asyncio +import os +import secrets +import struct +import sys +from pathlib import Path + +# solana_transaction_status.py lives in cookbook/solana/. +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "solana")) + +import base58 +import solana_transaction_status as tx_status +from dotenv import load_dotenv +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.core import DataSliceOpts, MemcmpOpts, TxOptsModel +from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.message import MessageV0 +from solders.pubkey import Pubkey +from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed +from solders.transaction import VersionedTransaction +from spl.token.instructions import ( + close_account, + create_idempotent_associated_token_account, + get_associated_token_address, + initialize_account, +) +from spl.token.models import CloseAccountParams, InitializeAccountParams + +load_dotenv() + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +PRIVATE_KEY = os.environ.get("SOLANA_PRIVATE_KEY") + +DEFAULT_SLIPPAGE = 0.1 +PRIORITY_FEE_MICROLAMPORTS = 1_000 +COMPUTE_UNIT_LIMIT = 200_000 + +LAUNCHLAB_PROGRAM = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") +STONKFUN_PLATFORM_CONFIGS = { + Pubkey.from_string("4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7"), + Pubkey.from_string("6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt"), +} +TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") +SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") +WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112") + +SELL_EXACT_IN_DISCRIMINATOR = bytes([149, 39, 222, 155, 211, 124, 152, 26]) +SHARE_FEE_RATE = 0 +# Token-2022: TransferFeeExtension, then HarvestWithheldTokensToMint. +HARVEST_WITHHELD_TO_MINT = bytes([26, 4]) + +TOKEN_ACCOUNT_SIZE = 165 +TOKEN_ACCOUNT_RENT = 2_039_280 + +POOL_STATE_SIZE = 429 +POOL_PUBKEYS_OFFSET = 8 + 8 + 5 + 10 * 8 + 5 * 8 +POOL_BASE_MINT_OFFSET = POOL_PUBKEYS_OFFSET + 2 * 32 +FEE_RATE_DENOMINATOR = 1_000_000 +GLOBAL_TRADE_FEE_RATE_OFFSET = 8 + 8 + 1 + 2 + 8 +PLATFORM_FEE_RATE_OFFSET = 8 + 8 + 32 + 32 + 3 * 8 +PLATFORM_CREATOR_FEE_RATE_OFFSET = PLATFORM_FEE_RATE_OFFSET + 8 + 64 + 256 + 256 + 32 +TOKEN_2022_EXTENSIONS_OFFSET = 166 +TRANSFER_FEE_CONFIG_EXTENSION = 1 + + +def decode_pool(data: bytes) -> dict: + """Decode the PoolState fields a trade needs.""" + u64s = struct.unpack_from("<10Q", data, 8 + 8 + 5) + pubkeys = [ + Pubkey.from_bytes( + data[POOL_PUBKEYS_OFFSET + 32 * i : POOL_PUBKEYS_OFFSET + 32 * (i + 1)] + ) + for i in range(7) + ] + return { + "status": data[17], + "base_decimals": data[18], + "quote_decimals": data[19], + "virtual_base": u64s[2], + "virtual_quote": u64s[3], + "real_base": u64s[4], + "real_quote": u64s[5], + "global_config": pubkeys[0], + "platform_config": pubkeys[1], + "base_mint": pubkeys[2], + "quote_mint": pubkeys[3], + "base_vault": pubkeys[4], + "quote_vault": pubkeys[5], + "creator": pubkeys[6], + } + + +def transfer_fee(mint_data: bytes, epoch: int) -> tuple[int, int] | None: + """The transfer fee in force for `epoch`, as (basis points, max fee raw). + + None when the mint has no TransferFeeConfig extension at all, which is what + decides whether there is anything to harvest. + """ + offset = TOKEN_2022_EXTENSIONS_OFFSET + while offset + 4 <= len(mint_data): + kind, length = struct.unpack_from(" = newer[0] else older + return active[2], active[1] + if kind == 0 and length == 0: + break + offset += 4 + length + return None + + +def expected_quote_out(pool: dict, amount_in: int, fee_rate: int, tax: tuple) -> int: + """Raw quote paid out for selling `amount_in` raw tokens. + + The transfer fee is withheld on the way into the pool, so the curve trades + on what arrives. The curve fee is then taken from the quote side, rounded up. + """ + bps, max_fee = tax + withheld = min(-(-amount_in * bps // 10_000), max_fee) if bps else 0 + arrived = amount_in - withheld + base_reserve = pool["virtual_base"] - pool["real_base"] + quote_reserve = pool["virtual_quote"] + pool["real_quote"] + gross = quote_reserve * arrived // (base_reserve + arrived) + return gross - -(-gross * fee_rate // FEE_RATE_DENOMINATOR) + + +def pda(seeds: list[bytes]) -> Pubkey: + """A LaunchLab program-derived address.""" + return Pubkey.find_program_address(seeds, LAUNCHLAB_PROGRAM)[0] + + +async def find_pool(client: AsyncClient, mint: Pubkey) -> Pubkey: + """Find a coin's pool by its base mint, whatever it is quoted in.""" + response = await client.get_program_accounts( + LAUNCHLAB_PROGRAM, + encoding="base64", + data_slice=DataSliceOpts(offset=0, length=0), + filters=[ + POOL_STATE_SIZE, + MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=str(mint)), + ], + ) + if not response.value: + raise ValueError(f"No LaunchLab pool found for {mint}") + return response.value[0].pubkey + + +def build_sell_exact_in( + pool_address: Pubkey, + pool: dict, + payer: Pubkey, + user_base_account: Pubkey, + user_quote_account: Pubkey, + quote_token_program: Pubkey, + amount_in: int, + minimum_amount_out: int, +) -> Instruction: + """Same 18 accounts as the buy; only the discriminator and the direction differ.""" + quote_mint = pool["quote_mint"] + accounts = [ + AccountMeta(payer, is_signer=True, is_writable=True), + AccountMeta(pda([b"vault_auth_seed"]), is_signer=False, is_writable=False), + AccountMeta(pool["global_config"], is_signer=False, is_writable=False), + AccountMeta(pool["platform_config"], is_signer=False, is_writable=False), + AccountMeta(pool_address, is_signer=False, is_writable=True), + AccountMeta(user_base_account, is_signer=False, is_writable=True), + AccountMeta(user_quote_account, is_signer=False, is_writable=True), + AccountMeta(pool["base_vault"], is_signer=False, is_writable=True), + AccountMeta(pool["quote_vault"], is_signer=False, is_writable=True), + AccountMeta(pool["base_mint"], is_signer=False, is_writable=False), + AccountMeta(quote_mint, is_signer=False, is_writable=False), + AccountMeta(TOKEN_2022_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(quote_token_program, is_signer=False, is_writable=False), + AccountMeta(pda([b"__event_authority"]), is_signer=False, is_writable=False), + AccountMeta(LAUNCHLAB_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pda([bytes(pool["platform_config"]), bytes(quote_mint)]), + is_signer=False, + is_writable=True, + ), + AccountMeta( + pda([bytes(pool["creator"]), bytes(quote_mint)]), + is_signer=False, + is_writable=True, + ), + ] + data = SELL_EXACT_IN_DISCRIMINATOR + struct.pack( + " Instruction: + """Move the fees withheld in `token_account` into the mint. Needs no signer.""" + return Instruction( + TOKEN_2022_PROGRAM, + HARVEST_WITHHELD_TO_MINT, + [ + AccountMeta(mint, is_signer=False, is_writable=True), + AccountMeta(token_account, is_signer=False, is_writable=True), + ], + ) + + +async def sell( + mint: Pubkey, + amount: float | None, + slippage: float, + *, + close: bool, + dry_run: bool, +) -> None: + """Sell `amount` tokens, or the whole balance when it is None.""" + payer = Keypair.from_bytes(base58.b58decode(PRIVATE_KEY)) + user = payer.pubkey() + + async with AsyncClient(RPC_ENDPOINT, commitment=Confirmed) as client: + pool_address = await find_pool(client, mint) + pool = decode_pool((await client.get_account_info(pool_address)).value.data) + if pool["platform_config"] not in STONKFUN_PLATFORM_CONFIGS: + raise ValueError(f"{mint} is a LaunchLab coin, but not a StonkFun one") + if pool["status"] != 0: + raise ValueError(f"{mint} has left the curve (status {pool['status']})") + + user_base_account = get_associated_token_address(user, mint, TOKEN_2022_PROGRAM) + if (await client.get_account_info(user_base_account)).value is None: + raise ValueError(f"You hold no {mint}") + held = int( + (await client.get_token_account_balance(user_base_account)).value.amount + ) + base_unit = 10 ** pool["base_decimals"] + amount_in = held if amount is None else int(amount * base_unit) + if amount_in <= 0 or amount_in > held: + raise ValueError(f"Cannot sell {amount_in} raw; you hold {held}") + if close and amount_in != held: + raise ValueError("--close needs the whole balance sold; drop --amount") + + quote_mint = pool["quote_mint"] + global_info, platform_info, mint_info, quote_info = ( + await client.get_multiple_accounts( + [pool["global_config"], pool["platform_config"], mint, quote_mint] + ) + ).value + fee_rate = ( + struct.unpack_from(" = {minimum_out / quote_unit:.10f} quote") + + instructions = [ + set_compute_unit_limit(COMPUTE_UNIT_LIMIT), + set_compute_unit_price(PRIORITY_FEE_MICROLAMPORTS), + ] + + is_sol = quote_mint == WSOL_MINT + if is_sol: + seed = secrets.token_hex(16) + user_quote_account = Pubkey.create_with_seed(user, seed, TOKEN_PROGRAM) + instructions += [ + create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=user_quote_account, + base=user, + seed=seed, + lamports=TOKEN_ACCOUNT_RENT, + space=TOKEN_ACCOUNT_SIZE, + owner=TOKEN_PROGRAM, + ) + ), + initialize_account( + InitializeAccountParams( + program_id=TOKEN_PROGRAM, + account=user_quote_account, + mint=WSOL_MINT, + owner=user, + ) + ), + ] + else: + user_quote_account = get_associated_token_address( + user, quote_mint, quote_token_program + ) + instructions.append( + create_idempotent_associated_token_account( + user, user, quote_mint, token_program_id=quote_token_program + ) + ) + + instructions.append( + build_sell_exact_in( + pool_address, + pool, + user, + user_base_account, + user_quote_account, + quote_token_program, + amount_in, + minimum_out, + ) + ) + if is_sol: + instructions.append( + close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM, + account=user_quote_account, + dest=user, + owner=user, + ) + ) + ) + if close: + # Harvesting needs the mint to have a transfer fee at all; a + # standard coin has nothing withheld and closes directly. + if tax is not None: + instructions.append(build_harvest_to_mint(mint, user_base_account)) + instructions.append( + close_account( + CloseAccountParams( + program_id=TOKEN_2022_PROGRAM, + account=user_base_account, + dest=user, + owner=user, + ) + ) + ) + + blockhash = (await client.get_latest_blockhash()).value.blockhash + transaction = VersionedTransaction( + MessageV0.try_compile(user, instructions, [], blockhash), [payer] + ) + + if dry_run: + result = await client.simulate_transaction(transaction) + print(f"\nSimulated: error={result.value.err}") + print(f"Compute units: {result.value.units_consumed}") + if result.value.err: + for line in result.value.logs or []: + print(f" {line}") + return + + signature = ( + await client.send_transaction( + transaction, + opts=TxOptsModel(skip_preflight=True, preflight_commitment=Confirmed), + ) + ).value + print(f"Sent: https://explorer.solana.com/tx/{signature}") + await tx_status.confirm_and_assert(client, signature) + print("Confirmed") + + +def main() -> None: + """Parse the command line and run the sell.""" + parser = argparse.ArgumentParser(description="Sell a StonkFun coin into its pool") + parser.add_argument("mint", help="The coin's mint address") + parser.add_argument( + "--amount", + type=float, + default=None, + help="Tokens to sell, in whole tokens (default: your whole balance)", + ) + parser.add_argument( + "--slippage", + type=float, + default=DEFAULT_SLIPPAGE, + help=f"How far below the expected payout to accept (default {DEFAULT_SLIPPAGE})", + ) + parser.add_argument( + "--close", + action="store_true", + help="Harvest withheld fees and close the coin's account after selling all of it", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Simulate the sell instead of submitting it — spends nothing", + ) + args = parser.parse_args() + asyncio.run( + sell( + Pubkey.from_string(args.mint), + args.amount, + args.slippage, + close=args.close, + dry_run=args.dry_run, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/docs/cookbook-conventions.md b/docs/cookbook-conventions.md index b3f2a4f..a458c52 100644 --- a/docs/cookbook-conventions.md +++ b/docs/cookbook-conventions.md @@ -20,7 +20,7 @@ Rules for adding or changing a script under `cookbook/`. - **Files are `_ _ [_ ].py`**, all snake_case. The protocol repeats what the directory says, on purpose — a basename is what shows up in an editor tab, a grep hit or a docs link. - - protocol: `pumpfun`, `pumpswap`, `letsbonk`, `solana`, `anchor` + - protocol: `pumpfun`, `pumpswap`, `letsbonk`, `stonkfun`, `solana`, `anchor` - verb: `buy`, `sell`, `create`, `snipe`, `listen`, `watch`, `read`, `derive`, `decode`, `check`, `capture`, `find` - noun: `token`, `price`, `curve`, `pool`, `balances`, `transaction`, `migrations` @@ -43,7 +43,7 @@ Rules for adding or changing a script under `cookbook/`. so the script dies before printing its own usage. - Don't read config from environment variables either — `.env` is for endpoints and keys, not for trade parameters no usage line mentions. - - `sys.argv` never appears at module level. The seven listeners take no input + - `sys.argv` never appears at module level. The nine listeners take no input and are exempt; they are listed in the verifier. - Fixtures are `raw_ _from_ .json`, next to the script that reads them. - **Cite a URL only after checking it resolves.** diff --git a/docs/launchlab-protocol.md b/docs/launchlab-protocol.md new file mode 100644 index 0000000..2d80db9 --- /dev/null +++ b/docs/launchlab-protocol.md @@ -0,0 +1,87 @@ +# Raydium LaunchLab: letsbonk.fun and StonkFun + +Both platforms are pools on one program, Raydium LaunchLab +(`LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj`). The shared code is +`src/platforms/launchlab/`; `platforms/letsbonk/` and `platforms/stonkfun/` only +name their platform configs. The IDL is `idl/raydium_launchlab_idl.json`, pulled +from the program's on-chain IDL account. + +## Platforms are platform configs + +A launchpad on LaunchLab is the set of `PlatformConfig` accounts its pools are +created under. Every launch and every trade names one, at account 3. + +| Platform | Platform configs | +|---|---| +| letsbonk.fun | `5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA` | +| StonkFun, standard | `4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7` | +| StonkFun, reward | `6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt` | + +The geyser and blocks listeners subscribe on these accounts, not on the program +(`EventParser.get_stream_filter_accounts`), so other launchpads on LaunchLab stay +out of the stream. Two platforms on one program also mean nothing may key a +lookup by program id alone. + +## Launches + +StonkFun launches are `initialize_with_token_2022`, as legacy, v0 and v1 +transactions, sometimes issued by a router as an inner instruction. Read +accounts by IDL name: `payer` is 0 and `creator` is 1, and the pool's `creator` +— which keys the creator fee vault — is the `creator` account, not the payer. +The quote mint is account 7 and its token program account 11. The transfer fee +is the `transfer_fee_extension_param` argument. + +Everything a buy needs is in the instruction, so the parsers set +`state_from_event`. The program's `PoolCreateEvent` carries no base mint, quote +mint or platform config, which is why there is no logs listener for LaunchLab. + +## Trades + +`buy_exact_in` and `sell_exact_in` take the IDL's 15 accounts, then three the +program reads positionally: the system program, the platform fee vault +`PDA([platform_config, quote_mint])` and the creator fee vault +`PDA([creator, quote_mint])`. The pool is `PDA(["pool", base_mint, quote_mint])`. +Every one of these is keyed by the pool's quote mint. + +- **`buy_exact_in` spends all of `amount_in`.** The trader hands an exact-in + builder the configured amount, unpadded (`spends_exact_amount_in`). +- **Price is `(virtual_quote + real_quote) / (virtual_base - real_base)`.** The + virtual reserves are fixed at launch. Pricing off them alone returns the + launch price forever, and take-profit and stop-loss never fire. +- **Fees:** `trade_fee_rate` (GlobalConfig) + `fee_rate` + `creator_fee_rate` + (PlatformConfig), in parts per million, read live by the program. A buy pays + them from its input and a sell from its output, rounded up. Curve outputs + round down. `platforms/launchlab/curve_math.py` reproduces both to the unit. + +## Quote assets + +The launcher picks the quote: SOL, an xStock, another coin. A SOL-quoted trade +settles through a throwaway wrapped-SOL account opened and closed in the same +transaction. Any other quote is paid from, and paid into, the wallet's own +account for that mint. The bot never buys a quote asset, so the balance must +already be there, and a sell leaves the proceeds in that asset. The quote's token +program comes from the launch instruction. xStocks are Token-2022 mints. + +xStocks carry a pause switch, a permanent delegate, a scaled UI amount and a +transfer-hook extension with no program set. If the issuer sets a hook program, +every transfer needs that program's extra accounts, and the builder does not +pass them. + +## Transfer fees + +StonkFun reward coins are Token-2022 mints with a 1% or 3% transfer fee. + +- A buy delivers its output less the fee, and **`minimum_amount_out` is checked + against what arrives**. A floor computed without the fee reverts `6004`. +- A sell sends its amount and the pool trades on what arrives, less the fee. +- The fee is withheld **inside the receiving account**: the bot's own account, + on a buy. Token-2022 refuses to close an account holding withheld fees + (`0x23`), so cleanup runs the permissionless `HarvestWithheldTokensToMint` + first. Standard coins' mints have no transfer-fee extension, and harvesting + against one fails, so only harvest when the account holds withheld fees. + +## Verifiers + +`verify_launchlab_trades`, `verify_launchlab_launch_parsing` and +`verify_cleanup_harvests_withheld_fees` cover all of the above against captured +mainnet transactions. diff --git a/docs/regression-tests.md b/docs/regression-tests.md index f0e74f7..86e2504 100644 --- a/docs/regression-tests.md +++ b/docs/regression-tests.md @@ -40,8 +40,11 @@ individual scripts for a subset. | `verify_cookbook_arguments.py` | every cookbook script takes its input as a command-line argument | | `verify_documentation_links.py` | no known-dead URL is back; `--live` fetches every one and fails on 4xx/5xx | | `verify_no_rpc_credentials_logged.py` | credentials masked in every log record, including a URL passed as a non-`str` argument, and every site that installs a root handler installs the redaction first | -| `verify_pumpfun_prices_without_hardcoded_sol_unit.py` | nothing in `core/`, `trading/` or `platforms/pumpfun/` scales an amount by `LAMPORTS_PER_SOL`; the letsbonk sites left out of scope are still the documented ones | +| `verify_prices_without_hardcoded_sol_unit.py` | nothing in `core/`, `trading/` or `platforms/` scales an amount by `LAMPORTS_PER_SOL` | | `verify_pumpswap_account_layout.py` | pump-amm's `pool-v2` account is gated on `coin_creator`, the buyback pair stays last, and base-token decimals are resolved rather than assumed; `--live` re-reads the authorized recipients from `GlobalConfig` | +| `verify_launchlab_trades.py` | against real StonkFun trades, the LaunchLab curve math reproduces each buy and sell to the unit — fee rate read from the configs, transfer fee from the mint; price comes from virtual plus real reserves; the swap names the on-chain 18 accounts with fee vaults keyed by the pool's quote mint; an exact-in buy gets the configured amount unpadded; buy and sell floors are net of curve fee and transfer fee; single-token mode waits past a coin its filters refuse instead of exiting on it | +| `verify_launchlab_launch_parsing.py` | legacy, v0 and v1 StonkFun launches parse from every account slot over blockSubscribe and geyser alike, lookup-table accounts included; the creator slot wins over the payer; a router-issued launch is found; letsbonk and StonkFun each keep to their own platform configs; LaunchLab listeners subscribe on platform configs, pump.fun on its program | +| `verify_cleanup_harvests_withheld_fees.py` | cleanup harvests withheld Token-2022 transfer fees into the mint before closing a token account, and sends a bare close when nothing is withheld | Two mainnet simulations, also no funds moved: diff --git a/idl/raydium_launchlab_idl.json b/idl/raydium_launchlab_idl.json index 70c90c5..6f2d807 100644 --- a/idl/raydium_launchlab_idl.json +++ b/idl/raydium_launchlab_idl.json @@ -36,6 +36,7 @@ "The user performing the swap operation", "Must sign the transaction and pay for fees" ], + "writable": true, "signer": true }, { @@ -133,50 +134,24 @@ { "name": "quote_token_mint", "docs": [ - "The mint of the quote token" + "The mint of the quote token", + "Used for transfer fee calculations if applicable" ] }, { "name": "base_token_program", "docs": [ - "SPL Token program for base token transfers" + "Token program owning the base token: either SPL Token or Token-2022" ] }, { "name": "quote_token_program", "docs": [ - "SPL Token program for quote token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -225,6 +200,7 @@ "The user performing the swap operation", "Must sign the transaction and pay for fees" ], + "writable": true, "signer": true }, { @@ -322,50 +298,24 @@ { "name": "quote_token_mint", "docs": [ - "The mint of the quote token" + "The mint of the quote token", + "Used for transfer fee calculations if applicable" ] }, { "name": "base_token_program", "docs": [ - "SPL Token program for base token transfers" + "Token program owning the base token: either SPL Token or Token-2022" ] }, { "name": "quote_token_program", "docs": [ - "SPL Token program for quote token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -538,9 +488,8 @@ { "name": "token_program", "docs": [ - "SPL Token program for the quote token" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { "name": "system_program", @@ -705,9 +654,8 @@ { "name": "token_program", "docs": [ - "SPL program for input token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { "name": "system_program", @@ -888,9 +836,8 @@ { "name": "token_program", "docs": [ - "SPL program for input token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { "name": "system_program", @@ -1084,10 +1031,9 @@ { "name": "base_token_program", "docs": [ - "SPL Token program for the base token", - "Must be the standard Token program" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "SPL Token program or Token-2022 program for the base token", + "Bound to the base mint's owner via `base_token_mint.mint::token_program`" + ] }, { "name": "system_program", @@ -1106,6 +1052,263 @@ ], "args": [] }, + { + "name": "close_platform_allow_config", + "docs": [ + "Stop allowing the global config to be used to create pools with the platform config", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 82, + 205, + 36, + 216, + 16, + 6, + 240, + 215 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The owner of the platform config, it receives the rent of the closed account" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the global config was allowed for" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "The global config the platform no longer allows" + ] + }, + { + "name": "platform_allow_config", + "docs": [ + "The account that records the allowed global config" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 97, + 108, + 108, + 111, + 119, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + } + ], + "args": [] + }, + { + "name": "close_platform_curve_rule", + "docs": [ + "Close the launch param rule of the global config, the platform then no longer", + "restricts the launch params of the global config", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 194, + 114, + 82, + 45, + 136, + 88, + 169, + 159 + ], + "accounts": [ + { + "name": "curve_rule_authority", + "docs": [ + "The curve rule manager of the platform config or the platform admin itself,", + "it receives the rent of the closed account, see PlatformConfig::curve_rule_manager" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The launch param rule to be closed" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + } + ], + "args": [] + }, + { + "name": "collect_excess_lamports", + "docs": [ + "Collect excess lamports, including the token accounts and mints owned by the", + "program authorities and the program pda accounts.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 28, + 189, + 19, + 40, + 164, + 176, + 118, + 17 + ], + "accounts": [ + { + "name": "collect_lamports_wallet", + "docs": [ + "Only admin or collect_lamports can collect lamports" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "authority, the platform fee vault authority or the creator fee vault authority" + ] + }, + { + "name": "token_program", + "docs": [ + "The SPL program to perform token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "token_program_2022", + "docs": [ + "The SPL program 2022 to perform token transfers" + ], + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "args": [] + }, { "name": "collect_fee", "docs": [ @@ -1196,9 +1399,8 @@ { "name": "token_program", "docs": [ - "SPL program for input token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] } ], "args": [] @@ -1293,9 +1495,8 @@ { "name": "token_program", "docs": [ - "SPL program for input token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] } ], "args": [] @@ -1327,12 +1528,11 @@ "name": "owner", "docs": [ "The protocol owner/admin account", - "Must match the predefined admin address", + "Must match the predefined admin address or the create config authority", "Has authority to create and modify protocol configurations" ], "writable": true, - "signer": true, - "address": "GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ" + "signer": true }, { "name": "global_config", @@ -1381,7 +1581,8 @@ "name": "quote_token_mint", "docs": [ "The mint address of the quote token (token used for buying)", - "This will be the standard token used for all pools with this config" + "This will be the standard token used for all pools with this config", + "May be owned by either the SPL Token program or the Token-2022 program" ] }, { @@ -1435,6 +1636,130 @@ } ] }, + { + "name": "create_platform_allow_config", + "docs": [ + "Allow the global config to be used to create pools with the platform config,", + "it takes effect only when platform_config.restrict_global_config is 1", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 69, + 71, + 168, + 7, + 214, + 250, + 107, + 102 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The owner of the platform config, it pays for the initialization costs" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the global config is allowed for" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "The global config the platform allows to create pools with" + ] + }, + { + "name": "platform_allow_config", + "docs": [ + "The account that records the allowed global config" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 97, + 108, + 108, + 111, + 119, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, { "name": "create_platform_config", "docs": [ @@ -1521,6 +1846,9 @@ }, { "name": "transfer_fee_extension_authority" + }, + { + "name": "platform_vesting_wallet" } ], "args": [ @@ -1534,6 +1862,202 @@ } ] }, + { + "name": "create_platform_curve_rule", + "docs": [ + "Create an empty launch param rule for the global config, the check groups are", + "added with the `update_platform_curve_rule` instruction. It is signed by the", + "curve rule manager wallet or by the platform admin.", + "It takes effect only when platform_config.restrict_curve_param is 1", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 148, + 71, + 180, + 136, + 122, + 144, + 59, + 21 + ], + "accounts": [ + { + "name": "curve_rule_authority", + "docs": [ + "The curve rule manager of the platform config or the platform admin itself,", + "it pays for the initialization costs, see PlatformConfig::curve_rule_manager" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The account that records the launch param check groups of the global config" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "create_platform_vesting_account", + "docs": [ + "Create vesting account", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `share` - The share amount of base token to be vested", + "" + ], + "discriminator": [ + 146, + 71, + 173, + 69, + 98, + 19, + 15, + 106 + ], + "accounts": [ + { + "name": "platform_vesting_wallet", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "beneficiary", + "docs": [ + "The beneficiary is used to receive the allocated linear release of tokens.", + "Once this account is set, it cannot be modified, so please ensure the validity of this account,", + "otherwise, the unlocked tokens will not be claimable." + ], + "writable": true + }, + { + "name": "platform_config", + "docs": [ + "Platform config account to be changed" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account" + ], + "writable": true + }, + { + "name": "platform_vesting_record", + "docs": [ + "The vesting record account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 101, + 115, + 116, + 105, + 110, + 103 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "beneficiary" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, { "name": "create_vesting_account", "docs": [ @@ -1634,7 +2158,7 @@ { "name": "initialize", "docs": [ - "Initializes a new trading pool", + "Deprecated, it always fails with `NotApproved`, use `initialize_v2`", "# Arguments", "", "* `ctx` - The context of accounts containing pool and token information", @@ -1869,33 +2393,7 @@ "address": "SysvarRent111111111111111111111111111111111" }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -1903,7 +2401,7 @@ ], "args": [ { - "name": "base_mint_param", + "name": "_base_mint_param", "type": { "defined": { "name": "MintParams" @@ -1911,7 +2409,7 @@ } }, { - "name": "curve_param", + "name": "_curve_param", "type": { "defined": { "name": "CurveParams" @@ -1919,7 +2417,7 @@ } }, { - "name": "vesting_param", + "name": "_vesting_param", "type": { "defined": { "name": "VestingParams" @@ -1931,7 +2429,8 @@ { "name": "initialize_v2", "docs": [ - "Initializes a new trading pool", + "Initializes a new trading pool,", + "pool created by this instruction must be migrated to cpswap after fundraising ends, i.e., curve_param.migrate_type = 1", "# Arguments", "", "* `ctx` - The context of accounts containing pool and token information", @@ -2139,9 +2638,8 @@ { "name": "quote_token_program", "docs": [ - "SPL Token program for the quote token" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { "name": "metadata_program", @@ -2166,33 +2664,7 @@ "address": "SysvarRent111111111111111111111111111111111" }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -2436,9 +2908,8 @@ { "name": "quote_token_program", "docs": [ - "SPL Token program for the quote token" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { "name": "system_program", @@ -2448,33 +2919,7 @@ "address": "11111111111111111111111111111111" }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -2565,10 +3010,6 @@ "The mint for the quote token (token used to buy)" ] }, - { - "name": "openbook_program", - "address": "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX" - }, { "name": "market", "docs": [ @@ -2576,51 +3017,6 @@ ], "writable": true }, - { - "name": "request_queue", - "docs": [ - "Account created and asigned to openbook_program but not been initialized" - ], - "writable": true - }, - { - "name": "event_queue", - "docs": [ - "Account created and asigned to openbook_program but not been initialized" - ], - "writable": true - }, - { - "name": "bids", - "docs": [ - "Account created and asigned to openbook_program but not been initialized" - ], - "writable": true - }, - { - "name": "asks", - "docs": [ - "Account created and asigned to openbook_program but not been initialized" - ], - "writable": true - }, - { - "name": "market_vault_signer" - }, - { - "name": "market_base_vault", - "docs": [ - "Token account that holds the market's base tokens" - ], - "writable": true - }, - { - "name": "market_quote_vault", - "docs": [ - "Token account that holds the market's quote tokens" - ], - "writable": true - }, { "name": "amm_program", "address": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" @@ -2698,57 +3094,6 @@ } } }, - { - "name": "amm_open_orders", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "account", - "path": "amm_program" - }, - { - "kind": "account", - "path": "market" - }, - { - "kind": "const", - "value": [ - 111, - 112, - 101, - 110, - 95, - 111, - 114, - 100, - 101, - 114, - 95, - 97, - 115, - 115, - 111, - 99, - 105, - 97, - 116, - 101, - 100, - 95, - 115, - 101, - 101, - 100 - ] - } - ], - "program": { - "kind": "account", - "path": "amm_program" - } - } - }, { "name": "amm_lp_mint", "writable": true, @@ -3104,20 +3449,7 @@ "address": "SysvarRent111111111111111111111111111111111" } ], - "args": [ - { - "name": "base_lot_size", - "type": "u64" - }, - { - "name": "quote_lot_size", - "type": "u64" - }, - { - "name": "market_vault_signer_nonce", - "type": "u8" - } - ] + "args": [] }, { "name": "migrate_to_cpswap", @@ -3150,14 +3482,16 @@ { "name": "base_mint", "docs": [ - "The mint for the base token (token being sold)" + "The mint for the base token (token being sold)", + "May be owned by either token program, the instruction picks the matching one" ], "writable": true }, { "name": "quote_mint", "docs": [ - "The mint for the quote token (token used to buy)" + "The mint for the quote token (token used to buy)", + "May be owned by either token program, the instruction picks the matching one" ] }, { @@ -3506,19 +3840,22 @@ "writable": true }, { - "name": "base_token_program", + "name": "token_program", "docs": [ - "SPL Token program for the base token", - "Must be the standard Token program" - ] - }, - { - "name": "quote_token_program", - "docs": [ - "SPL Token program for the quote token" + "The legacy SPL Token program.", + "Always required: the cpswap lp mint and the locked liquidity fee NFT live on it, and it", + "serves the base or quote token whenever that mint is a legacy mint." ], "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, + { + "name": "token_program_2022", + "docs": [ + "The Token-2022 program, serving the base or quote token when that mint is a", + "Token-2022 mint. Always required, even when neither mint uses it." + ], + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, { "name": "associated_token_program", "docs": [ @@ -3551,37 +3888,53 @@ "args": [] }, { - "name": "remove_platform_curve_param", + "name": "remove_platform_curve_rule", "docs": [ - "Remove platform launch param", + "Remove one check group of the launch param rule", "# Arguments", "", "* `ctx` - The context of accounts", - "* `index` - The index of the curve param to remove", + "* `group_id` - The id of the group to remove", "" ], "discriminator": [ - 27, - 30, - 62, - 169, - 93, - 224, - 24, - 145 + 236, + 77, + 154, + 138, + 32, + 145, + 127, + 219 ], "accounts": [ { - "name": "platform_admin", + "name": "curve_rule_authority", "docs": [ - "The account paying for the initialization costs" + "The curve rule manager of the platform config or the platform admin itself,", + "it receives the rent the account shrinks by,", + "see PlatformConfig::curve_rule_manager" ], + "writable": true, "signer": true }, { "name": "platform_config", "docs": [ - "Platform config account to be changed" + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The launch param rule to be changed" ], "writable": true, "pda": { @@ -3599,25 +3952,40 @@ 109, 95, 99, - 111, - 110, - 102, - 105, - 103 + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 ] }, { "kind": "account", - "path": "platform_admin" + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" } ] } + }, + { + "name": "system_program", + "docs": [ + "Required to settle the rent of the resized account" + ], + "address": "11111111111111111111111111111111" } ], "args": [ { - "name": "index", - "type": "u8" + "name": "group_id", + "type": "u16" } ] }, @@ -3650,6 +4018,7 @@ "The user performing the swap operation", "Must sign the transaction and pay for fees" ], + "writable": true, "signer": true }, { @@ -3747,50 +4116,24 @@ { "name": "quote_token_mint", "docs": [ - "The mint of the quote token" + "The mint of the quote token", + "Used for transfer fee calculations if applicable" ] }, { "name": "base_token_program", "docs": [ - "SPL Token program for base token transfers" + "Token program owning the base token: either SPL Token or Token-2022" ] }, { "name": "quote_token_program", "docs": [ - "SPL Token program for quote token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -3840,6 +4183,7 @@ "The user performing the swap operation", "Must sign the transaction and pay for fees" ], + "writable": true, "signer": true }, { @@ -3937,50 +4281,24 @@ { "name": "quote_token_mint", "docs": [ - "The mint of the quote token" + "The mint of the quote token", + "Used for transfer fee calculations if applicable" ] }, { "name": "base_token_program", "docs": [ - "SPL Token program for base token transfers" + "Token program owning the base token: either SPL Token or Token-2022" ] }, { "name": "quote_token_program", "docs": [ - "SPL Token program for quote token transfers" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "Token program owning the quote token: either SPL Token or Token-2022" + ] }, { - "name": "event_authority", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 - ] - } - ] - } + "name": "event_authority" }, { "name": "program" @@ -4030,8 +4348,7 @@ "docs": [ "The global config owner or admin" ], - "signer": true, - "address": "GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ" + "signer": true }, { "name": "global_config", @@ -4128,30 +4445,35 @@ ] }, { - "name": "update_platform_curve_param", + "name": "update_platform_curve_rule", "docs": [ - "Update platform launch param", + "Add or replace one check group of the launch param rule.", + "The constraints of a group must all hold together, a launch is allowed when it", + "satisfies any group, a group with no constraint allows everything.", "# Arguments", "", "* `ctx` - The context of accounts", - "* `bonding_curve_param` - Parameter to update", + "* `group_id` - The stable id of the group, an existing group with the id is replaced", + "* `constraints` - The constraints of the group, see `param_field` and `param_op`", "" ], "discriminator": [ - 138, - 144, - 138, - 250, - 220, - 128, - 4, - 57 + 90, + 100, + 206, + 196, + 54, + 117, + 120, + 139 ], "accounts": [ { - "name": "platform_admin", + "name": "curve_rule_authority", "docs": [ - "The account paying for the initialization costs" + "The curve rule manager of the platform config or the platform admin itself,", + "it pays the rent the account grows by, and receives the rent it shrinks by,", + "see PlatformConfig::curve_rule_manager" ], "writable": true, "signer": true @@ -4159,7 +4481,20 @@ { "name": "platform_config", "docs": [ - "Platform config account to be changed" + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The launch param rule to be changed" ], "writable": true, "pda": { @@ -4177,45 +4512,48 @@ 109, 95, 99, - 111, - 110, - 102, - 105, - 103 + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 ] }, { "kind": "account", - "path": "platform_admin" + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" } ] } }, - { - "name": "global_config", - "docs": [ - "Global configuration account containing protocol-wide settings", - "Includes settings like quote token mint and fee parameters" - ] - }, { "name": "system_program", "docs": [ - "System program for lamport transfers" + "Required to settle the rent of the resized account" ], "address": "11111111111111111111111111111111" } ], "args": [ { - "name": "index", - "type": "u8" + "name": "group_id", + "type": "u16" }, { - "name": "bonding_curve_param", + "name": "constraints", "type": { - "defined": { - "name": "BondingCurveParam" + "vec": { + "defined": { + "name": "ParamConstraint" + } } } } @@ -4236,6 +4574,19 @@ 217 ] }, + { + "name": "PlatformAllowConfig", + "discriminator": [ + 248, + 57, + 34, + 138, + 222, + 238, + 186, + 75 + ] + }, { "name": "PlatformConfig", "discriminator": [ @@ -4249,6 +4600,19 @@ 160 ] }, + { + "name": "PlatformCurveRule", + "discriminator": [ + 12, + 20, + 122, + 169, + 247, + 155, + 104, + 234 + ] + }, { "name": "PoolState", "discriminator": [ @@ -4435,6 +4799,61 @@ "code": 6020, "name": "CurveParamIsNotExist", "msg": "Curve param is not exist" + }, + { + "code": 6021, + "name": "InvalidTotalLockedAmount", + "msg": "Total locked amount must great or equal to the platform vesting share amount" + }, + { + "code": 6022, + "name": "InvalidPlatformAllowConfig", + "msg": "Invalid platform allow config account" + }, + { + "code": 6023, + "name": "CalculateOverflow", + "msg": "Calculation overflowed" + }, + { + "code": 6024, + "name": "InvalidPlatformCurveRule", + "msg": "Invalid platform curve rule account" + }, + { + "code": 6025, + "name": "CurveParamNotMatchPlatformRule", + "msg": "The curve param does not match any check group of the platform curve rule" + }, + { + "code": 6026, + "name": "CurveRuleGroupNotExist", + "msg": "The curve rule group is not exist" + }, + { + "code": 6027, + "name": "CurveRuleGroupsExceeded", + "msg": "The number of curve rule groups exceeds the limit" + }, + { + "code": 6028, + "name": "InvalidCurveRuleConstraint", + "msg": "The curve rule constraint is invalid" + }, + { + "code": 6029, + "name": "InvalidCurveRuleAuthority", + "msg": "The signer is neither the curve rule manager nor the platform admin" + }, + { + "code": 6030, + "name": "CurveRuleFieldNotSupportedByCurve", + "msg": "The curve rule field is not supported by the curve of the global config" + }, + { + "code": 6031, + "name": "LamportsCalculateError", + "msg": "Lamports calculate error" } ], "types": [ @@ -4455,78 +4874,6 @@ ] } }, - { - "name": "BondingCurveParam", - "type": { - "kind": "struct", - "fields": [ - { - "name": "migrate_type", - "docs": [ - "Migrate to AMM or CpSwap, 0: amm, 1: cpswap,", - "Neither 0 nor 1: invalid" - ], - "type": "u8" - }, - { - "name": "migrate_cpmm_fee_on", - "docs": [ - "The migrate fee on, 0 means fee on the quote token, 1 means fee on both token", - "Neither 0 nor 1: invalid" - ], - "type": "u8" - }, - { - "name": "supply", - "docs": [ - "The supply of the token,", - "0: invalid" - ], - "type": "u64" - }, - { - "name": "total_base_sell", - "docs": [ - "The total base sell of the token", - "0: invalid" - ], - "type": "u64" - }, - { - "name": "total_quote_fund_raising", - "docs": [ - "The total quote fund raising of the token", - "0: invalid" - ], - "type": "u64" - }, - { - "name": "total_locked_amount", - "docs": [ - "total amount of tokens to be unlocked", - "u64::MAX: invalid" - ], - "type": "u64" - }, - { - "name": "cliff_period", - "docs": [ - "Waiting time in seconds before unlocking after fundraising ends", - "u64::MAX: invalid" - ], - "type": "u64" - }, - { - "name": "unlock_period", - "docs": [ - "Unlocking period in seconds", - "u64::MAX: invalid" - ], - "type": "u64" - } - ] - } - }, { "name": "ClaimVestedEvent", "docs": [ @@ -4644,6 +4991,45 @@ ] } }, + { + "name": "CurveRuleGroup", + "docs": [ + "A set of constraints that must all hold together, e.g.", + "`supply = 50W && total_base_sell >= 20W`." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "group_id", + "docs": [ + "The stable id of the group, used to update or remove it" + ], + "type": "u16" + }, + { + "name": "epoch", + "docs": [ + "The epoch the group was written" + ], + "type": "u64" + }, + { + "name": "constraints", + "docs": [ + "The constraints of the group, an empty list allows everything" + ], + "type": { + "vec": { + "defined": { + "name": "ParamConstraint" + } + } + } + } + ] + } + }, { "name": "FixedCurve", "type": { @@ -4786,6 +5172,18 @@ ], "type": "pubkey" }, + { + "name": "padding_alignment", + "docs": [ + "padding alignment" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + }, { "name": "padding", "docs": [ @@ -4794,7 +5192,7 @@ "type": { "array": [ "u64", - 16 + 15 ] } } @@ -4882,6 +5280,76 @@ ] } }, + { + "name": "ParamConstraint", + "docs": [ + "A single check on one launch parameter, e.g. `(SUPPLY, GTE, 1_000_000)`.", + "A range check is expressed with two constraints(`GTE` + `LTE`) in the same group." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "field", + "docs": [ + "The parameter this constraint applies to, see `param_field`" + ], + "type": "u8" + }, + { + "name": "op", + "docs": [ + "The comparison operator, see `param_op`" + ], + "type": "u8" + }, + { + "name": "value", + "docs": [ + "The value to compare the actual launch parameter with" + ], + "type": "u128" + } + ] + } + }, + { + "name": "PlatformAllowConfig", + "docs": [ + "The existence of this account means the platform allows the global config to be used", + "with its platform config, it is only checked when the platform restricts the global", + "config, see PlatformConfig::restrict_global_config" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "platform_config", + "type": "pubkey" + }, + { + "name": "global_config", + "type": "pubkey" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, { "name": "PlatformConfig", "type": { @@ -4995,30 +5463,62 @@ ], "type": "pubkey" }, + { + "name": "platform_vesting_wallet", + "type": "pubkey" + }, + { + "name": "platform_vesting_scale", + "type": "u64" + }, + { + "name": "platform_cp_creator", + "docs": [ + "If a valid platform_cp_creator is configured for the platform,", + "it will be used as the creator for the AMM pool during migration to cpswap pool." + ], + "type": "pubkey" + }, + { + "name": "restrict_global_config", + "docs": [ + "Whether the platform restricts the global config used to create pools with this", + "platform config, 0: no restriction, 1: only the allowed global config can be used," + ], + "type": "u8" + }, + { + "name": "restrict_curve_param", + "docs": [ + "Whether the platform restricts the launch params of the pools created with this", + "platform config, 0: no restriction, 1: the launch params must satisfy the", + "`PlatformCurveRule` account of the used global config" + ], + "type": "u8" + }, + { + "name": "curve_rule_manager", + "docs": [ + "The wallet allowed to manage the `PlatformCurveRule` accounts of this platform", + "config, the platform admin is often a multisig wallet which is inconvenient for", + "the day to day rule changes. The platform admin can always manage the rules too,", + "and it is the only one that can set this wallet." + ], + "type": "pubkey" + }, { "name": "padding", "docs": [ - "padding for future updates" + "padding for future updates. The last 4 bytes were the length prefix of the removed", + "`curve_params` vec, the migration left them zeroed, so the layout keeps exactly the", + "byte count every platform config already has." ], "type": { "array": [ "u8", - 180 + 78 ] } - }, - { - "name": "curve_params", - "docs": [ - "The parameters for launching the pool" - ], - "type": { - "vec": { - "defined": { - "name": "PlatformCurveParam" - } - } - } } ] } @@ -5067,6 +5567,14 @@ { "name": "creator_fee_rate", "type": "u64" + }, + { + "name": "platform_vesting_scale", + "type": "u64" + }, + { + "name": "vesting_wallet", + "type": "pubkey" } ] } @@ -5134,46 +5642,89 @@ } } ] + }, + { + "name": "VestingWallet", + "fields": [ + "pubkey" + ] + }, + { + "name": "PlatformVestingScale", + "fields": [ + "u64" + ] + }, + { + "name": "PlatformCPCreator", + "fields": [ + "pubkey" + ] + }, + { + "name": "RestrictGlobalConfig", + "fields": [ + "u64" + ] + }, + { + "name": "RestrictCurveParam", + "fields": [ + "u64" + ] + }, + { + "name": "CurveRuleManager", + "fields": [ + "pubkey" + ] } ] } }, { - "name": "PlatformCurveParam", + "name": "PlatformCurveRule", + "docs": [ + "The platform launch parameter rule for one global config.", + "", + "The existence of this account is only checked when the platform restricts the launch", + "parameters, see `PlatformConfig::restrict_curve_param`. It narrows the launch parameters", + "down from what the global config allows, it can never loosen them." + ], "type": { "kind": "struct", "fields": [ { - "name": "epoch", - "docs": [ - "The epoch for update interval, 0 means not update" - ], - "type": "u64" + "name": "bump", + "type": "u8" }, { - "name": "index", + "name": "version", "docs": [ - "The curve params index" + "The layout version of the account" ], "type": "u8" }, { - "name": "global_config", + "name": "platform_config", "docs": [ - "The global config address" + "The platform config the rule belongs to" ], "type": "pubkey" }, { - "name": "bonding_curve_param", + "name": "global_config", "docs": [ - "bonding curve param" + "The global config the rule applies to" ], - "type": { - "defined": { - "name": "BondingCurveParam" - } - } + "type": "pubkey" + }, + { + "name": "epoch", + "docs": [ + "The epoch of the last update" + ], + "type": "u64" }, { "name": "padding", @@ -5183,9 +5734,24 @@ "type": { "array": [ "u64", - 50 + 8 ] } + }, + { + "name": "groups", + "docs": [ + "The check groups, a launch is allowed when it satisfies any of them.", + "An empty list means the platform does not restrict the launch parameters of", + "this global config." + ], + "type": { + "vec": { + "defined": { + "name": "CurveRuleGroup" + } + } + } } ] } @@ -5232,6 +5798,10 @@ { "name": "creator_fee_rate", "type": "u64" + }, + { + "name": "platform_vesting_scale", + "type": "u64" } ] } @@ -5341,7 +5911,7 @@ { "name": "migrate_type", "docs": [ - "Migrate to AMM or CpSwap, 0: amm, 1: cpswap" + "Migrate to AMM or CpSwap, 0: amm\uff0c 1: cpswap" ], "type": "u8" }, @@ -5508,6 +6078,13 @@ } } }, + { + "name": "platform_vesting_share", + "docs": [ + "platform vesting token amount" + ], + "type": "u64" + }, { "name": "padding", "docs": [ @@ -5516,7 +6093,7 @@ "type": { "array": [ "u8", - 62 + 54 ] } } @@ -5780,4 +6357,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/bot_runner.py b/src/bot_runner.py index f048b8f..25b6c3b 100644 --- a/src/bot_runner.py +++ b/src/bot_runner.py @@ -106,6 +106,7 @@ async def start_bot(config_path: str): # Quote asset configuration (pump.fun non-SOL pairs) quote_amounts=cfg["trade"].get("quote_amounts"), allowed_quote_mints=cfg["filters"].get("allowed_quote_mints"), + max_transfer_fee_bps=cfg["filters"].get("max_transfer_fee_bps"), # Exit strategy configuration exit_strategy=cfg["trade"].get("exit_strategy", "time_based"), take_profit_percentage=cfg["trade"].get("take_profit_percentage"), diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index f98d90d..fe394b0 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -1,5 +1,7 @@ import asyncio +import struct +from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey from spl.token.instructions import burn, close_account from spl.token.models import BurnParams, CloseAccountParams @@ -12,6 +14,41 @@ from utils.logger import get_logger logger = get_logger(__name__) +# Token-2022 account layout: 165-byte base account, one account-type byte, then +# type-length-value extensions. TransferFeeAmount holds the fee withheld here. +_EXTENSIONS_OFFSET = 166 +_TRANSFER_FEE_AMOUNT = 2 +# TransferFeeExtension, then HarvestWithheldTokensToMint. +_HARVEST_WITHHELD_TO_MINT = bytes([26, 4]) + + +def withheld_transfer_fee(account_data: bytes) -> int: + """Raw transfer fee withheld in a Token-2022 token account, 0 if none. + + Token-2022 refuses to close an account while this is non-zero. + """ + offset = _EXTENSIONS_OFFSET + while offset + 4 <= len(account_data): + kind, length = struct.unpack_from(" Instruction: + """Move the fee withheld in `token_account` into its mint. Needs no signer.""" + return Instruction( + SystemAddresses.TOKEN_2022_PROGRAM, + _HARVEST_WITHHELD_TO_MINT, + [ + AccountMeta(mint, is_signer=False, is_writable=True), + AccountMeta(token_account, is_signer=False, is_writable=True), + ], + ) + class AccountCleanupManager: """Handles safe cleanup of token accounts (ATA) after trading sessions.""" @@ -60,7 +97,7 @@ class AccountCleanupManager: try: try: - await self.client.get_account_info(ata) + account = await self.client.get_account_info(ata) except ValueError: logger.info(f"ATA {ata} does not exist or already closed.") return @@ -99,6 +136,13 @@ class AccountCleanupManager: ) return + # A coin with a transfer fee withholds it inside the receiving + # account, and Token-2022 will not close an account holding any. + withheld = withheld_transfer_fee(bytes(account.data)) + if withheld and token_program_id == SystemAddresses.TOKEN_2022_PROGRAM: + logger.info(f"Harvesting {withheld} withheld fee units from {ata}") + instructions.append(harvest_withheld_to_mint(mint, ata)) + # Include close account instruction logger.info(f"Closing ATA: {ata}") close_ix = close_account( diff --git a/src/config_loader.py b/src/config_loader.py index b2a0cf9..3f29182 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -39,6 +39,13 @@ CONFIG_VALIDATION_RULES = [ 100, "trade.max_exit_sell_attempts must be between 1 and 100", ), + ( + "filters.max_transfer_fee_bps", + int, + 0, + 10_000, + "filters.max_transfer_fee_bps must be between 0 and 10000", + ), ( "priority_fees.fixed_amount", int, @@ -81,13 +88,14 @@ VALID_VALUES = { "filters.listener_type": ["logs", "blocks", "geyser", "shreds"], "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"], "trade.exit_strategy": ["time_based", "tp_sl", "manual"], - "platform": ["pump_fun", "lets_bonk"], + "platform": ["pump_fun", "lets_bonk", "stonk_fun"], } # Platform-specific listener compatibility PLATFORM_LISTENER_COMPATIBILITY = { Platform.PUMP_FUN: ["logs", "blocks", "geyser", "shreds"], Platform.LETS_BONK: ["blocks", "geyser"], + Platform.STONK_FUN: ["blocks", "geyser"], } diff --git a/src/interfaces/core.py b/src/interfaces/core.py index 4a0a57d..07299f3 100644 --- a/src/interfaces/core.py +++ b/src/interfaces/core.py @@ -14,6 +14,7 @@ class Platform(Enum): PUMP_FUN = "pump_fun" LETS_BONK = "lets_bonk" + STONK_FUN = "stonk_fun" class ConfirmationStatus(Enum): @@ -95,6 +96,11 @@ class TokenInfo: quote_token_program_id: Pubkey | None = None virtual_quote_reserves: int | None = None + # Token-2022 transfer fee on the coin itself, in basis points. Withheld on + # every transfer, so a buy delivers this much less and a sell sends this + # much less into the pool. Zero on pump.fun; set on LaunchLab reward coins. + transfer_fee_bps: int = 0 + # True when creator, mayhem/cashback flags and quote_mint were read from the # on-chain CreateEvent, letting extreme_fast_mode skip the pre-buy curve # refresh — zero RPC calls between detection and submission. Listeners that @@ -175,6 +181,16 @@ class InstructionBuilder(ABC): """Get the platform this builder serves.""" pass + @property + def spends_exact_amount_in(self) -> bool: + """Whether a buy spends all of `amount_in` rather than capping at it. + + False means `amount_in` is a ceiling and the trader pads it for + slippage. True means the program spends exactly what it is given, so a + padded value would overspend by the padding. + """ + return False + @abstractmethod async def build_buy_instruction( self, @@ -427,6 +443,18 @@ class EventParser(ABC): """ pass + def get_stream_filter_accounts(self) -> list[Pubkey]: + """Get the accounts the executed-stream listeners subscribe on. + + Geyser and blockSubscribe deliver every transaction mentioning one of + these. Defaults to the program id. A platform that shares its program + with others narrows this to accounts only its own transactions touch. + + Returns: + Accounts to subscribe on + """ + return [self.get_program_id()] + def get_creation_filter_accounts(self) -> list[Pubkey]: """Get accounts to subscribe on when only creations are wanted. diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py index 585dde2..f7760a5 100644 --- a/src/monitoring/listener_factory.py +++ b/src/monitoring/listener_factory.py @@ -121,7 +121,7 @@ class ListenerFactory: """ if platform == Platform.PUMP_FUN: return ["logs", "blocks", "geyser", "shreds"] - elif platform == Platform.LETS_BONK: + elif platform in (Platform.LETS_BONK, Platform.STONK_FUN): return ["blocks", "geyser"] else: return ["blocks", "geyser"] # Default universal listeners diff --git a/src/monitoring/universal_block_listener.py b/src/monitoring/universal_block_listener.py index 1cede7f..d10595b 100644 --- a/src/monitoring/universal_block_listener.py +++ b/src/monitoring/universal_block_listener.py @@ -48,9 +48,12 @@ class UniversalBlockListener(BaseTokenListener): # Get event parsers for all platforms self.platform_parsers = {} + # Accounts to subscribe on; a pump.fun parser names its program, a + # LaunchLab one its platform configs. self.platform_program_ids = [] - # Map program IDs to their parsers for faster lookup - self.program_id_to_parser = {} + # (program id, platform, parser): a list, not a dict keyed by program, + # because several platforms can share one program. + self.program_id_to_parser = [] for platform in self.platforms: try: @@ -70,8 +73,10 @@ class UniversalBlockListener(BaseTokenListener): parser = implementations.event_parser self.platform_parsers[platform] = parser program_id_str = str(parser.get_program_id()) - self.platform_program_ids.append(program_id_str) - self.program_id_to_parser[program_id_str] = (platform, parser) + self.platform_program_ids += [ + str(account) for account in parser.get_stream_filter_accounts() + ] + self.program_id_to_parser.append((program_id_str, platform, parser)) logger.info( f"Registered platform {platform.value} with program ID {parser.get_program_id()}" @@ -184,7 +189,7 @@ class UniversalBlockListener(BaseTokenListener): ) await websocket.send(subscription_message) - logger.info(f"Subscribed to blocks mentioning program: {program_id}") + logger.info(f"Subscribed to blocks mentioning: {program_id}") async def _ping_loop(self, websocket: websockets.WebSocketServerProtocol) -> None: """Keep connection alive with pings. @@ -343,7 +348,7 @@ class UniversalBlockListener(BaseTokenListener): if not logs: return None - for program_id, (_platform, parser) in self.program_id_to_parser.items(): + for program_id, _platform, parser in self.program_id_to_parser: if not any(program_id in line for line in logs): continue # parse_token_creation_from_block contains its own failures and diff --git a/src/monitoring/universal_geyser_listener.py b/src/monitoring/universal_geyser_listener.py index 706220c..c1dd8ef 100644 --- a/src/monitoring/universal_geyser_listener.py +++ b/src/monitoring/universal_geyser_listener.py @@ -124,7 +124,9 @@ class UniversalGeyserListener(BaseTokenListener): ) parser = implementations.event_parser self.platform_parsers[platform] = parser - self.platform_program_ids.add(parser.get_program_id()) + # A pump.fun parser names its program; a LaunchLab one its + # platform configs, keeping other launchpads out of the stream. + self.platform_program_ids.update(parser.get_stream_filter_accounts()) logger.info( f"Registered platform {platform.value} with program ID {parser.get_program_id()}" @@ -188,7 +190,7 @@ class UniversalGeyserListener(BaseTokenListener): f"Monitoring platforms: {[p.value for p in self.platforms]}" ) logger.info( - f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}" + f"Monitoring accounts: {[str(pid) for pid in self.platform_program_ids]}" ) try: diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index 5817f44..f6bf314 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -100,7 +100,7 @@ class PlatformRegistry: address_provider = impl_classes["address_provider"]() # For platforms with IDL support, pass the parser to relevant classes - if idl_parser and platform in [Platform.LETS_BONK, Platform.PUMP_FUN]: + if idl_parser: instruction_builder = impl_classes["instruction_builder"]( idl_parser=idl_parser ) @@ -224,6 +224,25 @@ class PlatformFactory: except ImportError as e: print(f"Warning: Could not register LetsBonk platform: {e}") + try: + from platforms.stonkfun import ( + StonkFunAddressProvider, + StonkFunCurveManager, + StonkFunEventParser, + StonkFunInstructionBuilder, + ) + + self.registry.register_platform( + Platform.STONK_FUN, + StonkFunAddressProvider, + StonkFunInstructionBuilder, + StonkFunCurveManager, + StonkFunEventParser, + ) + + except ImportError as e: + print(f"Warning: Could not register StonkFun platform: {e}") + def create_for_platform( self, platform: Platform, client: SolanaClient, **config: Any ) -> PlatformImplementations: diff --git a/src/platforms/launchlab/__init__.py b/src/platforms/launchlab/__init__.py new file mode 100644 index 0000000..a08f061 --- /dev/null +++ b/src/platforms/launchlab/__init__.py @@ -0,0 +1,18 @@ +"""Raydium LaunchLab, shared by the launchpads built on it (letsbonk.fun, StonkFun). + +A launchpad here is a set of platform configs; the classes in this package are +complete apart from naming theirs and their `Platform`. +""" + +from .address_provider import LAUNCHLAB_PROGRAM, LaunchLabAddressProvider +from .curve_manager import LaunchLabCurveManager +from .event_parser import LaunchLabEventParser +from .instruction_builder import LaunchLabInstructionBuilder + +__all__ = [ + "LAUNCHLAB_PROGRAM", + "LaunchLabAddressProvider", + "LaunchLabCurveManager", + "LaunchLabEventParser", + "LaunchLabInstructionBuilder", +] diff --git a/src/platforms/launchlab/address_provider.py b/src/platforms/launchlab/address_provider.py new file mode 100644 index 0000000..4b15f24 --- /dev/null +++ b/src/platforms/launchlab/address_provider.py @@ -0,0 +1,175 @@ +"""Raydium LaunchLab addresses and PDA derivations, shared by the platforms on it.""" + +from typing import ClassVar, Final + +from solders.pubkey import Pubkey +from spl.token.instructions import get_associated_token_address + +from core.pubkeys import ( + SystemAddresses, + cached_quote_token_program, + normalize_quote_mint, +) +from interfaces.core import AddressProvider, TokenInfo + +LAUNCHLAB_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj" +) + + +def _pda(seeds: list[bytes]) -> Pubkey: + return Pubkey.find_program_address(seeds, LAUNCHLAB_PROGRAM)[0] + + +class LaunchLabAddressProvider(AddressProvider): + """Addresses for one launchpad built on Raydium LaunchLab. + + LaunchLab is one program serving many launchpads; a launchpad is the set of + platform configs its pools are created under. Subclasses name theirs. + + Every pool address and both fee vaults are keyed by the pool's quote mint, + so each derivation takes it from `TokenInfo.quote_mint`. Wrapped SOL is only + the default when a caller has not resolved the quote yet. + """ + + PLATFORM_CONFIGS: ClassVar[frozenset[Pubkey]] = frozenset() + + @property + def program_id(self) -> Pubkey: + """Get the LaunchLab program ID.""" + return LAUNCHLAB_PROGRAM + + def get_system_addresses(self) -> dict[str, Pubkey]: + """Get the system addresses plus the LaunchLab program.""" + return { + **SystemAddresses.get_all_system_addresses(), + "program": LAUNCHLAB_PROGRAM, + } + + def derive_pool_address( + self, base_mint: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: + """Derive the pool state PDA for a base/quote pair.""" + quote = normalize_quote_mint(quote_mint) + return _pda([b"pool", bytes(base_mint), bytes(quote)]) + + def derive_pool_vault(self, pool_state: Pubkey, mint: Pubkey) -> Pubkey: + """Derive the pool's vault for one side of the pair.""" + return _pda([b"pool_vault", bytes(pool_state), bytes(mint)]) + + def derive_user_token_account( + self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None + ) -> Pubkey: + """Derive a user's ATA. LaunchLab coins default to Token-2022.""" + return get_associated_token_address( + user, mint, token_program_id or SystemAddresses.TOKEN_2022_PROGRAM + ) + + def derive_authority_pda(self) -> Pubkey: + """Derive the PDA that signs for every pool vault.""" + return _pda([b"vault_auth_seed"]) + + def derive_event_authority_pda(self) -> Pubkey: + """Derive the PDA the program emits its CPI events through.""" + return _pda([b"__event_authority"]) + + def derive_creator_fee_vault(self, creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """Derive the vault the creator's share of curve fees accrues to.""" + return _pda([bytes(creator), bytes(quote_mint)]) + + def derive_platform_fee_vault( + self, platform_config: Pubkey, quote_mint: Pubkey + ) -> Pubkey: + """Derive the vault the platform's share of curve fees accrues to. + + One per platform config and quote mint, shared by every pool on both. + """ + return _pda([bytes(platform_config), bytes(quote_mint)]) + + def resolve_quote(self, token_info: TokenInfo) -> tuple[Pubkey, Pubkey]: + """Return the coin's (quote mint, quote token program). + + The program falls back to the warm cache, which knows wrapped SOL and + every configured quote mint; an unresolved mint reads as SPL Token. + """ + quote_mint = normalize_quote_mint(token_info.quote_mint) + program = token_info.quote_token_program_id or cached_quote_token_program( + quote_mint + ) + return quote_mint, program + + def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: + """Get the pool, its vaults and the program PDAs for a coin.""" + quote_mint, _ = self.resolve_quote(token_info) + pool_state = token_info.pool_state or self.derive_pool_address( + token_info.mint, quote_mint + ) + return { + "pool_state": pool_state, + "base_vault": token_info.base_vault + or self.derive_pool_vault(pool_state, token_info.mint), + "quote_vault": token_info.quote_vault + or self.derive_pool_vault(pool_state, quote_mint), + "authority": self.derive_authority_pda(), + "event_authority": self.derive_event_authority_pda(), + } + + def get_swap_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: + """Get every account buy_exact_in and sell_exact_in take, by IDL name. + + The two share one layout. `global_config` and `platform_config` vary per + pool and must come from the creation transaction or the pool state. + + Raises: + ValueError: If the coin's creator, global config or platform config + is unknown — the fee vault and config accounts cannot be guessed + """ + if not ( + token_info.creator + and token_info.global_config + and token_info.platform_config + ): + raise ValueError( + f"LaunchLab coin {token_info.mint} is missing its creator, global " + f"config or platform config; read the pool state first" + ) + quote_mint, quote_token_program = self.resolve_quote(token_info) + base_token_program = ( + token_info.token_program_id or SystemAddresses.TOKEN_2022_PROGRAM + ) + accounts = self.get_additional_accounts(token_info) + return { + **accounts, + "payer": user, + "global_config": token_info.global_config, + "platform_config": token_info.platform_config, + "user_base_token": self.derive_user_token_account( + user, token_info.mint, base_token_program + ), + "base_token_mint": token_info.mint, + "quote_token_mint": quote_mint, + "base_token_program": base_token_program, + "quote_token_program": quote_token_program, + "program": LAUNCHLAB_PROGRAM, + "system_program": SystemAddresses.SYSTEM_PROGRAM, + "platform_fee_vault": self.derive_platform_fee_vault( + token_info.platform_config, quote_mint + ), + "creator_fee_vault": self.derive_creator_fee_vault( + token_info.creator, quote_mint + ), + } + + def get_buy_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: + """Get every account a buy needs.""" + return self.get_swap_accounts(token_info, user) + + def get_sell_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: + """Get every account a sell needs.""" + return self.get_swap_accounts(token_info, user) diff --git a/src/platforms/launchlab/curve_manager.py b/src/platforms/launchlab/curve_manager.py new file mode 100644 index 0000000..b21820f --- /dev/null +++ b/src/platforms/launchlab/curve_manager.py @@ -0,0 +1,215 @@ +"""Raydium LaunchLab pool reads: state, price, curve fee and transfer fee.""" + +import struct +from time import monotonic +from typing import Any + +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from interfaces.core import CurveManager +from platforms.launchlab.curve_math import ( + FEE_RATE_DENOMINATOR, + buy_amount_out, + mint_transfer_fee, + sell_amount_out, + spot_price_raw, +) +from utils.idl_parser import IDLParser +from utils.logger import get_logger + +logger = get_logger(__name__) + +# GlobalConfig: discriminator, epoch u64, curve_type u8, index u16, migrate_fee u64. +_GLOBAL_TRADE_FEE_RATE_OFFSET = 8 + 8 + 1 + 2 + 8 +# PlatformConfig: discriminator, epoch, two wallets, three u64 scales, fee_rate; +# then name[64], web[256], img[256], cpswap_config, creator_fee_rate. +_PLATFORM_FEE_RATE_OFFSET = 8 + 8 + 32 + 32 + 3 * 8 +_PLATFORM_CREATOR_FEE_RATE_OFFSET = _PLATFORM_FEE_RATE_OFFSET + 8 + 64 + 256 + 256 + 32 + +# Fee rates are read live by the program on every trade, and a platform admin +# can change them, so a cached rate is re-read after this long. +_FEE_RATE_TTL_SECONDS = 300.0 + +_PUBKEY_FIELDS = ( + "global_config", + "platform_config", + "base_mint", + "quote_mint", + "base_vault", + "quote_vault", + "creator", +) + + +class LaunchLabCurveManager(CurveManager): + """Reads LaunchLab pools. Subclasses only name their platform. + + `get_pool_state` returns, besides the decoded pool: + + - `price_per_token`: whole quote units per whole token, from the real + reserves and the pool's own decimals + - `fee_rate`: total curve fee in parts per million, and `fee_fraction` + - `transfer_fee_bps` / `transfer_fee_max`: the coin's Token-2022 transfer + fee, zero for a mint without one + """ + + def __init__(self, client: SolanaClient, idl_parser: IDLParser) -> None: + self.client = client + self._idl_parser = idl_parser + self._fee_rates: dict[tuple[Pubkey, Pubkey], tuple[int, float]] = {} + self._transfer_fees: dict[Pubkey, tuple[int, int]] = {} + + async def get_pool_state( + self, pool_address: Pubkey, commitment: str | None = None + ) -> dict[str, Any]: + """Read and decode a pool, with its fee rate and transfer fee. + + Args: + commitment: Optional override; pass "processed" right after a + stream event so a pool created in that slot is readable + + Raises: + ValueError: If the pool account is missing or undecodable + """ + account = await self.client.get_account_info( + pool_address, commitment=commitment + ) + if account is None or not account.data: + raise ValueError(f"No data in pool state account {pool_address}") # noqa: TRY003 + pool = self._decode_pool(account.data) + return await self._with_fees(pool, commitment) + + async def get_pool_state_and_token_program( + self, pool_address: Pubkey, mint: Pubkey, commitment: str | None = None + ) -> tuple[dict[str, Any], Pubkey | None]: + """Read the pool and the coin's mint in one slot-consistent round trip. + + Returns: + (pool state as from get_pool_state, the mint's token program or None + if the mint was not readable) + + Raises: + ValueError: If the pool account is missing or undecodable + """ + pool_account, mint_account = await self.client.get_multiple_accounts( + [pool_address, mint], commitment=commitment + ) + if pool_account is None or not pool_account.data: + raise ValueError(f"No data in pool state account {pool_address}") # noqa: TRY003 + if mint_account is not None: + self._transfer_fees[mint] = mint_transfer_fee(mint_account.data) or (0, 0) + pool = self._decode_pool(pool_account.data) + state = await self._with_fees(pool, commitment) + return state, mint_account.owner if mint_account is not None else None + + def _decode_pool(self, data: bytes) -> dict[str, Any]: + decoded = self._idl_parser.decode_account_data( + data, "PoolState", skip_discriminator=True + ) + if not decoded: + raise ValueError("Failed to decode PoolState") # noqa: TRY003 + pool = dict(decoded) + for field in _PUBKEY_FIELDS: + pool[field] = Pubkey.from_string(str(pool[field])) + return pool + + async def _with_fees( + self, pool: dict[str, Any], commitment: str | None + ) -> dict[str, Any]: + """Attach price, curve fee and transfer fee, reading only what is not cached.""" + config_key = (pool["global_config"], pool["platform_config"]) + cached_rate = self._fee_rates.get(config_key) + rate_fresh = ( + cached_rate is not None + and monotonic() - cached_rate[1] < _FEE_RATE_TTL_SECONDS + ) + mint = pool["base_mint"] + + missing = [] if rate_fresh else [pool["global_config"], pool["platform_config"]] + if mint not in self._transfer_fees: + missing.append(mint) + if missing: + accounts = dict( + zip( + missing, + await self.client.get_multiple_accounts( + missing, commitment=commitment + ), + strict=True, + ) + ) + if not rate_fresh: + global_config = accounts[pool["global_config"]] + platform_config = accounts[pool["platform_config"]] + if global_config is None or platform_config is None: + raise ValueError( # noqa: TRY003 + f"Fee configs for pool on {pool['platform_config']} unreadable" + ) + rate = ( + struct.unpack_from( + " float: + """Spot price in whole quote units per whole token; 0.0 for an empty curve.""" + return (await self.get_pool_state(pool_address))["price_per_token"] + + async def calculate_buy_amount_out( + self, pool_address: Pubkey, amount_in: int + ) -> int: + """Raw tokens delivered for exactly `amount_in` raw quote, after fee and tax.""" + pool = await self.get_pool_state(pool_address) + return buy_amount_out( + pool, + amount_in, + pool["fee_rate"], + (pool["transfer_fee_bps"], pool["transfer_fee_max"]), + ) + + async def calculate_sell_amount_out( + self, pool_address: Pubkey, amount_in: int + ) -> int: + """Raw quote paid for sending `amount_in` raw tokens, after tax and fee.""" + pool = await self.get_pool_state(pool_address) + return sell_amount_out( + pool, + amount_in, + pool["fee_rate"], + (pool["transfer_fee_bps"], pool["transfer_fee_max"]), + ) + + async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]: + """Effective (base, quote) reserves the curve is trading against, raw.""" + pool = await self.get_pool_state(pool_address) + return ( + pool["virtual_base"] - pool["real_base"], + pool["virtual_quote"] + pool["real_quote"], + ) diff --git a/src/platforms/launchlab/curve_math.py b/src/platforms/launchlab/curve_math.py new file mode 100644 index 0000000..2a9e662 --- /dev/null +++ b/src/platforms/launchlab/curve_math.py @@ -0,0 +1,114 @@ +"""Raydium LaunchLab curve arithmetic: prices, fees and Token-2022 transfer fees. + +All amounts are raw integer units, with the program's own rounding: fees round +up, curve outputs round down. These reproduce `simulateTransaction` to the unit. +""" + +import struct + +# Fee rates on GlobalConfig and PlatformConfig are parts per million. +FEE_RATE_DENOMINATOR = 1_000_000 +BASIS_POINTS = 10_000 + +# Token-2022 mint layout: 82-byte base mint, padding to 165, one account-type +# byte, then type-length-value extensions. +_EXTENSIONS_OFFSET = 166 +_TRANSFER_FEE_CONFIG = 1 +# TransferFeeConfig: two authorities and a withheld amount (72 bytes), then the +# older and newer fees, each (epoch u64, maximum_fee u64, basis_points u16). +_OLDER_FEE_OFFSET = 72 +_NEWER_FEE_OFFSET = 90 + + +def _ceil_div(numerator: int, denominator: int) -> int: + return -(-numerator // denominator) + + +def mint_transfer_fee(mint_data: bytes) -> tuple[int, int] | None: + """A Token-2022 mint's transfer fee, as (basis points, maximum fee raw). + + Returns the higher of the scheduled older and newer fees. A rate change only + takes effect two epochs after it is set, and taking the higher one avoids an + epoch read while never underestimating what a transfer loses. + + Returns: + None when the mint has no TransferFeeConfig extension — then a transfer + loses nothing and there is nothing withheld to harvest. + """ + offset = _EXTENSIONS_OFFSET + while offset + 4 <= len(mint_data): + kind, length = struct.unpack_from("int: + """What Token-2022 withholds from a transfer of `amount`, rounded up and capped.""" + if not transfer_fee_bps: + return 0 + return min(_ceil_div(amount * transfer_fee_bps, BASIS_POINTS), maximum_fee) + + +def spot_price_raw(pool: dict) -> float: + """Raw quote units per raw base unit at the current point on the curve. + + The virtual reserves are fixed when the pool is created; the real reserves + record every trade since. Pricing off the virtual reserves alone returns the + launch price forever. + """ + base_reserve = pool["virtual_base"] - pool["real_base"] + quote_reserve = pool["virtual_quote"] + pool["real_quote"] + if base_reserve <= 0 or quote_reserve <= 0: + return 0.0 + return quote_reserve / base_reserve + + +def buy_amount_out( + pool: dict, + amount_in: int, + fee_rate: int, + transfer_fee: tuple[int, int] = (0, 0), +) -> int: + """Raw base tokens delivered to the buyer for exactly `amount_in` raw quote. + + The curve fee comes off the input first, the rest trades on the curve, and + the transfer fee is withheld on delivery. + + Args: + fee_rate: Total curve fee, parts per million (trade + platform + creator) + transfer_fee: (basis points, maximum fee raw) of the base mint + """ + net_in = amount_in - _ceil_div(amount_in * fee_rate, FEE_RATE_DENOMINATOR) + base_reserve = pool["virtual_base"] - pool["real_base"] + quote_reserve = pool["virtual_quote"] + pool["real_quote"] + out = base_reserve * net_in // (quote_reserve + net_in) + return out - withheld_fee(out, *transfer_fee) + + +def sell_amount_out( + pool: dict, + amount_in: int, + fee_rate: int, + transfer_fee: tuple[int, int] = (0, 0), +) -> int: + """Raw quote paid to the seller for sending `amount_in` raw base tokens. + + The transfer fee is withheld on the way into the pool, so the curve trades + on what arrives, and the curve fee then comes off the quote side. + """ + arrived = amount_in - withheld_fee(amount_in, *transfer_fee) + base_reserve = pool["virtual_base"] - pool["real_base"] + quote_reserve = pool["virtual_quote"] + pool["real_quote"] + gross = quote_reserve * arrived // (base_reserve + arrived) + return gross - _ceil_div(gross * fee_rate, FEE_RATE_DENOMINATOR) diff --git a/src/platforms/launchlab/event_parser.py b/src/platforms/launchlab/event_parser.py new file mode 100644 index 0000000..ad0023f --- /dev/null +++ b/src/platforms/launchlab/event_parser.py @@ -0,0 +1,229 @@ +"""Raydium LaunchLab launch parsing, filtered to one launchpad's platform configs.""" + +import base64 +from time import monotonic +from typing import Any, ClassVar + +import base58 +from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction + +from interfaces.core import EventParser, TokenInfo +from platforms.launchlab.address_provider import LAUNCHLAB_PROGRAM +from utils.idl_parser import IDLParser +from utils.logger import get_logger + +logger = get_logger(__name__) + +_INITIALIZE_VARIANTS = ("initialize", "initialize_v2", "initialize_with_token_2022") + + +class LaunchLabEventParser(EventParser): + """Turns a LaunchLab pool initialization into a TokenInfo. + + LaunchLab serves many launchpads from one program; a launch belongs to the + platform whose config it names. Subclasses set `PLATFORM_CONFIGS`, and any + launch under another config is ignored. + + Everything comes from the instruction's accounts and arguments: the pool's + creator is the `creator` account (not the payer, which may differ), the + quote mint and both token programs are accounts, and the transfer fee is the + argument the mint is initialized with. None of it is user-supplied in a way + the pool could disagree with, so the TokenInfo is marked `state_from_event` + and extreme_fast_mode can buy without reading the pool first. + + Logs alone cannot identify a launch: the program's PoolCreateEvent carries + neither the base mint, the quote mint nor the platform config. + """ + + PLATFORM_CONFIGS: ClassVar[frozenset[Pubkey]] = frozenset() + + def __init__(self, idl_parser: IDLParser) -> None: + self._idl_parser = idl_parser + discriminators = idl_parser.get_instruction_discriminators() + self._discriminators = [discriminators[name] for name in _INITIALIZE_VARIANTS] + + def get_program_id(self) -> Pubkey: + """Get the LaunchLab program ID.""" + return LAUNCHLAB_PROGRAM + + def get_stream_filter_accounts(self) -> list[Pubkey]: + """Subscribe on this launchpad's platform configs, not the whole program. + + Every pool and every trade on it names its platform config, so this + keeps other launchpads on LaunchLab out of the stream. + """ + return sorted(self.PLATFORM_CONFIGS, key=str) + + def get_creation_filter_accounts(self) -> list[Pubkey]: + """Same as the stream filter: a launch always names its platform config.""" + return self.get_stream_filter_accounts() + + def get_instruction_discriminators(self) -> list[bytes]: + """Discriminators of the three pool-initialization instructions.""" + return self._discriminators + + def parse_token_creation_from_logs( + self, + logs: list[str], # noqa: ARG002 + signature: str, # noqa: ARG002 + ) -> TokenInfo | None: + """Not supported: LaunchLab's logs do not name the coin or its launchpad.""" + return None + + def parse_token_creation_from_instruction( + self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes] + ) -> TokenInfo | None: + """Decode one initialize instruction into a TokenInfo. + + Returns: + None for anything but a pool initialization under one of this + platform's configs + """ + if not any(instruction_data.startswith(d) for d in self._discriminators): + return None + decoded = self._idl_parser.decode_instruction( + instruction_data, account_keys, accounts + ) + if not decoded or decoded["instruction_name"] not in _INITIALIZE_VARIANTS: + return None + + named = decoded["accounts"] + required = ( + "creator", + "global_config", + "platform_config", + "pool_state", + "base_mint", + "quote_mint", + "base_vault", + "quote_vault", + "base_token_program", + "quote_token_program", + ) + if any(named.get(name) is None for name in required): + return None + keys = {name: Pubkey.from_string(named[name]) for name in required} + if keys["platform_config"] not in self.PLATFORM_CONFIGS: + return None + + args = decoded["args"] + mint_params = args.get("base_mint_param") or {} + transfer_fee = args.get("transfer_fee_extension_param") or {} + payer = named.get("payer") + return TokenInfo( + name=mint_params.get("name", ""), + symbol=mint_params.get("symbol", ""), + uri=mint_params.get("uri", ""), + mint=keys["base_mint"], + platform=self.platform, + pool_state=keys["pool_state"], + base_vault=keys["base_vault"], + quote_vault=keys["quote_vault"], + global_config=keys["global_config"], + platform_config=keys["platform_config"], + user=Pubkey.from_string(payer) if payer else keys["creator"], + creator=keys["creator"], + token_program_id=keys["base_token_program"], + quote_mint=keys["quote_mint"], + quote_token_program_id=keys["quote_token_program"], + transfer_fee_bps=transfer_fee.get("transfer_fee_basis_points", 0), + state_from_event=True, + creation_timestamp=monotonic(), + ) + + def _parse_instructions( + self, instructions: list[tuple[int, list[int], bytes]], keys: list[bytes] + ) -> TokenInfo | None: + """Try each (program index, account indexes, data) against the program.""" + program = bytes(LAUNCHLAB_PROGRAM) + for program_index, accounts, data in instructions: + if program_index >= len(keys) or bytes(keys[program_index]) != program: + continue + token_info = self.parse_token_creation_from_instruction( + data, accounts, keys + ) + if token_info: + return token_info + return None + + def parse_token_creation_from_geyser( + self, transaction_info: Any + ) -> TokenInfo | None: + """Parse a geyser transaction update, top-level and inner instructions. + + Lookup-table accounts are appended after the static keys, writable then + read-only, which is the order instruction indexes refer to. + """ + try: + tx = transaction_info.transaction.transaction + message = tx.transaction.message + meta = tx.meta + keys = [ + *message.account_keys, + *meta.loaded_writable_addresses, + *meta.loaded_readonly_addresses, + ] + instructions = [ + (ix.program_id_index, list(ix.accounts), bytes(ix.data)) + for ix in message.instructions + ] + for group in meta.inner_instructions: + instructions += [ + (ix.program_id_index, list(ix.accounts), bytes(ix.data)) + for ix in group.instructions + ] + return self._parse_instructions(instructions, keys) + except Exception as e: # noqa: BLE001 + logger.debug(f"Failed to parse geyser transaction: {e}") + return None + + def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: + """Parse blockSubscribe transactions, base64 or JSON encoded. + + Inner instructions come from `meta.innerInstructions` with base58 data; + lookup-table accounts from `meta.loadedAddresses`. + """ + for tx in block_data.get("transactions", []): + try: + token_info = self._parse_block_transaction(tx) + except Exception as e: # noqa: BLE001 + logger.debug(f"Failed to parse block transaction: {e}") + continue + if token_info: + return token_info + return None + + def _parse_block_transaction(self, tx: dict) -> TokenInfo | None: + meta = tx.get("meta") or {} + if meta.get("err") is not None: + return None + raw = tx.get("transaction") + if isinstance(raw, list): + message = VersionedTransaction.from_bytes(base64.b64decode(raw[0])).message + static_keys = [bytes(key) for key in message.account_keys] + instructions = [ + (ix.program_id_index, list(ix.accounts), bytes(ix.data)) + for ix in message.instructions + ] + elif isinstance(raw, dict) and "message" in raw: + message = raw["message"] + static_keys = [bytes(Pubkey.from_string(k)) for k in message["accountKeys"]] + instructions = [ + (ix["programIdIndex"], ix["accounts"], base58.b58decode(ix["data"])) + for ix in message["instructions"] + ] + else: + return None + + loaded = meta.get("loadedAddresses") or {} + keys = static_keys + [ + bytes(Pubkey.from_string(k)) + for k in [*loaded.get("writable", []), *loaded.get("readonly", [])] + ] + for group in meta.get("innerInstructions") or []: + instructions += [ + (ix["programIdIndex"], ix["accounts"], base58.b58decode(ix["data"])) + for ix in group["instructions"] + ] + return self._parse_instructions(instructions, keys) diff --git a/src/platforms/launchlab/instruction_builder.py b/src/platforms/launchlab/instruction_builder.py new file mode 100644 index 0000000..04809ee --- /dev/null +++ b/src/platforms/launchlab/instruction_builder.py @@ -0,0 +1,245 @@ +"""Raydium LaunchLab buy_exact_in / sell_exact_in instructions, for any quote mint.""" + +import secrets +import struct + +from solders.instruction import AccountMeta, Instruction +from solders.pubkey import Pubkey +from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed +from spl.token.instructions import ( + close_account, + create_idempotent_associated_token_account, + get_associated_token_address, + initialize_account, +) +from spl.token.models import CloseAccountParams, InitializeAccountParams + +from core.pubkeys import ( + TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE, + TOKEN_ACCOUNT_SIZE, + WSOL_MINT, + SystemAddresses, +) +from interfaces.core import InstructionBuilder, TokenInfo +from platforms.launchlab.address_provider import LaunchLabAddressProvider +from utils.idl_parser import IDLParser + +# A front end may charge its own share fee on a trade; the bot charges none. +SHARE_FEE_RATE = 0 + +# The IDL's 15 named accounts, in order. Three more follow, read positionally by +# the program: the system program and the two fee vaults. +_SWAP_ACCOUNTS: list[tuple[str, bool]] = [ + ("payer", True), + ("authority", False), + ("global_config", False), + ("platform_config", False), + ("pool_state", True), + ("user_base_token", True), + ("user_quote_token", True), + ("base_vault", True), + ("quote_vault", True), + ("base_token_mint", False), + ("quote_token_mint", False), + ("base_token_program", False), + ("quote_token_program", False), + ("event_authority", False), + ("program", False), + ("system_program", False), + ("platform_fee_vault", True), + ("creator_fee_vault", True), +] + + +class LaunchLabInstructionBuilder(InstructionBuilder): + """Builds LaunchLab trades. Subclasses only name their platform. + + A SOL-quoted pool settles through a throwaway wrapped-SOL account created, + used and closed inside the transaction. Any other quote is paid from, and + paid into, the wallet's own account for that mint; the bot never acquires a + quote asset, so a buy of a non-SOL coin needs the balance already there. + """ + + def __init__(self, idl_parser: IDLParser) -> None: + discriminators = idl_parser.get_instruction_discriminators() + self._buy_exact_in = discriminators["buy_exact_in"] + self._sell_exact_in = discriminators["sell_exact_in"] + + @property + def spends_exact_amount_in(self) -> bool: + """buy_exact_in spends all of amount_in.""" + return True + + async def build_buy_instruction( + self, + token_info: TokenInfo, + user: Pubkey, + amount_in: int, + minimum_amount_out: int, + address_provider: LaunchLabAddressProvider, + ) -> list[Instruction]: + """Build a buy that spends exactly `amount_in` raw quote units. + + Args: + amount_in: Raw quote units to spend, all of them + minimum_amount_out: Raw base tokens that must arrive, after the + transfer fee on a reward coin + """ + accounts = address_provider.get_buy_instruction_accounts(token_info, user) + instructions = [ + create_idempotent_associated_token_account( + user, user, token_info.mint, accounts["base_token_program"] + ) + ] + quote_account, open_ixs, close_ixs = self._quote_account( + accounts, user, funding=amount_in + ) + instructions += open_ixs + instructions.append( + self._swap( + self._buy_exact_in, + accounts, + quote_account, + amount_in, + minimum_amount_out, + ) + ) + return instructions + close_ixs + + async def build_sell_instruction( + self, + token_info: TokenInfo, + user: Pubkey, + amount_in: int, + minimum_amount_out: int, + address_provider: LaunchLabAddressProvider, + ) -> list[Instruction]: + """Build a sell of exactly `amount_in` raw base tokens. + + Args: + amount_in: Raw base tokens to send + minimum_amount_out: Raw quote units that must be paid out + """ + accounts = address_provider.get_sell_instruction_accounts(token_info, user) + quote_account, open_ixs, close_ixs = self._quote_account( + accounts, user, funding=0 + ) + return [ + *open_ixs, + self._swap( + self._sell_exact_in, + accounts, + quote_account, + amount_in, + minimum_amount_out, + ), + *close_ixs, + ] + + def _quote_account( + self, accounts: dict[str, Pubkey], user: Pubkey, funding: int + ) -> tuple[Pubkey, list[Instruction], list[Instruction]]: + """The account the quote side settles through, with its setup and teardown. + + Returns: + (account, instructions before the swap, instructions after it) + """ + quote_mint = accounts["quote_token_mint"] + if quote_mint != WSOL_MINT: + quote_program = accounts["quote_token_program"] + create_ata = create_idempotent_associated_token_account( + user, user, quote_mint, quote_program + ) + ata = get_associated_token_address(user, quote_mint, quote_program) + return ata, [create_ata], [] + + # A fresh seed per trade: two trades in flight never collide on one + # address, and any WSOL the wallet already holds is left alone. + seed = secrets.token_hex(16) + wsol_account = Pubkey.create_with_seed( + user, seed, SystemAddresses.TOKEN_PROGRAM + ) + open_ixs = [ + create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=wsol_account, + base=user, + seed=seed, + lamports=funding + TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE, + space=TOKEN_ACCOUNT_SIZE, + owner=SystemAddresses.TOKEN_PROGRAM, + ) + ), + initialize_account( + InitializeAccountParams( + program_id=SystemAddresses.TOKEN_PROGRAM, + account=wsol_account, + mint=WSOL_MINT, + owner=user, + ) + ), + ] + close_ixs = [ + close_account( + CloseAccountParams( + program_id=SystemAddresses.TOKEN_PROGRAM, + account=wsol_account, + dest=user, + owner=user, + ) + ) + ] + return wsol_account, open_ixs, close_ixs + + def _swap( + self, + discriminator: bytes, + accounts: dict[str, Pubkey], + quote_account: Pubkey, + amount_in: int, + minimum_amount_out: int, + ) -> Instruction: + resolved = {**accounts, "user_quote_token": quote_account} + metas = [ + AccountMeta(resolved[name], is_signer=name == "payer", is_writable=writable) + for name, writable in _SWAP_ACCOUNTS + ] + data = discriminator + struct.pack( + " list[Pubkey]: + """Writable accounts a buy locks, for priority-fee estimation.""" + accounts = address_provider.get_buy_instruction_accounts(token_info, user) + return [ + accounts[name] + for name in ("pool_state", "base_vault", "quote_vault", "program") + ] + + def get_required_accounts_for_sell( + self, + token_info: TokenInfo, + user: Pubkey, + address_provider: LaunchLabAddressProvider, + ) -> list[Pubkey]: + """Writable accounts a sell locks, for priority-fee estimation.""" + accounts = address_provider.get_sell_instruction_accounts(token_info, user) + return [ + accounts[name] + for name in ("pool_state", "base_vault", "quote_vault", "program") + ] + + def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int: + """Compute units for a buy; simulated buys use about 70,000.""" + return config_override if config_override is not None else 150_000 + + def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int: + """Compute units for a sell; simulated sells use about 60,000.""" + return config_override if config_override is not None else 150_000 diff --git a/src/platforms/letsbonk/__init__.py b/src/platforms/letsbonk/__init__.py index 24f6f18..a702937 100644 --- a/src/platforms/letsbonk/__init__.py +++ b/src/platforms/letsbonk/__init__.py @@ -1,11 +1,62 @@ -"""letsbonk.fun platform exports. Registration is handled by the platform factory.""" +"""letsbonk.fun: Raydium LaunchLab pools under letsbonk.fun's platform config.""" + +from typing import Final + +from solders.pubkey import Pubkey + +from interfaces.core import Platform +from platforms.launchlab import ( + LaunchLabAddressProvider, + LaunchLabCurveManager, + LaunchLabEventParser, + LaunchLabInstructionBuilder, +) + +PLATFORM_CONFIGS: Final[frozenset[Pubkey]] = frozenset( + {Pubkey.from_string("5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA")} +) + + +class LetsBonkAddressProvider(LaunchLabAddressProvider): + """letsbonk.fun addresses.""" + + PLATFORM_CONFIGS = PLATFORM_CONFIGS + + @property + def platform(self) -> Platform: + """letsbonk.fun.""" + return Platform.LETS_BONK + + +class LetsBonkInstructionBuilder(LaunchLabInstructionBuilder): + """letsbonk.fun trades.""" + + @property + def platform(self) -> Platform: + """letsbonk.fun.""" + return Platform.LETS_BONK + + +class LetsBonkCurveManager(LaunchLabCurveManager): + """letsbonk.fun pool reads.""" + + @property + def platform(self) -> Platform: + """letsbonk.fun.""" + return Platform.LETS_BONK + + +class LetsBonkEventParser(LaunchLabEventParser): + """letsbonk.fun launches.""" + + PLATFORM_CONFIGS = PLATFORM_CONFIGS + + @property + def platform(self) -> Platform: + """letsbonk.fun.""" + return Platform.LETS_BONK -from .address_provider import LetsBonkAddressProvider -from .curve_manager import LetsBonkCurveManager -from .event_parser import LetsBonkEventParser -from .instruction_builder import LetsBonkInstructionBuilder -# Export implementations for direct use if needed __all__ = [ "LetsBonkAddressProvider", "LetsBonkCurveManager", diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py deleted file mode 100644 index 61c7c2d..0000000 --- a/src/platforms/letsbonk/address_provider.py +++ /dev/null @@ -1,397 +0,0 @@ -"""letsbonk.fun (Raydium LaunchLab) addresses and PDA derivations.""" - -from dataclasses import dataclass -from typing import Final - -from solders.pubkey import Pubkey -from spl.token.instructions import get_associated_token_address - -from core.pubkeys import SystemAddresses -from interfaces.core import AddressProvider, Platform, TokenInfo - - -@dataclass -class LetsBonkAddresses: - """LetsBonk (Raydium LaunchLab) program addresses.""" - - # Raydium LaunchLab program addresses - PROGRAM: Final[Pubkey] = Pubkey.from_string( - "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj" - ) - # NOT constant across pools: each pool is initialized with its own - # global_config, extracted during pool initialization into - # TokenInfo.global_config. The value below is only a fallback. - GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string( - "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX" - ) - # NOT constant across pools: platform_config defines a pool's fee structure - # and launch restrictions, and is extracted during pool initialization into - # TokenInfo.platform_config. The value below is only a fallback. - PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string( - "5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA" - ) - - -class LetsBonkAddressProvider(AddressProvider): - """LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface.""" - - @property - def platform(self) -> Platform: - """Get the platform this provider serves.""" - return Platform.LETS_BONK - - @property - def program_id(self) -> Pubkey: - """Get the main program ID for this platform.""" - return LetsBonkAddresses.PROGRAM - - def get_system_addresses(self) -> dict[str, Pubkey]: - """Get all system addresses required for LetsBonk. - - Returns: - Dictionary mapping address names to Pubkey objects - """ - # Get system addresses from the single source of truth - system_addresses = SystemAddresses.get_all_system_addresses() - - # Add LetsBonk specific addresses - letsbonk_addresses = { - # Raydium LaunchLab specific addresses - "program": LetsBonkAddresses.PROGRAM, - "global_config": LetsBonkAddresses.GLOBAL_CONFIG, - "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, - } - - # Combine system and platform-specific addresses - return {**system_addresses, **letsbonk_addresses} - - def derive_pool_address( - self, base_mint: Pubkey, quote_mint: Pubkey | None = None - ) -> Pubkey: - """Derive the pool state address for a token pair. - - For LetsBonk, this derives the pool state PDA using base_mint and WSOL. - - Args: - base_mint: Base token mint address - quote_mint: Quote token mint (defaults to WSOL) - - Returns: - Pool state address - """ - if quote_mint is None: - quote_mint = SystemAddresses.SOL_MINT - - pool_state, _ = Pubkey.find_program_address( - [b"pool", bytes(base_mint), bytes(quote_mint)], LetsBonkAddresses.PROGRAM - ) - return pool_state - - def derive_base_vault( - self, base_mint: Pubkey, quote_mint: Pubkey | None = None - ) -> Pubkey: - """Derive the base vault address for a token pair. - - Args: - base_mint: Base token mint address - quote_mint: Quote token mint (defaults to WSOL) - """ - if quote_mint is None: - quote_mint = SystemAddresses.SOL_MINT - - pool_state = self.derive_pool_address(base_mint, quote_mint) - - # Then derive the base vault using pool_vault seed - base_vault, _ = Pubkey.find_program_address( - [b"pool_vault", bytes(pool_state), bytes(base_mint)], - LetsBonkAddresses.PROGRAM, - ) - return base_vault - - def derive_quote_vault( - self, base_mint: Pubkey, quote_mint: Pubkey | None = None - ) -> Pubkey: - """Derive the quote vault address for a token pair. - - Args: - base_mint: Base token mint address - quote_mint: Quote token mint (defaults to WSOL) - """ - if quote_mint is None: - quote_mint = SystemAddresses.SOL_MINT - - pool_state = self.derive_pool_address(base_mint, quote_mint) - - # Then derive the quote vault using pool_vault seed - quote_vault, _ = Pubkey.find_program_address( - [b"pool_vault", bytes(pool_state), bytes(quote_mint)], - LetsBonkAddresses.PROGRAM, - ) - return quote_vault - - def derive_user_token_account( - self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None - ) -> Pubkey: - """Derive user's associated token account address. - - Args: - user: User's wallet address - mint: Token mint address - token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM - - Returns: - User's associated token account address - """ - if token_program_id is None: - token_program_id = SystemAddresses.TOKEN_2022_PROGRAM - return get_associated_token_address(user, mint, token_program_id) - - def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: - """Get LetsBonk-specific additional accounts needed for trading. - - Returns: - Dictionary of additional account addresses - """ - accounts = {} - - # Add pool state - derive if not present or use existing - if token_info.pool_state: - accounts["pool_state"] = token_info.pool_state - else: - accounts["pool_state"] = self.derive_pool_address(token_info.mint) - - # Add vault addresses - derive if not present or use existing - if token_info.base_vault: - accounts["base_vault"] = token_info.base_vault - else: - accounts["base_vault"] = self.derive_base_vault(token_info.mint) - - if token_info.quote_vault: - accounts["quote_vault"] = token_info.quote_vault - else: - accounts["quote_vault"] = self.derive_quote_vault(token_info.mint) - - accounts["authority"] = self.derive_authority_pda() - - accounts["event_authority"] = self.derive_event_authority_pda() - - return accounts - - def derive_authority_pda(self) -> Pubkey: - """Derive the authority PDA for Raydium LaunchLab. - - This PDA acts as the authority for pool vault operations. - """ - AUTH_SEED = b"vault_auth_seed" - authority_pda, _ = Pubkey.find_program_address( - [AUTH_SEED], LetsBonkAddresses.PROGRAM - ) - return authority_pda - - def derive_event_authority_pda(self) -> Pubkey: - """Derive the event authority PDA for Raydium LaunchLab. - - This PDA is used for emitting program events during swaps. - """ - EVENT_AUTHORITY_SEED = b"__event_authority" - event_authority_pda, _ = Pubkey.find_program_address( - [EVENT_AUTHORITY_SEED], LetsBonkAddresses.PROGRAM - ) - return event_authority_pda - - def derive_creator_fee_vault( - self, creator: Pubkey, quote_mint: Pubkey | None = None - ) -> 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 (defaults to WSOL) - """ - if quote_mint is None: - quote_mint = SystemAddresses.SOL_MINT - - creator_fee_vault, _ = Pubkey.find_program_address( - [bytes(creator), bytes(quote_mint)], LetsBonkAddresses.PROGRAM - ) - return creator_fee_vault - - def derive_platform_fee_vault( - self, platform_config: Pubkey | None = None, quote_mint: Pubkey | None = None - ) -> Pubkey: - """Derive the platform fee vault PDA. - - This vault accumulates platform fees from trades. - - Args: - platform_config: The platform config account (defaults to LetsBonk config) - quote_mint: The quote token mint (defaults to WSOL) - """ - if platform_config is None: - platform_config = LetsBonkAddresses.PLATFORM_CONFIG - - if quote_mint is None: - quote_mint = SystemAddresses.SOL_MINT - - platform_fee_vault, _ = Pubkey.find_program_address( - [bytes(platform_config), bytes(quote_mint)], LetsBonkAddresses.PROGRAM - ) - return platform_fee_vault - - def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey: - """Create a WSOL account address using createAccountWithSeed pattern. - - Args: - payer: The account that will pay for and own the new account - seed: String seed for deterministic account generation - - Returns: - New WSOL account address - """ - return Pubkey.create_with_seed(payer, seed, SystemAddresses.TOKEN_PROGRAM) - - def get_buy_instruction_accounts( - self, token_info: TokenInfo, user: Pubkey - ) -> dict[str, Pubkey]: - """Get all accounts needed for a buy instruction. - - Args: - user: User's wallet address - - Returns: - Dictionary of account addresses for buy instruction - """ - additional_accounts = self.get_additional_accounts(token_info) - - # Determine token program to use - token_program_id = ( - token_info.token_program_id - if token_info.token_program_id - else SystemAddresses.TOKEN_2022_PROGRAM - ) - - # Use global_config from TokenInfo if available, otherwise use default - global_config = ( - token_info.global_config - if token_info.global_config - else LetsBonkAddresses.GLOBAL_CONFIG - ) - - # Use platform_config from TokenInfo if available, otherwise use default - platform_config = ( - token_info.platform_config - if token_info.platform_config - else LetsBonkAddresses.PLATFORM_CONFIG - ) - - accounts = { - "payer": user, - "authority": additional_accounts["authority"], - "global_config": global_config, - "platform_config": platform_config, - "pool_state": additional_accounts["pool_state"], - "user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id), - "base_vault": additional_accounts["base_vault"], - "quote_vault": additional_accounts["quote_vault"], - "base_token_mint": token_info.mint, - "quote_token_mint": SystemAddresses.SOL_MINT, - "base_token_program": token_program_id, - "quote_token_program": SystemAddresses.TOKEN_PROGRAM, - "event_authority": additional_accounts["event_authority"], - "program": LetsBonkAddresses.PROGRAM, - "system_program": SystemAddresses.SYSTEM_PROGRAM, - "platform_fee_vault": self.derive_platform_fee_vault(platform_config), - } - - # Add creator fee vault if creator is known - if token_info.creator: - accounts["creator_fee_vault"] = self.derive_creator_fee_vault( - token_info.creator - ) - - return accounts - - def get_sell_instruction_accounts( - self, token_info: TokenInfo, user: Pubkey - ) -> dict[str, Pubkey]: - """Get all accounts needed for a sell instruction. - - Args: - user: User's wallet address - - Returns: - Dictionary of account addresses for sell instruction - """ - additional_accounts = self.get_additional_accounts(token_info) - - # Determine token program to use - token_program_id = ( - token_info.token_program_id - if token_info.token_program_id - else SystemAddresses.TOKEN_2022_PROGRAM - ) - - # Use global_config from TokenInfo if available, otherwise use default - global_config = ( - token_info.global_config - if token_info.global_config - else LetsBonkAddresses.GLOBAL_CONFIG - ) - - # Use platform_config from TokenInfo if available, otherwise use default - platform_config = ( - token_info.platform_config - if token_info.platform_config - else LetsBonkAddresses.PLATFORM_CONFIG - ) - - accounts = { - "payer": user, - "authority": additional_accounts["authority"], - "global_config": global_config, - "platform_config": platform_config, - "pool_state": additional_accounts["pool_state"], - "user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id), - "base_vault": additional_accounts["base_vault"], - "quote_vault": additional_accounts["quote_vault"], - "base_token_mint": token_info.mint, - "quote_token_mint": SystemAddresses.SOL_MINT, - "base_token_program": token_program_id, - "quote_token_program": SystemAddresses.TOKEN_PROGRAM, - "event_authority": additional_accounts["event_authority"], - "program": LetsBonkAddresses.PROGRAM, - "system_program": SystemAddresses.SYSTEM_PROGRAM, - "platform_fee_vault": self.derive_platform_fee_vault(platform_config), - } - - # Add creator fee vault if creator is known - if token_info.creator: - accounts["creator_fee_vault"] = self.derive_creator_fee_vault( - token_info.creator - ) - - return accounts - - def get_wsol_account_creation_accounts( - self, user: Pubkey, wsol_account: Pubkey - ) -> dict[str, Pubkey]: - """Get accounts needed for WSOL account creation and initialization. - - Args: - user: User's wallet address - wsol_account: WSOL account to be created - - Returns: - Dictionary of account addresses for WSOL operations - """ - return { - "payer": user, - "wsol_account": wsol_account, - "wsol_mint": SystemAddresses.SOL_MINT, - "owner": user, - "system_program": SystemAddresses.SYSTEM_PROGRAM, - "token_program": SystemAddresses.TOKEN_PROGRAM, - "rent": SystemAddresses.RENT, - } diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py deleted file mode 100644 index ad84d3a..0000000 --- a/src/platforms/letsbonk/curve_manager.py +++ /dev/null @@ -1,250 +0,0 @@ -"""letsbonk.fun CurveManager: pool reads and pricing, IDL-decoded.""" - -from typing import Any - -from solders.pubkey import Pubkey - -from core.client import SolanaClient -from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS -from interfaces.core import CurveManager, Platform -from platforms.letsbonk.address_provider import LetsBonkAddressProvider -from utils.idl_parser import IDLParser -from utils.logger import get_logger - -logger = get_logger(__name__) - - -class LetsBonkCurveManager(CurveManager): - """LetsBonk (Raydium LaunchLab) implementation of CurveManager interface.""" - - def __init__(self, client: SolanaClient, idl_parser: IDLParser): - """Initialize LetsBonk curve manager with injected IDL parser. - - Args: - client: Solana RPC client - idl_parser: Pre-loaded IDL parser for LetsBonk platform - """ - self.client = client - self.address_provider = LetsBonkAddressProvider() - self._idl_parser = idl_parser - - logger.info("LetsBonk curve manager initialized with injected IDL parser") - - @property - def platform(self) -> Platform: - """Get the platform this manager serves.""" - return Platform.LETS_BONK - - async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]: - """Get the current state of a LetsBonk pool. - - Args: - pool_address: Address of the pool state account - - Returns: - Dictionary containing pool state data - """ - try: - account = await self.client.get_account_info(pool_address) - if not account.data: - raise ValueError(f"No data in pool state account {pool_address}") - - # Decode pool state using injected IDL parser - pool_state_data = self._decode_pool_state_with_idl(account.data) - - return pool_state_data - - except Exception as e: - logger.exception("Failed to get pool state") - raise ValueError(f"Invalid pool state: {e!s}") - - async def calculate_price(self, pool_address: Pubkey) -> float: - """Calculate current token price from pool state. - - Args: - pool_address: Address of the pool state - - Returns: - Current token price in SOL - """ - pool_state = await self.get_pool_state(pool_address) - - # Use virtual reserves for price calculation - virtual_base = pool_state["virtual_base"] - virtual_quote = pool_state["virtual_quote"] - - if virtual_base <= 0 or virtual_quote <= 0: - raise ValueError("Invalid reserve state") - - # Price = quote_reserves / base_reserves (how much SOL per token) - price_lamports = virtual_quote / virtual_base - price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL - - return price_sol - - async def calculate_buy_amount_out( - self, pool_address: Pubkey, amount_in: int - ) -> int: - """Calculate expected tokens received for a buy operation. - - Uses the constant product AMM formula. - - Args: - pool_address: Address of the pool state - amount_in: Amount of SOL to spend (in lamports) - - Returns: - Expected amount of tokens to receive (in raw token units) - """ - pool_state = await self.get_pool_state(pool_address) - - virtual_base = pool_state["virtual_base"] - virtual_quote = pool_state["virtual_quote"] - - # Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in) - numerator = amount_in * virtual_base - denominator = virtual_quote + amount_in - - if denominator == 0: - return 0 - - tokens_out = numerator // denominator - return tokens_out - - async def calculate_sell_amount_out( - self, pool_address: Pubkey, amount_in: int - ) -> int: - """Calculate expected SOL received for a sell operation. - - Uses the constant product AMM formula. - - Args: - pool_address: Address of the pool state - amount_in: Amount of tokens to sell (in raw token units) - - Returns: - Expected amount of SOL to receive (in lamports) - """ - pool_state = await self.get_pool_state(pool_address) - - virtual_base = pool_state["virtual_base"] - virtual_quote = pool_state["virtual_quote"] - - # Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in) - numerator = amount_in * virtual_quote - denominator = virtual_base + amount_in - - if denominator == 0: - return 0 - - sol_out = numerator // denominator - return sol_out - - async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]: - """Get current pool reserves. - - Args: - pool_address: Address of the pool state - - Returns: - Tuple of (base_reserves, quote_reserves) in raw units - """ - pool_state = await self.get_pool_state(pool_address) - return (pool_state["virtual_base"], pool_state["virtual_quote"]) - - def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]: - """Decode pool state data using injected IDL parser. - - Args: - data: Raw account data - - Returns: - Dictionary with decoded pool state - - Raises: - ValueError: If IDL parsing fails - """ - # Use injected IDL parser to decode PoolState account data - decoded_pool_state = self._idl_parser.decode_account_data( - data, "PoolState", skip_discriminator=True - ) - - if not decoded_pool_state: - raise ValueError("Failed to decode pool state with IDL parser") - - # Extract the fields we need for trading calculations - # Based on the PoolState structure from the IDL - pool_data = { - "virtual_base": decoded_pool_state.get("virtual_base", 0), - "virtual_quote": decoded_pool_state.get("virtual_quote", 0), - "real_base": decoded_pool_state.get("real_base", 0), - "real_quote": decoded_pool_state.get("real_quote", 0), - "status": decoded_pool_state.get("status", 0), - "supply": decoded_pool_state.get("supply", 0), - "creator": decoded_pool_state.get( - "creator" - ), # Creator pubkey (as base58 string) - "base_vault": decoded_pool_state.get("base_vault"), # Base vault pubkey - "quote_vault": decoded_pool_state.get("quote_vault"), # Quote vault pubkey - } - - # Calculate additional metrics - # Validate reserves are positive before calculating price - if pool_data["virtual_base"] <= 0: - raise ValueError( - f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price" - ) - if pool_data["virtual_quote"] <= 0: - raise ValueError( - f"Invalid virtual_quote: {pool_data['virtual_quote']} - cannot calculate price" - ) - - pool_data["price_per_token"] = ( - (pool_data["virtual_quote"] / pool_data["virtual_base"]) - * (10**TOKEN_DECIMALS) - / LAMPORTS_PER_SOL - ) - - logger.debug( - f"Decoded pool state: virtual_base={pool_data['virtual_base']}, " - f"virtual_quote={pool_data['virtual_quote']}, " - f"price={pool_data['price_per_token']:.8f} SOL" - ) - - return pool_data - - async def validate_pool_state_structure(self, pool_address: Pubkey) -> bool: - """Validate that the pool state structure matches IDL expectations. - - Args: - pool_address: Address of the pool state - - Returns: - True if structure is valid, False otherwise - """ - try: - pool_state = await self.get_pool_state(pool_address) - - required_fields = [ - "virtual_base", - "virtual_quote", - "real_base", - "real_quote", - ] - - for field in required_fields: - if field not in pool_state: - logger.error(f"Missing required field: {field}") - return False - - if not isinstance(pool_state[field], int): - logger.error( - f"Field {field} is not an integer: {type(pool_state[field])}" - ) - return False - - return True - - except Exception: - logger.exception("Pool state validation failed") - return False diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py deleted file mode 100644 index 316e844..0000000 --- a/src/platforms/letsbonk/event_parser.py +++ /dev/null @@ -1,377 +0,0 @@ -"""letsbonk.fun EventParser: IDL-based parsing of token creation events.""" - -import base64 -import struct -from time import monotonic -from typing import Any - -from solders.pubkey import Pubkey -from solders.transaction import VersionedTransaction - -from core.pubkeys import SystemAddresses -from interfaces.core import EventParser, Platform, TokenInfo -from platforms.letsbonk.address_provider import LetsBonkAddressProvider -from utils.idl_parser import IDLParser -from utils.logger import get_logger - -logger = get_logger(__name__) - - -class LetsBonkEventParser(EventParser): - """LetsBonk implementation of EventParser interface with IDL-based parsing.""" - - def __init__(self, idl_parser: IDLParser): - """Initialize LetsBonk event parser with injected IDL parser. - - Args: - idl_parser: Pre-loaded IDL parser for LetsBonk platform - """ - self.address_provider = LetsBonkAddressProvider() - self._idl_parser = idl_parser - - # Get all initialize instruction discriminators from injected IDL parser - # LetsBonk has multiple initialize variants: initialize, initialize_v2, initialize_with_token_2022 - discriminators = self._idl_parser.get_instruction_discriminators() - self._initialize_discriminator_bytes_list = [ - discriminators["initialize"], - discriminators["initialize_v2"], - discriminators["initialize_with_token_2022"], - ] - self._initialize_discriminators = { - struct.unpack(" Platform: - """Get the platform this parser serves.""" - return Platform.LETS_BONK - - def parse_token_creation_from_logs( - self, logs: list[str], signature: str - ) -> TokenInfo | None: - """Parse token creation from LetsBonk transaction logs. - - Args: - logs: List of log strings from transaction - signature: Transaction signature - - Returns: - TokenInfo if token creation found, None otherwise - """ - # LetsBonk doesn't emit specific logs for token creation like pump.fun - # Token creation is identified through instruction parsing - return None - - def parse_token_creation_from_instruction( - self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes] - ) -> TokenInfo | None: - """Parse token creation from LetsBonk instruction data using injected IDL parser. - - Args: - instruction_data: Raw instruction data - accounts: List of account indices - account_keys: List of account public keys - - Returns: - TokenInfo if token creation found, None otherwise - """ - # Check if instruction starts with any of the initialize discriminators - if not any( - instruction_data.startswith(disc_bytes) - for disc_bytes in self._initialize_discriminator_bytes_list - ): - return None - - try: - # Helper to get account key - def get_account_key(index): - if index >= len(accounts): - return None - account_index = accounts[index] - if account_index >= len(account_keys): - return None - return Pubkey.from_bytes(account_keys[account_index]) - - # Parse instruction data using injected IDL parser - decoded = self._idl_parser.decode_instruction( - instruction_data, account_keys, accounts - ) - # Accept any of the initialize instruction variants - if not decoded or decoded["instruction_name"] not in { - "initialize", - "initialize_v2", - "initialize_with_token_2022", - }: - return None - - # Determine token program based on instruction variant - instruction_name = decoded["instruction_name"] - is_token_2022 = instruction_name == "initialize_with_token_2022" - token_program_id = ( - SystemAddresses.TOKEN_2022_PROGRAM - if is_token_2022 - else SystemAddresses.TOKEN_PROGRAM - ) - - args = decoded.get("args", {}) - - # Extract MintParams from the decoded arguments - base_mint_param = args.get("base_mint_param", {}) - if not base_mint_param: - return None - - # IDL account order for `initialize`: 0 creator (signer), - # 1 creator_ata, 2 global_config, 3 platform_config, 4 creator, - # 5 pool_state, 6 base_mint, 7 quote_mint (WSOL), 8 base_vault, - # 9 quote_vault, then others. - - creator = get_account_key(0) # First signer account (creator) - global_config = get_account_key(2) # global_config account - platform_config = get_account_key(3) # platform_config account - pool_state = get_account_key(5) # pool_state account - base_mint = get_account_key(6) # base_mint account - base_vault = get_account_key(8) # base_vault account - quote_vault = get_account_key(9) # quote_vault account - - if not all( - [ - creator, - global_config, - platform_config, - pool_state, - base_mint, - base_vault, - quote_vault, - ] - ): - logger.debug( - f"Missing required accounts: creator={creator}, global_config={global_config}, " - f"platform_config={platform_config}, pool_state={pool_state}, base_mint={base_mint}, " - f"base_vault={base_vault}, quote_vault={quote_vault}" - ) - return None - - return TokenInfo( - name=base_mint_param.get("name", ""), - symbol=base_mint_param.get("symbol", ""), - uri=base_mint_param.get("uri", ""), - mint=base_mint, - platform=Platform.LETS_BONK, - pool_state=pool_state, - base_vault=base_vault, - quote_vault=quote_vault, - global_config=global_config, - platform_config=platform_config, - user=creator, - creator=creator, - token_program_id=token_program_id, - creation_timestamp=monotonic(), - ) - - except Exception as e: - logger.debug(f"Failed to parse initialize instruction: {e}") - return None - - def parse_token_creation_from_geyser( - self, transaction_info: Any - ) -> TokenInfo | None: - """Parse token creation from Geyser transaction data. - - Args: - transaction_info: Geyser transaction information - - Returns: - TokenInfo if token creation found, None otherwise - """ - try: - if not hasattr(transaction_info, "transaction"): - return None - - tx = transaction_info.transaction.transaction.transaction - msg = getattr(tx, "message", None) - if msg is None: - return None - - for ix in msg.instructions: - # Skip non-LetsBonk program instructions - program_idx = ix.program_id_index - if program_idx >= len(msg.account_keys): - continue - - program_id = msg.account_keys[program_idx] - if bytes(program_id) != bytes(self.get_program_id()): - continue - - # Check if it's the LetsBonk platform config account - has_platform_config = False - for acc_idx in ix.accounts: - if acc_idx < len(msg.account_keys): - acc_key = msg.account_keys[acc_idx] - if bytes(acc_key) == bytes( - self.address_provider.get_system_addresses()[ - "platform_config" - ] - ): - has_platform_config = True - break - - if not has_platform_config: - continue - - # Process instruction data - token_info = self.parse_token_creation_from_instruction( - ix.data, ix.accounts, msg.account_keys - ) - if token_info: - return token_info - - return None - - except Exception as e: - logger.debug(f"Failed to parse geyser transaction: {e}") - return None - - def get_program_id(self) -> Pubkey: - """Get the Raydium LaunchLab program ID this parser monitors. - - Returns: - Raydium LaunchLab program ID - """ - return self.address_provider.program_id - - def get_instruction_discriminators(self) -> list[bytes]: - """Get instruction discriminators for token creation. - - Returns: - List of discriminator bytes to match (all initialize variants) - """ - return self._initialize_discriminator_bytes_list - - def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: - """Parse token creation from block data (for block listener). - - Args: - block_data: Block data from WebSocket - - Returns: - TokenInfo if token creation found, None otherwise - """ - try: - if "transactions" not in block_data: - return None - - for tx in block_data["transactions"]: - if not isinstance(tx, dict) or "transaction" not in tx: - continue - - # Decode base64 transaction data if needed - tx_data = tx["transaction"] - if isinstance(tx_data, list) and len(tx_data) > 0: - try: - tx_data_encoded = tx_data[0] - tx_data_decoded = base64.b64decode(tx_data_encoded) - transaction = VersionedTransaction.from_bytes(tx_data_decoded) - - for ix in transaction.message.instructions: - program_id = transaction.message.account_keys[ - ix.program_id_index - ] - - # Check if instruction is from LetsBonk program - if str(program_id) != str(self.get_program_id()): - continue - - ix_data = bytes(ix.data) - - # Check for any initialize discriminator variant - if len(ix_data) >= 8: - discriminator = struct.unpack("= len(message["accountKeys"]): - continue - - program_id_str = message["accountKeys"][program_idx] - if program_id_str != str(self.get_program_id()): - continue - - # Decode instruction data - ix_data = base64.b64decode(ix["data"]) - - if len(ix_data) >= 8: - discriminator = struct.unpack("Platform: - """Get the platform this builder serves.""" - return Platform.LETS_BONK - - async def build_buy_instruction( - self, - token_info: TokenInfo, - user: Pubkey, - amount_in: int, - minimum_amount_out: int, - address_provider: AddressProvider, - ) -> list[Instruction]: - """Build buy instruction(s) for LetsBonk using buy_exact_in. - - Args: - user: User's wallet address - amount_in: Amount of SOL to spend (in lamports) - minimum_amount_out: Minimum tokens expected (raw token units) - address_provider: Platform address provider - - Returns: - List of instructions needed for the buy operation - """ - instructions = [] - - # Get all required accounts - accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - - # Determine token program to use - token_program_id = ( - token_info.token_program_id - if token_info.token_program_id - else SystemAddresses.TOKEN_2022_PROGRAM - ) - - # 1. Create idempotent ATA for base token - ata_instruction = create_idempotent_associated_token_account( - user, # payer - user, # owner - token_info.mint, # mint - token_program_id, # token program (dynamic for token2022 support) - ) - instructions.append(ata_instruction) - - # 2. Create WSOL account with seed (temporary account for the transaction) - wsol_seed = self._generate_wsol_seed(user) - wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed) - - # Account creation cost + amount to spend - account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE - total_lamports = amount_in + account_creation_lamports - - create_wsol_ix = create_account_with_seed( - CreateAccountWithSeedParams( - from_pubkey=user, - to_pubkey=wsol_account, - base=user, - seed=wsol_seed, - lamports=total_lamports, - space=TOKEN_ACCOUNT_SIZE, - owner=SystemAddresses.TOKEN_PROGRAM, - ) - ) - instructions.append(create_wsol_ix) - - # 3. Initialize WSOL account - initialize_wsol_ix = self._create_initialize_account_instruction( - wsol_account, SystemAddresses.SOL_MINT, user - ) - instructions.append(initialize_wsol_ix) - - # 4. Build buy_exact_in instruction with correct account ordering - buy_accounts = [ - AccountMeta(pubkey=user, is_signer=True, is_writable=False), # payer - AccountMeta( - pubkey=accounts_info["authority"], is_signer=False, is_writable=False - ), # authority - AccountMeta( - pubkey=accounts_info["global_config"], - is_signer=False, - is_writable=False, - ), # global_config - AccountMeta( - pubkey=accounts_info["platform_config"], - is_signer=False, - is_writable=False, - ), # platform_config - AccountMeta( - pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True - ), # pool_state - AccountMeta( - pubkey=accounts_info["user_base_token"], - is_signer=False, - is_writable=True, - ), # user_base_token - AccountMeta( - pubkey=wsol_account, is_signer=False, is_writable=True - ), # user_quote_token (WSOL account) - AccountMeta( - pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True - ), # base_vault - AccountMeta( - pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True - ), # quote_vault - AccountMeta( - pubkey=token_info.mint, is_signer=False, is_writable=False - ), # base_token_mint - AccountMeta( - pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False - ), # quote_token_mint - AccountMeta( - pubkey=accounts_info["base_token_program"], - is_signer=False, - is_writable=False, - ), # base_token_program - AccountMeta( - pubkey=accounts_info["quote_token_program"], - is_signer=False, - is_writable=False, - ), # quote_token_program - AccountMeta( - pubkey=accounts_info["event_authority"], - is_signer=False, - is_writable=False, - ), # event_authority - AccountMeta( - pubkey=accounts_info["program"], is_signer=False, is_writable=False - ), # program - ] - - # Add remaining accounts (required by the program for fee collection) - # These are not explicitly listed in IDL but required by the program - buy_accounts.append( - AccountMeta( - pubkey=accounts_info["system_program"], - is_signer=False, - is_writable=False, - ) - ) # #16: System Program - buy_accounts.append( - AccountMeta( - pubkey=accounts_info["platform_fee_vault"], - is_signer=False, - is_writable=True, - ) - ) # #17: Platform fee vault - if "creator_fee_vault" in accounts_info: - buy_accounts.append( - AccountMeta( - pubkey=accounts_info["creator_fee_vault"], - is_signer=False, - is_writable=True, - ) - ) # #18: Creator fee vault - - # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate - SHARE_FEE_RATE = 0 # No sharing fee - instruction_data = ( - self._buy_exact_in_discriminator - + struct.pack("list[Instruction]: - """Build sell instruction(s) for LetsBonk using sell_exact_in. - - Args: - user: User's wallet address - amount_in: Amount of tokens to sell (raw token units) - minimum_amount_out: Minimum SOL expected (in lamports) - address_provider: Platform address provider - - Returns: - List of instructions needed for the sell operation - """ - instructions = [] - - # Get all required accounts - accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - - # 1. Create WSOL account with seed (to receive SOL) - wsol_seed = self._generate_wsol_seed(user) - wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed) - - # Minimal account creation cost - account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE - - create_wsol_ix = create_account_with_seed( - CreateAccountWithSeedParams( - from_pubkey=user, - to_pubkey=wsol_account, - base=user, - seed=wsol_seed, - lamports=account_creation_lamports, - space=TOKEN_ACCOUNT_SIZE, - owner=SystemAddresses.TOKEN_PROGRAM, - ) - ) - instructions.append(create_wsol_ix) - - # 2. Initialize WSOL account - initialize_wsol_ix = self._create_initialize_account_instruction( - wsol_account, SystemAddresses.SOL_MINT, user - ) - instructions.append(initialize_wsol_ix) - - # 3. Build sell_exact_in instruction with correct account ordering - sell_accounts = [ - AccountMeta(pubkey=user, is_signer=True, is_writable=False), # payer - AccountMeta( - pubkey=accounts_info["authority"], is_signer=False, is_writable=False - ), # authority - AccountMeta( - pubkey=accounts_info["global_config"], - is_signer=False, - is_writable=False, - ), # global_config - AccountMeta( - pubkey=accounts_info["platform_config"], - is_signer=False, - is_writable=False, - ), # platform_config - AccountMeta( - pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True - ), # pool_state - AccountMeta( - pubkey=accounts_info["user_base_token"], - is_signer=False, - is_writable=True, - ), # user_base_token (tokens being sold) - AccountMeta( - pubkey=wsol_account, is_signer=False, is_writable=True - ), # user_quote_token (WSOL received) - AccountMeta( - pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True - ), # base_vault (receives tokens) - AccountMeta( - pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True - ), # quote_vault (sends WSOL) - AccountMeta( - pubkey=token_info.mint, is_signer=False, is_writable=False - ), # base_token_mint - AccountMeta( - pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False - ), # quote_token_mint - AccountMeta( - pubkey=accounts_info["base_token_program"], - is_signer=False, - is_writable=False, - ), # base_token_program - AccountMeta( - pubkey=accounts_info["quote_token_program"], - is_signer=False, - is_writable=False, - ), # quote_token_program - AccountMeta( - pubkey=accounts_info["event_authority"], - is_signer=False, - is_writable=False, - ), # event_authority - AccountMeta( - pubkey=accounts_info["program"], is_signer=False, is_writable=False - ), # program - ] - - # Add remaining accounts (required by the program for fee collection) - # These are not explicitly listed in IDL but required by the program - sell_accounts.append( - AccountMeta( - pubkey=accounts_info["system_program"], - is_signer=False, - is_writable=False, - ) - ) # #16: System Program - sell_accounts.append( - AccountMeta( - pubkey=accounts_info["platform_fee_vault"], - is_signer=False, - is_writable=True, - ) - ) # #17: Platform fee vault - if "creator_fee_vault" in accounts_info: - sell_accounts.append( - AccountMeta( - pubkey=accounts_info["creator_fee_vault"], - is_signer=False, - is_writable=True, - ) - ) # #18: Creator fee vault - - # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate - SHARE_FEE_RATE = 0 # No sharing fee - instruction_data = ( - self._sell_exact_in_discriminator - + struct.pack("list[Pubkey]: - """Get list of accounts required for buy operation (for priority fee calculation). - - Args: - user: User's wallet address - address_provider: Platform address provider - - Returns: - List of account addresses that will be accessed - """ - accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - - return [ - accounts_info["pool_state"], - accounts_info["user_base_token"], - accounts_info["base_vault"], - accounts_info["quote_vault"], - token_info.mint, - SystemAddresses.SOL_MINT, - accounts_info["program"], - ] - - def get_required_accounts_for_sell( - self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider - ) -> list[Pubkey]: - """Get list of accounts required for sell operation (for priority fee calculation). - - Args: - user: User's wallet address - address_provider: Platform address provider - - Returns: - List of account addresses that will be accessed - """ - accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - - return [ - accounts_info["pool_state"], - accounts_info["user_base_token"], - accounts_info["base_vault"], - accounts_info["quote_vault"], - token_info.mint, - SystemAddresses.SOL_MINT, - accounts_info["program"], - ] - - def _generate_wsol_seed(self, user: Pubkey) -> str: - """Generate a unique seed for WSOL account creation. - - Args: - user: User's wallet address - - Returns: - Unique seed string for WSOL account - """ - # Generate a unique seed based on timestamp and user pubkey - seed_data = f"{int(time.time())}{user!s}" - return hashlib.sha256(seed_data.encode()).hexdigest()[:32] - - def _create_initialize_account_instruction( - self, account: Pubkey, mint: Pubkey, owner: Pubkey - ) -> Instruction: - """Create an InitializeAccount instruction for the Token Program. - - Args: - account: The account to initialize - mint: The token mint - - Returns: - Instruction for initializing the account - """ - accounts = [ - AccountMeta(pubkey=account, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=owner, is_signer=False, is_writable=False), - AccountMeta( - pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False - ), - ] - - # InitializeAccount instruction discriminator (instruction 1 in Token Program) - data = bytes([1]) - - return Instruction( - program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts - ) - - def _create_close_account_instruction( - self, 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), - ] - - # CloseAccount instruction discriminator (instruction 9 in Token Program) - data = bytes([9]) - - return Instruction( - program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts - ) - - def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int: - """Get the recommended compute unit limit for LetsBonk buy operations. - - Args: - config_override: Optional override from configuration - - Returns: - Compute unit limit appropriate for buy operations - """ - if config_override is not None: - return config_override - # Buy operations: ATA creation + WSOL creation/init/close + buy instruction - return 150_000 - - def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int: - """Get the recommended compute unit limit for LetsBonk sell operations. - - Args: - config_override: Optional override from configuration - - Returns: - Compute unit limit appropriate for sell operations - """ - if config_override is not None: - return config_override - # Sell operations: WSOL creation/init/close + sell instruction - return 150_000 diff --git a/src/platforms/stonkfun/__init__.py b/src/platforms/stonkfun/__init__.py new file mode 100644 index 0000000..daa042c --- /dev/null +++ b/src/platforms/stonkfun/__init__.py @@ -0,0 +1,58 @@ +"""StonkFun: Raydium LaunchLab pools under StonkFun's two platform configs.""" + +from interfaces.core import Platform +from platforms.launchlab import ( + LaunchLabAddressProvider, + LaunchLabCurveManager, + LaunchLabEventParser, + LaunchLabInstructionBuilder, +) +from platforms.stonkfun.addresses import PLATFORM_CONFIGS + + +class StonkFunAddressProvider(LaunchLabAddressProvider): + """StonkFun addresses.""" + + PLATFORM_CONFIGS = PLATFORM_CONFIGS + + @property + def platform(self) -> Platform: + """StonkFun.""" + return Platform.STONK_FUN + + +class StonkFunInstructionBuilder(LaunchLabInstructionBuilder): + """StonkFun trades.""" + + @property + def platform(self) -> Platform: + """StonkFun.""" + return Platform.STONK_FUN + + +class StonkFunCurveManager(LaunchLabCurveManager): + """StonkFun pool reads.""" + + @property + def platform(self) -> Platform: + """StonkFun.""" + return Platform.STONK_FUN + + +class StonkFunEventParser(LaunchLabEventParser): + """StonkFun launches.""" + + PLATFORM_CONFIGS = PLATFORM_CONFIGS + + @property + def platform(self) -> Platform: + """StonkFun.""" + return Platform.STONK_FUN + + +__all__ = [ + "StonkFunAddressProvider", + "StonkFunCurveManager", + "StonkFunEventParser", + "StonkFunInstructionBuilder", +] diff --git a/src/platforms/stonkfun/addresses.py b/src/platforms/stonkfun/addresses.py new file mode 100644 index 0000000..c5f854c --- /dev/null +++ b/src/platforms/stonkfun/addresses.py @@ -0,0 +1,17 @@ +"""StonkFun's LaunchLab platform configs.""" + +from typing import Final + +from solders.pubkey import Pubkey + +# Standard coins carry no transfer fee; reward coins are Token-2022 mints with a +# 1% or 3% transfer fee, paid out to holders. +STANDARD_PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string( + "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7" +) +REWARD_PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string( + "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt" +) +PLATFORM_CONFIGS: Final[frozenset[Pubkey]] = frozenset( + {STANDARD_PLATFORM_CONFIG, REWARD_PLATFORM_CONFIG} +) diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 91440c3..d66a487 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -208,6 +208,9 @@ class PlatformAwareBuyer(Trader): # Read from the curve on the regular path, back-derived from the # fixed token count in extreme_fast_mode, which reads no curve. token_price_sol: float | None = None + # Read from chain on the regular path only; extreme_fast_mode sizes + # without it. + pool_state: dict = {} if self.extreme_fast_mode: # Zero-RPC hot path: when the CreateEvent carried the canonical @@ -275,12 +278,25 @@ class PlatformAwareBuyer(Trader): f"zero/invalid price" ) token_amount = quote_amount / token_price_sol + # LaunchLab takes its curve fee from the spend and, on a reward + # coin, withholds the transfer fee from the tokens delivered. + # Both are known, so leave them out of the expected amount + # rather than spending slippage tolerance on them. + token_amount *= 1 - pool_state.get("fee_fraction", 0.0) + token_amount *= 1 - pool_state.get("transfer_fee_bps", 0) / 10_000 minimum_token_amount = token_amount * (1 - self.slippage) minimum_token_amount_raw = int(minimum_token_amount * 10**TOKEN_DECIMALS) - # Max spend with slippage, in the quote mint's raw units. - max_quote_amount_raw = int(quote_amount * quote_unit * (1 + self.slippage)) + # An exact-in program spends all of amount_in, so it gets the + # configured amount; otherwise amount_in is a ceiling padded for + # slippage. In the quote mint's raw units either way. + if getattr(instruction_builder, "spends_exact_amount_in", False): + max_quote_amount_raw = int(quote_amount * quote_unit) + else: + max_quote_amount_raw = int( + quote_amount * quote_unit * (1 + self.slippage) + ) instructions = await instruction_builder.build_buy_instruction( token_info, @@ -407,9 +423,8 @@ class PlatformAwareBuyer(Trader): if token_info.platform == Platform.PUMP_FUN: if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: return token_info.bonding_curve - elif token_info.platform == Platform.LETS_BONK: - if hasattr(token_info, "pool_state") and token_info.pool_state: - return token_info.pool_state + elif token_info.pool_state: + return token_info.pool_state return address_provider.derive_pool_address(token_info.mint) @@ -506,17 +521,21 @@ class PlatformAwareBuyer(Trader): # The quote asset decides which balance is spent and how amounts scale, # so it must come from the curve rather than a listener guess. _refresh_quote_mint(token_info, pool_state) + token_info.transfer_fee_bps = pool_state.get( + "transfer_fee_bps", token_info.transfer_fee_bps + ) fresh_creator = pool_state.get("creator") - if fresh_creator and hasattr(address_provider, "derive_creator_vault"): + if fresh_creator: new_creator = ( Pubkey.from_string(fresh_creator) if isinstance(fresh_creator, str) else fresh_creator ) token_info.creator = new_creator - token_info.creator_vault = address_provider.derive_creator_vault( - new_creator - ) + if hasattr(address_provider, "derive_creator_vault"): + token_info.creator_vault = address_provider.derive_creator_vault( + new_creator + ) self._apply_token_program(token_info, fresh_token_program, address_provider) return None @@ -565,7 +584,7 @@ class PlatformAwareBuyer(Trader): ) -> Pubkey: """Get the address where SOL is sent during a buy transaction. - pump.fun: the bonding curve. letsbonk: the quote_vault (WSOL vault). + pump.fun: the bonding curve. LaunchLab platforms: the pool's quote vault. Args: address_provider: Platform-specific address provider @@ -580,10 +599,8 @@ class PlatformAwareBuyer(Trader): if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: return token_info.bonding_curve return address_provider.derive_pool_address(token_info.mint) - elif token_info.platform == Platform.LETS_BONK: - if hasattr(token_info, "quote_vault") and token_info.quote_vault: - return token_info.quote_vault - return address_provider.derive_quote_vault(token_info.mint) + elif token_info.quote_vault: + return token_info.quote_vault raise NotImplementedError( f"SOL destination not implemented for platform {token_info.platform.value}. " @@ -674,6 +691,10 @@ class PlatformAwareSeller(Trader): # Fall back to the listener's quote asset if the refresh fails. quote_mint = normalize_quote_mint(token_info.quote_mint) + # Protocol deductions on a sell, known up front: LaunchLab withholds + # the transfer fee on the way into the pool and takes its curve fee + # from the quote side. Zero where the pool state carries neither. + sell_deductions = (0.0, 0) # The sell account list is 16 (non-cashback) vs 17 (cashback) and # fee_recipient differs in mayhem mode; both can change between buy @@ -694,6 +715,10 @@ class PlatformAwareSeller(Trader): "is_cashback_coin", token_info.is_cashback_coin ) quote_mint = _refresh_quote_mint(token_info, pool_state) + sell_deductions = ( + pool_state.get("fee_fraction", 0.0), + pool_state.get("transfer_fee_bps", 0), + ) # The program may delegate BC.creator to a PFEE-owned PDA after # the initial creator buy, so the create-time vault on # token_info goes stale before the sell lands and the sell @@ -708,9 +733,10 @@ class PlatformAwareSeller(Trader): else fresh_creator ) token_info.creator = new_creator - token_info.creator_vault = address_provider.derive_creator_vault( - new_creator - ) + if hasattr(address_provider, "derive_creator_vault"): + token_info.creator_vault = ( + address_provider.derive_creator_vault(new_creator) + ) except Exception as e: # noqa: BLE001 logger.warning( f"Could not refresh curve flags before sell ({e}); " @@ -741,7 +767,13 @@ class PlatformAwareSeller(Trader): # Calculate expected quote output with slippage protection, in the # quote mint's raw units. - expected_quote_output = token_balance_decimal * token_price_sol + fee_fraction, transfer_fee_bps = sell_deductions + expected_quote_output = ( + token_balance_decimal + * (1 - transfer_fee_bps / 10_000) + * token_price_sol + * (1 - fee_fraction) + ) min_quote_output = max( 1, int((expected_quote_output * (1 - self.slippage)) * quote_unit), @@ -845,9 +877,8 @@ class PlatformAwareSeller(Trader): if token_info.platform == Platform.PUMP_FUN: if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: return token_info.bonding_curve - elif token_info.platform == Platform.LETS_BONK: - if hasattr(token_info, "pool_state") and token_info.pool_state: - return token_info.pool_state + elif token_info.pool_state: + return token_info.pool_state # Fallback to deriving the address using platform provider return address_provider.derive_pool_address(token_info.mint) diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index f4643f6..db4650e 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -160,6 +160,7 @@ class UniversalTrader: # Quote asset configuration (pump.fun non-SOL pairs) quote_amounts: dict[str, float] | None = None, allowed_quote_mints: list[str] | None = None, + max_transfer_fee_bps: int | None = None, # Exit strategy configuration exit_strategy: str = "time_based", take_profit_percentage: float | None = None, @@ -235,6 +236,9 @@ class UniversalTrader: self.quote_amounts, self.allowed_quote_mints = _resolve_quote_config( buy_amount, quote_amounts, allowed_quote_mints ) + # None takes every coin; a LaunchLab reward coin's transfer fee is paid + # on the buy and again on the sell. + self.max_transfer_fee_bps = max_transfer_fee_bps # Create platform-aware traders self.buyer, self.seller = ( @@ -436,13 +440,17 @@ class UniversalTrader: nonlocal found_token token_key = str(token.mint) - # Only process if not already processed and fresh - if token_key not in self.processed_tokens: - # Record when the token was discovered - self.token_timestamps[token_key] = monotonic() - found_token = token - self.processed_tokens.add(token_key) - token_found.set() + if token_key in self.processed_tokens: + return + self.processed_tokens.add(token_key) + skip_reason = self._skip_reason(token) + if skip_reason: + logger.info(f"Skipping {token.symbol} - {skip_reason}; still waiting") + return + # Record when the token was discovered + self.token_timestamps[token_key] = monotonic() + found_token = token + token_found.set() listener_task = asyncio.create_task( self.token_listener.listen_for_tokens( @@ -606,6 +614,27 @@ class UniversalTrader: if took_item: self.token_queue.task_done() + def _skip_reason(self, token_info: TokenInfo) -> str | None: + """Why the configured filters refuse this coin, or None to trade it. + + Checked on detection as well as before the buy: single-token mode would + otherwise spend its one token on a coin it was never going to buy. + """ + quote_mint = normalize_quote_mint(token_info.quote_mint) + if self.allowed_quote_mints is not None and quote_mint not in self.allowed_quote_mints: + return f"quote mint {quote_mint} not in allowed_quote_mints" + if quote_mint not in self.quote_amounts: + return f"no buy amount configured for quote mint {quote_mint}" + if ( + self.max_transfer_fee_bps is not None + and token_info.transfer_fee_bps > self.max_transfer_fee_bps + ): + return ( + f"transfer fee {token_info.transfer_fee_bps} bps exceeds " + f"max_transfer_fee_bps {self.max_transfer_fee_bps}" + ) + return None + async def _handle_token(self, token_info: TokenInfo) -> None: """Handle a new token creation event.""" try: @@ -616,23 +645,11 @@ class UniversalTrader: ) return - # Cheaper to drop an unconfigured quote asset here than on-chain. + skip_reason = self._skip_reason(token_info) + if skip_reason: + logger.info(f"Skipping {token_info.symbol} - {skip_reason}") + return token_quote_mint = normalize_quote_mint(token_info.quote_mint) - if ( - self.allowed_quote_mints is not None - and token_quote_mint not in self.allowed_quote_mints - ): - logger.info( - f"Skipping {token_info.symbol} - quote mint {token_quote_mint} " - f"not in allowed_quote_mints" - ) - return - if token_quote_mint not in self.quote_amounts: - logger.info( - f"Skipping {token_info.symbol} - no buy amount configured for " - f"quote mint {token_quote_mint}" - ) - return # Wait for pool/curve to stabilize (unless in extreme fast mode) if not self.extreme_fast_mode: diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py index d851c1d..6177872 100644 --- a/src/utils/idl_manager.py +++ b/src/utils/idl_manager.py @@ -28,6 +28,7 @@ class IDLManager: # Define IDL paths for each platform self._idl_paths = { Platform.LETS_BONK: project_root / "idl" / "raydium_launchlab_idl.json", + Platform.STONK_FUN: project_root / "idl" / "raydium_launchlab_idl.json", Platform.PUMP_FUN: project_root / "idl" / "pump_fun_idl.json", } diff --git a/tests/regression/raw_stonkfun_launches_from_gettransaction.json b/tests/regression/raw_stonkfun_launches_from_gettransaction.json new file mode 100644 index 0000000..9c0de96 --- /dev/null +++ b/tests/regression/raw_stonkfun_launches_from_gettransaction.json @@ -0,0 +1,2180 @@ +[ + { + "transaction": { + "signatures": [ + "2FFqEKPYpMQfMqJyT5QUopqZqtLNUKKub5LhdsRTwDussUSdVGBB11uGBFU2B3pveuRjQZGaE7bsHVr6eEWXWhcw", + "5JAvff8a3ofe6shxrA1ZNAsE2LS7m1opZ3MFiA4wum1owvinWvAYLLf82Pz6eb958pA3Rm2wFugzcuyTXPiRZvgT" + ], + "message": { + "header": { + "numRequiredSignatures": 2, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 13 + }, + "accountKeys": [ + "9ePSmJyUxsXcWcjxeUpqJSxq3R65MvuFuLPkSWvU43TX", + "J2V7tRwJqwr9a1gpL3VMQRuJVbebcH2YbvNezfiMhGUr", + "3h1dPNcWXyCA7k35WzwJvTsFLfGd2PJxgzMdKbArwDFW", + "3PXSXWnjmkEdmnFbakEw13QDpGzvs2mkxvTTgb7q5m3r", + "53PrcuNbuYHGh9g3iW7ugdkxaFjF938M5Cwi5SbzwJjQ", + "5eHSkCVRtkxuS3QD9CUsgMR2H5iqDvdBx6yeHFCoycZ9", + "AmGcpyjsHR19mRYVHqtbc8xhnGFGSraWsdbBeXUzm8Dp", + "DgFe7zmq6x3AvJCVVgEJKQ97D73MRfdchLyQMEGhi6eT", + "kqnHSBPiWDYdE5FaQynUo5GxNzMEfGndJ5kP3rRFCRB", + "11111111111111111111111111111111", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr", + "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7", + "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX", + "9emka7JGUqvjzXvd95KQTPBEdBU6Ds43f238jXhBvUmN", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "ComputeBudget111111111111111111111111111111", + "GvAsQKFETH8YwRWxAYLFUy6L83frgHRsnw1FwC1asmJJ", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "So11111111111111111111111111111111111111112", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh" + ], + "recentBlockhash": "9D9fexPkQnFAc69oFgBHA1FSBPGys1dUymKuzei6DvCC", + "instructions": [ + { + "programIdIndex": 15, + "accounts": [], + "data": "JzwPro", + "stackHeight": 1 + }, + { + "programIdIndex": 15, + "accounts": [], + "data": "3Sy41WEwNLnT", + "stackHeight": 1 + }, + { + "programIdIndex": 17, + "accounts": [ + 0, + 0, + 12, + 11, + 21, + 7, + 1, + 18, + 3, + 8, + 20, + 19, + 9, + 10, + 17, + 16, + 13 + ], + "data": "2hfEDAjTUtm3vPSUgSrRwKHCaQwmoAe7JzorSFNdAV6Js8HxQw9J6g9bUZbKZJn1dawCXHbQonWgSpmPgD2SdH9c6v3AqBrcJpLgRC5SBJEwR26jPp5ZfquWKuu4ymYfmQbBdguYzErhk3bfXsw19XCgZ4CdQcze5zairoso8EaxoinmastFJPuFRXBQM9Kt6FroTYnmdmvmWG29CcEK4QD2RxJQCJK", + "stackHeight": 1 + }, + { + "programIdIndex": 14, + "accounts": [ + 0, + 5, + 0, + 1, + 9, + 20 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 14, + "accounts": [ + 0, + 6, + 0, + 18, + 9, + 19 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 6 + ], + "data": "3Bxs3zwhE1jnACsh", + "stackHeight": 1 + }, + { + "programIdIndex": 19, + "accounts": [ + 6 + ], + "data": "J", + "stackHeight": 1 + }, + { + "programIdIndex": 17, + "accounts": [ + 0, + 21, + 12, + 11, + 7, + 5, + 6, + 3, + 8, + 1, + 18, + 20, + 19, + 10, + 17, + 9, + 4, + 2 + ], + "data": "HtTvTxyWwMDLwXbUc2xc36NcgHRrBHunNoDFQSCB58YX", + "stackHeight": 1 + } + ] + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 40000, + "preBalances": [ + 455363864, + 0, + 1488440, + 0, + 13707356, + 0, + 0, + 0, + 0, + 1, + 0, + 7461120, + 3473651, + 3285120, + 3388612899, + 1, + 0, + 37580031, + 1849199403457, + 200653906, + 70228638, + 0 + ], + "postBalances": [ + 43843064, + 2672080, + 1488440, + 1488440, + 17707356, + 1513840, + 1488440, + 2829560, + 397488440, + 1, + 0, + 7461120, + 3473651, + 3285120, + 3388612899, + 1, + 0, + 37580031, + 1849199403457, + 200653906, + 70228638, + 0 + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 9, + "accounts": [ + 0, + 7 + ], + "data": "1111A71TsJGSaa1UiQSKZ4yVqL1DpygmcfSsvo69sdiGM5Ks9nrBUto1igVGBvDynJ3ovh", + "stackHeight": 2 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 8 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6po1LhhgEJXngDjhxmUTshDA8EHdXaMJUsXdMg3PE", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 8, + 18 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 17, + "accounts": [ + 10 + ], + "data": "BYvyNscVgFryjwkNVux2yCyQ9JSJyRV1BjZueHty4Ndc8ck1mGw65rZbzWG7Rt2nzPdxC8oimdUWDzoRie8J45QA3SFitGc1UTnHh12korNjgSiN9Sb3SrS5rKLSTxDytnzJEicnG4DK2tWRBBSKCfSDGGCpNV86WHyKxLrrwAzWRanoNayJGZCojmmDustAKH4u6me5Cf3mpsrLdv9mttC4vLbD4HafAwQidRXEwbDivXX4aznntYgJ3vSsw1LHAWbiP3jMLFcUJoJnuMQDoWKgjtWUoHSmf7aNZqxqocezk1AwVRBf46rtxY6vVrW6G2wVC8UE3uoAfRGSq9C5SB2Kjh22qM", + "stackHeight": 2 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 1 + ], + "data": "111157R57fqNzgMjCi7yd8cD1yQhPAwhedzS6uysMaAGbDUZUYiji1TswMPKaASzZW6nQT", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 1 + ], + "data": "GC3i2jSx8awwe9xBfbR9LBTrzgFPUE9b56kGnez3cWMeYXSo4R6qEon4omYizeAaHFKb4CqpyJTVhUUVtNvrD4Tn4A", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 1 + ], + "data": "2zt6gyV18w3CBL8Tv57G9RzANS3VMJyjznTCcauvNDJT4JVD", + "stackHeight": 2 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 1 + ], + "data": "3Bxs4H2sxF3bsTmq", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 1, + 21, + 1, + 21 + ], + "data": "9CMBBZmPMXrKueTdeG9CFffhYuwV8JWz5gA9yMhyvf2wYPPJ8JQ7KgVUH6yCxsUQpspVt1mb55Mqv3UWHFY7DQaPgr1fUAy4zW76mndWv5YAiLCRiYW6HCf4RrBGMimkaowPUKbyt", + "stackHeight": 2 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 3 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6poF6eUPB5PTNmp7193deYLtFuPpthPgpJkQGEsQf", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 3, + 1 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 1, + 3, + 21 + ], + "data": "6ApXSNCamGdm", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 1, + 21 + ], + "data": "31tb", + "stackHeight": 2 + } + ] + }, + { + "index": 3, + "instructions": [ + { + "programIdIndex": 20, + "accounts": [ + 1 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 5 + ], + "data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 5 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 5, + 1 + ], + "data": "6Vn67hWWHFioSn6qUje2AnRQDYJftmgAxcFZbw6cHMBtV", + "stackHeight": 2 + } + ] + }, + { + "index": 4, + "instructions": [ + { + "programIdIndex": 19, + "accounts": [ + 18 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 6 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6po1LhhgEJXngDjhxmUTshDA8EHdXaMJUsXdMg3PE", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 6 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 6, + 18 + ], + "data": "6Vn67hWWHFioSn6qUje2AnRQDYJftmgAxcFZbw6cHMBtV", + "stackHeight": 2 + } + ] + }, + { + "index": 7, + "instructions": [ + { + "programIdIndex": 17, + "accounts": [ + 10 + ], + "data": "EwDfpErTWwQhCAycT1hw3kx1ib7q1Ai91G1rZpVSCmxtPJZnawXeeFcA6qWNReXyqa8Evy1Ejit4jK9yrV6rZQyipBFQuj9jMR8WAxf6DcrvUXVfWipeFv24CBeZ4wuGLdFX96RD4sGF19Svx5SGUPRtskegw2hbk69HPxBprAifgRmiKD1bzStu41Dn4SbU3nZuCguyobXVYQMPaAR6", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 6, + 18, + 8, + 0 + ], + "data": "g7LBEyrpJ9FPe", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 3, + 1, + 5, + 21 + ], + "data": "hA1h3rnWSxa2Z", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 8, + 18, + 4, + 21 + ], + "data": "g6yW9ZhChU1ig", + "stackHeight": 2 + } + ] + } + ], + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: InitializeWithToken2022", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 229 of 576629 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 551655 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: l9fiCXahc668WnK8H/XQnJnnVbPmn99tjTGeFKYydoanNtaQ27SPuIBxXPORC0FoTfnhiU+stSMx6Gfv2xk4GkfFPpeJg9hSVxqOAcjfeCD51ms8c2W40eSvqBt4VMwu91zvWL0Ihn4GCAAAAFNwYWNlWFNJAwAAAFhTSUUAAABodHRwczovL2dhdGV3YXkuaXJ5cy54eXovNTlFQ2dFTHBVZUFWSkdnUTlleTZ3ZnVvWGU4QTZZV3VLUVhxYlJqTmo0amsAAIDGpH6NAwAAeMX7UdECAAASZcoTAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: MetadataPointerInstruction::Initialize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 925 of 545366 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeMint2", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1772 of 543057 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: TokenMetadataInstruction: Initialize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3352 of 535746 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2108 of 527015 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: MintTo", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1707 of 511221 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: SetAuthority", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1044 of 507847 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 95186 of 599700 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: GetAccountDataSize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1523 of 497754 compute units", + "Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeImmutableOwner", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 491409 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2153 of 488340 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 18610 of 504514 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 179 of 479053 compute units", + "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 37 of 473964 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 229 of 471503 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 14913 of 485904 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 201 of 470841 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: BuyExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 442331 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe68WnK8H/XQnJnnVbPmn99tjTGeFKYydoanNtaQ27SPuAB4xftR0QIA3nQOPunPAwDXrzD8BgAAAAAAAAAAAAAAAAAAAAAAAABStqOhrgwAAMA4ixcAAAAAAITXFwAAAABStqOhrgwAAEBCDwAAAAAAAAk9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 112 of 438799 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2442 of 436629 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 112 of 428273 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 45188 of 470640 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success" + ], + "preTokenBalances": [ + { + "accountIndex": 2, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 4, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 0.011851787, + "decimals": 9, + "amount": "11851787", + "uiAmountString": "0.011851787" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "postTokenBalances": [ + { + "accountIndex": 2, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 3, + "mint": "J2V7tRwJqwr9a1gpL3VMQRuJVbebcH2YbvNezfiMhGUr", + "uiTokenAmount": { + "uiAmount": 986055824.296366, + "decimals": 6, + "amount": "986055824296366", + "uiAmountString": "986055824.296366" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 0.015851787, + "decimals": 9, + "amount": "15851787", + "uiAmountString": "0.015851787" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 5, + "mint": "J2V7tRwJqwr9a1gpL3VMQRuJVbebcH2YbvNezfiMhGUr", + "uiTokenAmount": { + "uiAmount": 13944175.703634, + "decimals": 6, + "amount": "13944175703634", + "uiAmountString": "13944175.703634" + }, + "owner": "9ePSmJyUxsXcWcjxeUpqJSxq3R65MvuFuLPkSWvU43TX", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 6, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9ePSmJyUxsXcWcjxeUpqJSxq3R65MvuFuLPkSWvU43TX", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 8, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 0.396, + "decimals": 9, + "amount": "396000000", + "uiAmountString": "0.396" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [] + }, + "computeUnitsConsumed": 174548, + "costUnits": 179760 + }, + "version": "legacy", + "slot": 453217184, + "transactionIndex": 132, + "blockTime": 1791106392 + }, + { + "transaction": { + "signatures": [ + "4FqXvdwKnch1JHoViWFqQra7vKHGV8h3XqkAV6dLVyWBN7pSM9ms4uRLqX8QDvyWKQiG9kdNbfruPXnnt8CsXBfE", + "3Yx7FFwLqqw2Tx7jr42pLBi7hVJUyApaVPNRxV6KPatoK5w49FdbMTMk3N11Y7V2Ba6BrzsyuJkx4QERLWYeLQdx" + ], + "message": { + "header": { + "numRequiredSignatures": 2, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 8 + }, + "accountKeys": [ + "FkKXAcfrso5t53QVxnKzHQaypDpCc7jQF2qpZENhYz2A", + "E3myentprJHSWdUvD4LDS3A48QMnUNQVUaHwSmDNsm7k", + "8S5qhNkfYHmFzhxDQhSPRxBcKgGTEh3oytRi8YDiXgd4", + "4q9JphKCvswFJNVNbXJrqx2w1Y9ixENUfQerqtWR6qhC", + "F2vzerkGnvwRHhL3Gq8y9KDhCE2eYs8TDbiWnB8xTmKg", + "CvGqLK7Z1pWPkpAJemSc7eZfP8su8hekFsqqGLvTcMba", + "DAZBdCzr5CtpJd54vzk5oAmPJGvbYQoz321DXMfNYm3u", + "21QAak2hbDR4VGQBPr5Y8sxF7bbzqxkkDnA1rrJFz9Dn", + "Gqn4iqqHuLNF5C2q9U9LMuBpufAfAoSw2p63aynJPEvd", + "AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum", + "ComputeBudget111111111111111111111111111111", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "7QV2swQrGvArbdrLFmg2861hdCZLEKBNiM77j4hvvE1g", + "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7", + "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "11111111111111111111111111111111", + "9NBdYh5YDHpJGNGfjby4xZP53RrXdwTrDPxjpe7k1SNJ", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + ], + "recentBlockhash": "6PWUswQV2ytFX4HJejBVsTHjuVRWt5b1NTJgCnJKX5ap", + "instructions": [ + { + "programIdIndex": 10, + "accounts": [], + "data": "HNHdmh", + "stackHeight": 1 + }, + { + "programIdIndex": 10, + "accounts": [], + "data": "3dgRf8s6ueV5", + "stackHeight": 1 + }, + { + "programIdIndex": 11, + "accounts": [ + 0, + 0, + 12, + 13, + 18, + 2, + 1, + 14, + 3, + 4, + 19, + 20, + 15, + 21, + 11, + 16 + ], + "data": "eUFYzg8tjKYZ1wvGQgjCudEG9SVFwPTC9KsQhYjdcTuGauiQBLLrwqKtjVDA3Lb3tfXRv16Dnsb4dSKwMHLyf8aMXFqFhhwFaWEMrdGXSpkriAtrM3ng79aqTCLcf7xkoNxghnJUkUuxmqjak5qQj5H4TdjSyYEZHqvumVBa2eubzBKMTPWYuaHD", + "stackHeight": 1 + }, + { + "programIdIndex": 17, + "accounts": [ + 0, + 5, + 0, + 1, + 15, + 19 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 17, + "accounts": [ + 0, + 6, + 0, + 14, + 15, + 20 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 11, + "accounts": [ + 0, + 18, + 12, + 13, + 2, + 5, + 6, + 3, + 4, + 1, + 14, + 19, + 20, + 21, + 11, + 15, + 7, + 8 + ], + "data": "HtTvTxyWwMDTTePXDgMAGBFcmGS8Hz4VkDnApxW8DBuZ", + "stackHeight": 1 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 9 + ], + "data": "3Bxs4Bc3VYuGVB19", + "stackHeight": 1 + } + ], + "addressTableLookups": [ + { + "accountKey": "8UPRBJZEWvTTVfRHseNvfduJc6fkrM7UcAxLLHD52ubA", + "writableIndexes": [], + "readonlyIndexes": [ + 30, + 2, + 1, + 31 + ] + } + ] + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 310000, + "preBalances": [ + 602873503, + 0, + 0, + 0, + 0, + 0, + 1488440, + 1855569, + 0, + 10302805698, + 1, + 37580031, + 3160167, + 7461120, + 583528697, + 1, + 2397760, + 3388612899, + 0, + 70228638, + 200653906, + 0 + ], + "postBalances": [ + 590224943, + 2529840, + 2829560, + 1488440, + 1488440, + 1513840, + 1488440, + 1855569, + 1488440, + 10303805698, + 1, + 37580031, + 3160167, + 7461120, + 583528697, + 1, + 2397760, + 3388612899, + 0, + 70228638, + 200653906, + 0 + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 15, + "accounts": [ + 0, + 2 + ], + "data": "1111A71TsJGSaa1UiQSKZ4yVqL1DpygmcfSsvo69sdiGM5Ks9nrBUto1igVGBvDynJ3ovh", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 4 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6po1LhhgEJXngDjhxmUTshDA8EHdXaMJUsXdMg3PE", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 4, + 14 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 11, + "accounts": [ + 21 + ], + "data": "51QXw9Nm2rSawmhi6ghXkMq8gX3hkzwSUm9jwR48ZpcqQdM2iLTALk35tgdZpWgaZJhknVnL8pHW39gSpq4VpknVo3BdjjZA557TR6WCh8C2eRqZKAkraSAiXRCvoBTGyjdyDSNrAi2a9kvDtRZJmY7zCpusR6rN1b5QJwuhNBYMSsW5xS77n8LPHS84wEdBFS1A47mQjfW11E4iq9AacB1xcfZBVzhTyaxDG3WSkpJU2STWAitz5Ty9D29rLBJjhDWCJJohBe29B6YqaqoHGUNzoWZwViXcDeuehspkiJdL11fsR8qEXGxo", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 1 + ], + "data": "111157R57fqNzgMjCi7yd8cD1yQhPAwhedzS6uysMaAGbDUZUYiji1TswMPKaASzZW6nQT", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 1 + ], + "data": "GC3i2jSx8awwe9xBfbR9LBTrzgFPUE9b56kGnez3cWMeYXNpMGsCCKnRF4VLnk3b87MCJxUdgVKb5RM9ofNwi5eGh4", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 1 + ], + "data": "2zt6gyV18w3CBL8Tv57G9RzANS3VMJyjznTCcauvNDJT4JVD", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 1 + ], + "data": "3Bxs4Z4XoSUkiXeo", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 1, + 18, + 1, + 18 + ], + "data": "47XuYZuV6mhDDwS5NzbMvJNtoGp1CueadZopFzHpE5MBwZ71UMBezxBdKxu7Ux2aTjsUmFf1ZfMd6rFHkNGEsFhzs8DgZzcAhJS", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 3 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6poF6eUPB5PTNmp7193deYLtFuPpthPgpJkQGEsQf", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 3, + 1 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 1, + 3, + 18 + ], + "data": "6ApXSNCamGdm", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 1, + 18 + ], + "data": "31tb", + "stackHeight": 2 + } + ] + }, + { + "index": 3, + "instructions": [ + { + "programIdIndex": 19, + "accounts": [ + 1 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 5 + ], + "data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 5 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 5, + 1 + ], + "data": "6bt2C6pCfAeMiLXW23wXKmP2NK7Q21s16wx1g3p4Wr8T8", + "stackHeight": 2 + } + ] + }, + { + "index": 5, + "instructions": [ + { + "programIdIndex": 11, + "accounts": [ + 21 + ], + "data": "EwDfpErTWwQhCAycT1hw3koHYCuBXBFAxHxSyLd3bNsMaFWrq4u6n9WmXXf6VypR1MfrfJJX2MmbAL1Pg6Cjo64nEZa7zJZeNXswBpFV692Rqzg5Dqet1geV7y19EkDdvBYfekcFg7KYexJouNoFeJGu8zgpdaQXhvzCiKWeWUPG7GbPfVBo17Py1chc7i2sgPUdyqtJRxjHF6WtAsJg", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 6, + 14, + 4, + 0 + ], + "data": "h28F9gsXLkrxA", + "stackHeight": 2 + }, + { + "programIdIndex": 19, + "accounts": [ + 3, + 1, + 5, + 18 + ], + "data": "jKTzmMW1L2Y5j", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 8 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6po1LhhgEJXngDjhxmUTshDA8EHdXaMJUsXdMg3PE", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 8, + 14 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 8, + 18 + ], + "data": "bmb6c2NQ5ec2mNP5VePDnyirBTgi4bHDQxocrj1dkzuE9Hg", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 4, + 14, + 7, + 18 + ], + "data": "ipYAv7JHXDaVG", + "stackHeight": 2 + } + ] + } + ], + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: InitializeWithToken2022", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 233 of 1178288 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 1159550 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: l9fiCXahc65ubs0Elu8VOshpNxisTFnqRsvNMjtqCLyIHzK19iEkp9sciFIVyt6ShNVT/LLPYce8bj8D4i+MaPPKg9r5fdz/XyotLgzmyLOGjjg7xFKDHovn6A+G+hq5JK9WsuaqgFMGCQAAAE9SRSBQYXJ0eQUAAABQQVJUWSYAAABodHRwOi8vMjAxLjc4LjE0My4xNjozMDAwL20vakdaQVh2WEpKSwAAgMakfo0DAAB4xftR0QIAmjybkgkHAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: MetadataPointerInstruction::Initialize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 925 of 1153289 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeMint2", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1772 of 1150980 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: TokenMetadataInstruction: Initialize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3408 of 1143669 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2108 of 1134882 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: MintTo", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1707 of 1118406 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: SetAuthority", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1044 of 1115032 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 87960 of 1199700 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: GetAccountDataSize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1523 of 1106480 compute units", + "Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeImmutableOwner", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 1100135 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2153 of 1097066 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 17110 of 1111740 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 4338 of 1094630 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: BuyExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 1061943 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe5ubs0Elu8VOshpNxisTFnqRsvNMjtqCLyIHzK19iEkpwB4xftR0QIABXEOPunPAwBwvgTeewIAAAAAAAAAAAAAAAAAAAAAAAD8pNO6MS8AAP1zCFAgAAAASAe+uCAAAAD8pNO6MS8AAHYd8RQAAAAA1XXEUwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 1058411 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2442 of 1056248 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 233 of 1036256 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 118 of 1029729 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 1027519 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 65587 of 1090292 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success" + ], + "preTokenBalances": [ + { + "accountIndex": 6, + "mint": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "uiTokenAmount": { + "uiAmount": 1.4053841492, + "decimals": 11, + "amount": "140538414920", + "uiAmountString": "1.4053841492" + }, + "owner": "FkKXAcfrso5t53QVxnKzHQaypDpCc7jQF2qpZENhYz2A", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 7, + "mint": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "uiTokenAmount": { + "uiAmount": 0.01699085619, + "decimals": 11, + "amount": "1699085619", + "uiAmountString": "0.01699085619" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "postTokenBalances": [ + { + "accountIndex": 3, + "mint": "E3myentprJHSWdUvD4LDS3A48QMnUNQVUaHwSmDNsm7k", + "uiTokenAmount": { + "uiAmount": 948109365.664516, + "decimals": 6, + "amount": "948109365664516", + "uiAmountString": "948109365.664516" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "uiTokenAmount": { + "uiAmount": 1.39133030771, + "decimals": 11, + "amount": "139133030771", + "uiAmountString": "1.39133030771" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 5, + "mint": "E3myentprJHSWdUvD4LDS3A48QMnUNQVUaHwSmDNsm7k", + "uiTokenAmount": { + "uiAmount": 51890634.335484, + "decimals": 6, + "amount": "51890634335484", + "uiAmountString": "51890634.335484" + }, + "owner": "FkKXAcfrso5t53QVxnKzHQaypDpCc7jQF2qpZENhYz2A", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 6, + "mint": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 11, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "FkKXAcfrso5t53QVxnKzHQaypDpCc7jQF2qpZENhYz2A", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 7, + "mint": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "uiTokenAmount": { + "uiAmount": 0.03104469768, + "decimals": 11, + "amount": "3104469768", + "uiAmountString": "0.03104469768" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 8, + "mint": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 11, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [ + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr" + ] + }, + "computeUnitsConsumed": 175445, + "costUnits": 180957 + }, + "version": 0, + "slot": 453223655, + "transactionIndex": 1007, + "blockTime": 1791108127 + }, + { + "transaction": { + "signatures": [ + "2FG9RsWQ6Gz9JNT8K1rwTiypfkAjDFSe4xdGwp1uJYTnZu8tDCkNmpXyNX89S6b7iXZfJA4eTCgtrhSQkvfJmvPi", + "4j2UKhd1o4rCATGrwUZT9vhFnFdkr4C9nHRRJPFDvPwadkzFNvqCnXM1uHREg4bg5QCbwJg4gyfUzhQpBAQEVFUV" + ], + "message": { + "header": { + "numRequiredSignatures": 2, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 15 + }, + "accountKeys": [ + "GpTXmkdvrTajqkzX1fBmC4BUjSboF9dHgfnqPqj8WAc4", + "H4Jz31z5JKrPKtbndUqvpj29KK6kMFdcnduwgmMu6BPE", + "2L51BajY7jEtJfTq75GTg4PciGvc8ezXL5TroS2rbGQz", + "39fNSJTFGRL4uYEJ1smwtsNyk2c3jbotBbSSZMhGR6oo", + "8PpmjHBaoa3RuwtjTiyLmWmPVeaPa9WXMAAwqLrSifTL", + "8icQPczZZE2QCYYSToFrbHKtsfKhCVoyAnmYcEo1VAQX", + "AJguv6iuWYn9Ta6uVCs12LwS8K7TiVerquTtJvKieKMN", + "ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C", + "BiGFR8fZ4f5yUHeBgJh8Az8YwneoF963b27mefoMs6ag", + "ENqFi3sRvxVLneBVRBdu9i29xgPsbNdszDXrkbT7j5zK", + "GD4ML46x9M3QRtpWmAqh3y7dSyfNgZvdQg5rEJrvWHTe", + "H8ovNygGe6HzZr5AcAsgWX3Z8pdJLsBrqXHHZE3k52dF", + "HDDws6cgVgwzWjNMZx2xtm6TX6WbCZv31UvTMWDxn7KP", + "11111111111111111111111111111111", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr", + "2GEEVhDZR8o9HXueDLUYkjHhtVrvQeR9pH8sNHCmiF7U", + "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt", + "6VNAfzQKMWvqRimbSzLDf68wC6yjvW5yLn9vteZN7TQJ", + "8XPkRh3iD9LKzXVWe3RKzWpcrSyrxgcbMaddJhd5VFDR", + "9mfQRzhyaACD49VhrSwCWKjrHVLcNfhxSdhg2hDSwCd8", + "AJEoYSg3KFGm8VLaZqPf4QC8pkQHFikfmVg57AZv1HHZ", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "AxUfULyBzPc8LA9HwBePCg3E5PYGgjJjCBgvqGMaGnHd", + "J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd" + ], + "recentBlockhash": "BTr1pfaeSBNzRcuxHXHAuWHdoFP4Ga2bCqwFi2vAZDRL", + "instructions": [ + { + "programIdIndex": 14, + "accounts": [ + 0, + 0, + 23, + 20, + 16, + 12, + 1, + 17, + 10, + 11, + 15, + 15, + 13, + 18, + 14, + 22, + 19 + ], + "data": "64j4YcktApTuXMCxgV6EqT1GdL4a1NuU1ZZaNyj3irqkrih8RUeVLThbMy6P4AzV24QZtkPKfLC6mupjMuKDo55ipH89Qbm9bd6rNA4xTXL7ZuBjQ5AkM92EkzBshpaHvemb1QTbh3UBYjk3z6Rf1Q6VgxE64Jwcd6w5b1hN1sjAGWMkSyUDGn8hS52bxojeKw7j4aEn1CcTtA1RUh9mhXgeKrTy", + "stackHeight": 1 + }, + { + "programIdIndex": 27, + "accounts": [ + 0, + 3, + 17, + 15, + 25, + 13, + 21, + 9, + 24, + 4, + 26, + 5 + ], + "data": "Adpdc9EWs4rTH8BtJ1ANn1hrV", + "stackHeight": 1 + }, + { + "programIdIndex": 25, + "accounts": [ + 0, + 6, + 0, + 1, + 13, + 15 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 27, + "accounts": [ + 0, + 16, + 23, + 20, + 12, + 6, + 3, + 10, + 11, + 1, + 17, + 15, + 15, + 18, + 14, + 13, + 2, + 8 + ], + "data": "eFY3z5yi8g4gU2gwb83ZLnjNv3yYqTV2Jttfou", + "stackHeight": 1 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 7 + ], + "data": "3Bxs4TeGXQSxfHP5", + "stackHeight": 1 + } + ], + "transactionConfig": { + "priorityFee": 100000, + "computeUnitLimit": 450000, + "loadedAccountsDataSizeLimit": 67108864, + "heapSize": null + } + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 110000, + "preBalances": [ + 129040989346, + 0, + 1918899, + 1559560, + 1559560, + 1559560, + 0, + 13793249157, + 1539240, + 1559560, + 0, + 0, + 0, + 1, + 37580031, + 70228638, + 0, + 2390453155, + 0, + 5136480, + 7461120, + 8082025715, + 1346200, + 3473040, + 5701587531, + 3388612899, + 3122048759, + 1141440 + ], + "postBalances": [ + 129027650386, + 3230880, + 1918899, + 1559560, + 1559560, + 1559560, + 1574800, + 13795749157, + 1539240, + 1559560, + 1554480, + 1539240, + 2829560, + 1, + 37580031, + 70228638, + 0, + 2390453155, + 0, + 5136480, + 7461120, + 8082025715, + 1346200, + 3473040, + 5701587531, + 3388612899, + 3122048759, + 1141440 + ], + "innerInstructions": [ + { + "index": 0, + "instructions": [ + { + "programIdIndex": 13, + "accounts": [ + 0, + 12 + ], + "data": "1111A71TsJGSaa1UiQSKZ4yVqL1DpygmcfSsvo69sdiGM5Ks9nrBUto1igVGBvDynJ3ovh", + "stackHeight": 2 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 11 + ], + "data": "11117BTzp9mqgi9gDNg5vGsBzuJ6Wz6MAa2GSz5ghLR5ZTLE9edjrYAafKQBjtR3pUGbwH", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 11, + 17 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 14, + "accounts": [ + 18 + ], + "data": "YQpjfNToTVKih1vyVaiu6wGTWCHrR4UmkSjnuP2tMeX5zWFm5c27ZdA5ct9iaWrspVMCBKQXJPHTe6eNUo7uy7HWrDH2K56uFxCPA3Uroq87ZWHuEfNyLD6RVVc7wK1ULwrY4zgyaGak1oHXF4qNFG3NmnuUa8fSuhQBWLWLGrTVd9L3ZGu8613fUpitzzjLY3zdaESUVrpiMgGW4NP8EH6xzYhxm4hmEAJMYSqJqMUXkuzK4UrrbHBC7T8sBPAYz191aSHqo3AQnpJZrPYbw1MknoM8tGYwXeLiquRkt9uqXXow2aCeRtDvbKnpf8YUfmV1XdRsVqXHxeJM6c9zSFbPeD5", + "stackHeight": 2 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 1 + ], + "data": "11119qC9S6RjadZKNJd7gUETiekAzf3sifZHg7NLLkH4ko3XKDeQqusy3wPodHJwiHH89H", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 1 + ], + "data": "GC3i2jSx8awwe9xBfbR9LBTrzgFPUE9b56kGnez3cWMeYXRptHFRHaESp1JbucU1dpjtB3fYRTCUJYfDRfcwrbrgxY", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 1 + ], + "data": "qvXRAunCdnBm99WG3BiFfAWwLAV6gYxFZ6wU7CLoaQZti6F2sKHXFoFzAh2jJ9bzKCaCMprkXjKwyEYvB47sNioiPCh4KUmuRajNVUveYF", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 1 + ], + "data": "2zt6gyV18w3CBL8Tv57G9RzANS3VMJyjznTCcauvNDJT4JVD", + "stackHeight": 2 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 1 + ], + "data": "3Bxs4WPTr8ovHT3M", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 1, + 16, + 1, + 16 + ], + "data": "RQCBLNkR6mx2z5HJzVHfyhZ4xHqN7xEV2JX32LDSq9tvmDLv6zduB682YUuLZ8d3deDWTv1LXiTM8BkgMNTSATxXYBemjh5RcgDZFb75U9nrR9EWhLf4MQRXHWiRyfCZAVTUpD", + "stackHeight": 2 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 10 + ], + "data": "11112new3Me3FtTSrL91tKXcGereUVFfDiVZVL4xNPePdcRxfkFg3ygear9sWUBgaGS28B", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 10, + 1 + ], + "data": "6Me2xrDzxrcNpKp56fPY3ZoB26CGupyi4dVBHHMpdbdvf", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 1, + 10, + 16 + ], + "data": "6ApXSNCamGdm", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 1, + 16 + ], + "data": "31tb", + "stackHeight": 2 + } + ] + }, + { + "index": 1, + "instructions": [ + { + "programIdIndex": 25, + "accounts": [ + 0, + 9, + 21, + 17, + 13, + 15 + ], + "data": "2", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 3, + 17, + 9, + 0 + ], + "data": "i3wwVX6B7mn5D", + "stackHeight": 2 + }, + { + "programIdIndex": 25, + "accounts": [ + 0, + 4, + 24, + 17, + 13, + 15 + ], + "data": "2", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 3, + 17, + 4, + 0 + ], + "data": "iEjKXdcVyRu6P", + "stackHeight": 2 + }, + { + "programIdIndex": 25, + "accounts": [ + 0, + 5, + 26, + 17, + 13, + 15 + ], + "data": "2", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 3, + 17, + 5, + 0 + ], + "data": "gXDAc4b8fJ1ou", + "stackHeight": 2 + } + ] + }, + { + "index": 2, + "instructions": [ + { + "programIdIndex": 15, + "accounts": [ + 1 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 6 + ], + "data": "11116HS1DMnnBqTpJa6mzvPcTm2pPdEFkAc5gJ1bhWfxUnJuL9sR38LruXZQHYwFsVLBYP", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 6 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 15, + "accounts": [ + 6, + 1 + ], + "data": "6cxAChxAj9Jra7F636pP6g2csELBcettzPaxgtRQwoK32", + "stackHeight": 2 + } + ] + }, + { + "index": 3, + "instructions": [ + { + "programIdIndex": 14, + "accounts": [ + 0, + 16, + 23, + 20, + 12, + 6, + 3, + 10, + 11, + 1, + 17, + 15, + 15, + 18, + 14, + 13, + 2, + 8 + ], + "data": "HtTvTxyWwMDYfxiWapV7K8x6CbsogAoB7pHKx2caFwCB", + "stackHeight": 2 + }, + { + "programIdIndex": 14, + "accounts": [ + 18 + ], + "data": "EwDfpErTWwQhCAycT1hw3m3tpNATEPHNz6dCmysZg1fQKy7BGhP7A7jZPAWZiq9GPCBE2JiPktzDuJ9wq84EsqTEet9kfstdNAqX5dzYmzaK1hAkxS5zArSYcbGJMs9S2rtVHAhUMdboXPY7pnAuwEv3m2w8jopmoKQNakN1QQY4F1H7RfYDti8PTyyuhzr9bYAvDcYHpTU72Lo6erqe", + "stackHeight": 3 + }, + { + "programIdIndex": 15, + "accounts": [ + 3, + 17, + 11, + 0 + ], + "data": "hkLTbyEshGUkf", + "stackHeight": 3 + }, + { + "programIdIndex": 15, + "accounts": [ + 10, + 1, + 6, + 16 + ], + "data": "hWMNuLQEJYxTX", + "stackHeight": 3 + }, + { + "programIdIndex": 15, + "accounts": [ + 11, + 17, + 2, + 16 + ], + "data": "iCn29B5QAaL2B", + "stackHeight": 3 + } + ] + } + ], + "logMessages": [ + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: InitializeWithToken2022", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program log: Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3572 of 428233 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 391676 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: l9fiCXahc67w3Jk5rSZTTGjQ4gpPIs9TmhTgDAg4OXYvWfGJNZFNOusHmaBwEQ68cpDUP89oGvDTgiNiEU9dZms+Gjw3e8Ftgk55etpLZie9qey31RKVPxvBT61Aw9Iq/Be40X4narsGDAAAAFNjcm9ua2ZpbmtsZQwAAABTY3JvbmtmaW5rbGU2AAAAaHR0cHM6Ly9tZXRhZGF0YS5qN3RyYWNrZXIuaW8vbWV0YWRhdGEvRXM0WnJjT0ZsOS5qc29uAACAxqR+jQMAAHjF+1HRAgC/J0WBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: MetadataPointerInstruction::Initialize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 925 of 385214 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1280 of 382801 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeMint2", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1940 of 380136 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: TokenMetadataInstruction: Initialize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3742 of 372530 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2743 of 363083 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: MintTo", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1876 of 346137 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: SetAuthority", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1044 of 342593 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 110712 of 450000 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd invoke [1]", + "Program log: Instruction: SplitSupply", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]", + "Program log: CreateIdempotent", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 5956 of 331597 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3818 of 323593 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]", + "Program log: CreateIdempotent", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 11956 of 308339 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3818 of 294335 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]", + "Program log: CreateIdempotent", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 5956 of 285081 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3818 of 277077 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd consumed 66633 of 339288 compute units", + "Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: GetAccountDataSize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1570 of 267394 compute units", + "Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb tgAAAAAAAAA=", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeImmutableOwner", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 261002 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2799 of 257932 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 17805 of 272655 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd invoke [1]", + "Program log: Instruction: BuyLaunchlabDelta", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program log: Instruction: BuyExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [3]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 219021 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe7w3Jk5rSZTTGjQ4gpPIs9TmhTgDAg4OXYvWfGJNZFNOgB4xftR0QIAm8YOPunPAwChR/uHAAAAAAAAAAAAAAAAAAAAAAAAAABuQ4cU7YkAAHKtYRYAAAAAgTSqFgAAAABuQ4cU7YkAAGqBDgAAAAAApQU6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3710 of 215069 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3432 of 209300 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3602 of 190957 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 63069 of 247743 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd consumed 71398 of 254850 compute units", + "Program return: J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd g3hI/YuIAAA=", + "Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd success", + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success" + ], + "preTokenBalances": [ + { + "accountIndex": 2, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.27654191, + "decimals": 8, + "amount": "27654191", + "uiAmountString": "0.27654191" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 3, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 7.90547425, + "decimals": 8, + "amount": "790547425", + "uiAmountString": "7.90547425" + }, + "owner": "GpTXmkdvrTajqkzX1fBmC4BUjSboF9dHgfnqPqj8WAc4", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "AJEoYSg3KFGm8VLaZqPf4QC8pkQHFikfmVg57AZv1HHZ", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 5, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "AxUfULyBzPc8LA9HwBePCg3E5PYGgjJjCBgvqGMaGnHd", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 8, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 9, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "6VNAfzQKMWvqRimbSzLDf68wC6yjvW5yLn9vteZN7TQJ", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "postTokenBalances": [ + { + "accountIndex": 2, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.31456724, + "decimals": 8, + "amount": "31456724", + "uiAmountString": "0.31456724" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 3, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "GpTXmkdvrTajqkzX1fBmC4BUjSboF9dHgfnqPqj8WAc4", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 1.44353959, + "decimals": 8, + "amount": "144353959", + "uiAmountString": "1.44353959" + }, + "owner": "AJEoYSg3KFGm8VLaZqPf4QC8pkQHFikfmVg57AZv1HHZ", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 5, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 1.36764704, + "decimals": 8, + "amount": "136764704", + "uiAmountString": "1.36764704" + }, + "owner": "AxUfULyBzPc8LA9HwBePCg3E5PYGgjJjCBgvqGMaGnHd", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 6, + "mint": "H4Jz31z5JKrPKtbndUqvpj29KK6kMFdcnduwgmMu6BPE", + "uiTokenAmount": { + "uiAmount": 150134831.216771, + "decimals": 6, + "amount": "150134831216771", + "uiAmountString": "150134831.216771" + }, + "owner": "GpTXmkdvrTajqkzX1fBmC4BUjSboF9dHgfnqPqj8WAc4", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 8, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 9, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 1.29175449, + "decimals": 8, + "amount": "129175449", + "uiAmountString": "1.29175449" + }, + "owner": "6VNAfzQKMWvqRimbSzLDf68wC6yjvW5yLn9vteZN7TQJ", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 10, + "mint": "H4Jz31z5JKrPKtbndUqvpj29KK6kMFdcnduwgmMu6BPE", + "uiTokenAmount": { + "uiAmount": 848348655.336594, + "decimals": 6, + "amount": "848348655336594", + "uiAmountString": "848348655.336594" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 11, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 3.7645078, + "decimals": 8, + "amount": "376450780", + "uiAmountString": "3.7645078" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [] + }, + "computeUnitsConsumed": 266698, + "costUnits": 273285 + }, + "version": 1, + "slot": 453238203, + "transactionIndex": 856, + "blockTime": 1791112009 + } +] \ No newline at end of file diff --git a/tests/regression/raw_stonkfun_token_accounts_from_getaccountinfo.json b/tests/regression/raw_stonkfun_token_accounts_from_getaccountinfo.json new file mode 100644 index 0000000..d9edcae --- /dev/null +++ b/tests/regression/raw_stonkfun_token_accounts_from_getaccountinfo.json @@ -0,0 +1,14 @@ +{ + "without_transfer_fee_extension": { + "address": "5u3DePFfCNoPbAnNBdTjpBKa4U95H79xSybvQK2Krusq", + "mint": "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "owner": "6UMjzh6TrEK3yrFSPs6B2tZuu9aAzFoNwsv9FYyV1Rsr", + "data_base64": "RHh7dBC3/lHQaWdgPCTwozTajj0CL/VCKuq5oqNNc4VRTJGX/HRokoEqi+lp4ZekKNJzs0HeptmEfWb2YXq2ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcAAAA=" + }, + "with_withheld_fee": { + "address": "ExBCrAP7zPjfuMrWnhZa74Schdwg54DWMpLQ9HjUkRjF", + "mint": "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "owner": "HPyj58oj68B4a9yW17oJYroGT7397h7zEvevjPe2oZ29", + "data_base64": "Vo7IhPZ0s4IUm2fJFKRC/QFQmnDKZ4NoOUTKewn4JQzzneh/re5OHEDmO1xyf+lBA1nPe9+4tS3UUxymsAGKorJq+jsSHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcAAAACAAgAFJbI4asAAAA=" + } +} \ No newline at end of file diff --git a/tests/regression/raw_stonkfun_trades_from_gettransaction.json b/tests/regression/raw_stonkfun_trades_from_gettransaction.json new file mode 100644 index 0000000..7c1a5cc --- /dev/null +++ b/tests/regression/raw_stonkfun_trades_from_gettransaction.json @@ -0,0 +1,2140 @@ +[ + { + "label": "standard sell sol", + "signature": "uzuSemMhJ5x9eRGkhvFt43prZa3MLW8xmm19TtJBoLXHWZidhaY5MR6kBQ3WQqSK2F41KjQgp9Hy7jE3w69n7A4", + "pool": "Bqbokd7TQq1RJG9bMEaeg4rixd9tNgfCt4PfbcJjY7ce", + "base_mint": "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "quote_mint": "So11111111111111111111111111111111111111112", + "creator": "3y1jpFtUBYepN6HvSqoT16xPMXbWKCspE4E7xUVr7mQh", + "global_config": "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX", + "platform_config": "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7", + "accounts_base64": { + "global_config": "lQicyqD8sNnBAwAAAAAAAAAAAAAAAAAAAAAAxAkAAAAAAAAQJwAAAAAAAICWmAAAAAAAADUMAAAAAABADQMAAAAAAPBJAgAAAAAAAPCClgUAAAAGm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAQyz4e5t0cIQzcDSX4MHTIDK0v9FiO4+tNA3FD3gRgUiDLPevnctauoG6DOOMPSKGFjctO/P9tXw/LdYKgFdwIUGML1SYnBjHI8sW0M/s3jLoiH369IfbHsD8odEFvzLFAYwvGw1LgPHgfQBOLmbBdQ7GAZ6dG85H/e4gZH21Q8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "platform_config": "oE6AAPhT5qAEBAAAAAAAAD5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5Pk+EKn0LDJ7I4k6gOjHJ+HmjXcMEdYkaqu5dnrAIL/lAQg8AAAAAAAAAAAAAAAAAAAAAAAAAAAAQJwAAAAAAAFN0b25rRnVuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABodHRwczovL3d3dy5zdG9ua2Z1bi54eXoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaHR0cHM6Ly93d3cuc3RvbmtmdW4ueHl6L3N0b25rZnVuLWxvZ28ucG5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKmyHxd+m5dujuEp9BrvN4SqxRSUkJWwAuYRfVnQNpaPAAAAAAAAAABALVyvepfFNJdljlYbJiPQ449TQPIIrdUKTvRUCwrLLj5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5AAAAAAAAAAA+T4QqfQsMnsjiTqA6Mcn4eaNdwwR1iRqq7l2esAgv+QABGv1mAdFtt7hdam3csaoALEDujVMj8YuY7QspWaQA7JUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base_mint": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDGpH6NAwAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAER4e3QQt/5R0GlnYDwk8KM02o49Ai/1QirquaKjTXOFEwCwAAeDqBUmIQ+UoOjoTSCHADs6QmDSnOedrP4hpOp3H6YoRHh7dBC3/lHQaWdgPCTwozTajj0CL/VCKuq5oqNNc4UIAAAAc3BhY2V4c2kIAAAAc3BhY2V4c2lQAAAAaHR0cHM6Ly9pcGZzLmlvL2lwZnMvYmFma3JlaWR0aHJ0Nnl0Znd5d2J6dGNvaTZnY3h4c3piNXZhZ3NybDZpcmlxb3ViZHBsbTVsc21rNmkAAAAA" + }, + "transaction": { + "transaction": { + "signatures": [ + "uzuSemMhJ5x9eRGkhvFt43prZa3MLW8xmm19TtJBoLXHWZidhaY5MR6kBQ3WQqSK2F41KjQgp9Hy7jE3w69n7A4" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 13 + }, + "accountKeys": [ + "6UMjzh6TrEK3yrFSPs6B2tZuu9aAzFoNwsv9FYyV1Rsr", + "4FT89XgghvrAmnFSjxru5YeE2b7VZXGcgLBrg8vVSHqx", + "53PrcuNbuYHGh9g3iW7ugdkxaFjF938M5Cwi5SbzwJjQ", + "5u3DePFfCNoPbAnNBdTjpBKa4U95H79xSybvQK2Krusq", + "astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF", + "BE5Nw2yZiz3MY6QvCBHcm7UEnXzAP9Raa7Mcsor8miwH", + "Bqbokd7TQq1RJG9bMEaeg4rixd9tNgfCt4PfbcJjY7ce", + "DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx", + "GXANSLYoKkdsfb5RabcULCsqf3WJzitqSGBi3C8HhMuq", + "J9BkR2Q9xJGyMnd41NCCyuXnbVqjUP8AWRBZYJKBiWnE", + "11111111111111111111111111111111", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr", + "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7", + "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "ComputeBudget111111111111111111111111111111", + "jitodontfront111111111116111111111111165533", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "So11111111111111111111111111111111111111112", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh" + ], + "recentBlockhash": "LFfxjqoxRfjbtfVitm8owDZ4EugYCAgfwAwPGDhSdof", + "instructions": [ + { + "programIdIndex": 16, + "accounts": [ + 17 + ], + "data": "FJ3Yj1", + "stackHeight": 1 + }, + { + "programIdIndex": 16, + "accounts": [], + "data": "3VbvkNnLHWs9", + "stackHeight": 1 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 5, + 0, + 19, + 10, + 20 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 18, + "accounts": [ + 0, + 22, + 14, + 12, + 6, + 3, + 5, + 1, + 9, + 13, + 19, + 21, + 20, + 11, + 18, + 10, + 2, + 8 + ], + "data": "B3F1THDgKfWbRcmMrts8Jknkrj6D3XpdzGHPJYdFJSrT", + "stackHeight": 1 + }, + { + "programIdIndex": 20, + "accounts": [ + 5, + 0, + 0 + ], + "data": "A", + "stackHeight": 1 + }, + { + "programIdIndex": 10, + "accounts": [ + 0, + 7 + ], + "data": "3Bxs4iDb7duck5EP", + "stackHeight": 1 + }, + { + "programIdIndex": 10, + "accounts": [ + 0, + 4 + ], + "data": "3Bxs4Bc3VYuGVB19", + "stackHeight": 1 + } + ] + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 284429, + "preBalances": [ + 20700094957, + 1488440, + 104478322, + 1513840, + 19194676381, + 0, + 2829560, + 2676631257408, + 1488440, + 31422053476, + 1, + 0, + 7461120, + 2753360, + 3473651, + 3388612899, + 1, + 0, + 37580031, + 1849280395886, + 200653906, + 70228638, + 0 + ], + "postBalances": [ + 21959318836, + 1488440, + 117371899, + 1513840, + 19195676381, + 0, + 2829560, + 2676643989815, + 1488440, + 30135919184, + 1, + 0, + 7461120, + 2753360, + 3473651, + 3388612899, + 1, + 0, + 37580031, + 1849280395886, + 200653906, + 70228638, + 0 + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 20, + "accounts": [ + 19 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 10, + "accounts": [ + 0, + 5 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6po1LhhgEJXngDjhxmUTshDA8EHdXaMJUsXdMg3PE", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 5 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 5, + 19 + ], + "data": "6Sc4QvtdG95atFLLxV2HWWs1JQ39zQ155eo5zk8fJJaJp", + "stackHeight": 2 + } + ] + }, + { + "index": 3, + "instructions": [ + { + "programIdIndex": 18, + "accounts": [ + 11 + ], + "data": "EwDfpErTWwQhCAycT1hw3ktxEWYzhGZ3LhSPTkddVZZZsEkz9qPX2kdzw7qM8PWTiEumHm6Y7gRL4ti8KQkuMSgeJKa5TQSqHyNGReifZYAUhcL2WPiUJgqFqVAkHCe3GK1xTL1VmMMmKVHcYvFTi7bdoQxvUniPN43kaausWtAtq11chK7ChpG4QiwTJL8yhB2HW3gygjReojmh47Ag", + "stackHeight": 2 + }, + { + "programIdIndex": 21, + "accounts": [ + 3, + 13, + 1, + 0 + ], + "data": "ix6VpPzKL3117", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 9, + 19, + 5, + 22 + ], + "data": "hsegBkoEL9k7J", + "stackHeight": 2 + }, + { + "programIdIndex": 20, + "accounts": [ + 9, + 19, + 2, + 22 + ], + "data": "hrdwpgBsXn6hW", + "stackHeight": 2 + } + ] + } + ], + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 179 of 138780 compute units", + "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 37 of 133691 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 229 of 131230 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 13413 of 144131 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: SellExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 102321 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe6hCBp7ohqpvmhk3RkBH+f2vJQfVgcK3zZXP0zRUdZrwwB4xftR0QIA3nQOPunPAwDXrzD8BgAAABaNi31X7AEAljOhHAcAAAA32jLg0eEBAB8qx88GAAAA37JYnYUKAACLHORLAAAAAGMvMQAAAAAAib3EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAB", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2442 of 99158 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 112 of 94655 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 112 of 87129 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 46410 of 130718 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 118 of 84308 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success" + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "uiTokenAmount": { + "uiAmount": 458664510.681834, + "decimals": 6, + "amount": "458664510681834", + "uiAmountString": "458664510.681834" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 2, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 0.102622753, + "decimals": 9, + "amount": "102622753", + "uiAmountString": "0.102622753" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 3, + "mint": "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "uiTokenAmount": { + "uiAmount": 11568986.763999, + "decimals": 6, + "amount": "11568986763999", + "uiAmountString": "11568986.763999" + }, + "owner": "6UMjzh6TrEK3yrFSPs6B2tZuu9aAzFoNwsv9FYyV1Rsr", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 8, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 9, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 31.420565036, + "decimals": 9, + "amount": "31420565036", + "uiAmountString": "31.420565036" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "uiTokenAmount": { + "uiAmount": 470233497.445833, + "decimals": 6, + "amount": "470233497445833", + "uiAmountString": "470233497.445833" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 2, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 0.11551633, + "decimals": 9, + "amount": "115516330", + "uiAmountString": "0.11551633" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 3, + "mint": "5cHHA3idGG1e7X1oMVuxTq2u8jWoY65iHvAC4Tc4H2ya", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 6, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "6UMjzh6TrEK3yrFSPs6B2tZuu9aAzFoNwsv9FYyV1Rsr", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 8, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 9, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 30.134430744, + "decimals": 9, + "amount": "30134430744", + "uiAmountString": "30.134430744" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [] + }, + "computeUnitsConsumed": 60541, + "costUnits": 65295 + }, + "version": "legacy", + "slot": 453240690, + "transactionIndex": 133, + "blockTime": 1791112673 + } + }, + { + "label": "reward buy xstock", + "signature": "dLsNUswJ1kbzVyg29aZgE6Dqe3DpD981uxFiAm1u1heSyNpaiTR26oZU3JUkoiF6KwSZDzfrtPukioYNbXbk9dg", + "pool": "51K3CNZcHKyAENexuu45Qia7fFkW3g7dtatqj2dPF2kr", + "base_mint": "WMgdN6XrdDxSamrZrKfEY6xe6QWD5bb9RaDuAbtwFPt", + "quote_mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "creator": "9jwfZT1V4rvG8ZRdfR8gQfu9yYKSmZeejBEhmpPgYtPv", + "global_config": "9mfQRzhyaACD49VhrSwCWKjrHVLcNfhxSdhg2hDSwCd8", + "platform_config": "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt", + "accounts_base64": { + "global_config": "lQicyqD8sNkABAAAAAAAAAAAAAAAAAAAAAAAxAkAAAAAAAAAAAAAAAAAAICWmAAAAAAAP0IPAAAAAAABAAAAAAAAAPBJAgAAAAAAAQAAAAAAAAAH6AObtK1M95w4PkPAxR3RhTdAUSt50PlCEpI0UdADgwyz4e5t0cIQzcDSX4MHTIDK0v9FiO4+tNA3FD3gRgUiDLPevnctauoG6DOOMPSKGFjctO/P9tXw/LdYKgFdwIUGML1SYnBjHI8sW0M/s3jLoiH369IfbHsD8odEFvzLFAYwvGw1LgPHgfQBOLmbBdQ7GAZ6dG85H/e4gZH21Q8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "platform_config": "oE6AAPhT5qAXBAAAAAAAAD5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5Pk+EKn0LDJ7I4k6gOjHJ+HmjXcMEdYkaqu5dnrAIL/lAQg8AAAAAAAAAAAAAAAAAAAAAAAAAAAAQJwAAAAAAAFN0b25rRnVuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABodHRwczovL3d3dy5zdG9ua2Z1bi54eXoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaHR0cHM6Ly93d3cuc3RvbmtmdW4ueHl6L3N0b25rZnVuLWxvZ28ucG5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKmyHxd+m5dujuEp9BrvN4SqxRSUkJWwAuYRfVnQNpaPAAAAAAAAAABALVyvepfFNJdljlYbJiPQ449TQPIIrdUKTvRUCwrLLj5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5AAAAAAAAAAA+T4QqfQsMnsjiTqA6Mcn4eaNdwwR1iRqq7l2esAgv+QABGv1mAdFtt7hdam3csaoALEDujVMj8YuY7QspWaQA7JUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base_mint": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDGpH6NAwAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeFOuUdHWjNEpJhcrFOdgPc4P3NFrhsjAI5HlcM0FtHAQBsAAeDqBUmIQ+UoOjoTSCHADs6QmDSnOedrP4hpOp3H6YoQC1cr3qXxTSXZY5WGyYj0OOPU0DyCK3VCk70VAsKyy7Y+GISTgkAABIEAAAAAAAAAIDGpH6NAwBkABIEAAAAAAAAAIDGpH6NAwBkABMA2wAHg6gVJiEPlKDo6E0ghwA7OkJg0pznnaz+IaTqdx+mKAeFOuUdHWjNEpJhcrFOdgPc4P3NFrhsjAI5HlcM0FtHCAAAAE1pbmloYXJkCAAAAE1JTklIQVJEewAAAGh0dHBzOi8veHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4Lnh4eC9tZXRhZGF0YS8yMjk3YTU5ZC03OGQwLTQwYTMtODJiZC00NzI5ODIxOTJiZDguanNvbgAAAAA=" + }, + "transaction": { + "transaction": { + "signatures": [ + "dLsNUswJ1kbzVyg29aZgE6Dqe3DpD981uxFiAm1u1heSyNpaiTR26oZU3JUkoiF6KwSZDzfrtPukioYNbXbk9dg" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 19 + }, + "accountKeys": [ + "7zCjgF9tzBHwH333ba1DTancC5YayaFNCm35pwcLt2MF", + "6QXmLfEH99oiy7LnRoqxQrfNzDhGm8NpaajXSvLWaoyq", + "76kRKbFxPmTXgKdzw9RrK6VakdnXKCAYW8tBXJFMdNXU", + "8FnX3xo2yYw3EUE6w3nQA4GfXGS9wpK6oj3veJpbFzLo", + "ATRsNGv2nDw7hSMfkUTBoVUDsFDwN7po7KbecyiGWNB4", + "2Y7HATmn9aJBcxCskE5V2U2epmjvkZmB51zTJBbhj4cU", + "99j6HcXChJQVHvVn5LtnziLtupQnmebpBz8qHTnJKmt7", + "ASAxmEaTT1HFe3mVC3zbKDE4tuB28W7732XQrEMBM5W2", + "935r9veSt8PkQgLGsmAz8sJsyGLvJTXXZvV5FSCLGGoc", + "YCAqrrtMnBbvSRMcixgwog63KYMkZ7Gw1C2fWeNsZCB", + "2s9VVoaXZCBP1hBTLyXyHmKyedDH6eKFN1SQUqEUJBt7", + "57y6UeWbo44vhW2iAjaRxAYk9cbUQm1cuqXheir9z8Y5", + "HpZGadqSQTcYiTVh1WpusQ2Uc9WFSxJVTbQH5vNGuJf9", + "GSQac5zyNtRhQqCr8ZAfhnboPXirS769Gt7A9wvmtBkP", + "B43DXbpywE5FwhsQifJjup9EaGw9G7YsmX15sMQs7ivo", + "WMgdN6XrdDxSamrZrKfEY6xe6QWD5bb9RaDuAbtwFPt", + "51K3CNZcHKyAENexuu45Qia7fFkW3g7dtatqj2dPF2kr", + "2Eq7zeRMU3MrJdsnCp6LbBf3mebLgK2Es1ySgK5DaptY", + "7hnZQPhyhqWbwnZkE4tsnUFaDgL3jfNffk6xKpfExF9W", + "2L51BajY7jEtJfTq75GTg4PciGvc8ezXL5TroS2rbGQz", + "jZs55R3yLtn8167pYFo45ZGeMG5PchYUgnYa5FgugBa", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "So11111111111111111111111111111111111111112", + "11111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf", + "BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi", + "Sysvar1nstructions1111111111111111111111111", + "J1to1yufRnoWn81KYg1XkTWzmKjnYSnmE2VY8DGUJ9Qv", + "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "9mfQRzhyaACD49VhrSwCWKjrHVLcNfhxSdhg2hDSwCd8", + "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr" + ], + "recentBlockhash": "nv3eef54Bv3yBY269fuaXDJwSgr8zTSajmNRuxDxM92", + "instructions": [ + { + "programIdIndex": 21, + "accounts": [ + 0, + 1, + 0, + 22, + 23, + 24 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 23, + "accounts": [ + 0, + 1 + ], + "data": "3Bxs4Tk799WawzBR", + "stackHeight": 1 + }, + { + "programIdIndex": 24, + "accounts": [ + 1 + ], + "data": "J", + "stackHeight": 1 + }, + { + "programIdIndex": 25, + "accounts": [ + 26, + 0, + 1, + 2, + 25, + 27, + 25, + 28, + 25, + 29, + 0, + 3, + 4, + 5, + 1, + 6, + 24, + 24, + 30, + 31, + 25, + 32, + 26, + 24, + 33, + 0, + 7, + 27, + 34, + 2, + 8, + 6, + 9, + 10, + 11, + 12, + 13 + ], + "data": "4pm4hNwxvXF14GVtuHwTqSZYYbF2xnzdb9fp6gN5aP3EVEJFKAvpCb2fhq", + "stackHeight": 1 + }, + { + "programIdIndex": 24, + "accounts": [ + 1, + 0, + 0 + ], + "data": "A", + "stackHeight": 1 + }, + { + "programIdIndex": 21, + "accounts": [ + 0, + 14, + 0, + 15, + 23, + 26 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 35, + "accounts": [ + 0, + 36, + 37, + 38, + 16, + 14, + 2, + 17, + 18, + 15, + 27, + 26, + 26, + 39, + 35, + 23, + 19, + 20 + ], + "data": "HtTvTxyWwMDNpRBegzBRDwDn8FvyE7p71g6aqMxy9L1u", + "stackHeight": 1 + }, + { + "programIdIndex": 26, + "accounts": [ + 15, + 14 + ], + "data": "2yq", + "stackHeight": 1 + } + ], + "transactionConfig": { + "priorityFee": 30000, + "computeUnitLimit": 462000, + "loadedAccountsDataSizeLimit": 33554432, + "heapSize": null + } + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 35000, + "preBalances": [ + 155202002677, + 0, + 1559560, + 16032294, + 19641369930614, + 2039285, + 1488440, + 3967480, + 1559560, + 1488440, + 121187520, + 19070400, + 3480000, + 1940560, + 0, + 3540760, + 2829560, + 1554480, + 1539240, + 1918899, + 1539240, + 3388612899, + 1849280395886, + 1, + 200653906, + 6950115527, + 70228638, + 2390453155, + 6711053, + 25305375, + 0, + 61720757056, + 46850581, + 523015235, + 535338983427, + 37580031, + 0, + 3473040, + 7461120, + 0 + ], + "postBalances": [ + 148553092877, + 0, + 1559560, + 16032294, + 19648017230614, + 2039285, + 1488440, + 3967480, + 1559560, + 1488440, + 121187520, + 19070400, + 3480000, + 1940560, + 1574800, + 3540760, + 2829560, + 1554480, + 1539240, + 1918899, + 1539240, + 3388612899, + 1849280395886, + 1, + 200653906, + 6950115527, + 70228638, + 2390453155, + 6711053, + 25305375, + 0, + 61720757056, + 46850581, + 523015235, + 535338983427, + 37580031, + 0, + 3473040, + 7461120, + 0 + ], + "innerInstructions": [ + { + "index": 0, + "instructions": [ + { + "programIdIndex": 24, + "accounts": [ + 22 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 23, + "accounts": [ + 0, + 1 + ], + "data": "111135fAZn8PZBEU8J8iUmUTdbmBE6po1LhhgEJXngDjhxmUTshDA8EHdXaMJUsXdMg3PE", + "stackHeight": 2 + }, + { + "programIdIndex": 24, + "accounts": [ + 1 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 24, + "accounts": [ + 1, + 22 + ], + "data": "6U7uQcSghH2ZmYX8ZgjCYwZDzgy8QPKX4ugCwKXJABAnD", + "stackHeight": 2 + } + ] + }, + { + "index": 3, + "instructions": [ + { + "programIdIndex": 29, + "accounts": [ + 0, + 3, + 4, + 5, + 1, + 6, + 24, + 24, + 30, + 31 + ], + "data": "7KfreuQCV4TXBWgXW4qnJEGF", + "stackHeight": 2 + }, + { + "programIdIndex": 24, + "accounts": [ + 1, + 4, + 0 + ], + "data": "3gM1VMtsXap7", + "stackHeight": 3 + }, + { + "programIdIndex": 24, + "accounts": [ + 5, + 6, + 3 + ], + "data": "3o3ao43q7EYw", + "stackHeight": 3 + }, + { + "programIdIndex": 25, + "accounts": [ + 28 + ], + "data": "QMqFu4fYGGeUEysFnenhAvgmgqusXLYLUqY98Km6ZzsDTHReTeK3HDBTksAXfxwrKLRiTZ5DvBEbS4SbZ9wqd878rwxrZH3VQkfLQqxjWLP5MgLzHuw8aZPHA2zSxqyreJnWENcchmTTuWiskMBzHx1XE3rZcgAaeB24RHuPbJLGzs1", + "stackHeight": 2 + }, + { + "programIdIndex": 32, + "accounts": [ + 26, + 24, + 33, + 0, + 7, + 27, + 34, + 2, + 8, + 6, + 9, + 10, + 11, + 12, + 13 + ], + "data": "4AoQRYXBdnCSijePJVfLCcUEaLwTEuBG3okLF4ovozdYZaNj81zTaEHFQPy", + "stackHeight": 2 + }, + { + "programIdIndex": 24, + "accounts": [ + 6, + 34, + 9, + 0 + ], + "data": "ifC7obQ88jneq", + "stackHeight": 3 + }, + { + "programIdIndex": 26, + "accounts": [ + 8, + 27, + 2, + 7 + ], + "data": "homaZrVE8L9Sw", + "stackHeight": 3 + }, + { + "programIdIndex": 25, + "accounts": [ + 28 + ], + "data": "QMqFu4fYGGeUEysFnenhAvDWgqp1W7DbrMv3z8JcyrP4Bu3Yyyj7irLW76wEzMiFqkMXcsUXJG1WLwjdCWzNTL6957kdfWSD7SPFG2av5YHKd9xmLGWHFM24DZmmE3mzH5geptFHhmbpJ89tDdYuHwz5C9RWDy9jhifXiQ8TNj9dxRu", + "stackHeight": 2 + } + ] + }, + { + "index": 5, + "instructions": [ + { + "programIdIndex": 26, + "accounts": [ + 15 + ], + "data": "84eT", + "stackHeight": 2 + }, + { + "programIdIndex": 23, + "accounts": [ + 0, + 14 + ], + "data": "11116HS1DMnnBqTpJa6mzvPcTm2pPdEFkAc5gJ1bhWfxUnJuL9sR38LruXZQHYwFsVLBYP", + "stackHeight": 2 + }, + { + "programIdIndex": 26, + "accounts": [ + 14 + ], + "data": "P", + "stackHeight": 2 + }, + { + "programIdIndex": 26, + "accounts": [ + 14, + 15 + ], + "data": "6U7uQcSghH2ZmYX8ZgjCYwZDzgy8QPKX4ugCwKXJABAnD", + "stackHeight": 2 + } + ] + }, + { + "index": 6, + "instructions": [ + { + "programIdIndex": 35, + "accounts": [ + 39 + ], + "data": "EwDfpErTWwQhCAycT1hw3khakhC7Yf4narP2oi6AbmWo45PyMKbrCNMsSf1wmHr9z53Hu35TJyNi6aBBFKE2NVDVa1mzSgNpdtxHV6LGM6x6CrLfL7NsTy8CZRkPMs5s4Zbq4XbMLoqqcAVBQwFR8YvwqnzhqBUi3GQnHwByr9punBVkZgmix5N6t3pnH78992PiUJiCDApeDcnQXiFJ", + "stackHeight": 2 + }, + { + "programIdIndex": 26, + "accounts": [ + 2, + 27, + 18, + 0 + ], + "data": "gNXz8fnAsW9ZZ", + "stackHeight": 2 + }, + { + "programIdIndex": 26, + "accounts": [ + 17, + 15, + 14, + 36 + ], + "data": "hACU34NB3i6N9", + "stackHeight": 2 + }, + { + "programIdIndex": 26, + "accounts": [ + 18, + 27, + 19, + 36 + ], + "data": "hYdyktfZXKDao", + "stackHeight": 2 + } + ] + } + ], + "logMessages": [ + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 179 of 456649 compute units", + "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 37 of 451560 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 229 of 449099 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 13413 of 462000 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 201 of 448437 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [1]", + "Program log: Instruction: Route", + "Program BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi invoke [2]", + "Program log: 7kvnxD8kJ3rCggdhmoY5D99fsTXnKUafyg78Vu48r6Cp", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 83 of 397989 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 76 of 377142 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program data: sZfWPwAAAAAAa8toRdxl0wzGwC2Fxpn6d3vSU9+KtE7JXlRxnyc1U/A=", + "Program BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi consumed 69094 of 444367 compute units", + "Program BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi success", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [2]", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 91 of 373398 compute units", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success", + "Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [2]", + "Program log: Instruction: SwapV2", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 338815 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3818 of 333952 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program data: 4cpJr5MroJaMK/dKrwPM6Kt235Hc2YdR1Fc1ECNNi6LWK86sTNvkIwCxTnZPn+ovQwEAAAAAAAAAW8W4xihPMEMBAAAAAAAAAMjNCjAAAAAAhdsgHgAAAAAAAAAAAAAAAAAAAAAAAAAAmVkFAAAAAACnzAAAAAAAAA==", + "Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc consumed 42622 of 369968 compute units", + "Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc success", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [2]", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 91 of 325442 compute units", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 123507 of 448236 compute units", + "Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 hdsgHgAAAAA=", + "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 118 of 324729 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: GetAccountDataSize", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1570 of 317848 compute units", + "Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb tgAAAAAAAAA=", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: Initialize the associated token account", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeImmutableOwner", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 311456 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: InitializeAccount3", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2799 of 308385 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 19308 of 324611 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: BuyExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 276583 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe47gySnmQAM14sxpFMr53vc6FgmHiTomOQ5xst5uB2oYwB4xftR0QIAZIgOPunPAwDCZdWRAAAAAEqIwUqwBwAAynYoAQAAAACcf9FFUqkAAFEfnR4AAAAAFRzUHQAAAABS9w/7oaEAAB0XEwAAAAAAcVxMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3710 of 272631 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3432 of 266862 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3602 of 254519 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 57067 of 305303 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [1]", + "Program log: TransferFeeInstruction: HarvestWithheldTokensToMint", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1342 of 248236 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success" + ], + "preTokenBalances": [ + { + "accountIndex": 2, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "7zCjgF9tzBHwH333ba1DTancC5YayaFNCm35pwcLt2MF", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 19641.358348595, + "decimals": 9, + "amount": "19641358348595", + "uiAmountString": "19641.358348595" + }, + "owner": "8FnX3xo2yYw3EUE6w3nQA4GfXGS9wpK6oj3veJpbFzLo", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 5, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "uiTokenAmount": { + "uiAmount": 2513860.806579, + "decimals": 6, + "amount": "2513860806579", + "uiAmountString": "2513860.806579" + }, + "owner": "8FnX3xo2yYw3EUE6w3nQA4GfXGS9wpK6oj3veJpbFzLo", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 6, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 6, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "7zCjgF9tzBHwH333ba1DTancC5YayaFNCm35pwcLt2MF", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 8, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 817.62162251, + "decimals": 8, + "amount": "81762162251", + "uiAmountString": "817.62162251" + }, + "owner": "ASAxmEaTT1HFe3mVC3zbKDE4tuB28W7732XQrEMBM5W2", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 9, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "uiTokenAmount": { + "uiAmount": 291247.51167, + "decimals": 6, + "amount": "291247511670", + "uiAmountString": "291247.51167" + }, + "owner": "ASAxmEaTT1HFe3mVC3zbKDE4tuB28W7732XQrEMBM5W2", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 17, + "mint": "WMgdN6XrdDxSamrZrKfEY6xe6QWD5bb9RaDuAbtwFPt", + "uiTokenAmount": { + "uiAmount": 991546250.16415, + "decimals": 6, + "amount": "991546250164150", + "uiAmountString": "991546250.16415" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 18, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.27448489, + "decimals": 8, + "amount": "27448489", + "uiAmountString": "0.27448489" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 19, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.45448763, + "decimals": 8, + "amount": "45448763", + "uiAmountString": "0.45448763" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 20, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "postTokenBalances": [ + { + "accountIndex": 2, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.05029744, + "decimals": 8, + "amount": "5029744", + "uiAmountString": "0.05029744" + }, + "owner": "7zCjgF9tzBHwH333ba1DTancC5YayaFNCm35pwcLt2MF", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "So11111111111111111111111111111111111111112", + "uiTokenAmount": { + "uiAmount": 19648.005648595, + "decimals": 9, + "amount": "19648005648595", + "uiAmountString": "19648.005648595" + }, + "owner": "8FnX3xo2yYw3EUE6w3nQA4GfXGS9wpK6oj3veJpbFzLo", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 5, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "uiTokenAmount": { + "uiAmount": 2513054.792171, + "decimals": 6, + "amount": "2513054792171", + "uiAmountString": "2513054.792171" + }, + "owner": "8FnX3xo2yYw3EUE6w3nQA4GfXGS9wpK6oj3veJpbFzLo", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 6, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 6, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "7zCjgF9tzBHwH333ba1DTancC5YayaFNCm35pwcLt2MF", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 8, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 812.56692422, + "decimals": 8, + "amount": "81256692422", + "uiAmountString": "812.56692422" + }, + "owner": "ASAxmEaTT1HFe3mVC3zbKDE4tuB28W7732XQrEMBM5W2", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 9, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "uiTokenAmount": { + "uiAmount": 292053.526078, + "decimals": 6, + "amount": "292053526078", + "uiAmountString": "292053.526078" + }, + "owner": "ASAxmEaTT1HFe3mVC3zbKDE4tuB28W7732XQrEMBM5W2", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 14, + "mint": "WMgdN6XrdDxSamrZrKfEY6xe6QWD5bb9RaDuAbtwFPt", + "uiTokenAmount": { + "uiAmount": 175939903.19482, + "decimals": 6, + "amount": "175939903194820", + "uiAmountString": "175939903.19482" + }, + "owner": "7zCjgF9tzBHwH333ba1DTancC5YayaFNCm35pwcLt2MF", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 17, + "mint": "WMgdN6XrdDxSamrZrKfEY6xe6QWD5bb9RaDuAbtwFPt", + "uiTokenAmount": { + "uiAmount": 813829176.229988, + "decimals": 6, + "amount": "813829176229988", + "uiAmountString": "813829176.229988" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 18, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 5.22884173, + "decimals": 8, + "amount": "522884173", + "uiAmountString": "5.22884173" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 19, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.50453164, + "decimals": 8, + "amount": "50453164", + "uiAmountString": "0.50453164" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 20, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [] + }, + "computeUnitsConsumed": 215106, + "costUnits": 226517 + }, + "version": 1, + "slot": 453240674, + "transactionIndex": 610, + "blockTime": 1791112668 + } + }, + { + "label": "reward sell xstock", + "signature": "37hc3CBzotq2sp26Eycb2NKWT1679NXZiugMZ74FZmNxSUkJ7GHnfQ5R1vJV2oQxRP5CvQdFSyPwNBSF3cN6q4XV", + "pool": "mm356Ydhc5Pt5oi6eq84s5yNSj9ibJCQjUYGeR76j1d", + "base_mint": "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "quote_mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "creator": "2oY5jginLgpFKBJPs7MQKyLYdCtPgKqWZJfYz8R8zuHT", + "global_config": "9mfQRzhyaACD49VhrSwCWKjrHVLcNfhxSdhg2hDSwCd8", + "platform_config": "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt", + "accounts_base64": { + "global_config": "lQicyqD8sNkABAAAAAAAAAAAAAAAAAAAAAAAxAkAAAAAAAAAAAAAAAAAAICWmAAAAAAAP0IPAAAAAAABAAAAAAAAAPBJAgAAAAAAAQAAAAAAAAAH6AObtK1M95w4PkPAxR3RhTdAUSt50PlCEpI0UdADgwyz4e5t0cIQzcDSX4MHTIDK0v9FiO4+tNA3FD3gRgUiDLPevnctauoG6DOOMPSKGFjctO/P9tXw/LdYKgFdwIUGML1SYnBjHI8sW0M/s3jLoiH369IfbHsD8odEFvzLFAYwvGw1LgPHgfQBOLmbBdQ7GAZ6dG85H/e4gZH21Q8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "platform_config": "oE6AAPhT5qAXBAAAAAAAAD5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5Pk+EKn0LDJ7I4k6gOjHJ+HmjXcMEdYkaqu5dnrAIL/lAQg8AAAAAAAAAAAAAAAAAAAAAAAAAAAAQJwAAAAAAAFN0b25rRnVuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABodHRwczovL3d3dy5zdG9ua2Z1bi54eXoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaHR0cHM6Ly93d3cuc3RvbmtmdW4ueHl6L3N0b25rZnVuLWxvZ28ucG5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKmyHxd+m5dujuEp9BrvN4SqxRSUkJWwAuYRfVnQNpaPAAAAAAAAAABALVyvepfFNJdljlYbJiPQ449TQPIIrdUKTvRUCwrLLj5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5AAAAAAAAAAA+T4QqfQsMnsjiTqA6Mcn4eaNdwwR1iRqq7l2esAgv+QABGv1mAdFtt7hdam3csaoALEDujVMj8YuY7QspWaQA7JUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base_mint": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDGpH6NAwAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFaOyIT2dLOCFJtnyRSkQv0BUJpwymeDaDlEynsJ+CUMAQBsAAeDqBUmIQ+UoOjoTSCHADs6QmDSnOedrP4hpOp3H6YoQC1cr3qXxTSXZY5WGyYj0OOPU0DyCK3VCk70VAsKyy6b6Lcu2gwAABkEAAAAAAAAAIDGpH6NAwAsARkEAAAAAAAAAIDGpH6NAwAsARMAtwAHg6gVJiEPlKDo6E0ghwA7OkJg0pznnaz+IaTqdx+mKFaOyIT2dLOCFJtnyRSkQv0BUJpwymeDaDlEynsJ+CUMIAAAAFVuZmluaXNoZWQgRmFibGUgb2YgdGhlIFNwYXJyb3dzAwAAAE9XTEQAAABodHRwczovL2dhdGV3YXkuaXJ5cy54eXovaUZqbXo1TTN5dGVMbVNyV2pHZDNiSGFEYmthSjFFdkVIR2FXRFFpUWdFdgAAAAA=" + }, + "transaction": { + "transaction": { + "signatures": [ + "37hc3CBzotq2sp26Eycb2NKWT1679NXZiugMZ74FZmNxSUkJ7GHnfQ5R1vJV2oQxRP5CvQdFSyPwNBSF3cN6q4XV" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 11 + }, + "accountKeys": [ + "5KXDF6QnqhBj72hDtJNkkpFaQVUfbFXNybMsp3DiK6tD", + "2L51BajY7jEtJfTq75GTg4PciGvc8ezXL5TroS2rbGQz", + "57VbjBpNwFQWebdFaoWLbS4kzKiRBstNswX3gVwURrE4", + "5VYmk3HsV67hiZRA3GPPHPExSJxwXJxgJhJzkJcaXN9q", + "7qKfybUR3aSog6PhJu4tMhvW8En8LQbeF1fQRQMoVk7j", + "8iKiJjW8DG9rgk9g6DBFiRjBsmzMjVjSGrZDemZEnYZb", + "HCfVmRMAxXm3u2zxTTmpFYXRRjdvomvGmy78JYN3Fiqv", + "mm356Ydhc5Pt5oi6eq84s5yNSj9ibJCQjUYGeR76j1d", + "11111111111111111111111111111111", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr", + "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt", + "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "9mfQRzhyaACD49VhrSwCWKjrHVLcNfhxSdhg2hDSwCd8", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "ComputeBudget111111111111111111111111111111", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8" + ], + "recentBlockhash": "7w6qxdrvvVcVatu7AnM1Gy7iC3NEDBaah9CXpAsg6vYa", + "instructions": [ + { + "programIdIndex": 14, + "accounts": [], + "data": "3Sy41WEwNLnT", + "stackHeight": 1 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 5, + 0, + 18, + 8, + 16 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 15, + "accounts": [ + 0, + 17, + 12, + 10, + 7, + 4, + 5, + 6, + 3, + 11, + 18, + 16, + 16, + 9, + 15, + 8, + 1, + 2 + ], + "data": "B3F1THDgKfWMiwNv8BjJZC7EWyGVsvnYJxY6qQSCPSP1", + "stackHeight": 1 + } + ] + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 25150, + "preBalances": [ + 141766002270, + 1918899, + 1539240, + 1539240, + 1574800, + 2136720, + 1554480, + 2829560, + 1, + 0, + 7461120, + 3357880, + 3473040, + 3388612899, + 1, + 37580031, + 70228638, + 0, + 2390453155 + ], + "postBalances": [ + 141765977120, + 1918899, + 1539240, + 1539240, + 1574800, + 2136720, + 1554480, + 2829560, + 1, + 0, + 7461120, + 3357880, + 3473040, + 3388612899, + 1, + 37580031, + 70228638, + 0, + 2390453155 + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 15, + "accounts": [ + 9 + ], + "data": "EwDfpErTWwQhCAycT1hw3kcCeCeMWy5t5gLG7mTCwMtzfoeJR9nRVYS5C4DNCZduQkcwaAXAbNxxhuPL3MwnBdhavzDLtY9JNtnGWSmS6Bh9cB7vNxvwC4ao7uuL2qpUpZoQUgbedFStaGYsqfvSNYTN6tFBvYjkjKCt3rJuDmxKLGxAVGv8LobVgPZseKdJNjXdTGWqvVNz6JCzo8fA", + "stackHeight": 2 + }, + { + "programIdIndex": 16, + "accounts": [ + 4, + 11, + 6, + 0 + ], + "data": "h392ePAArPwmP", + "stackHeight": 2 + }, + { + "programIdIndex": 16, + "accounts": [ + 3, + 18, + 5, + 17 + ], + "data": "iZxQ2ZqnhdARM", + "stackHeight": 2 + }, + { + "programIdIndex": 16, + "accounts": [ + 3, + 18, + 1, + 17 + ], + "data": "iWHwYP9LRk7vP", + "stackHeight": 2 + } + ] + } + ], + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 8857 of 402850 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: SellExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 365219 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe4Ld1jQB/ECJq7WAtzyGBpAYVf57Ydn7pqTn7QO6GfR9AB4xftR0QIAqTwPPunPAwC5Gx2IAAAAAJUfGFRzqQEAxcAzaQAAAACP11igR6QBAFnb9GYAAAAABki/sysFAADBtTcCAAAAAO9vAQAAAAAAvL8FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAB", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3378 of 361354 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3710 of 355918 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3602 of 343297 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 56979 of 393993 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success" + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.50565701, + "decimals": 8, + "amount": "50565701", + "uiAmountString": "0.50565701" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 2, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 3, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 17.74567021, + "decimals": 8, + "amount": "1774567021", + "uiAmountString": "17.74567021" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "uiTokenAmount": { + "uiAmount": 11722180.185746, + "decimals": 6, + "amount": "11722180185746", + "uiAmountString": "11722180.185746" + }, + "owner": "5KXDF6QnqhBj72hDtJNkkpFaQVUfbFXNybMsp3DiK6tD", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 5, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 140.47233596, + "decimals": 8, + "amount": "14047233596", + "uiAmountString": "140.47233596" + }, + "owner": "5KXDF6QnqhBj72hDtJNkkpFaQVUfbFXNybMsp3DiK6tD", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 6, + "mint": "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "uiTokenAmount": { + "uiAmount": 532212226.089067, + "decimals": 6, + "amount": "532212226089067", + "uiAmountString": "532212226.089067" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 0.50942465, + "decimals": 8, + "amount": "50942465", + "uiAmountString": "0.50942465" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 2, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 8, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 3, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 17.36984816, + "decimals": 8, + "amount": "1736984816", + "uiAmountString": "17.36984816" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 4, + "mint": "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "uiTokenAmount": { + "uiAmount": 5861090.092873, + "decimals": 6, + "amount": "5861090092873", + "uiAmountString": "5861090.092873" + }, + "owner": "5KXDF6QnqhBj72hDtJNkkpFaQVUfbFXNybMsp3DiK6tD", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 5, + "mint": "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", + "uiTokenAmount": { + "uiAmount": 140.84439037, + "decimals": 8, + "amount": "14084439037", + "uiAmountString": "140.84439037" + }, + "owner": "5KXDF6QnqhBj72hDtJNkkpFaQVUfbFXNybMsp3DiK6tD", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 6, + "mint": "6ptLjeR4tVBRi93CD3fo3mE2nu4wFixjbAiLxYwugW3m", + "uiTokenAmount": { + "uiAmount": 537897483.479153, + "decimals": 6, + "amount": "537897483479153", + "uiAmountString": "537897483.479153" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [] + }, + "computeUnitsConsumed": 65986, + "costUnits": 70108 + }, + "version": "legacy", + "slot": 453240685, + "transactionIndex": 149, + "blockTime": 1791112671 + } + }, + { + "label": "standard buy any", + "signature": "oBSPVUUNqDUNbFVdu4oJf4bYPdZPP62Vm7WUXhrXf5oscgk6M2wXtV1MGxw5pKbHs5Y17zD2hKJs2etjoxUgokn", + "pool": "DtTeNXP1SZk25wHCu4kcK75EuDr8ZpRrMhggVJXp7XBU", + "base_mint": "J32UdgMtSeEy5PHFswBi1gJkobDt3vho3gXcFG8t8Skd", + "quote_mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "creator": "6c2kt5XTSjoXYrMihcw4632LP6GrLX4yPsuzNFkrdJK6", + "global_config": "4Rb2joMnuDt9zRPuTYxUjQRGQ8oNBbKdXnCJSCojvBsW", + "platform_config": "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7", + "accounts_base64": { + "global_config": "lQicyqD8sNkABAAAAAAAAAAAAAAAAAAAAAAAxAkAAAAAAAAAAAAAAAAAAICWmAAAAAAAP0IPAAAAAAABAAAAAAAAAPBJAgAAAAAAAQAAAAAAAABOVBP6DR6jIjj1JhHBJ7XDrPeiREnJXbWaX7rvICx36Qyz4e5t0cIQzcDSX4MHTIDK0v9FiO4+tNA3FD3gRgUiDLPevnctauoG6DOOMPSKGFjctO/P9tXw/LdYKgFdwIUGML1SYnBjHI8sW0M/s3jLoiH369IfbHsD8odEFvzLFAYwvGw1LgPHgfQBOLmbBdQ7GAZ6dG85H/e4gZH21Q8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "platform_config": "oE6AAPhT5qAEBAAAAAAAAD5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5Pk+EKn0LDJ7I4k6gOjHJ+HmjXcMEdYkaqu5dnrAIL/lAQg8AAAAAAAAAAAAAAAAAAAAAAAAAAAAQJwAAAAAAAFN0b25rRnVuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABodHRwczovL3d3dy5zdG9ua2Z1bi54eXoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaHR0cHM6Ly93d3cuc3RvbmtmdW4ueHl6L3N0b25rZnVuLWxvZ28ucG5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKmyHxd+m5dujuEp9BrvN4SqxRSUkJWwAuYRfVnQNpaPAAAAAAAAAABALVyvepfFNJdljlYbJiPQ449TQPIIrdUKTvRUCwrLLj5PhCp9CwyeyOJOoDoxyfh5o13DBHWJGqruXZ6wCC/5AAAAAAAAAAA+T4QqfQsMnsjiTqA6Mcn4eaNdwwR1iRqq7l2esAgv+QABGv1mAdFtt7hdam3csaoALEDujVMj8YuY7QspWaQA7JUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base_mint": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy5IQ2dSLAwAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0bgj4azO7jrKEdQ6Ti6rTh6ZAQqMIQx7qY6BQA1GbOEwCfAAeDqBUmIQ+UoOjoTSCHADs6QmDSnOedrP4hpOp3H6Yo/RuCPhrM7uOsoR1DpOLqtOHpkBCowhDHupjoFADUZs4XAAAASFVOVCBDQU5BUlkgLSBURVNUIE9OTFkIAAAASFVOVFRFU1QwAAAAaHR0cHM6Ly9odW50LWNhbmFyeS52ZXJjZWwuYXBwL2NhbmFyeS90b2tlbi5qc29uAAAAAA==" + }, + "transaction": { + "transaction": { + "signatures": [ + "oBSPVUUNqDUNbFVdu4oJf4bYPdZPP62Vm7WUXhrXf5oscgk6M2wXtV1MGxw5pKbHs5Y17zD2hKJs2etjoxUgokn" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 12 + }, + "accountKeys": [ + "5R2ZGdUuxYh3SCKNLKogmwbt12fx82A6ToLTw4e2YPrX", + "3fcyYv7QEtiNJMBgmX6VnE7nEGNjUoYx3ftgVXNuqeUb", + "DtTeNXP1SZk25wHCu4kcK75EuDr8ZpRrMhggVJXp7XBU", + "3mpUhnoj1JyE2UMhUMnxmX3FAvBwUzei5b6ZoJ67UxHx", + "CDJY7XcgyDfViJz6hPNjUpFRUpWqFqGE1WFny3ViVkR2", + "G7DorTDPj4CZ6TEMD3RgUhyZonkstsY6Lx4FMcwYKs56", + "HGFRkuwb4uDnBJgjvHDLYD5Vc43uAxGad3Doimd6A44H", + "knZ3RWqdetdQ1Z6yPU1fVvp19snqBp6237aqiKx31vh", + "ComputeBudget111111111111111111111111111111", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "J32UdgMtSeEy5PHFswBi1gJkobDt3vho3gXcFG8t8Skd", + "11111111111111111111111111111111", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "4Rb2joMnuDt9zRPuTYxUjQRGQ8oNBbKdXnCJSCojvBsW", + "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7", + "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "2DPAtwB8L12vrMRExbLuyGnC7n2J5LNoZQSejeQGpwkr" + ], + "recentBlockhash": "2qEZhCVCi65xwV6t2HR5ZvhEHCNmQcWNaBJTg1jTRgji", + "instructions": [ + { + "programIdIndex": 8, + "accounts": [], + "data": "LEJDE7", + "stackHeight": 1 + }, + { + "programIdIndex": 8, + "accounts": [], + "data": "3gJqkocMWaMm", + "stackHeight": 1 + }, + { + "programIdIndex": 9, + "accounts": [ + 0, + 1, + 0, + 10, + 11, + 12 + ], + "data": "2", + "stackHeight": 1 + }, + { + "programIdIndex": 13, + "accounts": [ + 0, + 14, + 15, + 16, + 2, + 1, + 3, + 4, + 5, + 10, + 17, + 12, + 18, + 19, + 13, + 11, + 6, + 7 + ], + "data": "HtTvTxyWwMDSP4C13TFvxovYPKq24tKXZMz9y4mwMCpF", + "stackHeight": 1 + } + ], + "addressTableLookups": [] + } + }, + "meta": { + "err": null, + "status": { + "Ok": null + }, + "fee": 20000, + "preBalances": [ + 71660000, + 1513840, + 2829560, + 1488440, + 1488440, + 1488440, + 2039280, + 1488440, + 1, + 3388612899, + 2667000, + 1, + 70228638, + 37580031, + 0, + 3473040, + 7461120, + 46705822553, + 200653906, + 0 + ], + "postBalances": [ + 71640000, + 1513840, + 2829560, + 1488440, + 1488440, + 1488440, + 2039280, + 1488440, + 1, + 3388612899, + 2667000, + 1, + 70228638, + 37580031, + 0, + 3473040, + 7461120, + 46705822553, + 200653906, + 0 + ], + "innerInstructions": [ + { + "index": 3, + "instructions": [ + { + "programIdIndex": 13, + "accounts": [ + 19 + ], + "data": "EwDfpErTWwQhCAycT1hw3kxN2ogT2VDJsTiapFmtzAc73XQu53ghMo25w6aA2v3xDyp413vrk5icHjBYT8oyYBPnfPn9r6vf3kmqyMS3Q8KQ2ADecaFQjFCV8rtnFKAi7k9VvccKUs5zHFXcsTehSnhfyvsJyxtFB4mCwpn1gTCp8BZB7KxiuLBYSF445xRQ24vFVh4qyciwZS1aAmqz", + "stackHeight": 2 + }, + { + "programIdIndex": 18, + "accounts": [ + 3, + 17, + 5, + 0 + ], + "data": "gsPP5oyx9kEjS", + "stackHeight": 2 + }, + { + "programIdIndex": 12, + "accounts": [ + 4, + 10, + 1, + 14 + ], + "data": "gdpVjoqifJoDo", + "stackHeight": 2 + }, + { + "programIdIndex": 18, + "accounts": [ + 5, + 17, + 6, + 14 + ], + "data": "gR7P8QrDndSuE", + "stackHeight": 2 + } + ] + } + ], + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", + "Program log: CreateIdempotent", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 4357 of 149700 compute units", + "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [1]", + "Program log: Instruction: BuyExactIn", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj invoke [2]", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 626 of 117014 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success", + "Program data: vdt/007mYe6/ev3AeqLd+vvAik0R8fhDFoHBMRRMByhV7kNX8WRVdwB4xftR0QIA5HAOPunPAwCEV8ASMg4AAICfK6q8BAAASFG3uREAAACpbxlL1wQAAOYB2x0SAAAAPDFoZQAAAAAp0O2gGgAAAIbmQAAAAAAAGJoDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 113482 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", + "Program log: Instruction: TransferChecked", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2442 of 111319 compute units", + "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 99963 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj consumed 48194 of 145343 compute units", + "Program LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj success" + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": "J32UdgMtSeEy5PHFswBi1gJkobDt3vho3gXcFG8t8Skd", + "uiTokenAmount": { + "uiAmount": 1150680.020651, + "decimals": 6, + "amount": "1150680020651", + "uiAmountString": "1150680.020651" + }, + "owner": "5R2ZGdUuxYh3SCKNLKogmwbt12fx82A6ToLTw4e2YPrX", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 3, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": 3.875460511, + "decimals": 9, + "amount": "3875460511", + "uiAmountString": "3.875460511" + }, + "owner": "5R2ZGdUuxYh3SCKNLKogmwbt12fx82A6ToLTw4e2YPrX", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 4, + "mint": "J32UdgMtSeEy5PHFswBi1gJkobDt3vho3gXcFG8t8Skd", + "uiTokenAmount": { + "uiAmount": 994791644.651648, + "decimals": 6, + "amount": "994791644651648", + "uiAmountString": "994791644.651648" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 5, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": 139.414987008, + "decimals": 9, + "amount": "139414987008", + "uiAmountString": "139.414987008" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 6, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": 15.562960228, + "decimals": 9, + "amount": "15562960228", + "uiAmountString": "15.562960228" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 7, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": "J32UdgMtSeEy5PHFswBi1gJkobDt3vho3gXcFG8t8Skd", + "uiTokenAmount": { + "uiAmount": 1265049.110228, + "decimals": 6, + "amount": "1265049110228", + "uiAmountString": "1265049.110228" + }, + "owner": "5R2ZGdUuxYh3SCKNLKogmwbt12fx82A6ToLTw4e2YPrX", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 3, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": 2.174133347, + "decimals": 9, + "amount": "2174133347", + "uiAmountString": "2.174133347" + }, + "owner": "5R2ZGdUuxYh3SCKNLKogmwbt12fx82A6ToLTw4e2YPrX", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 4, + "mint": "J32UdgMtSeEy5PHFswBi1gJkobDt3vho3gXcFG8t8Skd", + "uiTokenAmount": { + "uiAmount": 994677275.562071, + "decimals": 6, + "amount": "994677275562071", + "uiAmountString": "994677275.562071" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "accountIndex": 5, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": 141.0993009, + "decimals": 9, + "amount": "141099300900", + "uiAmountString": "141.0993009" + }, + "owner": "WLHv2UAZm6z4KyaaELi5pjdbJh6RESMva1Rnn8pJVVh", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 6, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": 15.5799735, + "decimals": 9, + "amount": "15579973500", + "uiAmountString": "15.5799735" + }, + "owner": "56XVRVAsgWv6ADaxzoNnbL38LMoWKM5WiSAhrAWUbd2p", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "accountIndex": 7, + "mint": "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", + "uiTokenAmount": { + "uiAmount": null, + "decimals": 9, + "amount": "0", + "uiAmountString": "0" + }, + "owner": "9sHpTfmVpCfP2zexRNK6j38NBchMv1RWpdXPK5NEcZan", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "rewards": [], + "loadedAddresses": { + "writable": [], + "readonly": [] + }, + "computeUnitsConsumed": 52851, + "costUnits": 56998 + }, + "version": 0, + "slot": 453240534, + "transactionIndex": 155, + "blockTime": 1791112631 + } + } +] \ No newline at end of file diff --git a/tests/regression/verify_cleanup_harvests_withheld_fees.py b/tests/regression/verify_cleanup_harvests_withheld_fees.py new file mode 100644 index 0000000..5fb53a0 --- /dev/null +++ b/tests/regression/verify_cleanup_harvests_withheld_fees.py @@ -0,0 +1,162 @@ +"""Verify cleanup harvests withheld transfer fees before closing a token account. + +A Token-2022 coin with a transfer fee withholds the fee inside the account that +receives it — the bot's own account, on a buy — and Token-2022 refuses to close +an account still holding withheld fees (custom error 0x23). Cleanup used to send +a bare CloseAccount, which reverts for every LaunchLab reward coin, leaving its +rent locked and the wallet not back to SOL only. + +Fixture: `raw_stonkfun_token_accounts_from_getaccountinfo.json`, two real +Token-2022 token accounts: one holding withheld fees from a StonkFun reward +coin, one from a standard coin with no transfer-fee extension. + +Offline machine checks, no network and no funds moved: + + A. The withheld amount reads from the account's TransferFeeAmount + extension; an account without the extension, and a classic SPL account, + read as zero. + B. Cleanup of the account holding withheld fees sends HarvestWithheldTokensToMint + (mint and account, writable, no signer) before CloseAccount, in one + transaction. + C. Cleanup of an account with nothing withheld sends CloseAccount alone. + +Usage: + uv run tests/regression/verify_cleanup_harvests_withheld_fees.py +""" + +import asyncio +import base64 +import json +import sys +from pathlib import Path +from types import SimpleNamespace + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from solders.pubkey import Pubkey # noqa: E402 + +from cleanup import manager as cleanup_manager # noqa: E402 +from cleanup.manager import AccountCleanupManager, withheld_transfer_fee # noqa: E402 +from core.pubkeys import SystemAddresses # noqa: E402 + +FIXTURE = Path(__file__).with_name( + "raw_stonkfun_token_accounts_from_getaccountinfo.json" +) +HARVEST_WITHHELD_TO_MINT = bytes([26, 4]) +CLOSE_ACCOUNT = bytes([9]) + + +def _accounts() -> dict: + return json.loads(FIXTURE.read_text()) + + +class _CleanupClient: + """Serves one token account and records what cleanup sends.""" + + def __init__(self, data: bytes) -> None: + self.data = data + self.sent: list = [] + + async def get_account_info(self, _address: Pubkey) -> SimpleNamespace: + return SimpleNamespace(data=self.data, owner=SystemAddresses.TOKEN_2022_PROGRAM) + + async def get_token_account_balance(self, _address: Pubkey) -> int: + return 0 + + async def build_and_send_transaction(self, instructions: list, *_a, **_k) -> str: + self.sent.append(instructions) + return "STUB_SIGNATURE" + + async def confirm_transaction(self, *_a, **_k) -> bool: + return True + + +async def _no_sleep(_seconds: float) -> None: + return None + + +def _cleanup(entry: dict) -> list: + address = Pubkey.from_string(entry["address"]) + client = _CleanupClient(base64.b64decode(entry["data_base64"])) + wallet = SimpleNamespace( + pubkey=Pubkey.from_string(entry["owner"]), + keypair=None, + get_associated_token_address=lambda _mint, _program: address, + ) + cleanup_manager.asyncio.sleep = _no_sleep + manager = AccountCleanupManager(client, wallet, priority_fee_manager=None) + asyncio.run( + manager.cleanup_ata( + Pubkey.from_string(entry["mint"]), SystemAddresses.TOKEN_2022_PROGRAM + ) + ) + return client.sent + + +def check_a_withheld_amount_reads() -> bool: + """A: withheld amount from the extension; zero without one.""" + accounts = _accounts() + withheld = withheld_transfer_fee( + base64.b64decode(accounts["with_withheld_fee"]["data_base64"]) + ) + none = withheld_transfer_fee( + base64.b64decode(accounts["without_transfer_fee_extension"]["data_base64"]) + ) + classic = withheld_transfer_fee(bytes(165)) + ok = withheld > 0 and none == 0 and classic == 0 + if not ok: + print(f" withheld={withheld} without_extension={none} classic={classic}") + return ok + + +def check_b_harvest_before_close() -> bool: + """B: harvest (mint, account; writable, unsigned) then close, one transaction.""" + entry = _accounts()["with_withheld_fee"] + sent = _cleanup(entry) + if len(sent) != 1 or len(sent[0]) != 2: # noqa: PLR2004 + print(f" sent {sent}") + return False + harvest, close = sent[0] + metas = [(str(m.pubkey), m.is_writable, m.is_signer) for m in harvest.accounts] + ok = ( + harvest.program_id == SystemAddresses.TOKEN_2022_PROGRAM + and bytes(harvest.data) == HARVEST_WITHHELD_TO_MINT + and metas == [(entry["mint"], True, False), (entry["address"], True, False)] + and bytes(close.data) == CLOSE_ACCOUNT + ) + if not ok: + print(f" harvest={harvest} close={close}") + return ok + + +def check_c_close_alone_without_withheld() -> bool: + """C: nothing withheld -> CloseAccount alone.""" + sent = _cleanup(_accounts()["without_transfer_fee_extension"]) + ok = len(sent) == 1 and [bytes(ix.data) for ix in sent[0]] == [CLOSE_ACCOUNT] + if not ok: + print(f" sent {sent}") + return ok + + +def main() -> int: + """Run every check and report.""" + checks = [ + ("A: withheld amount reads from the account", check_a_withheld_amount_reads), + ("B: harvest precedes close", check_b_harvest_before_close), + ( + "C: close alone when nothing is withheld", + check_c_close_alone_without_withheld, + ), + ] + passed = 0 + for label, check in checks: + ok = check() + print(f"{'PASS' if ok else 'FAIL'} {label}") + passed += ok + print(f"\n{passed}/{len(checks)} checks passed") + return 0 if passed == len(checks) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/regression/verify_cookbook_arguments.py b/tests/regression/verify_cookbook_arguments.py index b749dee..3094fc3 100644 --- a/tests/regression/verify_cookbook_arguments.py +++ b/tests/regression/verify_cookbook_arguments.py @@ -48,6 +48,8 @@ NO_INPUT = { "pumpfun_listen_migrations_logsubscribe.py", "pumpfun_listen_migrations_programsubscribe.py", "pumpfun_capture_transactions_blocksubscribe.py", + "stonkfun_listen_tokens_geyser.py", + "stonkfun_listen_tokens_blocksubscribe.py", } diff --git a/tests/regression/verify_launchlab_launch_parsing.py b/tests/regression/verify_launchlab_launch_parsing.py new file mode 100644 index 0000000..7de75d6 --- /dev/null +++ b/tests/regression/verify_launchlab_launch_parsing.py @@ -0,0 +1,315 @@ +"""Verify LaunchLab launches parse into the TokenInfo the pool will agree with. + +letsbonk.fun and StonkFun share one program, Raydium LaunchLab; a launch belongs +to the platform whose config it names. The letsbonk parser this replaced read +the creator from account 0 — the payer, which the program does not have to +make the creator — never read the quote mint, missed launches issued by a +router as inner instructions, and in its geyser path dropped every account +behind an address lookup table. + +Fixture: `raw_stonkfun_launches_from_gettransaction.json`, real StonkFun +launches sent as a legacy, a v0 and a v1 transaction. + +Offline machine checks, no network and no funds moved: + + A. Each launch parses over blockSubscribe JSON into the instruction's own + accounts: creator from the `creator` slot, payer as `user`, pool, vaults, + both configs, the quote mint and both token programs, the transfer fee + argument, and `state_from_event` set. + B. The same launch parses identically from a geyser frame. + C. A launch whose payer is not its creator reports the creator slot. + D. A launch issued as an inner instruction is found, over both routes. + E. Each platform keeps to its own configs: the letsbonk parser ignores every + StonkFun launch, and claims one rewritten to name letsbonk's config. + F. The stream listeners subscribe on the platform configs for LaunchLab and + on the program for pump.fun, and a block listener running both LaunchLab + platforms routes a StonkFun launch to StonkFun. + +Usage: + uv run tests/regression/verify_launchlab_launch_parsing.py +""" + +import copy +import json +import sys +from pathlib import Path + +import base58 + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from solders.pubkey import Pubkey # noqa: E402 + +from geyser.generated import geyser_pb2 # noqa: E402 +from interfaces.core import Platform # noqa: E402 +from monitoring.universal_block_listener import UniversalBlockListener # noqa: E402 +from platforms.launchlab import LAUNCHLAB_PROGRAM # noqa: E402 +from platforms.letsbonk import PLATFORM_CONFIGS as LETSBONK_CONFIGS # noqa: E402 +from platforms.letsbonk import LetsBonkEventParser # noqa: E402 +from platforms.pumpfun import PumpFunEventParser # noqa: E402 +from platforms.stonkfun import StonkFunEventParser # noqa: E402 +from platforms.stonkfun.addresses import ( + PLATFORM_CONFIGS as STONKFUN_CONFIGS, +) +from utils.idl_manager import get_idl_manager # noqa: E402 + +FIXTURE = Path(__file__).with_name("raw_stonkfun_launches_from_gettransaction.json") + +PARSER = get_idl_manager().get_parser(Platform.STONK_FUN) +STONKFUN = StonkFunEventParser(PARSER) +LETSBONK = LetsBonkEventParser(PARSER) +INITIALIZE = bytes( + next( + i + for i in json.load( + (PROJECT_ROOT / "idl" / "raydium_launchlab_idl.json").open() + )["instructions"] + if i["name"] == "initialize_with_token_2022" + )["discriminator"] +) +OTHER_CREATOR = "CreatorXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1" + +# initialize_with_token_2022 account slots, from the IDL. +SLOTS = { + "payer": 0, + "creator": 1, + "global_config": 2, + "platform_config": 3, + "pool_state": 5, + "base_mint": 6, + "quote_mint": 7, + "base_vault": 8, + "quote_vault": 9, + "base_token_program": 10, + "quote_token_program": 11, +} + + +def _launches() -> list[dict]: + return json.loads(FIXTURE.read_text()) + + +def _keys(tx: dict) -> list[str]: + loaded = tx["meta"].get("loadedAddresses") or {} + return ( + tx["transaction"]["message"]["accountKeys"] + + loaded.get("writable", []) + + loaded.get("readonly", []) + ) + + +def _initialize(tx: dict) -> tuple[dict, dict]: + """The launch instruction and its accounts by slot name.""" + keys = _keys(tx) + for ix in tx["transaction"]["message"]["instructions"]: + if keys[ix["programIdIndex"]] == str(LAUNCHLAB_PROGRAM) and base58.b58decode( + ix["data"] + ).startswith(INITIALIZE): + return ix, { + name: keys[ix["accounts"][slot]] for name, slot in SLOTS.items() + } + raise AssertionError("fixture launch has no top-level initialize") + + +def _parse_block(parser, tx: dict): + return parser.parse_token_creation_from_block( + {"transactions": [{"transaction": tx["transaction"], "meta": tx["meta"]}]} + ) + + +def _geyser_update(tx: dict) -> geyser_pb2.SubscribeUpdate: + """A geyser frame carrying the same transaction as the JSON fixture.""" + update = geyser_pb2.SubscribeUpdate() + info = update.transaction.transaction + message = info.transaction.message + json_message = tx["transaction"]["message"] + message.account_keys.extend( + base58.b58decode(k) for k in json_message["accountKeys"] + ) + for ix in json_message["instructions"]: + compiled = message.instructions.add() + compiled.program_id_index = ix["programIdIndex"] + compiled.accounts = bytes(ix["accounts"]) + compiled.data = base58.b58decode(ix["data"]) + loaded = tx["meta"].get("loadedAddresses") or {} + info.meta.loaded_writable_addresses.extend( + base58.b58decode(k) for k in loaded.get("writable", []) + ) + info.meta.loaded_readonly_addresses.extend( + base58.b58decode(k) for k in loaded.get("readonly", []) + ) + for group in tx["meta"].get("innerInstructions") or []: + inner = info.meta.inner_instructions.add() + inner.index = group["index"] + for ix in group["instructions"]: + compiled = inner.instructions.add() + compiled.program_id_index = ix["programIdIndex"] + compiled.accounts = bytes(ix["accounts"]) + compiled.data = base58.b58decode(ix["data"]) + return update + + +def _fields(token_info) -> dict: + return { + "creator": str(token_info.creator), + "payer": str(token_info.user), + "pool_state": str(token_info.pool_state), + "base_mint": str(token_info.mint), + "quote_mint": str(token_info.quote_mint), + "base_vault": str(token_info.base_vault), + "quote_vault": str(token_info.quote_vault), + "global_config": str(token_info.global_config), + "platform_config": str(token_info.platform_config), + "base_token_program": str(token_info.token_program_id), + "quote_token_program": str(token_info.quote_token_program_id), + } + + +def check_a_block_route() -> bool: + """A: block JSON -> every field from its own account slot.""" + ok = True + for tx in _launches(): + _, accounts = _initialize(tx) + token_info = _parse_block(STONKFUN, tx) + if token_info is None: + print(f" version {tx.get('version')}: not parsed") + ok = False + continue + args = PARSER.decode_instruction( + base58.b58decode(_initialize(tx)[0]["data"]), [], [] + )["args"] + fee = (args.get("transfer_fee_extension_param") or {}).get( + "transfer_fee_basis_points", 0 + ) + good = ( + _fields(token_info) == accounts + and token_info.transfer_fee_bps == fee + and token_info.state_from_event + and token_info.platform == Platform.STONK_FUN + ) + if not good: + print( + f" version {tx.get('version')}: {_fields(token_info)} vs {accounts}" + ) + ok &= good + return ok + + +def check_b_geyser_route() -> bool: + """B: geyser frame -> the same TokenInfo as the block route.""" + ok = True + for tx in _launches(): + from_geyser = STONKFUN.parse_token_creation_from_geyser(_geyser_update(tx)) + from_block = _parse_block(STONKFUN, tx) + good = from_geyser is not None and _fields(from_geyser) == _fields(from_block) + if not good: + print(f" version {tx.get('version')}: geyser gave {from_geyser}") + ok &= good + return ok + + +def check_c_payer_is_not_creator() -> bool: + """C: a separate creator account is the creator; the payer is only the user.""" + tx = copy.deepcopy(next(t for t in _launches() if t.get("version") == "legacy")) + message = tx["transaction"]["message"] + message["accountKeys"].append(OTHER_CREATOR) + ix, accounts = _initialize(tx) + ix["accounts"][SLOTS["creator"]] = len(message["accountKeys"]) - 1 + token_info = _parse_block(STONKFUN, tx) + ok = ( + token_info is not None + and str(token_info.creator) == OTHER_CREATOR + and str(token_info.user) == accounts["payer"] + ) + if not ok: + print( + f" creator={token_info and token_info.creator} user={token_info and token_info.user}" + ) + return ok + + +def check_d_inner_instruction() -> bool: + """D: a launch a router issued by CPI is found over both routes.""" + tx = copy.deepcopy(next(t for t in _launches() if t.get("version") == "legacy")) + message = tx["transaction"]["message"] + ix, _ = _initialize(tx) + message["instructions"].remove(ix) + tx["meta"].setdefault("innerInstructions", []).append( + {"index": 0, "instructions": [ix]} + ) + from_block = _parse_block(STONKFUN, tx) + from_geyser = STONKFUN.parse_token_creation_from_geyser(_geyser_update(tx)) + ok = from_block is not None and from_geyser is not None + if not ok: + print(f" block={from_block is not None} geyser={from_geyser is not None}") + return ok + + +def check_e_platforms_keep_to_their_configs() -> bool: + """E: letsbonk ignores StonkFun launches, and claims one under its own config.""" + ignored = all(_parse_block(LETSBONK, tx) is None for tx in _launches()) + tx = copy.deepcopy(next(t for t in _launches() if t.get("version") == "legacy")) + message = tx["transaction"]["message"] + message["accountKeys"].append(str(next(iter(LETSBONK_CONFIGS)))) + ix, _ = _initialize(tx) + ix["accounts"][SLOTS["platform_config"]] = len(message["accountKeys"]) - 1 + claimed = _parse_block(LETSBONK, tx) + ok = ( + ignored + and claimed is not None + and claimed.platform == Platform.LETS_BONK + and _parse_block(STONKFUN, tx) is None + ) + if not ok: + print(f" ignored={ignored} claimed={claimed}") + return ok + + +def check_f_stream_filters_and_routing() -> bool: + """F: subscriptions per platform, and shared-program routing in the block listener.""" + pump = PumpFunEventParser(get_idl_manager().get_parser(Platform.PUMP_FUN)) + filters_ok = ( + set(STONKFUN.get_stream_filter_accounts()) == set(STONKFUN_CONFIGS) + and set(LETSBONK.get_stream_filter_accounts()) == set(LETSBONK_CONFIGS) + and pump.get_stream_filter_accounts() == [pump.get_program_id()] + ) + listener = UniversalBlockListener( + "wss://offline.invalid", platforms=[Platform.LETS_BONK, Platform.STONK_FUN] + ) + subscribed = {Pubkey.from_string(a) for a in listener.platform_program_ids} + routed = [listener._parse_from_logs(tx) for tx in _launches()] + ok = ( + filters_ok + and subscribed == set(STONKFUN_CONFIGS) | set(LETSBONK_CONFIGS) + and all(r is not None and r.platform == Platform.STONK_FUN for r in routed) + ) + if not ok: + print(f" filters_ok={filters_ok} subscribed={subscribed} routed={routed}") + return ok + + +def main() -> int: + """Run every check and report.""" + checks = [ + ("A: block route reads every account slot", check_a_block_route), + ("B: geyser route matches the block route", check_b_geyser_route), + ("C: creator slot, not payer", check_c_payer_is_not_creator), + ("D: router-issued launch found", check_d_inner_instruction), + ("E: platforms keep to their configs", check_e_platforms_keep_to_their_configs), + ( + "F: stream filters and shared-program routing", + check_f_stream_filters_and_routing, + ), + ] + passed = 0 + for label, check in checks: + ok = check() + print(f"{'PASS' if ok else 'FAIL'} {label}") + passed += ok + print(f"\n{passed}/{len(checks)} checks passed") + return 0 if passed == len(checks) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/regression/verify_launchlab_trades.py b/tests/regression/verify_launchlab_trades.py new file mode 100644 index 0000000..1285d28 --- /dev/null +++ b/tests/regression/verify_launchlab_trades.py @@ -0,0 +1,512 @@ +"""Verify LaunchLab trades price, size and build the way the program settles them. + +Every StonkFun and letsbonk.fun coin is a Raydium LaunchLab pool. The letsbonk +code this replaced priced off the pool's virtual reserves alone — fixed at +launch — so every pool read as its launch price forever; passed a slippage- +padded amount to `buy_exact_in`, which spends all of it; ignored the curve fee +and the Token-2022 transfer fee; and hardcoded wrapped SOL as the quote. + +Fixture: `raw_stonkfun_trades_from_gettransaction.json`, real StonkFun trades — +a SOL-quoted standard sell, xStock-quoted reward trades (a buy of a 1% +transfer-fee coin, a sell of a 3% one), a STONK-quoted standard buy — each with the GlobalConfig, +PlatformConfig and base mint accounts it traded against. The program's own +TradeEvent in each transaction records the reserves before the trade. + +Offline machine checks, no network and no funds moved: + + A. The curve manager reads the fee rate and transfer fee from the fixture + accounts, and the curve math then reproduces each trade to the unit: a + buy delivers exactly the buyer's token balance change, a sell pays exactly + the event's amount out, and the pool receives exactly the sent amount less + the transfer fee. + B. The price comes from virtual plus real reserves, and differs from the + launch price on every fixture. + C. The builder's swap instruction names the same 18 accounts, in order, as + the trade on chain — fee vaults keyed by the pool's own quote mint — and + its writable flags match the IDL. + D. The buyer hands an exact-in builder the configured amount unpadded, and + floors tokens net of curve fee and transfer fee; a builder that is not + exact-in still gets the padded ceiling. + E. The seller's floor is net of transfer fee and curve fee. + F. Single-token mode passes over a coin its filters refuse — an unconfigured + quote asset, a transfer fee above `max_transfer_fee_bps` — and waits for + one it can buy, instead of exiting on the first launch it sees. + +Usage: + uv run tests/regression/verify_launchlab_trades.py +""" + +import asyncio +import base64 +import json +import struct +import sys +from pathlib import Path +from types import SimpleNamespace + +import base58 + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from solders.pubkey import Pubkey # noqa: E402 + +from core.pubkeys import WSOL_MINT, SystemAddresses # noqa: E402 +from interfaces.core import Platform, TokenInfo # noqa: E402 +from platforms.launchlab import LAUNCHLAB_PROGRAM # noqa: E402 +from platforms.launchlab.curve_math import ( # noqa: E402 + buy_amount_out, + sell_amount_out, + withheld_fee, +) +from platforms.launchlab.instruction_builder import _SWAP_ACCOUNTS # noqa: E402 +from platforms.stonkfun import ( # noqa: E402 + StonkFunAddressProvider, + StonkFunCurveManager, + StonkFunInstructionBuilder, +) +from trading import platform_aware # noqa: E402 +from trading.platform_aware import PlatformAwareBuyer, PlatformAwareSeller # noqa: E402 +from trading.universal_trader import UniversalTrader # noqa: E402 +from utils.idl_manager import get_idl_manager # noqa: E402 + +FIXTURE = Path(__file__).with_name("raw_stonkfun_trades_from_gettransaction.json") +IDL = json.loads((PROJECT_ROOT / "idl" / "raydium_launchlab_idl.json").read_text()) + +# Anchor's emit_cpi wraps an event in a self-invocation with this tag. +EVENT_CPI_TAG = bytes.fromhex("e445a52e51cb9a1d") +TRADE_EVENT = bytes( + next(e for e in IDL["events"] if e["name"] == "TradeEvent")["discriminator"] +) +BUY_EXACT_IN = bytes( + next(i for i in IDL["instructions"] if i["name"] == "buy_exact_in")["discriminator"] +) + +PARSER = get_idl_manager().get_parser(Platform.STONK_FUN) +PROVIDER = StonkFunAddressProvider() +TRADER = Pubkey.from_string("11111111111111111111111111111112") + + +class _FixtureClient: + """Serves the fixture's config and mint accounts by address.""" + + def __init__(self, accounts: dict[Pubkey, bytes]) -> None: + self.accounts = accounts + + async def get_multiple_accounts( + self, + pubkeys: list[Pubkey], + commitment: str | None = None, # noqa: ARG002 + ) -> list[SimpleNamespace]: + return [SimpleNamespace(data=self.accounts[k], owner=None) for k in pubkeys] + + +def _keys(tx: dict) -> list[str]: + loaded = tx["meta"].get("loadedAddresses") or {} + return ( + tx["transaction"]["message"]["accountKeys"] + + loaded.get("writable", []) + + loaded.get("readonly", []) + ) + + +def _instructions(tx: dict): + yield from tx["transaction"]["message"]["instructions"] + for group in tx["meta"].get("innerInstructions") or []: + yield from group["instructions"] + + +def _trade(entry: dict) -> dict: + """The swap instruction, its TradeEvent, and the signer's balance changes.""" + tx = entry["transaction"] + keys = _keys(tx) + swap, event = None, None + for ix in _instructions(tx): + if keys[ix["programIdIndex"]] != str(LAUNCHLAB_PROGRAM): + continue + data = base58.b58decode(ix["data"]) + if data[:8] == EVENT_CPI_TAG and data[8:16] == TRADE_EVENT: + body = data[16:] + values = struct.unpack_from("<13Q", body, 32) + event = dict( + zip( + ( + "total_base_sell", + "virtual_base", + "virtual_quote", + "real_base_before", + "real_quote_before", + "real_base_after", + "real_quote_after", + "amount_in", + "amount_out", + "protocol_fee", + "platform_fee", + "creator_fee", + "share_fee", + ), + values, + strict=True, + ) + ) + elif swap is None and len(ix["accounts"]) == len(_SWAP_ACCOUNTS): + swap = {"data": data, "accounts": [keys[i] for i in ix["accounts"]]} + + signer = tx["transaction"]["message"]["accountKeys"][0] + + def balance(section: str, mint: str) -> int: + return sum( + int(b["uiTokenAmount"]["amount"]) + for b in tx["meta"][section] + if b.get("owner") == signer and b["mint"] == mint + ) + + base_delta = balance("postTokenBalances", entry["base_mint"]) - balance( + "preTokenBalances", entry["base_mint"] + ) + return {"swap": swap, "event": event, "base_delta": base_delta} + + +def _pool_before(entry: dict, event: dict, base_decimals: int = 6) -> dict: + quote_decimals = ( + next( + b["uiTokenAmount"]["decimals"] + for b in entry["transaction"]["meta"]["postTokenBalances"] + if b["mint"] == entry["quote_mint"] + ) + if entry["quote_mint"] != str(WSOL_MINT) + else 9 + ) + return { + "virtual_base": event["virtual_base"], + "virtual_quote": event["virtual_quote"], + "real_base": event["real_base_before"], + "real_quote": event["real_quote_before"], + "base_decimals": base_decimals, + "quote_decimals": quote_decimals, + "global_config": Pubkey.from_string(entry["global_config"]), + "platform_config": Pubkey.from_string(entry["platform_config"]), + "base_mint": Pubkey.from_string(entry["base_mint"]), + } + + +async def _priced_pool(entry: dict, event: dict) -> dict: + """Run the fixture pool through the curve manager's fee and price step.""" + accounts = { + Pubkey.from_string(entry[name]): base64.b64decode( + entry["accounts_base64"][name] + ) + for name in ("global_config", "platform_config", "base_mint") + } + manager = StonkFunCurveManager(_FixtureClient(accounts), PARSER) + return await manager._with_fees(_pool_before(entry, event), None) + + +def check_a_math_reproduces_trades() -> bool: + """A: fee rate and transfer fee read from accounts; amounts exact to the unit.""" + ok = True + for entry in json.loads(FIXTURE.read_text()): + trade = _trade(entry) + event = trade["event"] + pool = asyncio.run(_priced_pool(entry, event)) + tax = (pool["transfer_fee_bps"], pool["transfer_fee_max"]) + is_buy = trade["swap"]["data"][:8] == BUY_EXACT_IN + if is_buy: + predicted = buy_amount_out(pool, event["amount_in"], pool["fee_rate"], tax) + good = ( + predicted + == trade["base_delta"] + == event["amount_out"] - withheld_fee(event["amount_out"], *tax) + ) + detail = f"predicted {predicted}, delivered {trade['base_delta']}" + else: + sent = -trade["base_delta"] + predicted = sell_amount_out(pool, sent, pool["fee_rate"], tax) + good = predicted == event["amount_out"] and event[ + "amount_in" + ] == sent - withheld_fee(sent, *tax) + detail = f"predicted {predicted}, paid {event['amount_out']}" + print( + f" {entry['label']}: fee {pool['fee_rate']} ppm, tax " + f"{pool['transfer_fee_bps']} bps, {detail}" + ) + ok &= good + return ok + + +def check_b_price_uses_real_reserves() -> bool: + """B: price from virtual + real reserves, not the launch price.""" + ok = True + for entry in json.loads(FIXTURE.read_text()): + event = _trade(entry)["event"] + pool = asyncio.run(_priced_pool(entry, event)) + scale = 10 ** pool["base_decimals"] / 10 ** pool["quote_decimals"] + expected = ( + (event["virtual_quote"] + event["real_quote_before"]) + / (event["virtual_base"] - event["real_base_before"]) + * scale + ) + launch = event["virtual_quote"] / event["virtual_base"] * scale + good = ( + abs(pool["price_per_token"] - expected) <= expected * 1e-12 + and abs(pool["price_per_token"] - launch) > launch * 1e-6 + ) + if not good: + print( + f" {entry['label']}: price {pool['price_per_token']} vs {expected}" + ) + ok &= good + return ok + + +def check_c_builder_matches_chain() -> bool: + """C: the builder's 18 accounts equal the on-chain trade's, flags per the IDL.""" + ok = True + builder = StonkFunInstructionBuilder(PARSER) + for entry in json.loads(FIXTURE.read_text()): + swap = _trade(entry)["swap"] + chain = swap["accounts"] + user = Pubkey.from_string(chain[0]) + token_info = TokenInfo( + name="", + symbol="", + uri="", + mint=Pubkey.from_string(entry["base_mint"]), + platform=Platform.STONK_FUN, + pool_state=Pubkey.from_string(entry["pool"]), + global_config=Pubkey.from_string(entry["global_config"]), + platform_config=Pubkey.from_string(entry["platform_config"]), + creator=Pubkey.from_string(entry["creator"]), + token_program_id=SystemAddresses.TOKEN_2022_PROGRAM, + quote_mint=Pubkey.from_string(entry["quote_mint"]), + quote_token_program_id=Pubkey.from_string(chain[12]), + ) + is_buy = swap["data"][:8] == BUY_EXACT_IN + build = ( + builder.build_buy_instruction if is_buy else builder.build_sell_instruction + ) + instructions = asyncio.run(build(token_info, user, 1, 1, PROVIDER)) + ours = next(ix for ix in instructions if ix.program_id == LAUNCHLAB_PROGRAM) + built = [str(meta.pubkey) for meta in ours.accounts] + # user_quote_token is the trader's own choice of account: a throwaway + # WSOL account for SOL, usually the ATA otherwise. + mismatched = [ + name + for i, (name, _) in enumerate(_SWAP_ACCOUNTS) + if built[i] != chain[i] and name != "user_quote_token" + ] + if mismatched: + print(f" {entry['label']}: differs at {mismatched}") + ok &= not mismatched + + idl_swap = next(i for i in IDL["instructions"] if i["name"] == "buy_exact_in") + idl_writable = {a["name"]: bool(a.get("writable")) for a in idl_swap["accounts"]} + flag_mismatch = [ + name + for name, writable in _SWAP_ACCOUNTS + if name in idl_writable and name != "payer" and writable != idl_writable[name] + ] + if flag_mismatch: + print(f" writable flags differ from the IDL at {flag_mismatch}") + return ok and not flag_mismatch + + +class _StubClient: + async def build_and_send_transaction( + self, *_args: object, **_kwargs: object + ) -> str: + return "STUB_SIGNATURE" + + async def confirm_transaction(self, *_args: object, **_kwargs: object) -> bool: + return False + + async def confirm_transaction_detailed(self, *_args: object, **_kwargs: object): + return platform_aware.ConfirmationStatus.REVERTED + + +def _stub_implementations(pool_state: dict, captured: dict, *, exact_in: bool): + async def build(_token_info, _user, amount_in, minimum_out, _provider): + captured["amount_in"], captured["minimum_out"] = amount_in, minimum_out + return ["stub-instruction"] + + class Curve: + async def get_pool_state(self, _pool, commitment=None): # noqa: ARG002 + return pool_state + + return SimpleNamespace( + address_provider=PROVIDER, + instruction_builder=SimpleNamespace( + spends_exact_amount_in=exact_in, + build_buy_instruction=build, + build_sell_instruction=build, + get_required_accounts_for_buy=lambda *_a, **_k: [], + get_required_accounts_for_sell=lambda *_a, **_k: [], + get_buy_compute_unit_limit=lambda _o: 150_000, + get_sell_compute_unit_limit=lambda _o: 150_000, + ), + curve_manager=Curve(), + ) + + +async def _no_fee(_accounts: list) -> None: + return None + + +POOL_STATE = { + "price_per_token": 0.00000003, + "fee_fraction": 0.0125, + "transfer_fee_bps": 100, + "quote_mint": WSOL_MINT, + "creator": str(TRADER), +} +TOKEN = TokenInfo( + name="T", + symbol="T", + uri="", + mint=TRADER, + platform=Platform.STONK_FUN, + pool_state=TRADER, + quote_mint=WSOL_MINT, + creator=TRADER, +) + + +def _buy(*, exact_in: bool) -> dict: + captured: dict = {} + platform_aware.get_platform_implementations = lambda _p, _c: _stub_implementations( + POOL_STATE, captured, exact_in=exact_in + ) + buyer = PlatformAwareBuyer( + _StubClient(), + SimpleNamespace(pubkey=TRADER, keypair=None), + SimpleNamespace(calculate_priority_fee=_no_fee), + amount=0.01, + slippage=0.2, + max_retries=1, + ) + asyncio.run(buyer.execute(TOKEN)) + return captured + + +def check_d_buyer_sizing() -> bool: + """D: exact-in gets the unpadded amount; the token floor is net of fee and tax.""" + exact = _buy(exact_in=True) + padded = _buy(exact_in=False) + expected_tokens = 0.01 / POOL_STATE["price_per_token"] * (1 - 0.0125) * (1 - 0.01) + expected_floor = int(expected_tokens * (1 - 0.2) * 10**6) + ok = ( + exact["amount_in"] == 10_000_000 + and padded["amount_in"] == int(0.01 * 10**9 * 1.2) + and abs(exact["minimum_out"] - expected_floor) <= 1 + ) + if not ok: + print(f" exact={exact} padded={padded} expected floor {expected_floor}") + return ok + + +def check_e_seller_floor() -> bool: + """E: the sell floor is net of transfer fee and curve fee.""" + captured: dict = {} + platform_aware.get_platform_implementations = lambda _p, _c: _stub_implementations( + POOL_STATE, captured, exact_in=True + ) + seller = PlatformAwareSeller( + _StubClient(), + SimpleNamespace(pubkey=TRADER, keypair=None), + SimpleNamespace(calculate_priority_fee=_no_fee), + slippage=0.2, + max_retries=1, + ) + tokens, price = 1_000_000.0, 0.00000003 + asyncio.run(seller.execute(TOKEN, tokens, price)) + expected = int(tokens * (1 - 0.01) * price * (1 - 0.0125) * (1 - 0.2) * 10**9) + ok = abs(captured.get("minimum_out", -10) - expected) <= 1 + if not ok: + print(f" floor {captured.get('minimum_out')} expected {expected}") + return ok + + +def check_f_single_token_mode_waits_past_refused_coins() -> bool: + """F: the first launch is an xStock coin, the second taxed 3%, the third buyable.""" + spcxx = Pubkey.from_string("Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8") + launches = [ + TokenInfo( + name="", + symbol="XSTOCK", + uri="", + mint=Pubkey.new_unique(), + platform=Platform.STONK_FUN, + quote_mint=spcxx, + ), + TokenInfo( + name="", + symbol="TAXED", + uri="", + mint=Pubkey.new_unique(), + platform=Platform.STONK_FUN, + quote_mint=WSOL_MINT, + transfer_fee_bps=300, + ), + TokenInfo( + name="", + symbol="BUYABLE", + uri="", + mint=Pubkey.new_unique(), + platform=Platform.STONK_FUN, + quote_mint=WSOL_MINT, + transfer_fee_bps=100, + ), + ] + + class Listener: + async def listen_for_tokens(self, callback, *_args): + # Launches arrive apart, so the waiter can wake on the first one + # before the next overwrites it. + for launch in launches: + await callback(launch) + await asyncio.sleep(0.05) + await asyncio.sleep(3600) + + trader = SimpleNamespace( + token_listener=Listener(), + match_string=None, + bro_address=None, + token_wait_timeout=5, + processed_tokens=set(), + token_timestamps={}, + allowed_quote_mints=None, + quote_amounts={WSOL_MINT: 0.001}, + max_transfer_fee_bps=100, + ) + trader._skip_reason = lambda token: UniversalTrader._skip_reason(trader, token) + found = asyncio.run(UniversalTrader._wait_for_token(trader)) + ok = found is not None and found.symbol == "BUYABLE" + if not ok: + print(f" picked {found and found.symbol}") + return ok + + +def main() -> int: + """Run every check and report.""" + checks = [ + ("A: curve math reproduces real trades", check_a_math_reproduces_trades), + ("B: price from virtual + real reserves", check_b_price_uses_real_reserves), + ("C: builder accounts match the chain", check_c_builder_matches_chain), + ("D: buyer sizing for exact-in", check_d_buyer_sizing), + ("E: seller floor net of fees", check_e_seller_floor), + ( + "F: single-token mode waits past refused coins", + check_f_single_token_mode_waits_past_refused_coins, + ), + ] + passed = 0 + for label, check in checks: + ok = check() + print(f"{'PASS' if ok else 'FAIL'} {label}") + passed += ok + print(f"\n{passed}/{len(checks)} checks passed") + return 0 if passed == len(checks) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/regression/verify_prices_without_hardcoded_sol_unit.py b/tests/regression/verify_prices_without_hardcoded_sol_unit.py new file mode 100644 index 0000000..ed63ca9 --- /dev/null +++ b/tests/regression/verify_prices_without_hardcoded_sol_unit.py @@ -0,0 +1,102 @@ +"""Verify no trading or platform code scales a price by a SOL literal. + +A coin's price is denominated in whatever it trades against, so converting raw +reserves to whole units means dividing by *that mint's* unit, not by +`LAMPORTS_PER_SOL`. `quote_units_per_token` raising for an unresolved mint keeps +that honest wherever the unit is looked up -- but it says nothing about code that +writes the divisor in by hand, which is how three helpers on the pump.fun curve +manager, and the LaunchLab curve manager after them, came to price everything +as SOL. + +Scope: `core/`, `trading/` and every platform under `platforms/`. + +Offline machine check, no network and no funds moved: + + A. No module in scope divides or multiplies anything by LAMPORTS_PER_SOL. + +Usage: + uv run tests/regression/verify_prices_without_hardcoded_sol_unit.py +""" + +import ast +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +SRC = PROJECT_ROOT / "src" + +IN_SCOPE = ("core", "trading", "platforms") + +SOL_LITERAL = "LAMPORTS_PER_SOL" + + +def _in_scope(path: Path) -> bool: + """Whether a module falls under this check's directories. + + Args: + path: Module path anywhere under `src/` + + Returns: + True if the module is in one of IN_SCOPE + """ + rel = path.relative_to(SRC).as_posix() + return any(rel.startswith(f"{d}/") for d in IN_SCOPE) + + +def _scales_by_sol_literal(node: ast.AST) -> list[int]: + """Lines under a node that multiply or divide by the SOL literal. + + Args: + node: Any AST node to walk -- a module or a single function + + Returns: + Line numbers of the offending binary operations + """ + hits = [] + for sub in ast.walk(node): + if not isinstance(sub, ast.BinOp) or not isinstance(sub.op, ast.Div | ast.Mult): + continue + for side in (sub.left, sub.right): + if isinstance(side, ast.Name) and side.id == SOL_LITERAL: + hits.append(sub.lineno) + return hits + + +def check_no_hardcoded_sol_unit_in_pricing() -> None: + """Nothing in scope may scale any amount by LAMPORTS_PER_SOL. + + A flat rule rather than one keyed on function names: one of the helpers + this replaced was `calculate_expected_tokens`, whose name says nothing about + SOL while its body converts with it. Amounts are scaled by the quote mint's + unit; the constant stays for anything genuinely denominated in SOL, such as + a rent reserve or a fee budget, neither of which lives in these directories. + """ + offenders = [] + for path in sorted(SRC.rglob("*.py")): + if "generated" in path.parts or not _in_scope(path): + continue + for line in _scales_by_sol_literal(ast.parse(path.read_text())): + offenders.append( + f"{path.relative_to(PROJECT_ROOT)}:{line}: scales by {SOL_LITERAL}" + ) + + assert not offenders, ( + "a price was scaled by a SOL literal instead of the quote mint's unit:\n " + + "\n ".join(offenders) + ) + + +def main() -> None: + """Run the check and report.""" + print("=" * 72) + print("Verifying prices are not scaled by a SOL literal") + print("=" * 72) + + check_no_hardcoded_sol_unit_in_pricing() + print("no hardcoded SOL unit in scope -> OK") + + print("\nALL CHECKS PASSED") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/regression/verify_pumpfun_prices_without_hardcoded_sol_unit.py b/tests/regression/verify_pumpfun_prices_without_hardcoded_sol_unit.py deleted file mode 100644 index 2c690e0..0000000 --- a/tests/regression/verify_pumpfun_prices_without_hardcoded_sol_unit.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Verify the pump.fun and universal code paths never scale a price by a SOL literal. - -A coin's price is denominated in whatever it trades against, so converting raw -reserves to whole units means dividing by *that mint's* unit, not by -`LAMPORTS_PER_SOL`. `quote_units_per_token` raising for an unresolved mint keeps -that honest wherever the unit is looked up -- but it says nothing about code that -writes the divisor in by hand, which is exactly how three uncalled helpers on the -pump.fun curve manager came to price everything as SOL. - -Those helpers are gone. This check is what stops a fourth being written. - -Scope: `core/`, `trading/` and `platforms/pumpfun/`. - -`platforms/letsbonk/` is NOT covered, and this is a statement of where the rule -holds rather than an exemption granted for convenience. letsbonk genuinely -violates it -- `curve_manager.py` prices with -`* 10**TOKEN_DECIMALS / LAMPORTS_PER_SOL` and drops the `base_decimals`, -`quote_decimals` and `quote_mint` that its `PoolState` carries, so every pool is -priced as though it were SOL-quoted. Widening this check to `platforms/` means -fixing those two sites first; the runner has no way to express a known failure, -so a repo-wide version of this check would be red permanently and would train -everyone to stop reading the summary line. - -Offline machine checks, no network and no funds moved: - - A. No module in scope divides or multiplies anything by LAMPORTS_PER_SOL. - B. The letsbonk sites this check deliberately does not cover are still the - ones named above, so widening it later is a path-filter change against a - list that is already written down rather than a fresh investigation. - -Usage: - uv run tests/regression/verify_pumpfun_prices_without_hardcoded_sol_unit.py -""" - -import ast -import sys -from pathlib import Path - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -SRC = PROJECT_ROOT / "src" - -# Directories this rule covers. letsbonk is out; see the module docstring. -IN_SCOPE = ("core", "trading", "platforms/pumpfun") - -SOL_LITERAL = "LAMPORTS_PER_SOL" - -# The letsbonk sites this check does not cover, as (path, symbol). Kept here so -# widening the scope starts from a list rather than a search. -KNOWN_UNCOVERED = ( - ("src/platforms/letsbonk/curve_manager.py", "calculate_price"), - ("src/platforms/letsbonk/curve_manager.py", "_decode_pool_state_with_idl"), -) - - -def _in_scope(path: Path) -> bool: - """Whether a module falls under this check's directories. - - Args: - path: Module path anywhere under `src/` - - Returns: - True if the module is in one of IN_SCOPE - """ - rel = path.relative_to(SRC).as_posix() - return any(rel.startswith(f"{d}/") for d in IN_SCOPE) - - -def _scales_by_sol_literal(node: ast.AST) -> list[int]: - """Lines under a node that multiply or divide by the SOL literal. - - Args: - node: Any AST node to walk -- a module or a single function - - Returns: - Line numbers of the offending binary operations - """ - hits = [] - for sub in ast.walk(node): - if not isinstance(sub, ast.BinOp) or not isinstance(sub.op, ast.Div | ast.Mult): - continue - for side in (sub.left, sub.right): - if isinstance(side, ast.Name) and side.id == SOL_LITERAL: - hits.append(sub.lineno) - return hits - - -def check_no_hardcoded_sol_unit_in_pricing() -> None: - """Nothing in scope may scale any amount by LAMPORTS_PER_SOL. - - A flat rule rather than one keyed on function names: the three helpers this - replaced included `calculate_expected_tokens`, whose name says nothing about - SOL while its body converts with it. Nothing in scope legitimately needs the - literal -- amounts are scaled by the quote mint's unit, and the constant - itself stays for anything genuinely denominated in SOL, such as a rent - reserve or a fee budget, neither of which lives in these directories. - """ - offenders = [] - for path in sorted(SRC.rglob("*.py")): - if "generated" in path.parts or not _in_scope(path): - continue - for line in _scales_by_sol_literal(ast.parse(path.read_text())): - offenders.append( - f"{path.relative_to(PROJECT_ROOT)}:{line}: scales by {SOL_LITERAL}" - ) - - assert not offenders, ( - "a price was scaled by a SOL literal instead of the quote mint's unit:\n " - + "\n ".join(offenders) - ) - - -def check_uncovered_sites_unchanged() -> None: - """The letsbonk sites left out of scope must still be the documented ones. - - If letsbonk stops violating the rule, this check fails and the scope above - should widen. If it grows a third site, the same. Either way the docstring - stops being a claim nobody re-reads. - """ - still_offending = [] - for rel, symbol in KNOWN_UNCOVERED: - path = PROJECT_ROOT / rel - if not path.exists(): - continue - for node in ast.walk(ast.parse(path.read_text())): - if ( - isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef) - and node.name == symbol - and _scales_by_sol_literal(node) - ): - still_offending.append((rel, symbol)) - - assert still_offending == list(KNOWN_UNCOVERED), ( - "the out-of-scope letsbonk sites changed; widen this check's scope or " - f"update KNOWN_UNCOVERED.\n documented: {list(KNOWN_UNCOVERED)}\n" - f" found: {still_offending}" - ) - - -def main() -> None: - """Run every check and report.""" - print("=" * 72) - print("Verifying pump.fun prices are not scaled by a SOL literal") - print("=" * 72) - - checks = [ - ("no hardcoded SOL unit in scope", check_no_hardcoded_sol_unit_in_pricing), - ("the uncovered letsbonk sites are unchanged", check_uncovered_sites_unchanged), - ] - for label, fn in checks: - fn() - print(f"{label} -> OK") - - print("\nALL CHECKS PASSED") - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/cleanup_accounts.py b/tools/cleanup_accounts.py index 8966558..f01d653 100644 --- a/tools/cleanup_accounts.py +++ b/tools/cleanup_accounts.py @@ -16,6 +16,7 @@ from solders.pubkey import Pubkey from spl.token.instructions import burn, close_account from spl.token.models import BurnParams, CloseAccountParams +from cleanup.manager import harvest_withheld_to_mint, withheld_transfer_fee from core.client import SolanaClient from core.pubkeys import SystemAddresses from core.wallet import Wallet @@ -76,7 +77,7 @@ async def close_account_if_exists( """Safely close a token account if it exists and reclaim rent.""" try: try: - await client.get_account_info(account) + account_info = await client.get_account_info(account) except ValueError: logger.info(f"Account does not exist or already closed: {account}") return @@ -108,6 +109,12 @@ async def close_account_if_exists( ) instructions.append(burn_ix) + # Token-2022 will not close an account holding withheld transfer fees. + withheld = withheld_transfer_fee(bytes(account_info.data)) + if withheld and token_program == SystemAddresses.TOKEN_2022_PROGRAM: + logger.info(f"Harvesting {withheld} withheld fee units from {account}") + instructions.append(harvest_withheld_to_mint(mint, account)) + # Account exists, attempt to close it logger.info(f"Closing account: {account}") close_params = CloseAccountParams(