mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
* feat(idl): refresh to upstream 8109141 and decode omitted OptionU64 args Vendors pump.json, pump_amm.json and pump_fees.json from pump-fun/pump-public-docs@8109141 (holder rewards coins, cashback deprecation). create_v2 gains two trailing optionals: creator_fee_bps (OptionU64) and is_holder_reward (OptionBool). _is_optional_type only recognised OptionBool, so a create_v2 that omits creator_fee_bps failed the whole decode and the coin dropped out of the instruction path — the same failure #184 fixed for is_cashback_enabled. Reproduced against two live mainnet create_v2 transactions captured 2026-09-15: one omits creator_fee_bps (157-byte instruction data), one sends it (156 bytes) — both are added as getTransaction fixtures under learning-examples/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(learning-examples): accept 125-byte bonding curves in dataSize filters Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(learning-examples): replace unrun/recycled curve-size claims with dated live measurements The 45s/205-update/24-curve figures and the logsSubscribe/same-tx extend_account claim in the two graduating-token docstrings were carried over from before the 125/151 filter change and never re-verified under it. Replaced with fresh 2026-09-15 measurements: a length-unfiltered programSubscribe run, a direct 125->151 transition observed on one curve, and a live run of the Geyser script's own two-group build_subscribe_request confirming both filter groups actually deliver. Unreproduced claims are now marked UNVERIFIED in-line instead of stated flatly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(learning-examples): stop enumerating bonding-curve dataSize lengths extend_account can grow a curve to any length the program allows, so the CURVE_ACCOUNT_LENS = (125, 151) allowlist in both graduating-token scripts was whack-a-mole -- a live 256-byte curve was already missed. Drop the dataSize/datasize server-side filter entirely (discriminator + complete memcmp already restrict delivery to BondingCurve accounts) and replace the allowlist with a MIN_CURVE_LEN client-side floor. Collapse the Geyser script's two named filter groups back to one now that there's no length to fan out over. Extend verify_curve_account_sizes.py to assert neither script constructs a dataSize filter and that 125/151/256-byte curves all decode. Measured live 2026-09-15, filtered vs unfiltered subscribed simultaneously so both see identical trade activity: ~0.6-0.7% extra traffic on WebSocket, statistically zero on Geyser -- dropping the filter is effectively free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(learning-examples): remove untraceable bandwidth figure, session wording Review of the dataSize-enumeration fix found a docstring figure (90s window, 2/224/866 updates at 256/125/151 bytes) that no command in the fix report actually produced -- it was a byproduct of the discarded sequential-window run, never written down with its command+output. Drop it and rely only on the properly-documented 120s simultaneous measurement already backing the rest of the paragraph. Also replace two "carried from an earlier session" UNVERIFIED markers with the repo's own convention (UNVERIFIED: <what>, no reference to how or when the file was edited), and give both the update and byte ratio (1.006x / 1.007x) instead of a single derived percentage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(pumpfun): surface is_holder_reward and creator_fee_bps on TokenInfo Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(pumpfun): verify the USDC half of the creator_fee_bps claim The comment on TokenInfo.creator_fee_bps stated flatly that SOL- and USDC-paired coins always carry bps 0, but only the SOL half had been observed live. Found 10 USDC-paired bonding curves (via Dune's indexed CreateEvent table) and read each one directly through PumpFunCurveManager.get_pool_state: all 10 show creator_fee_bps=0, matching every custom-pair coin observed showing nonzero. Dated the verification in the comment instead of leaving it an unmarked claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(examples): create holder-reward coins; mark cashback legacy-only create_v2 now rejects is_cashback_enabled=[true] with 6082 CashbackDeprecated as of the 2026-09-15 upgrade. Add ENABLE_HOLDER_REWARD to mint_and_buy_v2.py so the trailing create_v2 args can reach is_holder_reward instead, and relabel the still-live cashback code paths in core.py, instruction_builder.py and curve_manager.py with the deprecation date. No cashback account handling is removed anywhere - existing cashback coins keep trading and accruing exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(pumpfun): resolve quote mints to their real token program A 2026-09-15 pump.fun upgrade lets create_v2 pair a coin with a Token-2022 quote mint (error 6064 now accepts "SPL Token or Token-2022"), and such coins already trade on mainnet. quote_token_program() only knew WSOL/USDC and silently defaulted everything else to SPL Token, deriving associated token accounts that don't exist on chain for any other quote mint. Add an async resolver that reads a mint's owner from chain and caches it for the process, pre-seeded with WSOL/USDC so they stay free. The bot warms this cache once at startup for every configured quote mint; the hot path (event parsing, address resolution) only ever reads the cache, preserving extreme_fast_mode's zero-RPC contract between detection and submission. quote_token_program() itself is untouched so its existing call sites keep their non-optional Pubkey return. Also fixes the standalone pump_v2.py learning-examples helper, which had the same bug in a form that ignored its argument entirely, and gives simulate_bot_buy_path.py a quote_amounts entry so it can exercise a non-SOL-paired coin instead of unconditionally skipping it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(core): name Anchor error codes when reporting a reverted transaction confirm_transaction only answers whether a transaction landed; meta.err carries the revert. Logging it raw as {'InstructionError': [idx, {'Custom': n}]} told an operator nothing, and pump.fun's 2026-09-15 upgrade added 22 new error codes to the pump program and 10 to pump-amm, so an unnamed number is now more likely to be wrong than useful. IDLParser.describe_error_code() looks a Custom(N) code up in an IDL's error table; SolanaClient logs the match alongside the raw meta.err at both revert sites, so a buy reverting with BuybackFeeRecipientMissing (issue #175) now prints "6062 BuybackFeeRecipientMissing" instead of a bare 6062. SolanaClient is shared across pump.fun and letsbonk.fun and has no record of which program a given transaction invoked, so this only checks pump.fun's table -- the program the bot's own buy_v2/sell_v2 call directly. A revert on a different program is described against the wrong table if the code number collides, or left unnamed otherwise; see the docstring on _describe_program_error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): correct get_account_info's return type annotation It was typed dict[str, Any] but solana-py's AsyncClient.get_account_info actually returns a solders Account object. Every real caller already reads attributes (.data, .owner) rather than subscripting, so this was latent rather than a live bug -- but the wrong annotation is what let those callers work by accident, and would mislead anyone who trusted the type and reached for result["owner"] instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): tag the revert description with the table it came from The name appended to a reverted transaction's log line looked like a fact about whichever program actually reverted, but it is only ever looked up in pump.fun's error table. This bot also trades letsbonk.fun, so a letsbonk revert whose numeric code happens to also exist in pump's table would get a confident, wrong name -- worse than the bare number it replaced, since a wrong name stops the reader from checking. Prefixing the description with "pump.fun IDL:" carries the same limitation the docstring already stated out to the log line itself, so it's visible without reading the source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: resync CLAUDE.md and README.md with the 2026-09-15 program upgrade The bonding curve is 125 bytes now (not 151, no more padding), create_v2 takes eight args instead of six, cashback creation is deprecated (6082 CashbackDeprecated) while existing cashback coins keep trading, quote assets are no longer just SOL/USDC (QuoteControl, not Global, is the authoritative mint registry), and holder-reward coins were undocumented entirely. Add verify_curve_account_sizes.py to both docs' verification lists, retire the superseded CU comments in instruction_builder.py (the 180k/120k limits are untouched), and fix two stale cashback comments plus a docstring in client.py that showed a msg suffix code 6062 doesn't have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(trading): refresh quote_token_program_id alongside quote_mint _refresh_quote_mint synced token_info.quote_mint from freshly-read curve state but left quote_token_program_id at its construction-time value. A stale non-None program id wins the `or` in PumpFunAddressProvider.resolve_quote, so a curve refresh that reveals a Token-2022 quote mint still resolves the associated_quote_* ATAs under SPL Token -- an address that does not exist on chain. cached_quote_token_program is a synchronous dict read, so this keeps extreme_fast_mode's zero-RPC-call contract between detection and submission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(examples): resolve the quote mint's real token program in manual buy/sell manual_buy.py and manual_sell.py derived the quote ATA's owning program through pump_v2.quote_token_program, which defaults any mint outside the WSOL/USDC map to SPL Token. A pump.fun upgrade verified 2026-09-15 lets create_v2 pair a coin with a Token-2022 quote mint (error 6064 now accepts "SPL Token or Token-2022"), so that default derives an ATA that does not exist on a Token-2022-quoted coin -- and these scripts submit real transactions, so the failure burns one. Both now call pump_v2.resolve_quote_token_program (free for WSOL/USDC via its cache, one RPC call otherwise) and thread the resolved program into both the buy_v2/sell_v2 instruction and the quote ATA creation. Verified against a real Token-2022-quoted pump.fun coin found via Dune (pumpdotfun_solana.pump_evt_createevent, mint G5Ve125Poi7KoDRP5uLsgu5NQdm3KoqfyUkq4cqtTo4r, quote_mint Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh, confirmed Token-2022-owned by RPC read): the old default derives associated_quote_bonding_curve FViEZvbcYjoSwGE2b37HiCXmPT3N7kSd1nQavFzcD7Yb, which does not exist on chain; the fixed resolution derives DfxcBKXTLDqobNEmR5JkaBeoKqNbJE1vKbtQVbowXz4x, which exists on chain and is owned by the Token-2022 program. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): correct get_multiple_accounts's return type annotation Declared as list[Any] while it actually returns a list of solders Account objects (or None for missing accounts) -- the same stale-annotation pattern already corrected on get_account_info. Checked the one caller, PumpFunCurveManager.get_pool_state_and_token_program, which already unpacks the two entries and reads .data / .owner off them directly rather than subscripting -- no live bug behind the wrong annotation here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(examples): use the public decode API in the optional-args verifier check_omitted_option_u64_decodes and check_option_u64_present_decodes reached into IDLParser._decode_instruction_args behind # noqa: SLF001 instead of the public decode_instruction(data, keys, accounts) already used by every other check in this file. The fixtures already carry valid accounts/keys (previously discarded as data, _, _), so the public method expresses the same check -- confirmed it returns byte-for-byte identical decoded args as the private call did. Confirmed the check stays load-bearing: reverting _is_optional_type to a narrow == "OptionBool" comparison drops the verifier to 2/7 checks passed, exit 1, including the two checks touched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct unverified claims from the final branch review Text-only fixes from the whole-branch review — no code defects found. - CLAUDE.md: create_v2 trailing-arg claim was circular (sourced from our own minter script) and wrong — the three committed fixtures show 0, 1 and 2 trailing args, never 3. Rewritten with the counted byte lengths and dated; the unobserved 3-arg form is now marked UNVERIFIED instead of asserted. - src/interfaces/core.py, CLAUDE.md: corrected an inflated "25 SOL-paired" sample to the actual 21 (25 coins captured live, 4 of them custom-pair). - instruction_builder.py: buy_v2/sell_v2 CU comments cited figures from a task brief that were never re-measured on this branch. Re-measured via simulate_v2_trades.py on 2026-09-15 (106,677 / ~77,196 CU) and updated the comments; the configured 180k/120k limits are unchanged. - simulate_bot_buy_path.py: removed a working-session reference ("this task") from a comment. - get_graduating_tokens.py, get_graduating_tokens_geyser.py's sibling docs (pump_v2.py, fetch_price.py, get_bonding_curve_status.py, verify_pumpportal_buy_path.py): made the curve-length story consistent (125/151/256, don't enumerate lengths) instead of a stale "125 or 151" framing; resolved an UNVERIFIED marker that mint_and_buy_v2.py already answers (extend_account can share a transaction with create_v2); removed a redundant restated clause in the bandwidth-measurement paragraph. Verified: all 7 verify_*/simulate offline+mainnet checks still exit 0, buy_v2/sell_v2 account counts unchanged at 27/26. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(trading): resolve quote-mint decimals instead of defaulting to 9 quote_decimals()/quote_units_per_token() defaulted every quote mint outside the WSOL/USDC map to SOL's 9 decimals, mis-scaling max_sol_cost/min_sol_output by up to 1000x for a custom Token-2022 quote mint (verified: 6 decimals). Extend resolve_quote_token_program's existing mint-account fetch to also parse and cache decimals from the same read (byte offset 44, shared by SPL Token and Token-2022), so the fix costs zero extra RPC calls and the zero-RPC extreme_fast_mode contract holds. Mirrored the same treatment in learning-examples/pump_v2.py's standalone copy. Also decode is_holder_reward in the instruction-only create_v2 parse path (a landed transaction proves it, since the program rejects the flag when globally disabled) while leaving creator_fee_bps at its default, since upstream docs say it's ignored for SOL/USDC-paired coins and nothing reads it today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(examples): strengthen optional-arg verifier assertions Fix verify_create_v2_optional_args.py to properly assert value equality rather than just presence when checking optional arguments: - check_omitted_option_u64_decodes: verify both creator_fee_bps and is_holder_reward are exactly None (not just present in dict) - check_option_u64_present_decodes: decode the fixture, verify creator_fee_bps shape is {"field_0": <value>}, and assert the actual value (0) rather than just non-null - Fixture decodes to creator_fee_bps={'field_0': 0}, is_holder_reward=None Also fix README.md line 166: "Three examples" → "Four examples" to match the four verification commands listed. Temporary revert of idl_parser.py's _is_optional_type confirms the strengthened assertions are load-bearing: changing from startswith("Option") back to == "OptionBool" causes 4 of 7 checks to fail. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkvFUgSiZoaF7w4xugrgBc --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
291 lines
10 KiB
Python
291 lines
10 KiB
Python
"""Simulate pump.fun buy_v2 / sell_v2 against mainnet without spending funds.
|
|
|
|
Builds both instructions through the bot's real code path (address provider,
|
|
instruction builder, curve manager) and runs them through
|
|
`simulateTransaction`, which executes the program against live state but never
|
|
submits. Reports the program error (if any) and the compute units consumed —
|
|
use the latter to tune `get_buy_compute_unit_limit` / `get_sell_compute_unit_limit`.
|
|
|
|
The wallet is only used to derive a pubkey and sign nothing: simulation runs
|
|
with sigVerify disabled.
|
|
|
|
Usage:
|
|
# simulate against a specific coin
|
|
uv run learning-examples/simulate_v2_trades.py <MINT>
|
|
|
|
# discover a fresh coin via geyser, then simulate against it
|
|
uv run learning-examples/simulate_v2_trades.py
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from base64 import b64encode
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
|
|
from dotenv import load_dotenv # noqa: E402
|
|
from solders.compute_budget import set_compute_unit_limit # noqa: E402
|
|
from solders.message import Message # noqa: E402
|
|
from solders.pubkey import Pubkey # noqa: E402
|
|
from solders.transaction import Transaction # noqa: E402
|
|
|
|
from core.client import SolanaClient # noqa: E402
|
|
from core.pubkeys import ( # noqa: E402
|
|
TOKEN_DECIMALS,
|
|
SystemAddresses,
|
|
normalize_quote_mint,
|
|
quote_units_per_token,
|
|
resolve_quote_token_program,
|
|
)
|
|
from core.wallet import Wallet # noqa: E402
|
|
from interfaces.core import Platform, TokenInfo # noqa: E402
|
|
from platforms import get_platform_implementations # noqa: E402
|
|
|
|
load_dotenv(PROJECT_ROOT / ".env")
|
|
|
|
# Buy this many whole tokens in the simulation. Small enough that the quote cost
|
|
# stays well under a test wallet's balance on a fresh curve.
|
|
SIMULATED_TOKEN_AMOUNT = 20
|
|
# Generous ceiling so simulation reports true consumption rather than hitting
|
|
# the limit. The real bot uses the builder's tuned values.
|
|
SIMULATION_CU_LIMIT = 400_000
|
|
|
|
|
|
async def discover_mint(timeout_seconds: float = 45.0) -> Pubkey | None:
|
|
"""Listen for a freshly created pump.fun coin via geyser.
|
|
|
|
Args:
|
|
timeout_seconds: How long to wait for a creation event
|
|
|
|
Returns:
|
|
Mint of the first coin seen, or None on timeout
|
|
"""
|
|
from monitoring.listener_factory import ListenerFactory
|
|
|
|
listener = ListenerFactory.create_listener(
|
|
listener_type="geyser",
|
|
geyser_endpoint=os.environ["GEYSER_ENDPOINT"],
|
|
geyser_api_token=os.environ["GEYSER_API_TOKEN"],
|
|
geyser_auth_type=os.environ.get("GEYSER_AUTH_TYPE", "x-token"),
|
|
platforms=[Platform.PUMP_FUN],
|
|
)
|
|
|
|
seen: list[TokenInfo] = []
|
|
|
|
async def on_token(token_info: TokenInfo) -> None:
|
|
seen.append(token_info)
|
|
|
|
task = asyncio.create_task(listener.listen_for_tokens(on_token))
|
|
try:
|
|
deadline = timeout_seconds / 0.5
|
|
for _ in range(int(deadline)):
|
|
if seen:
|
|
break
|
|
await asyncio.sleep(0.5)
|
|
finally:
|
|
task.cancel()
|
|
|
|
return seen[0].mint if seen else None
|
|
|
|
|
|
async def build_token_info(
|
|
mint: Pubkey, client: SolanaClient
|
|
) -> tuple[TokenInfo, dict]:
|
|
"""Assemble TokenInfo for an existing coin from its on-chain curve state.
|
|
|
|
Args:
|
|
mint: Base token mint
|
|
client: Solana RPC client
|
|
|
|
Returns:
|
|
Tuple of (TokenInfo, decoded curve state)
|
|
"""
|
|
implementations = get_platform_implementations(Platform.PUMP_FUN, client)
|
|
provider = implementations.address_provider
|
|
curve_manager = implementations.curve_manager
|
|
|
|
bonding_curve = provider.derive_pool_address(mint)
|
|
state = await curve_manager.get_pool_state(bonding_curve, commitment="processed")
|
|
|
|
creator = state["creator"]
|
|
creator = Pubkey.from_string(creator) if isinstance(creator, str) else creator
|
|
quote_mint = normalize_quote_mint(state["quote_mint"])
|
|
|
|
# Which token program owns the base mint decides the base ATA derivation.
|
|
account = await client.get_account_info(mint)
|
|
base_token_program = (
|
|
SystemAddresses.TOKEN_2022_PROGRAM
|
|
if str(account.owner) == str(SystemAddresses.TOKEN_2022_PROGRAM)
|
|
else SystemAddresses.TOKEN_PROGRAM
|
|
)
|
|
|
|
token_info = TokenInfo(
|
|
name="simulation",
|
|
symbol="SIM",
|
|
uri="",
|
|
mint=mint,
|
|
platform=Platform.PUMP_FUN,
|
|
bonding_curve=bonding_curve,
|
|
associated_bonding_curve=provider.derive_associated_bonding_curve(
|
|
mint, bonding_curve, base_token_program
|
|
),
|
|
creator=creator,
|
|
creator_vault=provider.derive_creator_vault(creator),
|
|
token_program_id=base_token_program,
|
|
is_mayhem_mode=state["is_mayhem_mode"],
|
|
is_cashback_coin=state["is_cashback_coin"],
|
|
quote_mint=quote_mint,
|
|
quote_token_program_id=await resolve_quote_token_program(
|
|
quote_mint, client.get_account_info
|
|
),
|
|
)
|
|
return token_info, state
|
|
|
|
|
|
async def simulate(
|
|
client: SolanaClient, wallet: Wallet, instructions: list, label: str
|
|
) -> bool:
|
|
"""Run a set of instructions through simulateTransaction and report.
|
|
|
|
Args:
|
|
client: Solana RPC client
|
|
wallet: Wallet whose pubkey pays and signs
|
|
instructions: Instructions to simulate
|
|
label: Human-readable name for output
|
|
|
|
Returns:
|
|
True if the simulation reported no program error
|
|
"""
|
|
blockhash = await client.get_latest_blockhash()
|
|
message = Message.new_with_blockhash(
|
|
[set_compute_unit_limit(SIMULATION_CU_LIMIT), *instructions],
|
|
wallet.pubkey,
|
|
blockhash,
|
|
)
|
|
transaction = Transaction([wallet.keypair], message, blockhash)
|
|
|
|
response = await client.post_rpc(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "simulateTransaction",
|
|
"params": [
|
|
b64encode(bytes(transaction)).decode(),
|
|
{
|
|
"encoding": "base64",
|
|
"sigVerify": False,
|
|
"replaceRecentBlockhash": True,
|
|
"commitment": "processed",
|
|
},
|
|
],
|
|
}
|
|
)
|
|
|
|
if not response or "result" not in response:
|
|
print(f" {label}: RPC call failed: {response}")
|
|
return False
|
|
|
|
value = response["result"]["value"]
|
|
err = value.get("err")
|
|
units = value.get("unitsConsumed")
|
|
account_count = len(instructions[-1].accounts)
|
|
|
|
print(f" {label}: accounts={account_count} unitsConsumed={units} err={err}")
|
|
if err:
|
|
for line in value.get("logs") or []:
|
|
if "Error" in line or "failed" in line or "Instruction:" in line:
|
|
print(f" {line}")
|
|
return err is None
|
|
|
|
|
|
async def main() -> int:
|
|
"""Simulate a buy_v2 and sell_v2 for one coin.
|
|
|
|
Returns:
|
|
Process exit code (0 if the buy simulation succeeded)
|
|
"""
|
|
mint_arg = sys.argv[1] if len(sys.argv) > 1 else None
|
|
|
|
if mint_arg:
|
|
mint = Pubkey.from_string(mint_arg)
|
|
else:
|
|
print("No mint given; listening for a fresh pump.fun coin via geyser...")
|
|
mint = await discover_mint()
|
|
if mint is None:
|
|
print("No coin seen before timeout. Pass a mint explicitly.")
|
|
return 2
|
|
|
|
client = SolanaClient(os.environ["SOLANA_NODE_RPC_ENDPOINT"])
|
|
wallet = Wallet(os.environ["SOLANA_PRIVATE_KEY"])
|
|
|
|
try:
|
|
token_info, state = await build_token_info(mint, client)
|
|
quote_mint = token_info.quote_mint
|
|
quote_unit = quote_units_per_token(quote_mint)
|
|
|
|
print(f"\nmint: {mint}")
|
|
print(f"bonding_curve: {token_info.bonding_curve}")
|
|
print(f"quote_mint: {quote_mint}")
|
|
print(f"base program: {token_info.token_program_id}")
|
|
print(
|
|
f"mayhem={token_info.is_mayhem_mode} "
|
|
f"cashback={token_info.is_cashback_coin} "
|
|
f"complete={state['complete']}"
|
|
)
|
|
print(f"price: {state['price_per_token']:.10f} quote/token")
|
|
print(f"wallet: {wallet.pubkey}\n")
|
|
|
|
implementations = get_platform_implementations(Platform.PUMP_FUN, client)
|
|
provider = implementations.address_provider
|
|
builder = implementations.instruction_builder
|
|
|
|
token_raw = SIMULATED_TOKEN_AMOUNT * 10**TOKEN_DECIMALS
|
|
# Cap the quote spend generously; simulation only needs it affordable.
|
|
max_quote_raw = int(
|
|
SIMULATED_TOKEN_AMOUNT * state["price_per_token"] * quote_unit * 2
|
|
) or int(0.001 * quote_unit)
|
|
|
|
print("simulating:")
|
|
buy_instructions = await builder.build_buy_v2_instruction(
|
|
token_info, wallet.pubkey, max_quote_raw, token_raw, provider
|
|
)
|
|
buy_ok = await simulate(client, wallet, buy_instructions, "buy_v2 ")
|
|
|
|
sell_instructions = await builder.build_sell_v2_instruction(
|
|
token_info, wallet.pubkey, token_raw, 1, provider
|
|
)
|
|
await simulate(client, wallet, sell_instructions, "sell_v2")
|
|
print(
|
|
" ^ expected to fail with AccountNotInitialized on "
|
|
"associated_base_user unless\n the wallet already holds this "
|
|
"coin. That error means all 26 accounts validated."
|
|
)
|
|
|
|
# Buy and sell in one transaction so the position exists mid-tx. This is
|
|
# the only way to get a representative sell_v2 CU number without
|
|
# actually holding the coin. Sell slightly less than bought to stay
|
|
# inside the realised balance.
|
|
combined = [*buy_instructions, *sell_instructions[:-1]]
|
|
resell_raw = int(token_raw * 0.9)
|
|
combined.append(
|
|
(
|
|
await builder.build_sell_v2_instruction(
|
|
token_info, wallet.pubkey, resell_raw, 1, provider
|
|
)
|
|
)[-1]
|
|
)
|
|
combined_ok = await simulate(client, wallet, combined, "buy+sell")
|
|
if combined_ok:
|
|
print(" ^ subtract the buy_v2 figure above to estimate sell_v2 CU")
|
|
|
|
return 0 if buy_ok else 1
|
|
finally:
|
|
await client.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(asyncio.run(main()))
|