mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-02 10:17:44 +00:00
02343b775b
Refresh the vendored IDLs from pump-fun/pump-public-docs @ 9c82f61 and move all pump.fun trading onto the v2 instruction interface. This is required, not optional: legacy buy/sell cannot trade coins paired against a quote asset other than SOL, and USDC is already whitelisted in the on-chain Global account. Protocol changes absorbed: - buy_v2 (27 accounts) / sell_v2 (26 accounts) replace the legacy instructions. Every account is mandatory and the order is identical for all coins, so the conditional cashback/mayhem account lists are gone. Legacy remains available via PumpFunInstructionBuilder(use_legacy_instructions=True). - BondingCurve is 151 bytes: virtual_sol_reserves -> virtual_quote_reserves, real_sol_reserves -> real_quote_reserves, plus quote_mint at offset 83. Old field names are kept as aliases so existing callers keep working. - v2 instruction data drops the track_volume OptionBool; amounts are in the quote mint's raw units rather than always lamports. - create_v2 carries a non-SOL quote mint as optional remaining accounts 17-19, and CreateEvent gained quote_mint, so extreme_fast_mode can resolve the quote asset without an extra fetch. USDC support: new trade.quote_amounts and filters.allowed_quote_mints config, accepting "sol"/"usdc" aliases or raw mints. Amounts are per-quote-mint because 1 USDC and 1 SOL are not interchangeable. A coin whose quote mint has no configured amount is skipped rather than traded at the wrong size, so SOL-only configs are unaffected. Bug fixes found while verifying: - The logs and blocks listeners set no websocket max_size, so any frame over 1 MiB closed the connection with 1009 and the token in it was lost. Raised to 32 MiB. - PumpSwap priced against the raw quote vault balance, ignoring the new Pool.virtual_quote_reserves (i128 at offset 245; live pools are 301 bytes). Upstream's note that this field is 0 everywhere is out of date: a live pool carries 17.58 SOL against a 148 SOL vault, a 10.15% price error. - The seller read curve state once at confirmed commitment and silently fell back to create-time values, risking a stale creator_vault and ConstraintSeeds. It now retries at processed, matching the buyer. - Account cleanup would burn wrapped SOL when force_burn was set, destroying value that closing the account returns. WSOL is now closed without burning. - The mint scripts treated a landed transaction as a successful one, so a reverted buy printed as success. They now assert the on-chain result. Compute unit limits retuned from mainnet measurements: buy 100k -> 180k, sell 60k -> 120k. Mint-and-buy is no longer atomic, because create_v2 plus buy_v2 exceeds the 1232-byte transaction limit; both mint scripts send two transactions. Adds learning-examples/pump_v2.py as one shared, standalone v2 toolkit for the example scripts, and three verification scripts: an offline layout check against the IDL, a no-funds mainnet simulation, and a live listener matrix that buys, sells and closes the ATA per listener. Verified on mainnet: all four listeners (geyser, logs, blocks, pumpportal) and all eight example scripts completed a real buy, sell and ATA close, each confirmed by reading the transaction result back rather than trusting confirmation alone. The USDC path is verified structurally only; no USDC-paired coin could be found on-chain to exercise it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
557 lines
21 KiB
Python
557 lines
21 KiB
Python
"""Cross-check the hardcoded buy_v2/sell_v2 account layouts against the IDL.
|
|
|
|
The v2 instructions take 27 and 26 mandatory accounts in a fixed order. Getting
|
|
one position or writability flag wrong produces an on-chain failure that is
|
|
awkward to debug, so this script diffs the layouts in
|
|
`platforms.pumpfun.instruction_builder` against `idl/pump_fun_idl.json` and also
|
|
recomputes every PDA/ATA the address provider derives, comparing each against
|
|
the seeds declared in the IDL.
|
|
|
|
Runs entirely offline — no RPC, no keys, no transactions.
|
|
|
|
Usage:
|
|
uv run learning-examples/verify_v2_account_layout.py
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT / "learning-examples"))
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
|
|
from solders.pubkey import Pubkey # noqa: E402
|
|
from spl.token.instructions import get_associated_token_address # noqa: E402
|
|
|
|
from core.pubkeys import USDC_MINT, WSOL_MINT, SystemAddresses # noqa: E402
|
|
from interfaces.core import Platform, TokenInfo # noqa: E402
|
|
from platforms.pumpfun.address_provider import ( # noqa: E402
|
|
PumpFunAddresses,
|
|
PumpFunAddressProvider,
|
|
)
|
|
from platforms.pumpfun.instruction_builder import ( # noqa: E402
|
|
_BUY_V2_ACCOUNTS,
|
|
_SELL_V2_ACCOUNTS,
|
|
)
|
|
|
|
IDL_PATH = PROJECT_ROOT / "idl" / "pump_fun_idl.json"
|
|
|
|
|
|
def load_idl_accounts(name: str) -> list[dict]:
|
|
"""Get the IDL account list for an instruction.
|
|
|
|
Args:
|
|
name: Instruction name
|
|
|
|
Returns:
|
|
List of IDL account definitions
|
|
"""
|
|
idl = json.loads(IDL_PATH.read_text())
|
|
for instruction in idl["instructions"]:
|
|
if instruction["name"] == name:
|
|
return instruction["accounts"]
|
|
raise KeyError(f"Instruction {name} not present in {IDL_PATH}")
|
|
|
|
|
|
def check_layout(name: str, layout: list[tuple[str, bool]]) -> list[str]:
|
|
"""Compare a hardcoded layout against the IDL's account list.
|
|
|
|
Args:
|
|
name: Instruction name
|
|
layout: Ordered (account name, is_writable) pairs from our builder
|
|
|
|
Returns:
|
|
List of human-readable problems (empty if the layout matches)
|
|
"""
|
|
idl_accounts = load_idl_accounts(name)
|
|
problems = []
|
|
|
|
if len(layout) != len(idl_accounts):
|
|
problems.append(
|
|
f"{name}: account count {len(layout)} != IDL {len(idl_accounts)}"
|
|
)
|
|
|
|
for index, (idl_account, ours) in enumerate(zip(idl_accounts, layout), start=1):
|
|
our_name, our_writable = ours
|
|
if idl_account["name"] != our_name:
|
|
problems.append(
|
|
f"{name}[{index}]: name {our_name!r} != IDL {idl_account['name']!r}"
|
|
)
|
|
idl_writable = bool(idl_account.get("writable"))
|
|
if idl_writable != our_writable:
|
|
problems.append(
|
|
f"{name}[{index}] {our_name}: writable={our_writable} "
|
|
f"!= IDL writable={idl_writable}"
|
|
)
|
|
if idl_account.get("signer") and our_name != "user":
|
|
problems.append(
|
|
f"{name}[{index}] {our_name}: IDL marks this a signer but only "
|
|
f"`user` is expected to sign"
|
|
)
|
|
|
|
return problems
|
|
|
|
|
|
def build_token_info(quote_mint: Pubkey, *, mayhem: bool) -> TokenInfo:
|
|
"""Construct a TokenInfo for a synthetic coin.
|
|
|
|
Args:
|
|
quote_mint: Quote mint to pair the coin against
|
|
mayhem: Whether the coin is in mayhem mode
|
|
|
|
Returns:
|
|
TokenInfo suitable for driving the address provider
|
|
"""
|
|
provider = PumpFunAddressProvider()
|
|
# Fixed, arbitrary mint/creator so results are reproducible.
|
|
mint = Pubkey.from_string("CU7nUQaJ4beyYjC3xAUrh5RiSjw14fhU6oWTwRBse8gj")
|
|
creator = Pubkey.from_string("5wyFsNExysbXf2hTtcn8Tqd3urs9Nv85Zx1zNdAfTMmX")
|
|
bonding_curve = provider.derive_pool_address(mint)
|
|
|
|
return TokenInfo(
|
|
name="layout-check",
|
|
symbol="CHK",
|
|
uri="",
|
|
mint=mint,
|
|
platform=Platform.PUMP_FUN,
|
|
bonding_curve=bonding_curve,
|
|
associated_bonding_curve=provider.derive_associated_bonding_curve(
|
|
mint, bonding_curve, SystemAddresses.TOKEN_2022_PROGRAM
|
|
),
|
|
creator=creator,
|
|
creator_vault=provider.derive_creator_vault(creator),
|
|
token_program_id=SystemAddresses.TOKEN_2022_PROGRAM,
|
|
is_mayhem_mode=mayhem,
|
|
quote_mint=quote_mint,
|
|
)
|
|
|
|
|
|
def check_derivations(quote_mint: Pubkey, *, mayhem: bool) -> list[str]:
|
|
"""Independently recompute every derived v2 account and compare.
|
|
|
|
Args:
|
|
quote_mint: Quote mint to pair the coin against
|
|
mayhem: Whether the coin is in mayhem mode
|
|
|
|
Returns:
|
|
List of mismatches (empty if all derivations agree)
|
|
"""
|
|
provider = PumpFunAddressProvider()
|
|
token_info = build_token_info(quote_mint, mayhem=mayhem)
|
|
user = Pubkey.from_string("Ba99j1dYxidfQZvuNGMaXGxJsUeWXu6VNW8damkrdLVd")
|
|
accounts = provider.get_buy_v2_instruction_accounts(token_info, user)
|
|
|
|
pump = PumpFunAddresses.PROGRAM
|
|
fee_program = PumpFunAddresses.FEE_PROGRAM
|
|
quote_program = SystemAddresses.TOKEN_PROGRAM
|
|
base_program = SystemAddresses.TOKEN_2022_PROGRAM
|
|
mint = token_info.mint
|
|
bonding_curve = token_info.bonding_curve
|
|
creator_vault = token_info.creator_vault
|
|
uva, _ = Pubkey.find_program_address(
|
|
[b"user_volume_accumulator", bytes(user)], pump
|
|
)
|
|
|
|
expected = {
|
|
"bonding_curve": Pubkey.find_program_address(
|
|
[b"bonding-curve", bytes(mint)], pump
|
|
)[0],
|
|
"creator_vault": Pubkey.find_program_address(
|
|
[b"creator-vault", bytes(token_info.creator)], pump
|
|
)[0],
|
|
"sharing_config": Pubkey.find_program_address(
|
|
[b"sharing-config", bytes(mint)], fee_program
|
|
)[0],
|
|
"global_volume_accumulator": Pubkey.find_program_address(
|
|
[b"global_volume_accumulator"], pump
|
|
)[0],
|
|
"user_volume_accumulator": uva,
|
|
"fee_config": Pubkey.find_program_address(
|
|
[b"fee_config", bytes(pump)], fee_program
|
|
)[0],
|
|
"event_authority": Pubkey.find_program_address([b"__event_authority"], pump)[0],
|
|
"associated_base_bonding_curve": get_associated_token_address(
|
|
bonding_curve, mint, base_program
|
|
),
|
|
"associated_quote_bonding_curve": get_associated_token_address(
|
|
bonding_curve, quote_mint, quote_program
|
|
),
|
|
"associated_base_user": get_associated_token_address(user, mint, base_program),
|
|
"associated_quote_user": get_associated_token_address(
|
|
user, quote_mint, quote_program
|
|
),
|
|
"associated_creator_vault": get_associated_token_address(
|
|
creator_vault, quote_mint, quote_program
|
|
),
|
|
"associated_quote_fee_recipient": get_associated_token_address(
|
|
accounts["fee_recipient"], quote_mint, quote_program
|
|
),
|
|
"associated_quote_buyback_fee_recipient": get_associated_token_address(
|
|
accounts["buyback_fee_recipient"], quote_mint, quote_program
|
|
),
|
|
"associated_user_volume_accumulator": get_associated_token_address(
|
|
uva, quote_mint, quote_program
|
|
),
|
|
"quote_mint": quote_mint,
|
|
"base_mint": mint,
|
|
"user": user,
|
|
"program": pump,
|
|
"fee_program": fee_program,
|
|
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
|
"associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
|
"base_token_program": base_program,
|
|
"quote_token_program": quote_program,
|
|
"global": PumpFunAddresses.GLOBAL,
|
|
}
|
|
|
|
problems = [
|
|
f"{key}: provider={accounts[key]} != expected={value}"
|
|
for key, value in expected.items()
|
|
if accounts[key] != value
|
|
]
|
|
|
|
# The fee recipient must come from the right documented set.
|
|
recipient_set = (
|
|
PumpFunAddresses.RESERVED_FEE_RECIPIENTS
|
|
if mayhem
|
|
else PumpFunAddresses.NORMAL_FEE_RECIPIENTS
|
|
)
|
|
if accounts["fee_recipient"] not in recipient_set:
|
|
problems.append(
|
|
f"fee_recipient {accounts['fee_recipient']} not in "
|
|
f"{'reserved' if mayhem else 'normal'} fee recipient set"
|
|
)
|
|
if accounts["buyback_fee_recipient"] not in PumpFunAddresses.BUYBACK_FEE_RECIPIENTS:
|
|
problems.append(
|
|
f"buyback_fee_recipient {accounts['buyback_fee_recipient']} not in "
|
|
f"buyback fee recipient set"
|
|
)
|
|
|
|
# Every account must be distinct except where the program expects reuse.
|
|
if len(set(accounts.values())) != len(accounts):
|
|
duplicates = [
|
|
key
|
|
for key, value in accounts.items()
|
|
if list(accounts.values()).count(value) > 1
|
|
]
|
|
problems.append(f"duplicate account addresses for: {sorted(duplicates)}")
|
|
|
|
return problems
|
|
|
|
|
|
def check_instruction_encoding() -> list[str]:
|
|
"""Build real v2 instructions and assert their shape and data encoding.
|
|
|
|
Covers the USDC path, which cannot be exercised on-chain until a
|
|
USDC-paired coin exists.
|
|
|
|
Returns:
|
|
List of problems (empty if the instructions are well-formed)
|
|
"""
|
|
import asyncio
|
|
import struct
|
|
|
|
from interfaces.core import Platform as _Platform # noqa: F401
|
|
from platforms.pumpfun.instruction_builder import PumpFunInstructionBuilder
|
|
from utils.idl_manager import get_idl_manager
|
|
|
|
provider = PumpFunAddressProvider()
|
|
builder = PumpFunInstructionBuilder(get_idl_manager().get_parser(Platform.PUMP_FUN))
|
|
user = Pubkey.from_string("Ba99j1dYxidfQZvuNGMaXGxJsUeWXu6VNW8damkrdLVd")
|
|
problems = []
|
|
|
|
for quote_mint, label, expect_quote_ata in (
|
|
(WSOL_MINT, "SOL-paired", False),
|
|
(USDC_MINT, "USDC-paired", True),
|
|
):
|
|
token_info = build_token_info(quote_mint, mayhem=False)
|
|
|
|
buy = asyncio.run(
|
|
builder.build_buy_v2_instruction(
|
|
token_info, user, 1_500_000, 20_000_000, provider
|
|
)
|
|
)
|
|
sell = asyncio.run(
|
|
builder.build_sell_v2_instruction(
|
|
token_info, user, 20_000_000, 900_000, provider
|
|
)
|
|
)
|
|
|
|
# Instruction counts: base ATA always, quote ATA only for non-SOL.
|
|
expected_buy_ix = 3 if expect_quote_ata else 2
|
|
if len(buy) != expected_buy_ix:
|
|
problems.append(
|
|
f"{label}: buy produced {len(buy)} instructions, "
|
|
f"expected {expected_buy_ix}"
|
|
)
|
|
expected_sell_ix = 2 if expect_quote_ata else 1
|
|
if len(sell) != expected_sell_ix:
|
|
problems.append(
|
|
f"{label}: sell produced {len(sell)} instructions, "
|
|
f"expected {expected_sell_ix}"
|
|
)
|
|
|
|
if len(buy[-1].accounts) != 27:
|
|
problems.append(
|
|
f"{label}: buy_v2 has {len(buy[-1].accounts)} accounts != 27"
|
|
)
|
|
if len(sell[-1].accounts) != 26:
|
|
problems.append(
|
|
f"{label}: sell_v2 has {len(sell[-1].accounts)} accounts != 26"
|
|
)
|
|
|
|
# buy_v2 data: 8-byte discriminator + amount (tokens) + max_sol_cost.
|
|
# No trailing track_volume OptionBool, unlike the legacy buy.
|
|
buy_data = bytes(buy[-1].data)
|
|
if len(buy_data) != 24:
|
|
problems.append(
|
|
f"{label}: buy_v2 data is {len(buy_data)} bytes, expected 24 "
|
|
f"(discriminator + 2 u64, no track_volume)"
|
|
)
|
|
else:
|
|
amount, max_cost = struct.unpack("<QQ", buy_data[8:])
|
|
if amount != 20_000_000 or max_cost != 1_500_000:
|
|
problems.append(
|
|
f"{label}: buy_v2 args decoded as amount={amount}, "
|
|
f"max_sol_cost={max_cost}; expected 20000000 and 1500000"
|
|
)
|
|
|
|
sell_data = bytes(sell[-1].data)
|
|
if len(sell_data) != 24:
|
|
problems.append(
|
|
f"{label}: sell_v2 data is {len(sell_data)} bytes, expected 24"
|
|
)
|
|
else:
|
|
amount, min_out = struct.unpack("<QQ", sell_data[8:])
|
|
if amount != 20_000_000 or min_out != 900_000:
|
|
problems.append(
|
|
f"{label}: sell_v2 args decoded as amount={amount}, "
|
|
f"min_sol_output={min_out}; expected 20000000 and 900000"
|
|
)
|
|
|
|
# Exactly one signer, and it must be the user.
|
|
signers = [meta.pubkey for meta in buy[-1].accounts if meta.is_signer]
|
|
if signers != [user]:
|
|
problems.append(f"{label}: buy_v2 signers {signers} != [{user}]")
|
|
|
|
return problems
|
|
|
|
|
|
def check_quote_config() -> list[str]:
|
|
"""Assert quote-amount config resolution and alias handling.
|
|
|
|
Returns:
|
|
List of problems (empty if config resolution behaves correctly)
|
|
"""
|
|
from core.pubkeys import resolve_quote_amounts, resolve_quote_mint
|
|
from trading.universal_trader import _resolve_quote_config
|
|
|
|
problems = []
|
|
|
|
if resolve_quote_mint("usdc") != USDC_MINT:
|
|
problems.append("alias 'usdc' did not resolve to the USDC mint")
|
|
if resolve_quote_mint("sol") != WSOL_MINT:
|
|
problems.append("alias 'sol' did not resolve to wrapped SOL")
|
|
if resolve_quote_mint(str(USDC_MINT)) != USDC_MINT:
|
|
problems.append("raw USDC mint string did not resolve")
|
|
|
|
try:
|
|
resolve_quote_amounts({"usdc": 0})
|
|
problems.append("resolve_quote_amounts accepted a zero amount")
|
|
except ValueError:
|
|
pass
|
|
|
|
try:
|
|
resolve_quote_mint("not-a-mint")
|
|
problems.append("resolve_quote_mint accepted an invalid mint")
|
|
except ValueError:
|
|
pass
|
|
|
|
# SOL always present from buy_amount; USDC only when configured.
|
|
amounts, allowed = _resolve_quote_config(0.0001, None, None)
|
|
if amounts.get(WSOL_MINT) != 0.0001:
|
|
problems.append("SOL amount did not fall back to buy_amount")
|
|
if USDC_MINT in amounts:
|
|
problems.append("USDC present in amounts without being configured")
|
|
if allowed is not None:
|
|
problems.append("allowed_quote_mints should be None when unset")
|
|
|
|
amounts, allowed = _resolve_quote_config(0.0001, {"usdc": 5.0}, ["sol", "usdc"])
|
|
if amounts.get(USDC_MINT) != 5.0:
|
|
problems.append("configured USDC amount not resolved")
|
|
if allowed != {WSOL_MINT, USDC_MINT}:
|
|
problems.append(f"allowed_quote_mints resolved to {allowed}")
|
|
|
|
return problems
|
|
|
|
|
|
def check_examples_toolkit() -> list[str]:
|
|
"""Check learning-examples/pump_v2.py agrees with the IDL and with src/.
|
|
|
|
The examples carry their own standalone copy of the v2 layout so they stay
|
|
readable without importing src/. That copy is exactly the kind of thing that
|
|
silently drifts, so diff it against both sources of truth.
|
|
|
|
Returns:
|
|
List of problems (empty if the toolkit agrees)
|
|
"""
|
|
import pump_v2
|
|
|
|
from platforms.pumpfun.address_provider import PumpFunAddresses
|
|
|
|
problems = []
|
|
user = Pubkey.from_string("Ba99j1dYxidfQZvuNGMaXGxJsUeWXu6VNW8damkrdLVd")
|
|
mint = Pubkey.from_string("CU7nUQaJ4beyYjC3xAUrh5RiSjw14fhU6oWTwRBse8gj")
|
|
creator = Pubkey.from_string("5wyFsNExysbXf2hTtcn8Tqd3urs9Nv85Zx1zNdAfTMmX")
|
|
|
|
# Fee recipient sets must match src/ exactly.
|
|
for label, theirs, ours in (
|
|
(
|
|
"normal",
|
|
pump_v2.NORMAL_FEE_RECIPIENTS,
|
|
PumpFunAddresses.NORMAL_FEE_RECIPIENTS,
|
|
),
|
|
(
|
|
"reserved",
|
|
pump_v2.RESERVED_FEE_RECIPIENTS,
|
|
PumpFunAddresses.RESERVED_FEE_RECIPIENTS,
|
|
),
|
|
(
|
|
"buyback",
|
|
pump_v2.BUYBACK_FEE_RECIPIENTS,
|
|
PumpFunAddresses.BUYBACK_FEE_RECIPIENTS,
|
|
),
|
|
):
|
|
if theirs != ours:
|
|
problems.append(f"pump_v2 {label} fee recipients differ from src/")
|
|
|
|
# Discriminators must match the IDL.
|
|
idl = json.loads(IDL_PATH.read_text())
|
|
by_name = {i["name"]: i for i in idl["instructions"]}
|
|
for name, disc in (
|
|
("buy_v2", pump_v2.BUY_V2_DISCRIMINATOR),
|
|
("sell_v2", pump_v2.SELL_V2_DISCRIMINATOR),
|
|
):
|
|
expected = bytes(by_name[name]["discriminator"])
|
|
if disc != expected:
|
|
problems.append(
|
|
f"pump_v2 {name} discriminator {list(disc)} != IDL {list(expected)}"
|
|
)
|
|
|
|
# Account lists must match the IDL in order and writability, for both
|
|
# quote assets and both mayhem states.
|
|
for quote_mint, label in (
|
|
(pump_v2.WSOL_MINT, "SOL"),
|
|
(pump_v2.USDC_MINT, "USDC"),
|
|
):
|
|
for mayhem in (False, True):
|
|
for name, builder in (
|
|
("buy_v2", pump_v2.build_buy_v2_instruction),
|
|
("sell_v2", pump_v2.build_sell_v2_instruction),
|
|
):
|
|
kwargs = {
|
|
"base_mint": mint,
|
|
"creator": creator,
|
|
"user": user,
|
|
"quote_mint": quote_mint,
|
|
"is_mayhem_mode": mayhem,
|
|
}
|
|
if name == "buy_v2":
|
|
instruction = builder(
|
|
token_amount_raw=1, max_quote_cost_raw=2, **kwargs
|
|
)
|
|
else:
|
|
instruction = builder(
|
|
token_amount_raw=1, min_quote_output_raw=2, **kwargs
|
|
)
|
|
|
|
idl_accounts = by_name[name]["accounts"]
|
|
if len(instruction.accounts) != len(idl_accounts):
|
|
problems.append(
|
|
f"pump_v2 {name} ({label}, mayhem={mayhem}): "
|
|
f"{len(instruction.accounts)} accounts != IDL "
|
|
f"{len(idl_accounts)}"
|
|
)
|
|
continue
|
|
for index, (meta, idl_account) in enumerate(
|
|
zip(instruction.accounts, idl_accounts), start=1
|
|
):
|
|
if meta.is_writable != bool(idl_account.get("writable")):
|
|
problems.append(
|
|
f"pump_v2 {name}[{index}] {idl_account['name']} "
|
|
f"({label}, mayhem={mayhem}): writable "
|
|
f"{meta.is_writable} != IDL "
|
|
f"{bool(idl_account.get('writable'))}"
|
|
)
|
|
|
|
# Cross-check every address against the src/ provider, which the
|
|
# checks above already validated.
|
|
provider = PumpFunAddressProvider()
|
|
token_info = build_token_info(quote_mint, mayhem=mayhem)
|
|
resolved = (
|
|
provider.get_buy_v2_instruction_accounts(token_info, user)
|
|
if name == "buy_v2"
|
|
else provider.get_sell_v2_instruction_accounts(token_info, user)
|
|
)
|
|
layout = _BUY_V2_ACCOUNTS if name == "buy_v2" else _SELL_V2_ACCOUNTS
|
|
for (account_name, _), meta in zip(layout, instruction.accounts):
|
|
# Fee recipients are picked at random from a set, so compare
|
|
# membership rather than identity.
|
|
if "fee_recipient" in account_name:
|
|
continue
|
|
if resolved[account_name] != meta.pubkey:
|
|
problems.append(
|
|
f"pump_v2 {name} {account_name} ({label}, "
|
|
f"mayhem={mayhem}): {meta.pubkey} != src/ "
|
|
f"{resolved[account_name]}"
|
|
)
|
|
|
|
return problems
|
|
|
|
|
|
def main() -> int:
|
|
"""Run all layout and derivation checks.
|
|
|
|
Returns:
|
|
Process exit code (0 on success)
|
|
"""
|
|
all_problems = []
|
|
|
|
for name, layout in (("buy_v2", _BUY_V2_ACCOUNTS), ("sell_v2", _SELL_V2_ACCOUNTS)):
|
|
problems = check_layout(name, layout)
|
|
status = "OK" if not problems else f"{len(problems)} PROBLEM(S)"
|
|
print(f"{name}: {len(layout)} accounts vs IDL -> {status}")
|
|
all_problems.extend(problems)
|
|
|
|
for quote_mint, label in ((WSOL_MINT, "SOL-paired"), (USDC_MINT, "USDC-paired")):
|
|
for mayhem in (False, True):
|
|
problems = check_derivations(quote_mint, mayhem=mayhem)
|
|
tag = f"{label}, mayhem={mayhem}"
|
|
status = "OK" if not problems else f"{len(problems)} PROBLEM(S)"
|
|
print(f"derivations ({tag}) -> {status}")
|
|
all_problems.extend(problems)
|
|
|
|
for name, check in (
|
|
("instruction encoding", check_instruction_encoding),
|
|
("quote config resolution", check_quote_config),
|
|
("learning-examples pump_v2 toolkit", check_examples_toolkit),
|
|
):
|
|
problems = check()
|
|
status = "OK" if not problems else f"{len(problems)} PROBLEM(S)"
|
|
print(f"{name} -> {status}")
|
|
all_problems.extend(problems)
|
|
|
|
if all_problems:
|
|
print("\nProblems found:")
|
|
for problem in all_problems:
|
|
print(f" - {problem}")
|
|
return 1
|
|
|
|
print("\nAll v2 account layouts and derivations match the IDL.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|