feat(pumpfun): resync with the 2026-09-15 program upgrade (#202)

* 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>
This commit is contained in:
Anton Sauchyk
2026-09-16 09:36:44 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent a0540fdc9e
commit 55ae2afd82
30 changed files with 9536 additions and 1878 deletions
+89 -23
View File
@@ -106,6 +106,10 @@ is protoc — needed only to regenerate the `geyser_pb2` stubs in
# instruction encoding and quote-asset config against idl/pump_fun_idl.json
uv run learning-examples/verify_v2_account_layout.py
# Offline: 125/151/256-byte bonding curves all decode, and the graduating-
# token examples don't filter on account length
uv run learning-examples/verify_curve_account_sizes.py
# Mainnet, no funds moved: simulate buy_v2/sell_v2 for one coin, report CU
uv run learning-examples/simulate_v2_trades.py <MINT>
@@ -114,7 +118,7 @@ uv run learning-examples/simulate_bot_buy_path.py
uv run learning-examples/simulate_bot_buy_path.py --no-extreme-fast
```
Run all three after any pump.fun program upgrade. The simulations report
Run all four after any pump.fun program upgrade. The simulations report
`unitsConsumed`; use it to retune `get_buy_compute_unit_limit` /
`get_sell_compute_unit_limit` in `platforms/pumpfun/instruction_builder.py`.
@@ -287,14 +291,28 @@ The IDLs under `idl/` are vendored verbatim from `github.com/pump-fun/pump-publi
### Quote assets and the v2 trade instructions (current path)
- pump.fun supports quote assets other than SOL. `BondingCurve.quote_mint` is
`Pubkey::default()` (all zeros) for SOL-paired coins; USDC
(`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`) is whitelisted in `Global`.
**Legacy `buy`/`sell` cannot trade non-SOL-paired coins at all.**
- pump.fun supports quote assets other than SOL, and it's no longer just
SOL and USDC. `BondingCurve.quote_mint` is `Pubkey::default()` (all zeros)
for SOL-paired coins; USDC (`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`)
is one whitelisted entry in `Global`, but coins paired with Token-2022
quote mints are live on chain too. **Legacy `buy`/`sell` cannot trade
non-SOL-paired coins at all.**
- **`Global.whitelisted_quote_mints` is not the authoritative quote-mint
registry.** The 2026-09-15 upgrade added a `QuoteControl` account with its
own mint list, which is how a coin can pair with a mint `Global` never
lists. Error `6064` was relaxed to match — it used to require legacy SPL
Token and now accepts SPL Token or Token-2022.
- **The quote mint's token program is resolved from chain, not assumed.**
`resolve_quote_token_program` (`src/core/pubkeys.py`) reads a mint's owner
once — pre-seeded with WSOL/USDC so those stay free — and caches it for the
process; `cached_quote_token_program` is the hot-path read used by event
parsing and address resolution. The bot warms this cache once at startup
for every configured quote mint, so `extreme_fast_mode`'s zero-RPC contract
between detection and submission still holds.
- The bot uses **`buy_v2` (27 accounts)** and **`sell_v2` (26 accounts)**. Every
account is mandatory and the order is identical for every coin — SOL or USDC
paired, mayhem or not, cashback or not. `sell_v2` is `buy_v2` minus
`global_volume_accumulator`. Layouts live in `_BUY_V2_ACCOUNTS` /
account is mandatory and the order is identical for every coin — whatever
quote mint it's paired with, mayhem or not, cashback or not. `sell_v2` is
`buy_v2` minus `global_volume_accumulator`. Layouts live in `_BUY_V2_ACCOUNTS` /
`_SELL_V2_ACCOUNTS` in `platforms/pumpfun/instruction_builder.py` and are
machine-checked against the IDL by `learning-examples/verify_v2_account_layout.py`.
- v2 args carry **no `track_volume` OptionBool** (24-byte data: discriminator +
@@ -314,12 +332,24 @@ The IDLs under `idl/` are vendored verbatim from `github.com/pump-fun/pump-publi
### BondingCurve account layout
- The account is **151 bytes**: 8-byte discriminator, then
`virtual_token_reserves, virtual_quote_reserves, real_token_reserves,
real_quote_reserves, token_total_supply` (u64 each), `complete` (1B, offset 48),
`creator` (32B, offset 49), `is_mayhem_mode` (offset 81),
`is_cashback_coin` (offset 82), `quote_mint` (32B, offset 83), then 36 reserved
zero bytes. The documented struct is 115 bytes; the extra 36 are padding.
- **`create_v2` now allocates a 125-byte account, not 151.** The 36 reserved
padding bytes are gone, and three fields were appended after `quote_mint`:
`creator_fee_bps` (u64), `can_edit_creator_fee` (bool, reserved — always
false) and `is_holder_reward` (bool). Every field the bot reads sits before
where the padding used to start, and none of it moved.
- `extend_account` can grow a curve past 125 bytes, and a length allowlist is
whack-a-mole against that — don't filter on account length; decode any
length at or above 125 the same way:
| Length | What it is |
|---|---|
| 125 bytes | What `create_v2` allocates now (2026-09-15 upgrade) |
| 151 bytes | The old allocation size; still reachable via `extend_account` |
| 256 bytes | A rarer `extend_account` target, confirmed live 2026-09-15 |
`learning-examples/verify_curve_account_sizes.py` checks that all three
decode correctly and that the graduating-token examples don't filter on
account length.
- The SOL-named fields were **renamed**: `virtual_sol_reserves` →
`virtual_quote_reserves`, `real_sol_reserves` → `real_quote_reserves`. The
curve manager still exposes the old names as aliases, so pre-existing callers
@@ -339,15 +369,32 @@ The IDLs under `idl/` are vendored verbatim from `github.com/pump-fun/pump-publi
### Coin creation
- The IDL instruction is `create_v2` (snake_case). Args: `name (str),
symbol (str), uri (str), creator (pubkey), is_mayhem_mode (bool),
is_cashback_enabled (OptionBool 1B)`. `OptionBool` is a struct wrapping a single
bool — serialized as 1 byte, not 2.
- **`is_cashback_enabled` can be absent from the wire entirely.** Two mainnet
`create_v2` instructions carry `0001` and `00` after `creator`: one sends both
trailing args, the other omits the last. A decoder that reads a fixed number of
trailing bytes raises `IndexError` on roughly half of all coins. Decode
trailing args defensively and report a missing one as unset —
- The IDL instruction is `create_v2` (snake_case) and takes **eight args**:
`name (str), symbol (str), uri (str), creator (pubkey), is_mayhem_mode
(bool), is_cashback_enabled (OptionBool), creator_fee_bps (OptionU64),
is_holder_reward (OptionBool)`. The last two were added by the 2026-09-15
upgrade. `OptionBool` and `OptionU64` are single-field Anchor structs with
no presence tag — each serializes as its bare inner value, 1 byte and 8
bytes respectively, never a discriminated Option.
- **`is_cashback_enabled = [true]` is rejected as of 2026-09-15**, with error
`6082 CashbackDeprecated` — `create_v2` can no longer mint a new cashback
coin. Existing cashback coins are unaffected: they keep trading, keep
accruing, and remain claimable, so every cashback code path in this repo
(the legacy sell path's cashback branch, `is_cashback_coin` on `TokenInfo`,
etc.) stays live and must not be treated as dead.
- **The trailing args are positional, not independently optional.** Reaching
`is_holder_reward` (arg 8) means sending `is_cashback_enabled` (6) and
`creator_fee_bps` (7) first, even when both are false/zero. The three
committed fixtures show three different wire lengths: the blocksubscribe
fixture omits all trailing args (0 bytes after `is_mayhem_mode`), the
"omitted fee bps" getTransaction fixture sends `is_cashback_enabled` only
(1 byte), and the "with fee bps" getTransaction fixture sends
`is_cashback_enabled` + `creator_fee_bps` (9 bytes) — counted directly from
each fixture's instruction data, 2026-09-15. `is_holder_reward` (the
three-trailing-arg form) is legal per the IDL but has not been observed on
the wire — UNVERIFIED whether it is ever sent. A decoder that reads a
fixed number of trailing bytes raises `IndexError` on the shorter forms.
Decode trailing args defensively and report a missing one as unset —
`utils/idl_parser.py` does this for trailing option-typed args since #184
(`uv run learning-examples/verify_create_v2_optional_args.py` checks it).
- `create_v2` accounts 1-16 are in the IDL; accounts **17-19 are optional
@@ -372,6 +419,25 @@ The IDLs under `idl/` are vendored verbatim from `github.com/pump-fun/pump-publi
`quote_mint` from `CreateEvent` (which gained `quote_mint` and
`virtual_quote_reserves` as trailing fields).
### Holder reward coins
`create_v2` can set `is_holder_reward` so the creator fee is set aside for
holders instead of paid to a creator wallet. On such a coin
`BondingCurve.creator` holds a pump.fun address rather than the actual
creator's — that's expected, and the `creator_vault` derivation is unchanged
and still correct either way, since it derives from whatever `creator` the
curve carries. **No trade instruction changed**: `buy`, `sell`, `buy_v2`,
`sell_v2` and the PumpSwap instructions take identical accounts and arguments
whether or not a coin is a holder-reward coin. `Global.is_holder_reward_enabled`
can switch creation off globally.
`TokenInfo.is_holder_reward` and `TokenInfo.creator_fee_bps` surface this to
the bot — see the field comments on `TokenInfo` in `src/interfaces/core.py`
(dated 2026-09-15) for what's been verified live: SOL- and USDC-paired coins
keep `creator_fee_bps` at 0 (checked on 21 SOL-paired and 10 USDC-paired
coins), while every custom-pair coin checked in the same pass carried a
nonzero value.
### Legacy instructions (fallback only)
Retained behind `PumpFunInstructionBuilder(..., use_legacy_instructions=True)`.
+6 -5
View File
@@ -163,12 +163,13 @@ you leave it off. The `decode_from_*.py` scripts fall back to the saved fixtures
them (`raw_*.json`), which are recaptured from mainnet rather than hand-edited — a
stale fixture makes a working decoder look broken and a broken one look fine.
Two examples double as verification scripts to run after any pump.fun program upgrade:
Four examples double as verification scripts to run after any pump.fun program upgrade:
```bash
uv run learning-examples/verify_v2_account_layout.py # offline: account layouts, PDAs, encoding
uv run learning-examples/simulate_v2_trades.py <MINT> # mainnet simulation, no funds moved
uv run learning-examples/verify_tx_status_checks.py # offline: every example checks meta.err
uv run learning-examples/verify_v2_account_layout.py # offline: account layouts, PDAs, encoding
uv run learning-examples/verify_curve_account_sizes.py # offline: 125/151/256-byte curves all decode
uv run learning-examples/simulate_v2_trades.py <MINT> # mainnet simulation, no funds moved
uv run learning-examples/verify_tx_status_checks.py # offline: every example checks meta.err
```
Related docs: [Listening to pump.fun migrations](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-migrations-to-raydium) · [Sniping with only logsSubscribe](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-token-mint-using-only-logssubscribe)
@@ -187,7 +188,7 @@ For faster execution, Chainstack offers [Solana Trader nodes](https://docs.chain
## IDLs
The IDLs under [`idl/`](idl/) are vendored from [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs) — currently upstream commit `9c82f61`. To refresh, copy `pump.json`, `pump_amm.json`, and `pump_fees.json` into `pump_fun_idl.json`, `pump_swap_idl.json`, and `pump_fees.json`, and note the upstream commit in your commit message. Don't hand-edit them.
The IDLs under [`idl/`](idl/) are vendored from [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs) — currently upstream commit `8109141`. To refresh, copy `pump.json`, `pump_amm.json`, and `pump_fees.json` into `pump_fun_idl.json`, `pump_swap_idl.json`, and `pump_fees.json`, and note the upstream commit in your commit message. Don't hand-edit them.
The `buy_v2` / `sell_v2` account lists are complete in the IDL — that's the point of the v2 interface. The **legacy** `buy` / `sell` lists are not: the IDL omits PDAs the on-chain program requires. For anything outside v2, cross-check against a recent successful on-chain transaction before trusting the IDL.
+444 -1086
View File
File diff suppressed because it is too large Load Diff
+6157 -583
View File
File diff suppressed because it is too large Load Diff
+377 -86
View File
@@ -8,27 +8,22 @@
},
"instructions": [
{
"name": "admin_set_coin_creator",
"docs": [
"Overrides the coin creator for a canonical pump pool"
],
"name": "admin_cto_pool",
"discriminator": [
242,
40,
117,
145,
73,
96,
105,
104
45,
61,
165,
151,
104,
0,
49,
189
],
"accounts": [
{
"name": "admin_set_coin_creator_authority",
"signer": true,
"relations": [
"global_config"
]
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "global_config"
@@ -37,6 +32,79 @@
"name": "pool",
"writable": true
},
{
"name": "pool_authority",
"signer": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
112,
111,
111,
108,
45,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
},
{
"kind": "account",
"path": "pool.base_mint",
"account": "Pool"
}
],
"program": {
"kind": "const",
"value": [
1,
86,
224,
246,
147,
102,
90,
207,
68,
219,
21,
104,
191,
23,
91,
170,
81,
137,
203,
151,
245,
210,
255,
59,
101,
93,
43,
182,
253,
109,
24,
176
]
}
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "event_authority",
"pda": {
@@ -74,6 +142,16 @@
{
"name": "coin_creator",
"type": "pubkey"
},
{
"name": "is_holder_reward",
"type": "bool"
},
{
"name": "creator_fee_bps",
"type": {
"option": "u64"
}
}
]
},
@@ -1349,6 +1427,10 @@
},
{
"name": "claim_cashback",
"docs": [
"Pays out the user's accrued cashback. `user_wsol_token_account` may be any token account",
"of `quote_mint` owned by `user`, not only the associated one."
],
"discriminator": [
37,
58,
@@ -1469,60 +1551,7 @@
},
{
"name": "user_wsol_token_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "user"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "const",
"value": [
140,
151,
37,
143,
78,
36,
137,
241,
187,
61,
16,
41,
20,
142,
13,
131,
11,
90,
19,
153,
218,
255,
16,
132,
4,
142,
123,
216,
219,
233,
248,
89
]
}
}
"writable": true
},
{
"name": "system_program",
@@ -2526,6 +2555,30 @@
"name": "OptionBool"
}
}
},
{
"name": "creator_fee_bps",
"type": {
"defined": {
"name": "OptionU64"
}
}
},
{
"name": "can_edit_creator_fee",
"type": {
"defined": {
"name": "OptionBool"
}
}
},
{
"name": "is_holder_reward",
"type": {
"defined": {
"name": "OptionBool"
}
}
}
]
},
@@ -2753,6 +2806,7 @@
},
{
"name": "user",
"writable": true,
"signer": true
},
{
@@ -4848,6 +4902,79 @@
}
]
},
{
"name": "update_creator_fee_config",
"discriminator": [
61,
175,
160,
249,
66,
66,
136,
175
],
"accounts": [
{
"name": "admin",
"writable": true,
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "global_config",
"writable": true
},
{
"name": "system_program",
"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": "program"
}
],
"args": [
{
"name": "creator_fee_configurable",
"type": "bool"
},
{
"name": "max_configurable_creator_fee_bps",
"type": "u64"
}
]
},
{
"name": "update_fee_config",
"discriminator": [
@@ -5154,16 +5281,16 @@
],
"events": [
{
"name": "AdminSetCoinCreatorEvent",
"name": "AdminCtoPoolEvent",
"discriminator": [
45,
220,
93,
24,
25,
97,
172,
104
47,
35,
163,
249,
150,
157,
147,
122
]
},
{
@@ -5452,6 +5579,19 @@
234
]
},
{
"name": "UpdateCreatorFeeConfigEvent",
"discriminator": [
152,
198,
124,
124,
106,
246,
127,
191
]
},
{
"name": "UpdateFeeConfigEvent",
"discriminator": [
@@ -5673,7 +5813,7 @@
{
"code": 6047,
"name": "CoinCreatorMigratedToSharingConfig",
"msg": "coin creator has been migrated to sharing config, use pump_fees::reset_fee_sharing_config instead"
"msg": "coin creator has been migrated to sharing config"
},
{
"code": 6048,
@@ -5768,11 +5908,61 @@
"code": 6067,
"name": "SeedLockViolation",
"msg": "BOOST: lp_supply must never drop below the circulating LP mint supply"
},
{
"code": 6068,
"name": "CreatorFeeNotConfigurable",
"msg": "Configurable creator fee is disabled"
},
{
"code": 6069,
"name": "CreatorFeeBpsOutOfRange",
"msg": "Creator fee basis points must be between 1 and the configured maximum"
},
{
"code": 6070,
"name": "CreatorFeeNotEditable",
"msg": "Creator fee is not editable for this pool"
},
{
"code": 6071,
"name": "CreatorFeeNotAllowedForCashbackCoin",
"msg": "Cashback coins cannot have a creator fee"
},
{
"code": 6072,
"name": "SharingConfigNotActive",
"msg": "Sharing config is not active"
},
{
"code": 6073,
"name": "NotAuthorized",
"msg": "Not authorized"
},
{
"code": 6074,
"name": "HolderRewardCreatorImmutable",
"msg": "The coin creator of a holder-reward pool cannot be changed"
},
{
"code": 6075,
"name": "CtoNotAllowedForMayhemPool",
"msg": "CTO is not allowed on a mayhem-mode pool"
},
{
"code": 6076,
"name": "InvalidHolderRewardCoinCreator",
"msg": "A holder-reward pool's coin creator must be the holder-rewards PDA"
},
{
"code": 6077,
"name": "CreatorFeeNotConfigurableForQuote",
"msg": "Creator fee is not configurable on a SOL or USDC quote; the fee schedule applies"
}
],
"types": [
{
"name": "AdminSetCoinCreatorEvent",
"name": "AdminCtoPoolEvent",
"type": {
"kind": "struct",
"fields": [
@@ -5780,10 +5970,6 @@
"name": "timestamp",
"type": "i64"
},
{
"name": "admin_set_coin_creator_authority",
"type": "pubkey"
},
{
"name": "base_mint",
"type": "pubkey"
@@ -5799,6 +5985,22 @@
{
"name": "new_coin_creator",
"type": "pubkey"
},
{
"name": "is_holder_reward",
"type": "bool"
},
{
"name": "is_cashback_coin",
"type": "bool"
},
{
"name": "old_creator_fee_bps",
"type": "u64"
},
{
"name": "new_creator_fee_bps",
"type": "u64"
}
]
}
@@ -6094,6 +6296,14 @@
{
"name": "base_supply",
"type": "u64"
},
{
"name": "holder_rewards_bps",
"type": "u64"
},
{
"name": "holder_rewards",
"type": "u64"
}
]
}
@@ -6361,6 +6571,18 @@
{
"name": "is_mayhem_mode",
"type": "bool"
},
{
"name": "creator_fee_bps",
"type": "u64"
},
{
"name": "can_edit_creator_fee",
"type": "bool"
},
{
"name": "is_holder_reward",
"type": "bool"
}
]
}
@@ -6541,6 +6763,14 @@
}
}
}
},
{
"name": "exotic_flat_fees",
"type": {
"defined": {
"name": "Fees"
}
}
}
]
}
@@ -6685,6 +6915,14 @@
{
"name": "boost_enabled",
"type": "bool"
},
{
"name": "creator_fee_configurable",
"type": "bool"
},
{
"name": "max_configurable_creator_fee_bps",
"type": "u64"
}
]
}
@@ -6824,6 +7062,15 @@
]
}
},
{
"name": "OptionU64",
"type": {
"kind": "struct",
"fields": [
"u64"
]
}
},
{
"name": "Pool",
"type": {
@@ -6886,6 +7133,18 @@
"For non-boost pools, value is 0, so the behavior is identical to legacy pools."
],
"type": "i128"
},
{
"name": "creator_fee_bps",
"type": "u64"
},
{
"name": "can_edit_creator_fee",
"type": "bool"
},
{
"name": "is_holder_reward",
"type": "bool"
}
]
}
@@ -7039,6 +7298,14 @@
{
"name": "base_supply",
"type": "u64"
},
{
"name": "holder_rewards_bps",
"type": "u64"
},
{
"name": "holder_rewards",
"type": "u64"
}
]
}
@@ -7229,6 +7496,30 @@
]
}
},
{
"name": "UpdateCreatorFeeConfigEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "timestamp",
"type": "i64"
},
{
"name": "admin",
"type": "pubkey"
},
{
"name": "creator_fee_configurable",
"type": "bool"
},
{
"name": "max_configurable_creator_fee_bps",
"type": "u64"
}
]
}
},
{
"name": "UpdateFeeConfigEvent",
"type": {
@@ -108,9 +108,12 @@ class BondingCurveState:
"is_cashback_coin" / Flag,
)
# V5: V4 + quote_mint. Live accounts are 151 bytes — this struct covers 107 of the
# 143 data bytes and the remaining 36 are reserved padding. The quote-side reserves
# are in the quote mint's raw units, so a non-SOL coin must not be scaled by 1e9.
# V5: V4 + quote_mint. Live accounts are 125 bytes as created; extend_account
# can grow one to 151, 256, or any other length the program allows — this
# struct covers the fields needed here at the same offsets regardless of
# total length, and anything past it is left unread. The quote-side
# reserves are in the quote mint's raw units, so a non-SOL coin must not be
# scaled by 1e9.
_STRUCT_V5 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
@@ -13,7 +13,8 @@ Solana Tracker), not core Agave, and its `limit` is a *scan* budget rather than
result count, so answering this question with it means ~1000 sequential pages.
`programSubscribe` sidesteps the scan entirely. A curve can only approach
graduation by being traded, and every write to it pushes the full 151-byte account,
graduation by being traded, and every write to it pushes the full account — 125
bytes as `create_v2` allocates it, or 151 once `extend_account` has run on it —
so each notification carries everything needed to compute progress — there is no
state to accumulate and no cold start beyond the next trade. Verified accepted on
both a paid endpoint and the public `api.mainnet-beta.solana.com`.
@@ -49,10 +50,61 @@ migration want the 3-byte one, a wider funnel the 2-byte one.
Checked against mainnet by running the filtered and unfiltered subscriptions side by
side for a minute: same curves, nothing dropped, nothing extra.
`dataSize: 151` restricts this to the current curve layout. The original 49-byte
layout (no `creator` field) still has accounts with `complete = false`, but none of
them are written to any more — verified over a 45s window in which all 205 updates
across 24 curves were 151-byte accounts.
This script does **not** filter on `dataSize`. `create_v2` allocates the curve
at exactly 125 bytes, an account grows to 151 once `extend_account` has run on
it, and a rarer third length (256 bytes, confirmed live below) also exists.
`extend_account` can grow a curve to any length — nothing enumerates every
size it might produce — so a fixed `dataSize` allowlist is whack-a-mole: the
next length silently drops curves again, and the failure mode is invisible,
since the script just prints fewer results rather than an error. The
discriminator `memcmp` alone already restricts delivery to `BondingCurve`
accounts, so dropping `dataSize` costs no precision — only bandwidth (see the
measurement below). A `MIN_CURVE_LEN` floor still discards anything shorter
than the smallest real struct, so a stray short/legacy account can't reach the
decoder.
Confirmed live on 2026-09-15 two ways. First, directly: `getAccountInfo` on
`EJpNsfxnTB6mtVdzrTcgQ9xfywobHSSsUtu1Gh1GFvEg` (a 256-byte curve reported
elsewhere) returned 256 bytes, discriminator matching, owned by the pump
program, and it decoded cleanly through this repo's own IDL-driven decoder
(`PumpFunCurveManager._decode_curve_state_with_idl`) with sane reserves —
everything past the documented fields is zero padding. Second, over the
wire: a 120s `programSubscribe` window with this script's own filters
(discriminator + `complete = false`, no `dataSize`) took in 1,066 updates —
207 at 125 bytes, 853 at 151, and 6 at that same 256 length, among ordinary
traffic. None of the 1,066 were the legacy 49-byte layout (no `creator`
field). UNVERIFIED: whether that layout still has any `complete = false`
accounts left on chain, and whether anything still writes to them — not
re-measured here.
**Bandwidth trade-off, measured 2026-09-15.** A first attempt ran the
`dataSize`-filtered shape and the unfiltered shape back to back, 90s each,
and looked like unfiltered cost *less* (0.68x) — that was noise: pump.fun
trading volume swings a lot minute to minute, and two sequential windows just
land on different volume. Rerun with all three filter groups (`dataSize
125`, `dataSize 151`, and no `dataSize`) subscribed **simultaneously on one
connection**, so all three watch the identical trade stream over the same
120s: the two enumerated lengths together took in 1,060 updates / 589,279
bytes; the unfiltered subscription took in 1,066 updates / 593,509 bytes —
6 extra updates, 4,230 extra bytes, all of it the 256-byte curve neither
enumerated length can match. That is a **1.006x update ratio / 1.007x byte
ratio** — under 1% either way, not the double subscription's worth intuition
might suggest, because in this trade window virtually every update already
lands on one of the two dataSize-filtered lengths (125 or 151), and 256 is
rare. Dropping the filter is effectively free here; if a resize-happy period
ever shifts that mix, the cost scales with however much traffic sits outside
the two filtered lengths (125/151, i.e. the rarer 256-byte curves and
beyond), not with total volume.
UNVERIFIED: a curve was once observed going from 125 to 151 bytes, with
several 125-byte trades logged in between, suggesting `extend_account` ran as
its own later transaction rather than bundled into `create_v2` — not
re-measured here. `extend_account` **can** land in the same transaction as
`create_v2` — `learning-examples/mint_and_buy_v2.py` does exactly that,
appending `create_extend_account_instruction` right after
`create_pump_create_v2_instruction` in the same instruction list — so both
orderings occur; how common a further-resized (256-byte) curve is remains
unmeasured.
"""
import argparse
@@ -93,7 +145,13 @@ TOKEN_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
# See learning-examples/calculate_discriminator.py
BONDING_CURVE_DISCRIMINATOR: Final[bytes] = bytes.fromhex("17b7f83760d8ac60")
CURVE_ACCOUNT_LEN: Final[int] = 151
# create_v2 allocates the 125-byte struct; extend_account can grow it past
# that to any length the program allows (151 and 256 both confirmed live,
# 2026-09-15) — there is no fixed set of lengths to enumerate. This is a
# floor, not an allowlist: anything shorter than the smallest real struct is
# dropped client-side, everything at or above it is decoded and let through.
MIN_CURVE_LEN: Final[int] = 125
TOKEN_DECIMALS: Final[int] = 6
_RESERVES_OFFSET: Final[int] = 24 # real_token_reserves, u64 LE
@@ -150,6 +208,12 @@ def zero_prefix_gate(bound_raw: int) -> tuple[int, bytes] | None:
def build_filters(bound_raw: int) -> list[dict[str, Any]]:
"""Assemble the server-side `programSubscribe` filters.
No `dataSize` filter: `extend_account` can grow a curve past 125 bytes to
any length the program allows, so there is no fixed set of lengths to
match. The discriminator memcmp alone already restricts delivery to
`BondingCurve` accounts — see the module docstring for the bandwidth this
trades away and the client-side `MIN_CURVE_LEN` floor that replaces it.
Args:
bound_raw: Highest qualifying `real_token_reserves`, in raw units
@@ -167,7 +231,6 @@ def build_filters(bound_raw: int) -> list[dict[str, Any]]:
}
filters: list[dict[str, Any]] = [
{"dataSize": CURVE_ACCOUNT_LEN},
memcmp(0, BONDING_CURVE_DISCRIMINATOR),
memcmp(_COMPLETE_OFFSET, b"\x00"), # Not graduated yet
]
@@ -179,7 +242,13 @@ def build_filters(bound_raw: int) -> list[dict[str, Any]]:
def parse_curve(data: bytes) -> dict[str, Any]:
"""Decode the 151-byte bonding curve fields needed for a progress report.
"""Decode the bonding curve fields needed for a progress report.
Works at any length the account might arrive at — 125 bytes as created,
151 once extended, 256 confirmed live, or any other length the program
allows in the future — since every field read here sits in the first 115
bytes, well before where any resizing appends. Callers are expected to
apply `MIN_CURVE_LEN` first; this function does not re-check it.
Args:
data: Raw bonding curve account data
@@ -374,9 +443,15 @@ async def stream_once(
) -> None:
"""Subscribe and consume notifications until the connection drops.
One subscription is enough now that there is no `dataSize` filter to
fan out over — a curve at any length is delivered on this single
filter set. Only the subscription id handed back by our own ack is
treated as ours, so a notification from an unrelated subscription on this
connection (there should not be one) is ignored rather than mishandled.
Args:
reporter: Sink for decoded curve updates
filters: Server-side filters for the subscription
filters: The server-side filter list to install
Raises:
ConnectionRefusedError: If the endpoint rejects the subscription outright
@@ -399,10 +474,10 @@ async def stream_once(
}
)
)
ack = json.loads(await ws.recv())
if "error" in ack:
raise ConnectionRefusedError(str(ack["error"]))
subscription_id = ack["result"]
while True:
# ConnectionClosed deliberately propagates to the reconnect handler in
@@ -416,12 +491,16 @@ async def stream_once(
if message.get("method") != "programNotification":
continue
if message["params"]["subscription"] != subscription_id:
continue
value = message["params"]["result"]["value"]
await reporter.handle(
Pubkey.from_string(value["pubkey"]),
base64.b64decode(value["account"]["data"][0]),
)
data = base64.b64decode(value["account"]["data"][0])
if len(data) < MIN_CURVE_LEN:
# Shorter than the smallest real BondingCurve struct — not a
# shape the decoder should be trusted with. See MIN_CURVE_LEN.
continue
await reporter.handle(Pubkey.from_string(value["pubkey"]), data)
async def watch(min_progress: float) -> None:
@@ -438,7 +517,8 @@ async def watch(min_progress: float) -> None:
baseline = await fetch_initial_real_token_reserves(client)
print_banner(baseline, min_progress)
filters = build_filters(progress_to_bound(baseline, min_progress))
bound_raw = progress_to_bound(baseline, min_progress)
filters = build_filters(bound_raw)
reporter = GraduationReporter(client, baseline, min_progress)
while True:
@@ -14,9 +14,10 @@ and transaction signature behind every update, which the WebSocket feed does not
Why a subscription and not `getProgramAccounts`: the pump.fun program now owns over
10 million accounts, and every provider refuses to scan it — the rejection is on
program size, before filters apply. A curve can only approach graduation by being
traded, and every write pushes the full 151-byte account, so each update carries
everything needed to compute progress: no accumulated state, no cold start beyond the
next trade.
traded, and every write pushes the full account — 125 bytes as `create_v2` allocates
it, or 151 once `extend_account` has run on it — so each update carries everything
needed to compute progress: no accumulated state, no cold start beyond the next
trade.
Selecting a graduation threshold
--------------------------------
@@ -46,10 +47,44 @@ migration want the 3-byte one, a wider funnel the 2-byte one.
Checked against mainnet by running the filtered and unfiltered subscriptions side by
side for a minute: same curves, nothing dropped, nothing extra.
`datasize = 151` restricts this to the current curve layout. The original 49-byte
layout still has accounts with `complete = false`, but none of them are written to any
more — verified over a 45s window in which all 205 updates across 24 curves were
151-byte accounts.
This script does **not** filter on `datasize`. Earlier it named two
account-filter groups, one per enumerated length (125 and 151), because
Geyser ORs across named groups and a single group ANDs its own filters
together — there was no way to ask one group for "datasize 125 or 151".
That shape stopped being enough once a third live length (256 bytes) turned
up, and `extend_account` can grow a curve to any length the program allows,
so no finite set of named groups closes the gap for good. Dropping `datasize`
collapses back to the single group this script had before lengths were
enumerated at all: the discriminator memcmp alone already restricts delivery
to `BondingCurve` accounts, so nothing is lost by not naming a length. A
client-side `MIN_CURVE_LEN` floor discards anything shorter than the
smallest real struct, so a stray short/legacy account still can't reach the
decoder.
**Bandwidth trade-off, measured 2026-09-15 directly over Geyser** (not
borrowed from the WebSocket script's measurement — Geyser reports slot and
signature per update and its named-group model batches differently, so it
gets its own number). Two *separate* concurrent gRPC streams over the same
120s window, so both see the identical trade activity without the
sequential-window volume-swing problem (a first attempt comparing sequential
windows was unreliable for exactly that reason — see
`get_graduating_tokens.py`'s module docstring): one stream subscribed with
this script's old two named groups (`datasize` 125 and 151), the other with
a single unfiltered group. The filtered stream took in 6,754 updates /
2,340,863 proto bytes; the unfiltered stream took in 6,761 updates /
2,337,304 proto bytes — a 1.001x update ratio and a 0.998x byte ratio,
i.e. no measurable cost. The 7-update difference was the 256-byte curve
(`EJpNsfxnTB6mtVdzrTcgQ9xfywobHSSsUtu1Gh1GFvEg`) that no enumerated length
could ever match; the filtered stream structurally cannot see it at all.
`getAccountInfo` on that same curve, run directly the same day, confirms it:
still 256 bytes, discriminator intact, and it decodes cleanly through this
repo's own IDL-driven decoder — the bytes past the documented fields are
zero padding.
UNVERIFIED here: whether the original 49-byte layout (no `creator` field) is
still written anywhere. None of the 6,761 unfiltered updates in the
measurement above were that length, but that is one 120s window, not proof
of absence.
"""
import argparse
@@ -99,7 +134,13 @@ TOKEN_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
# See learning-examples/calculate_discriminator.py
BONDING_CURVE_DISCRIMINATOR: Final[bytes] = bytes.fromhex("17b7f83760d8ac60")
CURVE_ACCOUNT_LEN: Final[int] = 151
# create_v2 allocates the 125-byte struct; extend_account can grow it past
# that to any length the program allows (151 and 256 both confirmed live,
# 2026-09-15) — there is no fixed set of lengths to enumerate. This is a
# floor, not an allowlist: anything shorter than the smallest real struct is
# dropped client-side, everything at or above it is decoded and let through.
MIN_CURVE_LEN: Final[int] = 125
TOKEN_DECIMALS: Final[int] = 6
_RESERVES_OFFSET: Final[int] = 24 # real_token_reserves, u64 LE
@@ -157,6 +198,17 @@ def zero_prefix_gate(bound_raw: int) -> tuple[int, bytes] | None:
def build_subscribe_request(bound_raw: int) -> geyser_pb2.SubscribeRequest:
"""Build the Geyser account subscription for near-graduation curves.
One named group, with no `datasize` filter: `extend_account` can grow a
curve past 125 bytes to any length the program allows, so there is no
fixed set of lengths to enumerate across multiple named groups. (An
earlier version of this script did use two named groups, one per
enumerated length, because `SubscribeRequest.accounts` ANDs the filters
*inside* a group and there was no way to ask one group for "datasize 125
or 151" — see the module docstring for why that stopped being enough and
what it costs in bandwidth to drop entirely.) The discriminator memcmp
alone already restricts delivery to `BondingCurve` accounts, so a single
group loses no precision by not naming a length.
Args:
bound_raw: Highest qualifying `real_token_reserves`, in raw units
@@ -164,11 +216,11 @@ def build_subscribe_request(bound_raw: int) -> geyser_pb2.SubscribeRequest:
The subscription request
"""
request = geyser_pb2.SubscribeRequest()
gate = zero_prefix_gate(bound_raw)
accounts = request.accounts["graduating_curves"]
accounts.owner.append(str(PUMP_PROGRAM_ID))
accounts.filters.add().datasize = CURVE_ACCOUNT_LEN
discriminator = accounts.filters.add().memcmp
discriminator.offset = 0
discriminator.bytes = BONDING_CURVE_DISCRIMINATOR
@@ -177,7 +229,6 @@ def build_subscribe_request(bound_raw: int) -> geyser_pb2.SubscribeRequest:
not_complete.offset = _COMPLETE_OFFSET
not_complete.bytes = b"\x00" # Not graduated yet
gate = zero_prefix_gate(bound_raw)
if gate:
reserves = accounts.filters.add().memcmp
reserves.offset, reserves.bytes = gate
@@ -217,7 +268,13 @@ def create_geyser_connection() -> tuple[Any, grpc.aio.Channel]:
def parse_curve(data: bytes) -> dict[str, Any]:
"""Decode the 151-byte bonding curve fields needed for a progress report.
"""Decode the bonding curve fields needed for a progress report.
Works at any length the account might arrive at — 125 bytes as created,
151 once extended, 256 confirmed live, or any other length the program
allows in the future — since every field read here sits in the first 115
bytes, well before where any resizing appends. Callers are expected to
apply `MIN_CURVE_LEN` first; this function does not re-check it.
Args:
data: Raw bonding curve account data
@@ -422,6 +479,12 @@ async def stream_once(reporter: GraduationReporter, bound_raw: int) -> None:
continue
account = update.account.account
data = bytes(account.data)
if len(data) < MIN_CURVE_LEN:
# Shorter than the smallest real BondingCurve struct — not a
# shape the decoder should be trusted with. See MIN_CURVE_LEN.
continue
signature = (
str(Signature(bytes(account.txn_signature)))
if account.txn_signature
@@ -429,7 +492,7 @@ async def stream_once(reporter: GraduationReporter, bound_raw: int) -> None:
)
await reporter.handle(
Pubkey.from_bytes(bytes(account.pubkey)),
bytes(account.data),
data,
suffix=f" slot={update.account.slot} sig={signature}",
)
finally:
+7 -3
View File
@@ -37,7 +37,9 @@ QUOTE_SYMBOLS: Final[dict[Pubkey, str]] = {WSOL_MINT: "SOL", USDC_MINT: "USDC"}
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
# Data lengths, excluding the 8-byte discriminator: V2 stops after `creator`, V4
# runs through `quote_mint`. Live accounts are 151 bytes, so the tail is padding.
# runs through `quote_mint`. Live accounts are 125 bytes as created; extend_account
# can grow one to 151, 256, or any other length the program allows — the tail
# past V4 is unread here regardless of total length.
_V2_LENGTH: Final[int] = 73
_V4_LENGTH: Final[int] = 107
@@ -79,8 +81,10 @@ class BondingCurveState:
"is_mayhem_mode" / Flag,
)
# Current layout. The account is 151 bytes: this struct is 107 bytes after the
# 8-byte discriminator, and the remaining 36 are reserved padding.
# Current layout, covering the account as created (125 bytes) and, at the same
# offsets, as it reads once extend_account has grown it to 151, 256, or any
# other length. Trailing fields appended past this struct are not needed for
# a price read and are left unread.
_STRUCT_V4 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_quote_reserves" / Int64ul,
+34 -1
View File
@@ -35,6 +35,7 @@ from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.account import Account
from solders.compute_budget import set_compute_unit_price
from solders.instruction import Instruction
from solders.keypair import Keypair
@@ -112,6 +113,29 @@ async def get_pump_curve_state(
return pump_v2.BondingCurveState(response.value.data)
async def _get_account_info(conn: AsyncClient, address: Pubkey) -> Account:
"""Fetch an account, unwrapping AsyncClient's `.value` envelope.
Adapter for `pump_v2.resolve_quote_token_program`, which expects a
getter returning the account object (with an `.owner` attribute)
directly rather than solana-py's RPC response wrapper.
Args:
conn: Solana RPC client
address: Account to fetch
Returns:
The account object
Raises:
ValueError: If the account does not exist
"""
response = await conn.get_account_info(address)
if response.value is None:
raise ValueError(f"Could not fetch account info for {address}")
return response.value
def calculate_pump_curve_price(curve_state: pump_v2.BondingCurveState) -> float:
"""Price of one whole token in whole quote units.
@@ -175,6 +199,14 @@ async def buy_token(
token_amount = amount / token_price_sol
max_quote_cost = int(amount * quote_unit * (1 + slippage))
# The quote mint can be Token-2022-owned (verified 2026-09-15 upgrade
# note: error 6064 now accepts "SPL Token or Token-2022"), so its
# token program must be resolved rather than assumed. Free for WSOL
# and USDC (pre-seeded in pump_v2's cache); one RPC call otherwise.
quote_token_program_id = await pump_v2.resolve_quote_token_program(
quote_mint, lambda pk: _get_account_info(client, pk)
)
print(f"Quote asset: {quote_mint}")
print(f"Buying {token_amount:.6f} tokens, max cost {max_quote_cost} raw units")
@@ -188,6 +220,7 @@ async def buy_token(
quote_mint=quote_mint,
base_token_program=token_program,
is_mayhem_mode=curve_state.is_mayhem_mode,
quote_token_program_id=quote_token_program_id,
)
instructions = []
@@ -210,7 +243,7 @@ async def buy_token(
payer.pubkey(),
payer.pubkey(),
quote_mint,
token_program_id=pump_v2.quote_token_program(quote_mint),
token_program_id=quote_token_program_id,
)
)
instructions.append(buy_ix)
+34 -1
View File
@@ -9,6 +9,7 @@ from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.account import Account
from solders.compute_budget import set_compute_unit_price
from solders.keypair import Keypair
from solders.message import Message
@@ -119,6 +120,29 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey)
return 0
async def _get_mint_account_info(client: AsyncClient, address: Pubkey) -> Account:
"""Fetch an account, unwrapping AsyncClient's `.value` envelope.
Adapter for `pump_v2.resolve_quote_token_program`, which expects a
getter returning the account object (with an `.owner` attribute)
directly rather than solana-py's RPC response wrapper.
Args:
client: Solana RPC client
address: Account to fetch
Returns:
The account object
Raises:
ValueError: If the account does not exist
"""
response = await client.get_account_info(address)
if response.value is None:
raise ValueError(f"Could not fetch account info for {address}")
return response.value
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
"""Determines if a mint uses TokenProgram or Token2022Program."""
mint_info = await client.get_account_info(mint_address)
@@ -177,6 +201,14 @@ async def sell_token(
expected_output = float(token_balance_decimal) * float(token_price_sol)
min_quote_output = max(1, int(expected_output * (1 - slippage) * quote_unit))
# The quote mint can be Token-2022-owned (verified 2026-09-15 upgrade
# note: error 6064 now accepts "SPL Token or Token-2022"), so its
# token program must be resolved rather than assumed. Free for WSOL
# and USDC (pre-seeded in pump_v2's cache); one RPC call otherwise.
quote_token_program_id = await pump_v2.resolve_quote_token_program(
quote_mint, lambda pk: _get_mint_account_info(client, pk)
)
print(f"Selling {token_balance_decimal} tokens")
print(f"Quote asset: {quote_mint}")
print(
@@ -194,6 +226,7 @@ async def sell_token(
quote_mint=quote_mint,
base_token_program=token_program_id,
is_mayhem_mode=curve_state.is_mayhem_mode,
quote_token_program_id=quote_token_program_id,
)
instructions = [set_compute_unit_price(1_000)]
@@ -204,7 +237,7 @@ async def sell_token(
payer.pubkey(),
payer.pubkey(),
quote_mint,
token_program_id=pump_v2.quote_token_program(quote_mint),
token_program_id=quote_token_program_id,
)
)
instructions.append(sell_ix)
+43 -1
View File
@@ -30,6 +30,11 @@ MAX_SLIPPAGE = 0.3 # 30% slippage
PRIORITY_FEE_MICROLAMPORTS = 37_037 # Priority fee in microlamports
COMPUTE_UNIT_LIMIT = 350_000 # Compute unit limit for the transaction
ENABLE_MAYHEM_MODE = True # Set to True to enable mayhem mode
# Set to True to create a holder-reward coin: the creator fee is set aside for
# holders instead of paid to a creator wallet. Cashback was deprecated
# 2026-09-15 — create_v2 now rejects is_cashback_enabled=[true] with error
# 6082 (CashbackDeprecated) — so this is the only trailing-args path left.
ENABLE_HOLDER_REWARD = False
load_dotenv()
@@ -149,10 +154,20 @@ def create_pump_create_v2_instruction(
symbol: str,
uri: str,
is_mayhem_mode: bool = False,
is_holder_reward: bool = False,
) -> Instruction:
"""Create the pump.fun create_v2 instruction for Token2022.
Account order matches pump_fun_idl.json create_v2 instruction.
Args:
is_holder_reward: Sets the trailing is_holder_reward arg so the
creator fee is set aside for holders instead of a creator wallet.
Reaching it on the wire requires also sending is_cashback_enabled
and creator_fee_bps (see the data-building comment below) — the
three trailing args are positional, not independently addressable.
Cashback itself was deprecated 2026-09-15 (create_v2 error 6082),
so is_cashback_enabled is always sent False here.
"""
accounts = [
AccountMeta(pubkey=mint, is_signer=True, is_writable=True),
@@ -213,9 +228,34 @@ def create_pump_create_v2_instruction(
+ encode_string(symbol)
+ encode_string(uri)
+ encode_pubkey(creator)
+ struct.pack("<?", is_mayhem_mode) # OptionBool for is_mayhem_mode
+ struct.pack("<?", is_mayhem_mode) # is_mayhem_mode (plain bool)
)
if is_holder_reward:
# Trailing args are positional and independently omittable — the
# program does not require any of them, but sending is_holder_reward
# means sending is_cashback_enabled and creator_fee_bps first.
# idl/pump_fun_idl.json's `types` entries for both OptionBool and
# OptionU64 are single-field structs with no presence tag, so each
# serializes as its bare inner value: OptionBool as one byte, OptionU64
# as a little-endian u64 — never a bool-then-value pair. Confirmed by
# decoding live post-upgrade create_v2 instructions through this
# repo's IDLParser (2026-09-15): is_cashback_enabled decoded as
# {'field_0': False}, creator_fee_bps as {'field_0': 0}, matching this
# packing byte-for-byte.
# is_cashback_enabled (OptionBool, bare bool): always False here.
# Cashback is deprecated as of the 2026-09-15 upgrade; create_v2
# rejects [true] with 6082 CashbackDeprecated.
is_cashback_enabled = False
# creator_fee_bps (OptionU64, bare u64): unused for holder-reward
# coins created here, so 0.
creator_fee_bps = 0
data += (
struct.pack("<?", is_cashback_enabled)
+ struct.pack("<Q", creator_fee_bps)
+ struct.pack("<?", is_holder_reward)
)
return Instruction(PUMP_PROGRAM, data, accounts)
@@ -339,6 +379,7 @@ async def main():
print(f" Mint: {mint_keypair.pubkey()}")
print(f" Creator: {payer.pubkey()}")
print(f" Mayhem mode: {'Enabled' if ENABLE_MAYHEM_MODE else 'Disabled'}")
print(f" Holder reward: {'Enabled' if ENABLE_HOLDER_REWARD else 'Disabled'}")
# Derive PDAs
bonding_curve, _ = find_bonding_curve_address(mint_keypair.pubkey())
@@ -403,6 +444,7 @@ async def main():
symbol=TOKEN_SYMBOL,
uri=TOKEN_URI,
is_mayhem_mode=ENABLE_MAYHEM_MODE,
is_holder_reward=ENABLE_HOLDER_REWARD,
),
# Extend bonding curve account (required for frontend visibility)
create_extend_account_instruction(
+135 -13
View File
@@ -16,6 +16,8 @@ github.com/pump-fun/pump-public-docs
import secrets
import struct
from collections.abc import Awaitable, Callable
from typing import Any
from construct import Flag, Int64ul, Struct
from solders.instruction import AccountMeta, Instruction
@@ -42,6 +44,13 @@ WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
USDC_MINT = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
QUOTE_DECIMALS = {WSOL_MINT: 9, USDC_MINT: 6}
# Token program that owns each quote mint below. WSOL and USDC are legacy
# SPL Token, but 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 this two-entry map is not exhaustive -- see
# resolve_quote_token_program below for mints outside it.
QUOTE_TOKEN_PROGRAMS = {WSOL_MINT: TOKEN_PROGRAM, USDC_MINT: TOKEN_PROGRAM}
TOKEN_DECIMALS = 6
LAMPORTS_PER_SOL = 1_000_000_000
@@ -112,28 +121,124 @@ def is_sol_paired(quote_mint: Pubkey | None) -> bool:
return normalize_quote_mint(quote_mint) == WSOL_MINT
# Byte offset of the `decimals` field in a Mint account's raw data. SPL Token
# and Token-2022 mints share the same base layout -- COption<Pubkey>
# mint_authority (4-byte tag + 32-byte pubkey = 36 bytes), u64 supply
# (8 bytes), then u8 decimals at byte 44. Token-2022 extensions are appended
# *after* this base 82-byte struct, never rearranging it. Verified 2026-09-15
# by reading byte 44 off-chain for WSOL (9), USDC (6) and a live Token-2022
# quote mint (6, in a 690-byte account carrying extensions) -- all three
# matched their known decimals. Mirrors core/pubkeys.py's constant, kept as a
# separate copy because this module deliberately imports nothing from src/.
_MINT_DECIMALS_OFFSET = 44
def _parse_mint_decimals(data: bytes) -> int:
"""Read the `decimals` field out of a raw Mint account's byte layout.
Args:
data: Raw account data, e.g. `.data` on an `AsyncClient.get_account_info`
response's `.value`
Returns:
The mint's decimal count
Raises:
ValueError: If the data is too short to be a Mint account
"""
if len(data) <= _MINT_DECIMALS_OFFSET:
raise ValueError(
f"Account data is only {len(data)} bytes, too short to be a "
f"Mint account (decimals lives at byte {_MINT_DECIMALS_OFFSET})"
)
return data[_MINT_DECIMALS_OFFSET]
# Process-lifetime cache of quote mint -> decimals, pre-seeded so WSOL/USDC
# never cost an RPC call. Mirrors core/pubkeys.py's cache, kept as a separate
# copy because this module deliberately imports nothing from src/. Warmed by
# `resolve_quote_token_program` off the same mint-account fetch it already
# makes to resolve the token program, so this costs zero extra RPC calls.
_QUOTE_DECIMALS_CACHE = dict(QUOTE_DECIMALS)
def quote_units(quote_mint: Pubkey) -> int:
"""Raw units per whole unit of a quote mint.
"""Raw units per whole unit of a quote mint, from the warm cache.
Pre-seeded with WSOL (9) and USDC (6); any other quote mint is resolved
(and thus cached) by `resolve_quote_token_program` before it is traded.
Args:
quote_mint: Quote mint address
Returns:
1e9 for SOL, 1e6 for USDC
10 ** decimals for the quote mint (1e9 for SOL, 1e6 for USDC)
"""
return 10 ** QUOTE_DECIMALS.get(quote_mint, 9)
return 10 ** _QUOTE_DECIMALS_CACHE.get(quote_mint, 9)
def quote_token_program(quote_mint: Pubkey) -> Pubkey:
"""Token program owning a quote mint.
"""Token program owning a quote mint, defaulting to SPL Token.
Args:
quote_mint: Quote mint address
Returns:
Token program id (both current quote mints are legacy SPL Token)
Token program id for quote_mint if known, else SPL Token
"""
return TOKEN_PROGRAM
return QUOTE_TOKEN_PROGRAMS.get(quote_mint, TOKEN_PROGRAM)
# Process-lifetime cache of quote mint -> owning token program, pre-seeded so
# WSOL/USDC never cost an RPC call. Mirrors core/pubkeys.py's cache, kept as
# a separate copy because this module deliberately imports nothing from src/.
_QUOTE_TOKEN_PROGRAM_CACHE = dict(QUOTE_TOKEN_PROGRAMS)
async def resolve_quote_token_program(
quote_mint: Pubkey,
get_account_info: Callable[[Pubkey], Awaitable[Any]],
) -> Pubkey:
"""Resolve and cache the token program *and* decimals for a quote mint.
Returns instantly, with no RPC call, for a mint already known or
resolved by an earlier call in this process. Otherwise fetches the mint
account once -- a mint account's owner *is* the token program that
created it, and its raw data carries `decimals` at a fixed byte offset
shared by SPL Token and Token-2022 (see `_parse_mint_decimals`) -- and
caches both results. `build_v2_accounts` and its callers stay
synchronous; call this first and pass the result as
`quote_token_program_id` when trading a mint outside QUOTE_TOKEN_PROGRAMS.
Args:
quote_mint: Quote mint address to resolve
get_account_info: Async getter returning an account object with
`.owner` (Pubkey) and `.data` (bytes) attributes, e.g.
`AsyncClient.get_account_info` (unwrap `.value` from the RPC
response first)
Returns:
Token program id that owns the quote mint
Raises:
ValueError: If the mint's owner is neither SPL Token nor Token-2022,
or its account data is too short to carry a decimals field
"""
cached = _QUOTE_TOKEN_PROGRAM_CACHE.get(quote_mint)
if cached is not None:
return cached
account = await get_account_info(quote_mint)
owner = account.owner
if owner not in (TOKEN_PROGRAM, TOKEN_2022_PROGRAM):
raise ValueError(
f"Quote mint {quote_mint} is owned by {owner}, expected "
f"{TOKEN_PROGRAM} (SPL Token) or {TOKEN_2022_PROGRAM} (Token-2022)"
)
decimals = _parse_mint_decimals(bytes(account.data))
_QUOTE_TOKEN_PROGRAM_CACHE[quote_mint] = owner
_QUOTE_DECIMALS_CACHE[quote_mint] = decimals
return owner
def find_bonding_curve(mint: Pubkey) -> Pubkey:
@@ -256,9 +361,13 @@ def pick_buyback_fee_recipient() -> Pubkey:
class BondingCurveState:
"""Parsed pump.fun BondingCurve account.
The account is 151 bytes: the 115-byte documented struct followed by
reserved padding. The SOL-named reserve fields were renamed to quote fields
when non-SOL quote assets landed; the old names are kept as aliases.
The account is 125 bytes as created; an extend_account instruction (not
always in a separate transaction from create_v2 — see
learning-examples/mint_and_buy_v2.py) can grow it to 151, 256, or any
other length the program allows. The struct below covers the leading
fields, which are at the same offsets regardless of total length. The
SOL-named reserve fields were renamed to quote fields when non-SOL quote
assets landed; the old names are kept as aliases.
"""
_STRUCT = Struct(
@@ -338,9 +447,9 @@ class BondingCurveState:
"""
if not self.virtual_token_reserves or not self.virtual_quote_reserves:
return 0.0
return (
self.virtual_quote_reserves / 10 ** QUOTE_DECIMALS.get(self.quote_mint, 9)
) / (self.virtual_token_reserves / 10**TOKEN_DECIMALS)
return (self.virtual_quote_reserves / quote_units(self.quote_mint)) / (
self.virtual_token_reserves / 10**TOKEN_DECIMALS
)
def build_v2_accounts(
@@ -352,6 +461,7 @@ def build_v2_accounts(
base_token_program: Pubkey,
is_mayhem_mode: bool,
include_global_volume_accumulator: bool,
quote_token_program_id: Pubkey | None = None,
) -> list[AccountMeta]:
"""Build the ordered account list shared by buy_v2 and sell_v2.
@@ -363,11 +473,15 @@ def build_v2_accounts(
base_token_program: Token program owning base_mint
is_mayhem_mode: Selects which fee recipient set to draw from
include_global_volume_accumulator: True for buy_v2, False for sell_v2
quote_token_program_id: Token program owning quote_mint. Omit to use
`quote_token_program`'s SPL Token default; pass the result of
`resolve_quote_token_program` for a mint outside that default,
e.g. a Token-2022-paired coin.
Returns:
Ordered AccountMeta list (27 entries for buy_v2, 26 for sell_v2)
"""
quote_program = quote_token_program(quote_mint)
quote_program = quote_token_program_id or quote_token_program(quote_mint)
bonding_curve = find_bonding_curve(base_mint)
creator_vault = find_creator_vault(creator)
user_volume_accumulator = find_user_volume_accumulator(user)
@@ -426,6 +540,7 @@ def build_buy_v2_instruction(
quote_mint: Pubkey = WSOL_MINT,
base_token_program: Pubkey = TOKEN_2022_PROGRAM,
is_mayhem_mode: bool = False,
quote_token_program_id: Pubkey | None = None,
) -> Instruction:
"""Build a buy_v2 instruction.
@@ -438,6 +553,8 @@ def build_buy_v2_instruction(
quote_mint: Normalized quote mint
base_token_program: Token program owning base_mint
is_mayhem_mode: Whether the coin is in mayhem mode
quote_token_program_id: Token program owning quote_mint; see
`build_v2_accounts`
Returns:
The buy_v2 instruction
@@ -455,6 +572,7 @@ def build_buy_v2_instruction(
base_token_program=base_token_program,
is_mayhem_mode=is_mayhem_mode,
include_global_volume_accumulator=True,
quote_token_program_id=quote_token_program_id,
),
)
@@ -469,6 +587,7 @@ def build_sell_v2_instruction(
quote_mint: Pubkey = WSOL_MINT,
base_token_program: Pubkey = TOKEN_2022_PROGRAM,
is_mayhem_mode: bool = False,
quote_token_program_id: Pubkey | None = None,
) -> Instruction:
"""Build a sell_v2 instruction.
@@ -481,6 +600,8 @@ def build_sell_v2_instruction(
quote_mint: Normalized quote mint
base_token_program: Token program owning base_mint
is_mayhem_mode: Whether the coin is in mayhem mode
quote_token_program_id: Token program owning quote_mint; see
`build_v2_accounts`
Returns:
The sell_v2 instruction
@@ -498,5 +619,6 @@ def build_sell_v2_instruction(
base_token_program=base_token_program,
is_mayhem_mode=is_mayhem_mode,
include_global_volume_accumulator=False,
quote_token_program_id=quote_token_program_id,
),
)
@@ -0,0 +1,516 @@
{
"transaction": {
"signatures": [
"4Qg8NDcsfNerhfopXxPc5jLy8CazF7LiNcCsHiujTaHDeKW9Qtpoqatsm1wBhakHTBXvg86SijXvHZXQ7dKPcQay",
"2zRxY6ejjcgWyZXQ9Qw1bMbQXiSxHEvAsnC4dK4BpEn3DzssfXk8NJ85BfghnG9RjyXPjVLwQ7u4GqKb398WrNFM"
],
"message": {
"header": {
"numRequiredSignatures": 2,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 8
},
"accountKeys": [
"5htnuQWt3QynwBsbgPtaVQc8GKtgrdhxVK9cAYrfHQ44",
"PuuQ746gUr4mUkMzZhaYkgQomxQNpVgQfdEmZUVpump",
"FUQgZgguX5pbmkGb9q5F6CRgSbge9vQVmSB3dH6fTBw2",
"ByKBby4PpMC4CwRE5W5gxK9WBjgez4on7QCeESUvk3cS",
"MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e",
"BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s",
"78DmMwEqWzctoqVsZ4L7Y4GvEzn8H7mb5hEDCDeqbxHH",
"7FA7kDZzgJMLCUmLpJ6ZGaij89J7rF9ufTFHN3zUYYBH",
"C4aEqJScJH9YVM5j5hNVPc6eQ2Cwe4wcnDBkPyPjXQWs",
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
"TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM",
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
"11111111111111111111111111111111",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
"13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ",
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
],
"recentBlockhash": "FJMfH132bCp5AVX5bbKD4cASiieMe4jskLUKqZNMZCa5",
"instructions": [
{
"programIdIndex": 9,
"accounts": [
1,
10,
2,
3,
11,
0,
12,
13,
14,
4,
15,
5,
6,
7,
16,
9
],
"data": "5QAozyahaTZXuSNMGHGsRZqWXEPdJehECW5R4Fo9GvL815uXNSY6RotthmTWVoTQa4dpvE3xtUsmCVnSh1rCqSafHsBeKowxwFrXHou7yLAS25hJNYRq2k8muGeRTSNHTvwn85YFQv7cFyLrH6jied5nz912wB6rraBMLCxayLE2GFYJZnQa1aR12X92hgxNj7Hk2vjsYe3Ms6otXFk8kib",
"stackHeight": 1
},
{
"programIdIndex": 9,
"accounts": [
2,
0,
12,
16,
9
],
"data": "gCzGGoTR6NG",
"stackHeight": 1
},
{
"programIdIndex": 12,
"accounts": [
0,
8
],
"data": "3Bxs4Be78H4q9bW7",
"stackHeight": 1
}
],
"addressTableLookups": []
}
},
"meta": {
"err": null,
"status": {
"Ok": null
},
"fee": 10000,
"preBalances": [
245416040,
0,
0,
0,
11149834,
54342231298740,
0,
0,
7378644806,
8682612471,
1020235880,
4089837830,
1,
70128638,
3388612899,
3104160,
168261611
],
"postBalances": [
211983400,
2788920,
1417320,
1513840,
11149834,
54342231298740,
1188720,
1513840,
7403644806,
8682612471,
1020235880,
4089837830,
1,
70128638,
3388612899,
3104160,
168261611
],
"innerInstructions": [
{
"index": 0,
"instructions": [
{
"programIdIndex": 12,
"accounts": [
0,
1
],
"data": "111157R57fqNzgMjCi7yd8cD1yQhPAwhedzS6uysMaAGbDUZUYiji1TswMPKaASzZW6nQT",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1
],
"data": "GC3i2jSx8awwe9xBfbR9LBTrzgFPUE9b56kGnez3cWMeYXAAVCcWMbcbogkC4Bq6VUTuqKA1H7E3AcHF8xhjyCbRM8",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1
],
"data": "2zt6UCCHp66bJGRS4G7bTsjdxFh6FQ9sBEyRfGyPQKxYisAw",
"stackHeight": 2
},
{
"programIdIndex": 12,
"accounts": [
0,
2
],
"data": "11115RcDjJyNbKTcQYJAwRksdBxDFbdqq6wpi5qKCZjWvY6o2MzkfH4bS9bCroyjKqFHiw",
"stackHeight": 2
},
{
"programIdIndex": 14,
"accounts": [
0,
3,
2,
1,
12,
13
],
"data": "1",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1
],
"data": "84eT",
"stackHeight": 3
},
{
"programIdIndex": 12,
"accounts": [
0,
3
],
"data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq",
"stackHeight": 3
},
{
"programIdIndex": 13,
"accounts": [
3
],
"data": "P",
"stackHeight": 3
},
{
"programIdIndex": 13,
"accounts": [
3,
1
],
"data": "6bc7MVtDhow6S3EN7EzGaaAs4wVGTZfgCUMLu7rnUkLMz",
"stackHeight": 3
},
{
"programIdIndex": 12,
"accounts": [
0,
1
],
"data": "3Bxs4aPgqn2F4N4f",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1,
10,
1,
10
],
"data": "kLQeUPGqfy8Jzj4vZRCJQRoroNfBUdMEcwjecTFf7t4F8jzqKd93jebsg5NFMvw8yXEyKRrE8kTG9WwSTx19zfPpmoZwxDT9uWiHMNTLwYEDKXA2cqTmGo24UCxq4AxbwTZoCctRhB5Wp6VsRKL7YQf4S4ExfB7VEpejebgG",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1,
10
],
"data": "BjBG9fDQbu1raSNbBZQYubkmLijGDjgXJHpedBBGGrFrgSFFPaBAwnF",
"stackHeight": 2
},
{
"programIdIndex": 4,
"accounts": [
0,
15,
10,
5,
1,
7,
6,
12,
13,
14
],
"data": "TPGpxKtZ4cc",
"stackHeight": 2
},
{
"programIdIndex": 14,
"accounts": [
0,
7,
5,
1,
12,
13
],
"data": "1",
"stackHeight": 3
},
{
"programIdIndex": 13,
"accounts": [
1
],
"data": "84eT",
"stackHeight": 4
},
{
"programIdIndex": 12,
"accounts": [
0,
7
],
"data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq",
"stackHeight": 4
},
{
"programIdIndex": 13,
"accounts": [
7
],
"data": "P",
"stackHeight": 4
},
{
"programIdIndex": 13,
"accounts": [
7,
1
],
"data": "6Y5CywK9PapDLSofGZi8LSK3ktu8KPKXMhZ5N6USqvnXq",
"stackHeight": 4
},
{
"programIdIndex": 12,
"accounts": [
0,
6
],
"data": "111157ahBMy1h2Ko3fiGCyAzowWFaCUcNokZWgXLGpnL32Q7yHcf3ZYrAXhkiD3quiLXsG",
"stackHeight": 3
},
{
"programIdIndex": 13,
"accounts": [
1,
3,
10
],
"data": "6ApXSNCamGdm",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1,
7,
10
],
"data": "6ApXSNCamGdm",
"stackHeight": 2
},
{
"programIdIndex": 13,
"accounts": [
1,
10
],
"data": "31tb",
"stackHeight": 2
},
{
"programIdIndex": 9,
"accounts": [
16
],
"data": "FBrYRSy1kwJ1u29JtKfhgjxczZUv7HUHxF8MXLf6LX24egXHTk5T3XeSHwFD13xqf6ZUfnUfQANmoAnuu7vq2M4wRYvaNfDCaD2GtKGWXSPgWvLUL2WjS9GJDZQANH5yFntE7VxrZ4vrdLZ4H9wtwWyr3NakCuxcuAGVKHDewZsX5gARxtxhJ2kdqDzr5frN1pPHhQEKhuYCLVdeCAvo6NtqXJs4irYGQ9tM2hXN3B5u1SLv8RAuYyKAJigcGiCXXX5Lxcvria8YV8sqn67peHLrGwwbaJG3mSB66z7Kqjd9zdCRhhpeR45Fx5JjiEHdjQEicpN6SggDQDu6JtT8qhwzRhTM55xdtYkx7zP7MkphKykNA8B2A1hHWUvJpXRP6yrt2Qdys65ZeRVAxh5TET72AHvZrpiZFAmXZQGUfHncZMHXqbdLwSh3fs1QBugBXEY69oSy2v1dGBfQ3FyK1nHnczipBdGcSMHUwRCFJCa6gkvJDMriB6H5PbCtfh8nX59NuwYsqZ",
"stackHeight": 2
}
]
},
{
"index": 1,
"instructions": [
{
"programIdIndex": 12,
"accounts": [
0,
2
],
"data": "3Bxs4h15JFfuF4Zd",
"stackHeight": 2
},
{
"programIdIndex": 9,
"accounts": [
16
],
"data": "21448NTnPMYUBkX8r9jEFxU6ksoYq6DMu2BvqyaCfcTsV42tLXF71WFPG1uiTPjFKbEPVJVyYxVhrngEM4S7VwTRXgo47Ayw3PyVr4syFasNiNbyKnHKwBdtkZ6fQbKNi8zb1Wb1nWg6b3m",
"stackHeight": 2
}
]
}
],
"logMessages": [
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]",
"Program log: Instruction: CreateV2",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: MetadataPointerInstruction::Initialize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 925 of 390577 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: InitializeMint2",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1772 of 387989 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]",
"Program log: Create",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
"Program log: Instruction: GetAccountDataSize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1447 of 354662 compute units",
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Initialize the associated token account",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
"Program log: Instruction: InitializeImmutableOwner",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 348393 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
"Program log: Instruction: InitializeAccount3",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1969 of 345321 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 24447 of 367495 compute units",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: TokenMetadataInstruction: Initialize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3486 of 319872 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: TokenMetadataInstruction: UpdateAuthority",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3950 of 314136 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e invoke [2]",
"Program log: Instruction: InitializeMayhemState",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [3]",
"Program log: Create",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
"Program log: Instruction: GetAccountDataSize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1523 of 290745 compute units",
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [4]",
"Program 11111111111111111111111111111111 success",
"Program log: Initialize the associated token account",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
"Program log: Instruction: InitializeImmutableOwner",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 284400 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
"Program log: Instruction: InitializeAccount3",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2153 of 281327 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 17209 of 296079 compute units",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e consumed 45051 of 303722 compute units",
"Program MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: MintTo",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1714 of 256271 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: MintTo",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1714 of 252407 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: SetAuthority",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1044 of 248759 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program data: G3KpTd7rY3YTAAAARmVhciBPZiBNaXNzaW5nIE91dAQAAABGT01PUAAAAGh0dHBzOi8vaXBmcy5pby9pcGZzL2JhZmtyZWlkY2dsaTdmeXNkdWN6YmlhaDN3eTRtN25kbzd3dWdyN3J5czYyMmVmN3c1N3ZvNHl0b2J1Bd6YjA/etUnLmIqIVS9l5BFIbFEj8tdxOFLZ+BEow2/XCROHTojKZHppFYcblMcThMHCZdr2WIT4u4ujuAtcNUXoinRG1/5RVhcwhpCu30mv/Y/+ILIsRJqQ8yh5TBCtReiKdEbX/lFWFzCGkK7fSa/9j/4gsixEmpDzKHlMEK2kWqlqAAAAAAAQ2EfjzwMAAKwj/AYAAAAAeMX7UdECAACAxqR+jQMABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/wBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKwj/AYAAAAAAAAAAAAAAAA=",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2143 of 241189 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 165233 of 403000 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]",
"Program log: Instruction: ExtendAccount",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program data: YWHXkF2SFnzXCROHTojKZHppFYcblMcThMHCZdr2WIT4u4ujuAtcNUXoinRG1/5RVhcwhpCu30mv/Y/+ILIsRJqQ8yh5TBCtfQAAAAAAAACXAAAAAAAAAKRaqWoAAAAA",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2143 of 228500 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 11603 of 237767 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"preTokenBalances": [],
"postTokenBalances": [
{
"accountIndex": 3,
"mint": "PuuQ746gUr4mUkMzZhaYkgQomxQNpVgQfdEmZUVpump",
"uiTokenAmount": {
"uiAmount": 1000000000.0,
"decimals": 6,
"amount": "1000000000000000",
"uiAmountString": "1000000000"
},
"owner": "FUQgZgguX5pbmkGb9q5F6CRgSbge9vQVmSB3dH6fTBw2",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
},
{
"accountIndex": 7,
"mint": "PuuQ746gUr4mUkMzZhaYkgQomxQNpVgQfdEmZUVpump",
"uiTokenAmount": {
"uiAmount": 1000000000.0,
"decimals": 6,
"amount": "1000000000000000",
"uiAmountString": "1000000000"
},
"owner": "BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
}
],
"rewards": [],
"loadedAddresses": {
"writable": [],
"readonly": []
},
"computeUnitsConsumed": 176986,
"costUnits": 184290
},
"version": 0,
"slot": 447276538,
"transactionIndex": 104,
"blockTime": 1789483684
}
@@ -0,0 +1,805 @@
{
"transaction": {
"signatures": [
"4SUHkAKcAMfJ1VBsmNsScbjED39DiV7AKJRra4EP9kydjQGsgSbTowvjHhCqZf4fgMdgj6X74dgXrkA4JzV5jvx9",
"5KNbmSnrkgU85T3gSxE5mnhQS6iNF3fbRHY3D986D1avhqhcqjZmZXo9SMgzGbLeJanVR2GnDwJkFmHz6P51k3Gh"
],
"message": {
"header": {
"numRequiredSignatures": 2,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 4
},
"accountKeys": [
"CTfBTtxhtAyGEysdjp9owVQvjSwPjtrzEGB9ZgAKewsY",
"457V2vvjqXTMFzivq9tvBqhDaxfke2523hHDB6brpump",
"7ZKtHsaGAy2J51evHKT7sRyrx2wbpvq1v3yZsGCqbuwj",
"HHr7UNqvPmGQt7K52rsSUQqYwEr128YcVZ56jKUscHFS",
"HooDx47kiY59PfjupAbJLsvuurBFbRxyvNfUoRYMpPSs",
"J1gMd5zt3N9GXdV25yDEskrkMkjr2XrgUrNaKCmdssFQ",
"FWcYFDrC3coAbWrYd9d8biXRm4qGar6zQTFs2wc8jtRU",
"bZ1VXDu1LvKifh43bUU2goYdpaiFkGHuvyDobPUL22T",
"DtDMotQagrAPMzCj7cMZSkCTP62dcUKaFvMYw562mUQ3",
"3AGQhBYZ6gpjGQ84eg5cf6BasxwruxseC6h75tdM6gcR",
"3jT3tTnboBmjvmnJEb9p2zPNdZMzSVPu55cRGyY8w7on",
"86NQJNS9B5Gckr6vrANb6ep8QLZ4S8eAHBcZn37G3rvE",
"Ljs98bo2CbqkVxoqSdHjaUY9adBW6ZT1fGr5yBZFnkt",
"ComputeBudget111111111111111111111111111111",
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
"65MSTadJ4nF3cpojRzt8JmdTvm7X4QxRrt5Q9SChsvv8"
],
"recentBlockhash": "BJT9AtB2UCEwjGkKkiDsS5TDfvADt8nQ1gztkqgLg5s3",
"instructions": [
{
"programIdIndex": 13,
"accounts": [],
"data": "GppPrf",
"stackHeight": 1
},
{
"programIdIndex": 13,
"accounts": [],
"data": "3QAwFKa3MJAs",
"stackHeight": 1
},
{
"programIdIndex": 14,
"accounts": [
1,
23,
2,
3,
24,
0,
25,
26,
15,
17,
27,
18,
4,
5,
28,
14
],
"data": "zp5kwxfeVDopvuV1Sb9682PoxXkt6nWg4WBvZEVQVFq82mzpvHq5FcpcVnsoLU1KtpRApKnZ1EgATiP73QVM7EREt8MXFcRLvkscmBnkX86uMrZCUy6didXxQ3JjZsP7S784CTGkpCHpV4mSPcsnhft16NC691gfKroCvSeBBdRuR4233ood1aY5ewNSjkZxLo22yxEGc36UbcScBxMFm",
"stackHeight": 1
},
{
"programIdIndex": 15,
"accounts": [
0,
6,
0,
1,
25,
26
],
"data": "2",
"stackHeight": 1
},
{
"programIdIndex": 14,
"accounts": [
24,
1,
29,
26,
30,
15,
19,
20,
21,
22,
2,
3,
7,
0,
6,
8,
9,
10,
16,
31,
11,
12,
32,
33,
25,
28,
14
],
"data": "HnQ14hy6fxFzoqhh2G84QfJmcVuUWYtnT",
"stackHeight": 1
}
],
"addressTableLookups": [
{
"accountKey": "Hyif6eWb8x88RVrvjPfabsgRYnwkVnyByEXTVTXbUcyP",
"writableIndexes": [
16,
19,
40,
103,
29,
70
],
"readonlyIndexes": [
158,
1,
10,
11,
18,
3,
124,
12,
4,
14,
15
]
}
]
}
},
"meta": {
"err": null,
"status": {
"Ok": null
},
"fee": 63526,
"preBalances": [
1346772111,
0,
0,
0,
0,
0,
0,
0,
0,
6812783,
0,
1844400,
0,
1,
8682612471,
3388612899,
0,
11149834,
54342195450692,
15139539903,
682764690,
952747686,
381178645,
1020235880,
4089837830,
1,
70128638,
3104160,
168261611,
1828174417413,
200653906,
17016138,
37290293,
6046721
],
"postBalances": [
1276949906,
2743200,
60544498,
1513840,
1188720,
1513840,
1513840,
0,
0,
6990561,
0,
1844400,
0,
1,
8682612471,
3388612899,
0,
11149834,
54342195450692,
15140102866,
682764690,
952747686,
381178645,
1020235880,
4089837830,
1,
70128638,
3104160,
168261611,
1828174417413,
200653906,
17016138,
37290293,
6046721
],
"innerInstructions": [
{
"index": 2,
"instructions": [
{
"programIdIndex": 25,
"accounts": [
0,
1
],
"data": "111157R57fqNzgMjCi7yd8cD1yQhPAwhedzS6uysMaAGbDUZUYiji1TswMPKaASzZW6nQT",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1
],
"data": "GC3i2jSx8awwe9xBfbR9LBTrzgFPUE9b56kGnez3cWMeYXCqgnFLEEmeQyEi2kfggp6zr7wBzTVEjwvGnw7H6ZbRM8",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1
],
"data": "2zt6UCCHp66bJGRS4G7bTsjdxFh6FQ9sBEyRfGyPQKxYisAw",
"stackHeight": 2
},
{
"programIdIndex": 25,
"accounts": [
0,
2
],
"data": "11115RcDjJyNbKTcQYJAwRksdBxDFbdqq6wpi5qKCZjWvY6o2MzkfH4bS9bCroyjKqFHiw",
"stackHeight": 2
},
{
"programIdIndex": 15,
"accounts": [
0,
3,
2,
1,
25,
26
],
"data": "1",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1
],
"data": "84eT",
"stackHeight": 3
},
{
"programIdIndex": 25,
"accounts": [
0,
3
],
"data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq",
"stackHeight": 3
},
{
"programIdIndex": 26,
"accounts": [
3
],
"data": "P",
"stackHeight": 3
},
{
"programIdIndex": 26,
"accounts": [
3,
1
],
"data": "6Th2ZE574TpJ8LVkSNUeTMQRFSvXREg6icy9RMqteu4Nh",
"stackHeight": 3
},
{
"programIdIndex": 25,
"accounts": [
0,
1
],
"data": "3Bxs49242eL2SvqM",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1,
23,
1,
23
],
"data": "EJL2rdtAu8ZRnhS8UTzb6S5ju1s5iugFuuGX51NeDLSmY7EYGej6KcfJNqAypCbaw8PReTgyeJ7hkZ6WkhHr473s1dyMyYg1iZTgWSqHvEFh4tQumEU9tLr9oMzscDBNWQAhCcSG4NWjfy3iGR4mRzjtqjBE",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1,
23
],
"data": "BjBG9fDQbu1raSNbBZQYubkmLijGDjgXJHpedBBGGrFrgSFFPaBAwnF",
"stackHeight": 2
},
{
"programIdIndex": 17,
"accounts": [
0,
27,
23,
18,
1,
5,
4,
25,
26,
15
],
"data": "TPGpxKtZ4cc",
"stackHeight": 2
},
{
"programIdIndex": 15,
"accounts": [
0,
5,
18,
1,
25,
26
],
"data": "1",
"stackHeight": 3
},
{
"programIdIndex": 26,
"accounts": [
1
],
"data": "84eT",
"stackHeight": 4
},
{
"programIdIndex": 25,
"accounts": [
0,
5
],
"data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq",
"stackHeight": 4
},
{
"programIdIndex": 26,
"accounts": [
5
],
"data": "P",
"stackHeight": 4
},
{
"programIdIndex": 26,
"accounts": [
5,
1
],
"data": "6Y5CywK9PapDLSofGZi8LSK3ktu8KPKXMhZ5N6USqvnXq",
"stackHeight": 4
},
{
"programIdIndex": 25,
"accounts": [
0,
4
],
"data": "111157ahBMy1h2Ko3fiGCyAzowWFaCUcNokZWgXLGpnL32Q7yHcf3ZYrAXhkiD3quiLXsG",
"stackHeight": 3
},
{
"programIdIndex": 26,
"accounts": [
1,
3,
23
],
"data": "6ApXSNCamGdm",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1,
5,
23
],
"data": "6ApXSNCamGdm",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
1,
23
],
"data": "31tb",
"stackHeight": 2
},
{
"programIdIndex": 14,
"accounts": [
28
],
"data": "5MXGnktUgoyjUZyHBGwvivUtqhP3f5XwYh1Qgm9V5L9Szyg5izzVZN5k8aHKjJweyqjuowx4nKnUtCxv1Ag36WSknMzTXrJLYcSvZ86kv8ZPb9TDzcDSpXFZjeb582cSQXujWhV7zTvX5GjPBGRP7XkXkbTUGgkJ5RDk4evRz4weR1hpaZHozJrvV832cCGo3B9XwYsM3E3KgBjtbWA55Vvz5aJszgDHNtrk9bK8AhgMcXYiiHjSvS7zdfZgxz333X8T8L5pNj22m65PCuu2vTDGYfWGUWWsekgabnFyowQDv7juaoPyaURyRfcBuEkCBx9SaW5YDshTwKanpjhJxyyvG2dmtBauKeSkzvoFGgsi3YaoqFdzYCifDLw9B87NARNuAU56UFptNCh3iRaujdnGeFgKFfK2KWEdF3i65Now3nQHy4R5oKFkSfqFx8kjhdKuBn7dzgNMVJkaTRwVnKewZph9xvw9Mds3fepchmMWvgaVS2tHYxkj1FnJ8X",
"stackHeight": 2
}
]
},
{
"index": 3,
"instructions": [
{
"programIdIndex": 26,
"accounts": [
1
],
"data": "84eT",
"stackHeight": 2
},
{
"programIdIndex": 25,
"accounts": [
0,
6
],
"data": "111157Vt9WuCLrLm8Bv7v3tbuxTXo5Bq7vv6Fb1ngBYUyZPEU62jGZ4xnkMV46KCckXWDq",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
6
],
"data": "P",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
6,
1
],
"data": "6YbMrQ6VWB2F6WTy9pyM9RTrKELXD9e8gwBM14Fr8x6JW",
"stackHeight": 2
}
]
},
{
"index": 4,
"instructions": [
{
"programIdIndex": 33,
"accounts": [
32,
14
],
"data": "KX69VKtJ7u4E8WRrYUqgt3XUX8TcDnzuCpML1U78HbvSY3b6R3Fmk5hZdYT43YMC1VSWpVqHwFfau5",
"stackHeight": 2
},
{
"programIdIndex": 26,
"accounts": [
3,
1,
6,
2
],
"data": "gFy3oyCSMFkd3",
"stackHeight": 2
},
{
"programIdIndex": 25,
"accounts": [
0,
9
],
"data": "3Bxs4L3VyWkbyVJw",
"stackHeight": 2
},
{
"programIdIndex": 25,
"accounts": [
0,
2
],
"data": "3Bxs4MJPmHz6dCT1",
"stackHeight": 2
},
{
"programIdIndex": 25,
"accounts": [
0,
19
],
"data": "3Bxs448hzHNyyGJj",
"stackHeight": 2
},
{
"programIdIndex": 14,
"accounts": [
28
],
"data": "FBrYRSy1kwJFa1ngcrZxo8ec8SqdvGyhN429smPo3Asm6CkCASf4YG5uSwW4uj9d9BJuSsK4SXV8xkrVSZPBcTozrEtNohZCn4sN5Dp2SbotoMJZ7KczazKTFhSWxg2Epc5CHv88ku9n637VCNUp9gKzQyd9bGhyWWgaFafs2fzxDUmEHcXZzvnnyvN6qKh9gy8bEu9P7YUwmzXc52XmRHvSxbeCCf9zrq9f5wJKDmjxXAndAC4D3iVnZKcpsTtpiwk7ymuDyPQ8i9p2n3qE5kTRoUJdZoh9ToAetwpBdVUC8GLdwXGg1ddTCVgM93WJX1P29Ty8KcPW3JzFpKbRtic9bpVnk65MLd9WLthVQRkFUSD2Pg2MLrmHe8Y3PEGRGRo7abhHXbRjjkfZ19FYutdohu4rajkR1LuFen38jYkL6STNiMSn9EyWHqP8Eu43ic6d6iYG9m1wyQk2yvK2TCC3sqDpf5vBHCvsPWAxK1SDJmhjjTYniDi6VQXvVbRwnBKGUdz3TD",
"stackHeight": 2
}
]
}
],
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]",
"Program log: Instruction: CreateV2",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: MetadataPointerInstruction::Initialize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 925 of 254866 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: InitializeMint2",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1772 of 252278 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]",
"Program log: Create",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
"Program log: Instruction: GetAccountDataSize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1447 of 233951 compute units",
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Initialize the associated token account",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
"Program log: Instruction: InitializeImmutableOwner",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 227682 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
"Program log: Instruction: InitializeAccount3",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1969 of 224610 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 16947 of 239284 compute units",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: TokenMetadataInstruction: Initialize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3518 of 199161 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: TokenMetadataInstruction: UpdateAuthority",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3864 of 193393 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e invoke [2]",
"Program log: Instruction: InitializeMayhemState",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [3]",
"Program log: Create",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
"Program log: Instruction: GetAccountDataSize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1523 of 170088 compute units",
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [4]",
"Program 11111111111111111111111111111111 success",
"Program log: Initialize the associated token account",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
"Program log: Instruction: InitializeImmutableOwner",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 163743 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
"Program log: Instruction: InitializeAccount3",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2153 of 160670 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 17209 of 175422 compute units",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e consumed 45051 of 183065 compute units",
"Program MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: MintTo",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1714 of 135614 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: MintTo",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1714 of 131750 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: SetAuthority",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1044 of 128102 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program data: G3KpTd7rY3YLAAAAZG9nIHdpZiBjYXADAAAAY2FwUAAAAGh0dHBzOi8vaXBmcy5pby9pcGZzL2JhZmtyZWlncWt4c2lidHo1dGdlbHFlY2FnemhvNjZ3aG1xZHlzcnh4Y29yZ2ZuZml1Y3Zwcmt4dnppLaC2+V3Nh0HtdYKwc8/iu1Rv43LC2I3Xre76gUtUtl9hbg9dt5I6BnqHrQks9w64JoU7a+OAC5ddjukh4X9cpqpE0/SoD/gUsZ4GscW7mysoQU4ciKzgqTwazrOwWIpTqkTT9KgP+BSxngaxxbubKyhBThyIrOCpPBrOs7BYilOnWqlqAAAAAAAQ2EfjzwMAAKwj/AYAAAAAeMX7UdECAACAxqR+jQMABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/wBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKwj/AYAAAAAAAAAAAAAAAA=",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2143 of 120615 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 150134 of 267327 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]",
"Program log: CreateIdempotent",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: GetAccountDataSize",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1523 of 107433 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 101088 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: InitializeAccount3",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2153 of 98019 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 21610 of 117193 compute units",
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]",
"Program log: Instruction: BuyV2",
"Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ invoke [2]",
"Program log: Instruction: GetFeesWithQuoteMint",
"Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ consumed 3360 of 40593 compute units",
"Program return: pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ AAAAAAAAAABfAAAAAAAAAB4AAAAAAAAA",
"Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ success",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
"Program log: Instruction: TransferChecked",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2562 of 33794 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program data: vdt/007mYe4toLb5Xc2HQe11grBzz+K7VG/jcsLYjdet7vqBS1S2X3o5iAMAAAAADDdbg+wBAAABqkTT9KgP+BSxngaxxbubKyhBThyIrOCpPBrOs7BYilOnWqlqAAAAAHrlq/8GAAAA9Nh8xPbNAwB6OYgDAAAAAPRAanhlzwIAM5h6nrsTZ5mrWZJbE+XcizCB3wBdtCfBR4+kb/jDR6BfAAAAAAAAABOXCAAAAAAAqkTT9KgP+BSxngaxxbubKyhBThyIrOCpPBrOs7BYilMeAAAAAAAAAHK2AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAGJ1eQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHo5iAMAAAAAeuWr/wYAAAB6OYgDAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2143 of 17765 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 82839 of 95583 compute units",
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success"
],
"preTokenBalances": [
{
"accountIndex": 20,
"mint": "So11111111111111111111111111111111111111112",
"uiTokenAmount": {
"uiAmount": 0.68072541,
"decimals": 9,
"amount": "680725410",
"uiAmountString": "0.68072541"
},
"owner": "4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
},
{
"accountIndex": 22,
"mint": "So11111111111111111111111111111111111111112",
"uiTokenAmount": {
"uiAmount": 0.379139365,
"decimals": 9,
"amount": "379139365",
"uiAmountString": "0.379139365"
},
"owner": "5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
}
],
"postTokenBalances": [
{
"accountIndex": 3,
"mint": "457V2vvjqXTMFzivq9tvBqhDaxfke2523hHDB6brpump",
"uiTokenAmount": {
"uiAmount": 997884672.297204,
"decimals": 6,
"amount": "997884672297204",
"uiAmountString": "997884672.297204"
},
"owner": "7ZKtHsaGAy2J51evHKT7sRyrx2wbpvq1v3yZsGCqbuwj",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
},
{
"accountIndex": 5,
"mint": "457V2vvjqXTMFzivq9tvBqhDaxfke2523hHDB6brpump",
"uiTokenAmount": {
"uiAmount": 1000000000.0,
"decimals": 6,
"amount": "1000000000000000",
"uiAmountString": "1000000000"
},
"owner": "BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
},
{
"accountIndex": 6,
"mint": "457V2vvjqXTMFzivq9tvBqhDaxfke2523hHDB6brpump",
"uiTokenAmount": {
"uiAmount": 2115327.702796,
"decimals": 6,
"amount": "2115327702796",
"uiAmountString": "2115327.702796"
},
"owner": "CTfBTtxhtAyGEysdjp9owVQvjSwPjtrzEGB9ZgAKewsY",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
},
{
"accountIndex": 20,
"mint": "So11111111111111111111111111111111111111112",
"uiTokenAmount": {
"uiAmount": 0.68072541,
"decimals": 9,
"amount": "680725410",
"uiAmountString": "0.68072541"
},
"owner": "4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
},
{
"accountIndex": 22,
"mint": "So11111111111111111111111111111111111111112",
"uiTokenAmount": {
"uiAmount": 0.379139365,
"decimals": 9,
"amount": "379139365",
"uiAmountString": "0.379139365"
},
"owner": "5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
}
],
"rewards": [],
"loadedAddresses": {
"writable": [
"MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e",
"BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s",
"4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH",
"CoRuMWRuDAsjB5SDMmYEcDuyQyaH3L2vVFvwBy1FF8Kr",
"5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD",
"HjQjngTDqoHE6aaGhUqfz9aQ7WZcBRjy5xB8PScLSr8i"
],
"readonly": [
"TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM",
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
"11111111111111111111111111111111",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ",
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
"So11111111111111111111111111111111111111112",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y",
"8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt",
"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
]
},
"computeUnitsConsumed": 254883,
"costUnits": 265447
},
"version": 0,
"slot": 447276547,
"transactionIndex": 654,
"blockTime": 1789483687
}
+14 -1
View File
@@ -25,12 +25,16 @@ 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, set_compute_unit_price # noqa: E402
from solders.compute_budget import ( # noqa: E402
set_compute_unit_limit,
set_compute_unit_price,
)
from solders.message import Message # noqa: E402
from solders.transaction import Transaction # noqa: E402
from core.client import SolanaClient # noqa: E402
from core.priority_fee.manager import PriorityFeeManager # noqa: E402
from core.pubkeys import USDC_MINT # noqa: E402
from core.wallet import Wallet # noqa: E402
from interfaces.core import Platform, TokenInfo # noqa: E402
from monitoring.listener_factory import ListenerFactory # noqa: E402
@@ -43,6 +47,14 @@ EXTREME_FAST_TOKEN_AMOUNT = 20
# Matches retries.wait_after_creation in the bot configs. Only used when
# extreme_fast_mode is off, where the buyer reads the curve at `confirmed`.
CURVE_STABILIZE_SECONDS = 15
# Without a trade.quote_amounts entry, PlatformAwareBuyer only trades
# SOL-paired coins and skips anything else with "No configured buy amount"
# — including non-SOL-paired coins, which this script also needs to be able
# to trade. USDC is the one non-SOL quote mint with a fixed, well-known
# amount scale; a coin paired with any other quote mint (Token-2022
# included) still needs its own entry here, keyed by the exact mint, to be
# tradeable in this script.
QUOTE_AMOUNTS = {USDC_MINT: 0.01}
async def wait_for_token(timeout_seconds: float = 90.0) -> TokenInfo | None:
@@ -192,6 +204,7 @@ async def main() -> int:
max_retries=1,
extreme_fast_token_amount=EXTREME_FAST_TOKEN_AMOUNT,
extreme_fast_mode=extreme_fast,
quote_amounts=QUOTE_AMOUNTS,
)
if not extreme_fast:
+4 -2
View File
@@ -37,8 +37,8 @@ from core.pubkeys import ( # noqa: E402
TOKEN_DECIMALS,
SystemAddresses,
normalize_quote_mint,
quote_token_program,
quote_units_per_token,
resolve_quote_token_program,
)
from core.wallet import Wallet # noqa: E402
from interfaces.core import Platform, TokenInfo # noqa: E402
@@ -138,7 +138,9 @@ async def build_token_info(
is_mayhem_mode=state["is_mayhem_mode"],
is_cashback_coin=state["is_cashback_coin"],
quote_mint=quote_mint,
quote_token_program_id=quote_token_program(quote_mint),
quote_token_program_id=await resolve_quote_token_program(
quote_mint, client.get_account_info
),
)
return token_info, state
@@ -18,6 +18,11 @@ Offline machine checks, no network and no funds moved:
None tag, Some tag, and omitted-trailing forms.
5. The pump.fun event parser turns the raw fixture instruction into a
TokenInfo with is_cashback_coin=False and state_from_event=False.
6. A post-2026-09-15 create_v2 that omits the trailing creator_fee_bps
(OptionU64, program upgrade 8109141) decodes instead of dropping the
coin — issue #184 reintroduced by a new optional-typed arg.
7. A post-upgrade create_v2 that does send creator_fee_bps reads the
value back.
Usage:
uv run learning-examples/verify_create_v2_optional_args.py
@@ -29,6 +34,8 @@ import struct
import sys
from pathlib import Path
import base58
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "src"))
@@ -45,6 +52,22 @@ FIXTURE = (
/ "blocksubscribe-transactions"
/ "raw_create_tx_from_blocksubscribe.json"
)
# Post-upgrade (2026-09-15, program 8109141) getTransaction fixtures — real
# mainnet create_v2 instructions, one omitting the trailing creator_fee_bps
# (OptionU64) and one sending it. Captured with maxSupportedTransactionVersion:
# 0 and encoding: json, so account keys/instruction data are base58 strings
# rather than a raw base64 VersionedTransaction like the blocksubscribe
# fixture above.
FIXTURE_OMITTED_FEE_BPS = (
PROJECT_ROOT
/ "learning-examples"
/ "raw_create_v2_omitted_fee_bps_from_gettransaction.json"
)
FIXTURE_WITH_FEE_BPS = (
PROJECT_ROOT
/ "learning-examples"
/ "raw_create_v2_with_fee_bps_from_gettransaction.json"
)
IDL_PATH = PROJECT_ROOT / "idl" / "pump_fun_idl.json"
CREATE_V2_DISCRIMINATOR = bytes.fromhex("d6904cec5f8b31b4")
@@ -68,6 +91,37 @@ def _fixture_create_v2() -> tuple[bytes, list[int], list[bytes]]:
raise ValueError("fixture has no create_v2 instruction") # noqa: TRY003
def _fixture_create_v2_from_gettransaction(
path: Path,
) -> tuple[bytes, list[int], list[bytes]]:
"""Raw create_v2 data, account indices and keys from a getTransaction fixture.
Captured with `encoding: json`, so instruction data and account keys are
base58 strings straight off the RPC response — unlike the blocksubscribe
fixture above, there is no raw VersionedTransaction to decode.
"""
fixture = json.loads(path.read_text())
message = fixture["transaction"]["message"]
account_keys = [base58.b58decode(key) for key in message["accountKeys"]]
for ix in message["instructions"]:
data = base58.b58decode(ix["data"])
if data.startswith(CREATE_V2_DISCRIMINATOR):
return data, list(ix["accounts"]), account_keys
raise ValueError( # noqa: TRY003
f"{path.name} has no top-level create_v2 instruction"
)
def _fixture_create_v2_omitted_fee_bps() -> tuple[bytes, list[int], list[bytes]]:
"""create_v2 that omits the trailing creator_fee_bps (OptionU64)."""
return _fixture_create_v2_from_gettransaction(FIXTURE_OMITTED_FEE_BPS)
def _fixture_create_v2_with_fee_bps() -> tuple[bytes, list[int], list[bytes]]:
"""create_v2 that sends creator_fee_bps on the wire."""
return _fixture_create_v2_from_gettransaction(FIXTURE_WITH_FEE_BPS)
def check_omitted_trailing_option_decodes() -> bool:
"""The 145-byte wire form (no is_cashback_enabled byte) must decode."""
data, accounts, keys = _fixture_create_v2()
@@ -156,6 +210,57 @@ def check_event_parser_reads_raw_fixture() -> bool:
return ok
def check_omitted_option_u64_decodes() -> bool:
"""create_v2 omitting creator_fee_bps must decode, not drop the coin.
Post-upgrade create_v2 carries two trailing optionals: creator_fee_bps
(OptionU64) and is_holder_reward (OptionBool). Live transactions omit
either or both. Dropping those coins is issue #184 all over again.
"""
parser = _parser()
data, accounts, keys = _fixture_create_v2_omitted_fee_bps()
decoded = parser.decode_instruction(data, keys, accounts)
if decoded is None:
print(" FAIL create_v2 with omitted creator_fee_bps decoded as None")
return False
args = decoded["args"]
for name in ("creator_fee_bps", "is_holder_reward"):
value = args.get(name)
if value is not None:
print(f" FAIL {name} omitted but decoded as {value}, expected None")
return False
print(
" OK omitted optionals are None: creator_fee_bps=None, is_holder_reward=None"
)
return True
def check_option_u64_present_decodes() -> bool:
"""A create_v2 that does send creator_fee_bps reads the value back."""
parser = _parser()
data, accounts, keys = _fixture_create_v2_with_fee_bps()
decoded = parser.decode_instruction(data, keys, accounts)
if decoded is None:
print(" FAIL present creator_fee_bps did not decode")
return False
args = decoded["args"]
creator_fee_bps = args.get("creator_fee_bps")
is_holder_reward = args.get("is_holder_reward")
# OptionU64 is wrapped as {"field_0": <value>}
if not isinstance(creator_fee_bps, dict) or "field_0" not in creator_fee_bps:
print(f" FAIL creator_fee_bps has unexpected shape: {creator_fee_bps}")
return False
expected_value = creator_fee_bps["field_0"]
if is_holder_reward is not None:
print(f" FAIL is_holder_reward should be None, got {is_holder_reward}")
return False
print(
f" OK creator_fee_bps={expected_value} (wrapped as {creator_fee_bps}), "
f"is_holder_reward=None"
)
return True
def main() -> int:
checks = [
(
@@ -169,6 +274,11 @@ def main() -> int:
("mandatory args still enforced", check_mandatory_args_still_enforced),
("native Option<u64> decodes", check_native_option_decodes),
("event parser reads the raw fixture", check_event_parser_reads_raw_fixture),
(
"create_v2 omitting creator_fee_bps decodes",
check_omitted_option_u64_decodes,
),
("create_v2 with creator_fee_bps decodes", check_option_u64_present_decodes),
]
failed = 0
for label, check in checks:
@@ -0,0 +1,164 @@
"""Offline + mainnet: the bonding curve is 125 bytes as created, and grows.
The 2026-09-15 program upgrade appended creator_fee_bps (u64),
can_edit_creator_fee (bool) and is_holder_reward (bool) to BondingCurve and
dropped the 36 reserved padding bytes. create_v2 now allocates exactly 125
bytes; extend_account can grow an account past that to any length the
program allows — 151 and 256 are both confirmed live. A dataSize allowlist
is whack-a-mole against that: the next length silently drops curves again.
Anything that watches for curves must not filter on dataSize at all, and must
decode correctly regardless of which length turns up.
Moves no funds.
"""
from __future__ import annotations
import json
import struct
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
CURVE_LEN_CREATED = 125
CURVE_LEN_EXTENDED = 151
CURVE_LEN_RESIZED = 256 # confirmed live 2026-09-15, see the two graduating-
# token scripts' module docstrings for the getAccountInfo + decode evidence
_VIRTUAL_TOKEN_RESERVES = 1_073_000_000_000_000
def _synthetic_curve(length: int) -> bytes:
"""Build a valid BondingCurve account image at the given total length.
`length` must be at least `CURVE_LEN_CREATED` — the smallest shape the
program ever writes. Anything at or above that is a legal `extend_account`
target: this appends zero-byte padding to reach it, matching what live
125/151/256-byte curves actually look like past their documented fields.
Args:
length: Total account length in bytes, including the discriminator
Returns:
Account bytes matching the IDL's field order and discriminator
Raises:
ValueError: If length is shorter than CURVE_LEN_CREATED
"""
if length < CURVE_LEN_CREATED:
raise ValueError(f"unexpected curve length: {length}") # noqa: TRY003
idl = json.loads((PROJECT_ROOT / "idl" / "pump_fun_idl.json").read_text())
disc = next(
bytes(a["discriminator"])
for a in idl["accounts"]
if a["name"] == "BondingCurve"
)
reserves = struct.pack(
"<QQQQQ",
_VIRTUAL_TOKEN_RESERVES, # virtual_token_reserves
30_000_000_000, # virtual_quote_reserves
793_100_000_000_000, # real_token_reserves
0, # real_quote_reserves
1_000_000_000_000_000, # token_total_supply
)
body = (
reserves
+ b"\x00" # complete
+ bytes(32) # creator
+ b"\x00" # is_mayhem_mode
+ b"\x00" # is_cashback_coin
+ bytes(32) # quote_mint = Pubkey::default() (SOL-paired)
+ struct.pack("<Q", 0) # creator_fee_bps
+ b"\x00" # can_edit_creator_fee
+ b"\x00" # is_holder_reward
)
account = disc + body
return account + bytes(length - len(account))
def check_all_lengths_decode() -> bool:
"""A synthetic 125-, 151- and 256-byte curve all decode identically."""
from platforms.pumpfun.curve_manager import PumpFunCurveManager # noqa: PLC0415
from utils.idl_parser import IDLParser # noqa: PLC0415
parser = IDLParser("idl/pump_fun_idl.json")
manager = PumpFunCurveManager(client=None, idl_parser=parser)
ok = True
for length in (CURVE_LEN_CREATED, CURVE_LEN_EXTENDED, CURVE_LEN_RESIZED):
data = _synthetic_curve(length)
try:
state = manager._decode_curve_state_with_idl(data) # noqa: SLF001
except Exception as exc: # noqa: BLE001 - verifier reports, doesn't raise
print(f" FAIL {length}B curve failed to decode: {exc}")
ok = False
continue
if state["virtual_token_reserves"] != _VIRTUAL_TOKEN_RESERVES:
print(f" FAIL {length}B curve decoded wrong reserves")
ok = False
else:
print(f" OK {length}B curve decodes")
return ok
def check_no_datasize_filter() -> bool:
"""Neither graduating-token example may pre-filter by dataSize/datasize.
`extend_account` can grow a curve to any length the program allows, so
enumerating lengths is whack-a-mole — the fix is to not filter on length
at all, not to enumerate one more. Checked by looking for the actual
filter-construction syntax (a quoted `"dataSize":` dict key in the
WebSocket script, a `.datasize =` protobuf field assignment in the
Geyser one) rather than a bare substring match, since both scripts'
docstrings legitimately discuss `dataSize`/`datasize` in prose.
"""
ok = True
needles = {
"learning-examples/bonding-curve-progress/get_graduating_tokens.py": (
'"dataSize":'
),
"learning-examples/bonding-curve-progress/get_graduating_tokens_geyser.py": (
".datasize ="
),
}
for rel, needle in needles.items():
text = Path(rel).read_text()
if needle in text:
print(f" FAIL {rel} still constructs a dataSize filter")
ok = False
else:
print(f" OK {rel} has no dataSize filter")
return ok
def main() -> int:
"""Run every check and report pass/fail.
Returns:
0 if every check passed, 1 otherwise
"""
checks = [
(
"125/151/256-byte curves all decode via the IDL parser",
check_all_lengths_decode,
),
("graduating-token scripts don't filter on dataSize", check_no_datasize_filter),
]
failed = 0
for label, check in checks:
print(f"{label}:")
try:
ok = check()
except Exception as error: # noqa: BLE001 - report and continue
print(f" FAIL {type(error).__name__}: {error}")
failed += 1
continue
failed += 0 if ok else 1
print(f"\n{len(checks) - failed}/{len(checks)} checks passed")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
@@ -45,8 +45,19 @@ WRONG_BC = Pubkey.from_string("Vote111111111111111111111111111111111111111")
PROVIDER = PumpFunAddressProvider()
def _fabricated_curve_bytes(creator: Pubkey, *, is_mayhem: bool) -> bytes:
"""Build a 151-byte BondingCurve account image matching the IDL layout."""
def _fabricated_curve_bytes(
creator: Pubkey, *, is_mayhem: bool, curve_len: int = 125
) -> bytes:
"""Build a BondingCurve account image matching the IDL layout.
`create_v2` allocates exactly 125 bytes; pass curve_len=151 to get the
shape an account has once extend_account has run on it (padded with
reserved zero bytes past the struct). `extend_account` can in fact grow a
curve to 151, 256, or any other length the program allows — this helper
only exercises 125 and 151 because those are the two shapes the checks
below need — but every length decodes identically, since everything read
here sits at the same offsets regardless of total size.
"""
idl = json.loads((PROJECT_ROOT / "idl" / "pump_fun_idl.json").read_text())
disc = next(
bytes(a["discriminator"])
@@ -61,7 +72,7 @@ def _fabricated_curve_bytes(creator: Pubkey, *, is_mayhem: bool) -> bytes:
0, # real_quote_reserves
1_000_000_000_000, # token_total_supply
)
return (
account = (
disc
+ reserves
+ b"\x00" # complete
@@ -69,8 +80,11 @@ def _fabricated_curve_bytes(creator: Pubkey, *, is_mayhem: bool) -> bytes:
+ (b"\x01" if is_mayhem else b"\x00") # is_mayhem_mode
+ b"\x00" # is_cashback_coin
+ bytes(32) # quote_mint = Pubkey::default() (SOL-paired)
+ bytes(36) # reserved padding
+ struct.pack("<Q", 0) # creator_fee_bps
+ b"\x00" # can_edit_creator_fee
+ b"\x00" # is_holder_reward
)
return account + bytes(curve_len - len(account))
def _pumpportal_token_info(**overrides: object) -> TokenInfo:
@@ -216,10 +230,14 @@ def check_b_still_buys_when_curve_readable() -> bool:
return ok
def check_c_curve_manager_batch_read() -> bool:
"""C: curve manager reads curve + mint owner in one batch call."""
def _check_curve_manager_batch_read(curve_len: int) -> bool:
"""C: curve manager reads curve + mint owner in one batch call.
Args:
curve_len: Bonding curve account length to fabricate (125 or 151)
"""
creator = TRADER
curve_bytes = _fabricated_curve_bytes(creator, is_mayhem=True)
curve_bytes = _fabricated_curve_bytes(creator, is_mayhem=True, curve_len=curve_len)
class BatchClient:
def __init__(self) -> None:
@@ -258,11 +276,22 @@ def check_c_curve_manager_batch_read() -> bool:
)
if not ok:
print(
f" batch_calls={client.batch_calls} token_program={token_program} state={state}"
f" curve_len={curve_len} batch_calls={client.batch_calls} "
f"token_program={token_program} state={state}"
)
return ok
def check_c_curve_manager_batch_read() -> bool:
"""C: curve manager decodes a curve at its as-created length (125 bytes)."""
return _check_curve_manager_batch_read(125)
def check_c_curve_manager_batch_read_extended_curve() -> bool:
"""C: curve manager decodes a curve extend_account has grown to 151 bytes."""
return _check_curve_manager_batch_read(151)
def check_c_buyer_corrects_token_program() -> bool:
"""C: buyer applies the batch-read token program and re-derives the ATA."""
@@ -322,6 +351,10 @@ def main() -> int:
"C: curve manager batch-reads curve + mint owner",
check_c_curve_manager_batch_read,
),
(
"C: curve manager batch-reads a 151-byte extended curve",
check_c_curve_manager_batch_read_extended_curve,
),
(
"C: buyer corrects token_program_id and ATA",
check_c_buyer_corrects_token_program,
+81 -9
View File
@@ -11,6 +11,7 @@ import aiohttp
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Processed
from solana.rpc.types import TxOpts
from solders.account import Account
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.hash import Hash
from solders.instruction import Instruction
@@ -22,12 +23,19 @@ from solders.transaction import Transaction
from core.pubkeys import is_sol_paired
from core.rpc_rate_limiter import TokenBucketRateLimiter
from interfaces.core import Platform
from utils.idl_manager import get_idl_parser
from utils.logger import get_logger
logger = get_logger(__name__)
HTTP_TOO_MANY_REQUESTS = 429
# Length of the `[instruction_index, error_detail]` pair inside
# `{"InstructionError": [...]}` -- fixed by the RPC's `meta.err` shape, not a
# tunable.
INSTRUCTION_ERROR_PAIR_LEN = 2
def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
"""
@@ -58,6 +66,60 @@ def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
return Instruction(COMPUTE_BUDGET_PROGRAM, data, [])
def _describe_program_error(err: object) -> str | None:
"""Best-effort human name for an Anchor `Custom(N)` error in `meta.err`.
Digs `{"InstructionError": [idx, {"Custom": n}]}` out of `meta.err` and
looks the code up in pump.fun's IDL error table. `SolanaClient` is shared
across pump.fun and letsbonk.fun and carries no record of which program a
given transaction actually invoked, so this only ever checks pump.fun's
table (`idl/pump_fun_idl.json`) -- the program the bot's own `buy_v2` /
`sell_v2` call directly, and the source of issue #175's
`BuybackFeeRecipientMissing`. A revert on a different program (letsbonk's
Raydium LaunchLab program, or one raised inside a pump-amm or pump-fees
CPI) is described against the wrong table if its numeric code happens to
also be defined there, and left unnamed otherwise -- picking the right
table per invoked program id is not attempted here.
Any shape this does not recognize (a non-Anchor failure such as compute
budget exhaustion, `MaxLoadedAccountsDataSizeExceeded`, or a top-level
string error) is reported as `None`, never raised.
Args:
err: The raw `meta.err` value from a `getTransaction` response.
Returns:
A description like "pump.fun IDL: 6062 BuybackFeeRecipientMissing"
for a code pump.fun's IDL defines (the IDL error entry's own `msg`,
if it has one, follows after a colon — 6062 has none, so there's no
suffix here), or None if the shape doesn't match or the code is not
in that table. The "pump.fun IDL:" prefix is
deliberate: it is the only table checked, so it must stay visible in
the rendered string, not just in this docstring -- a reader looking
at a log line, not this source file, still needs to know the name is
pump.fun's interpretation and not a fact about whichever program
actually reverted.
"""
if not isinstance(err, dict):
return None
instruction_error = err.get("InstructionError")
if (
not isinstance(instruction_error, list)
or len(instruction_error) != INSTRUCTION_ERROR_PAIR_LEN
):
return None
detail = instruction_error[1]
if not isinstance(detail, dict):
return None
code = detail.get("Custom")
if not isinstance(code, int):
return None
description = get_idl_parser(Platform.PUMP_FUN).describe_error_code(code)
if description is None:
return None
return f"pump.fun IDL: {description}"
class SolanaClient:
"""Abstraction for Solana RPC client operations."""
@@ -151,7 +213,7 @@ class SolanaClient:
async def get_account_info(
self, pubkey: Pubkey, commitment: str | None = None
) -> dict[str, Any]:
) -> Account:
"""Get account info from the blockchain.
Args:
@@ -160,10 +222,13 @@ class SolanaClient:
fresh state right after a geyser event; default "confirmed")
Returns:
Account info response
The solders `Account` (verified 2026-09-15: `response.value` from
`AsyncClient.get_account_info` is a `solders.account.Account`, not
a dict -- callers read attributes like `.data` and `.owner`, never
subscript it).
Raises:
ValueError: If account doesn't exist or has no data
ValueError: If account doesn't exist
"""
await self._rate_limiter.acquire()
client = await self.get_client()
@@ -177,7 +242,7 @@ class SolanaClient:
async def get_multiple_accounts(
self, pubkeys: list[Pubkey], commitment: str | None = None
) -> list[Any]:
) -> list[Account | None]:
"""Get several accounts in one slot-consistent RPC round trip.
A single getMultipleAccounts response is served by one node at one
@@ -189,7 +254,10 @@ class SolanaClient:
commitment: Optional commitment override (default "confirmed")
Returns:
One entry per pubkey, in order; None for accounts that don't exist
One entry per pubkey, in order -- each a solders `Account` (same
type as `get_account_info` returns; attributes like `.data` and
`.owner`, never subscriptable) or None for accounts that don't
exist.
"""
await self._rate_limiter.acquire()
client = await self.get_client()
@@ -385,8 +453,10 @@ class SolanaClient:
tx_err = result.get("meta", {}).get("err")
if tx_err:
detail = _describe_program_error(tx_err)
logger.error(
f"Transaction {signature[:16]}... confirmed but failed: {tx_err}"
+ (f" ({detail})" if detail else "")
)
return False
@@ -462,7 +532,11 @@ class SolanaClient:
# Check for transaction execution errors (e.g., MaxLoadedAccountsDataSizeExceeded)
tx_err = meta.get("err")
if tx_err:
logger.error(f"Transaction {signature[:16]}... failed with error: {tx_err}")
detail = _describe_program_error(tx_err)
logger.error(
f"Transaction {signature[:16]}... failed with error: {tx_err}"
+ (f" ({detail})" if detail else "")
)
return None, None
# Get tokens received from pre/post token balance diff
@@ -550,9 +624,7 @@ class SolanaClient:
return best
async def _get_transaction_result(
self, signature: str | Signature
) -> dict | None:
async def _get_transaction_result(self, signature: str | Signature) -> dict | None:
"""Fetch transaction result from RPC.
Args:
+138 -6
View File
@@ -4,7 +4,8 @@ This module contains only system-level addresses that are shared across all plat
Platform-specific addresses are handled by their respective AddressProvider implementations.
"""
from typing import Final
from collections.abc import Awaitable, Callable
from typing import Any, Final
from solders.pubkey import Pubkey
@@ -54,17 +55,67 @@ QUOTE_DECIMALS: Final[dict[Pubkey, int]] = {
USDC_MINT: 6,
}
# Token program that owns each quote mint. Both current quote mints are
# legacy SPL Token, but the v2 instructions take quote_token_program
# separately from base_token_program, so keep them decoupled.
# Token program that owns each quote mint. WSOL and USDC are legacy SPL
# Token, but the v2 instructions take quote_token_program separately from
# base_token_program, so keep them decoupled. 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 this two-entry map is not
# exhaustive of every tradeable quote mint -- see
# resolve_quote_token_program below for mints outside it.
QUOTE_TOKEN_PROGRAMS: Final[dict[Pubkey, Pubkey]] = {
WSOL_MINT: TOKEN_PROGRAM,
USDC_MINT: TOKEN_PROGRAM,
}
# Byte offset of the `decimals` field in a Mint account's raw data. SPL Token
# and Token-2022 mints share the same base layout -- COption<Pubkey>
# mint_authority (4-byte tag + 32-byte pubkey = 36 bytes), u64 supply
# (8 bytes), then u8 decimals at byte 44. Token-2022 extensions are appended
# *after* this base 82-byte struct, never rearranging it. Verified 2026-09-15
# by reading byte 44 off-chain for WSOL (9), USDC (6) and a live Token-2022
# quote mint (6, in a 690-byte account carrying extensions) -- all three
# matched their known decimals.
_MINT_DECIMALS_OFFSET: Final[int] = 44
def _parse_mint_decimals(data: bytes) -> int:
"""Read the `decimals` field out of a raw Mint account's byte layout.
Args:
data: Raw account data, e.g. `.data` on the solders `Account` from
`SolanaClient.get_account_info`
Returns:
The mint's decimal count
Raises:
ValueError: If the data is too short to be a Mint account
"""
if len(data) <= _MINT_DECIMALS_OFFSET:
raise ValueError(
f"Account data is only {len(data)} bytes, too short to be a "
f"Mint account (decimals lives at byte {_MINT_DECIMALS_OFFSET})"
)
return data[_MINT_DECIMALS_OFFSET]
# Process-lifetime cache of quote mint -> decimals. Pre-seeded from
# QUOTE_DECIMALS so WSOL/USDC never cost an RPC call. Every other entry is
# added by resolve_quote_token_program, which reads decimals off the same
# mint-account fetch it already makes to resolve the token program -- so
# warming this cache costs zero RPC calls beyond that one read per mint.
_QUOTE_DECIMALS_CACHE: dict[Pubkey, int] = dict(QUOTE_DECIMALS)
def quote_decimals(quote_mint: Pubkey) -> int:
"""Get the decimal count for a quote mint, defaulting to SOL's 9.
"""Get a quote mint's decimal count from the warm cache -- zero RPC.
Mirrors `cached_quote_token_program`'s shape: pre-seeded with WSOL (9)
and USDC (6), and every other configured quote mint is resolved (and
thus cached) once at startup by `resolve_quote_token_program`, so an
uncached mint here is one the bot will refuse to start trading rather
than one this silently mis-scales -- see that function's docstring.
Args:
quote_mint: Quote mint address
@@ -72,7 +123,7 @@ def quote_decimals(quote_mint: Pubkey) -> int:
Returns:
Number of decimals used by the quote mint
"""
return QUOTE_DECIMALS.get(quote_mint, 9)
return _QUOTE_DECIMALS_CACHE.get(quote_mint, 9)
def quote_units_per_token(quote_mint: Pubkey) -> int:
@@ -99,6 +150,87 @@ def quote_token_program(quote_mint: Pubkey) -> Pubkey:
return QUOTE_TOKEN_PROGRAMS.get(quote_mint, TOKEN_PROGRAM)
# Process-lifetime cache of quote mint -> owning token program. Pre-seeded
# from QUOTE_TOKEN_PROGRAMS so WSOL/USDC never cost an RPC call. Every other
# entry is added by resolve_quote_token_program, normally once per mint at
# bot startup, so the hot path (cached_quote_token_program) never has to make
# a chain read to learn a configured quote mint's token program.
_QUOTE_TOKEN_PROGRAM_CACHE: dict[Pubkey, Pubkey] = dict(QUOTE_TOKEN_PROGRAMS)
async def resolve_quote_token_program(
quote_mint: Pubkey,
get_account_info: Callable[[Pubkey], Awaitable[Any]],
) -> Pubkey:
"""Resolve and cache the token program *and* decimals for a quote mint.
Returns instantly, with no RPC call, for a mint already in
QUOTE_TOKEN_PROGRAMS or resolved by an earlier call in this process.
Otherwise fetches the mint account once -- a mint account's owner *is*
the token program that created it, and its raw data carries `decimals`
at a fixed byte offset shared by SPL Token and Token-2022 (see
`_parse_mint_decimals`) -- and caches both results for the rest of the
process. One fetch resolves both facts, so this never costs more than
one RPC call per quote mint even though it warms two caches.
This does one RPC call in the worst case, so callers on a zero-RPC hot
path (e.g. `extreme_fast_mode`) must not call it directly; read
`cached_quote_token_program` / `quote_decimals` instead once this has
warmed the caches.
Args:
quote_mint: Quote mint address to resolve
get_account_info: Async getter returning an account object with
`.owner` (Pubkey) and `.data` (bytes) attributes, e.g.
`SolanaClient.get_account_info`
Returns:
Token program id that owns the quote mint
Raises:
ValueError: If the mint's owner is neither SPL Token nor Token-2022,
or its account data is too short to carry a decimals field.
Left for the caller to leave uncaught at startup -- trading a
quote mint the bot cannot correctly size or derive accounts for
is worse than refusing to start.
"""
cached = _QUOTE_TOKEN_PROGRAM_CACHE.get(quote_mint)
if cached is not None:
return cached
account = await get_account_info(quote_mint)
owner = account.owner
if owner not in (TOKEN_PROGRAM, TOKEN_2022_PROGRAM):
raise ValueError(
f"Quote mint {quote_mint} is owned by {owner}, expected "
f"{TOKEN_PROGRAM} (SPL Token) or {TOKEN_2022_PROGRAM} (Token-2022)"
)
decimals = _parse_mint_decimals(bytes(account.data))
_QUOTE_TOKEN_PROGRAM_CACHE[quote_mint] = owner
_QUOTE_DECIMALS_CACHE[quote_mint] = decimals
return owner
def cached_quote_token_program(quote_mint: Pubkey) -> Pubkey:
"""Read a quote mint's token program from the warm cache -- zero RPC.
Returns whatever `resolve_quote_token_program` cached for this mint.
A mint that was never resolved falls back to `quote_token_program`'s
SPL Token default. That is safe for the bot's own hot path:
`_resolve_quote_config` only ever trades a quote mint the operator
configured an amount for, and every configured mint is resolved (and
thus cached) once at startup, so an uncached mint here is one the bot
will skip before it ever reaches a buy.
Args:
quote_mint: Quote mint address
Returns:
Cached token program id, or the SPL Token default if unresolved
"""
return _QUOTE_TOKEN_PROGRAM_CACHE.get(quote_mint, quote_token_program(quote_mint))
def normalize_quote_mint(quote_mint: Pubkey | None) -> Pubkey:
"""Resolve a bonding curve's quote_mint into a concrete mint address.
+22 -1
View File
@@ -47,7 +47,28 @@ class TokenInfo:
creator_vault: Pubkey | None = None
token_program_id: Pubkey | None = None # Token or Token2022 program
is_mayhem_mode: bool = False # pump.fun mayhem mode flag
is_cashback_coin: bool = False # pump.fun cashback coin flag
# pump.fun cashback coin flag. Cashback was deprecated 2026-09-15 —
# create_v2 rejects new cashback coins with 6082 CashbackDeprecated — but
# existing cashback coins keep trading and accruing cashback exactly as
# before, so every cashback code path that reads this flag stays live.
is_cashback_coin: bool = False
# Holder rewards coins (2026-09-15 upgrade): the creator fee is set aside
# for holders instead of a creator wallet, and BondingCurve.creator holds a
# pump.fun address. Trading is identical — creator_vault derivation and the
# buy_v2/sell_v2 account lists are unchanged — so this is informational and
# available for filtering. creator_fee_bps is non-zero only on custom-pair
# coins; SOL- and USDC-paired coins use the standard fee schedule (bps 0).
# Verified live on 2026-09-15: 21 SOL-paired CreateEvents (bps 0 on all;
# the 90-second capture window saw 25 coins total, 4 of them custom-pair)
# and 10 USDC-paired bonding curves — located via Dune's indexed
# pumpdotfun_solana.pump_evt_createevent table, then read directly by
# deriving each curve PDA and calling PumpFunCurveManager.get_pool_state —
# also bps 0 on all. Every custom-pair coin observed in the same window
# showed a nonzero value (30, 125, 1, 300 bps).
is_holder_reward: bool = False
creator_fee_bps: int = 0
# Quote asset (pump.fun v2 instructions). SOL-paired coins carry
# Pubkey::default() on-chain; normalize_quote_mint() maps that to wrapped
+10 -3
View File
@@ -12,7 +12,11 @@ from typing import ClassVar, Final
from solders.pubkey import Pubkey
from spl.token.instructions import get_associated_token_address
from core.pubkeys import SystemAddresses, normalize_quote_mint, quote_token_program
from core.pubkeys import (
SystemAddresses,
cached_quote_token_program,
normalize_quote_mint,
)
from interfaces.core import AddressProvider, Platform, TokenInfo
@@ -391,7 +395,7 @@ class PumpFunAddressProvider(AddressProvider):
Tuple of (quote_mint, quote_token_program)
"""
quote_mint = normalize_quote_mint(token_info.quote_mint)
quote_program = token_info.quote_token_program_id or quote_token_program(
quote_program = token_info.quote_token_program_id or cached_quote_token_program(
quote_mint
)
return quote_mint, quote_program
@@ -419,7 +423,10 @@ class PumpFunAddressProvider(AddressProvider):
Both instructions take the same 26 accounts; buy_v2 additionally takes
global_volume_accumulator. All accounts are mandatory — there are no
optional or conditional accounts on the v2 interface, regardless of
mayhem/cashback/quote-mint combination.
mayhem/cashback/holder-reward/quote-mint combination. (Cashback is
legacy-only since the 2026-09-15 upgrade — create_v2 no longer mints
new cashback coins — but existing cashback coins still trade through
this same account set.)
Args:
token_info: Token information
+7
View File
@@ -251,7 +251,14 @@ class PumpFunCurveManager(CurveManager):
"complete": decoded_curve_state.get("complete", False),
"creator": decoded_curve_state.get("creator", ""),
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
# is_cashback_coin: decoded for every coin regardless of when it
# was created. Cashback creation was deprecated 2026-09-15
# (create_v2 error 6082 CashbackDeprecated), but this flag keeps
# driving the legacy sell path's cashback account branch for
# coins that already have it set.
"is_cashback_coin": decoded_curve_state.get("is_cashback_coin", False),
"is_holder_reward": decoded_curve_state.get("is_holder_reward", False),
"creator_fee_bps": decoded_curve_state.get("creator_fee_bps", 0),
"quote_mint": quote_mint,
"is_sol_paired": is_sol_paired(quote_mint),
}
+36 -4
View File
@@ -15,8 +15,8 @@ from solders.transaction import VersionedTransaction
from core.pubkeys import (
SystemAddresses,
cached_quote_token_program,
normalize_quote_mint,
quote_token_program,
)
from interfaces.core import EventParser, Platform, TokenInfo
from platforms.pumpfun.address_provider import PumpFunAddresses
@@ -329,8 +329,10 @@ class PumpFunEventParser(EventParser):
token_program_id=token_program_id,
is_mayhem_mode=fields.get("is_mayhem_mode", False),
is_cashback_coin=fields.get("is_cashback_enabled", False),
is_holder_reward=bool(fields.get("is_holder_reward", False)),
creator_fee_bps=int(fields.get("creator_fee_bps", 0) or 0),
quote_mint=quote_mint,
quote_token_program_id=quote_token_program(quote_mint),
quote_token_program_id=cached_quote_token_program(quote_mint),
virtual_quote_reserves=fields.get("virtual_quote_reserves"),
state_from_event=state_from_event,
creation_timestamp=monotonic(),
@@ -420,7 +422,11 @@ class PumpFunEventParser(EventParser):
else SystemAddresses.TOKEN_PROGRAM
)
# Extract cashback flag from OptionBool struct (decoded as {"field_0": bool})
# Extract cashback flag from OptionBool struct (decoded as
# {"field_0": bool}). Cashback creation was deprecated 2026-09-15
# (create_v2 rejects a new true here with 6082 CashbackDeprecated),
# but older create_v2 instructions still carry it and existing
# cashback coins still trade, so it's still decoded here.
is_cashback_raw = args.get("is_cashback_enabled")
is_cashback = (
is_cashback_raw.get("field_0", False)
@@ -430,6 +436,31 @@ class PumpFunEventParser(EventParser):
else False
)
# Extract the holder-reward flag from its OptionBool struct, same
# wrapper shape as is_cashback_enabled above. Unlike `creator`,
# this one is safe to trust from instruction args: a succeeded
# create_v2 carrying `true` here means the coin genuinely is a
# holder-reward coin -- the program itself rejects the request
# when the feature is globally disabled, so a landed transaction
# is proof, not merely an unverified claim.
is_holder_reward_raw = args.get("is_holder_reward")
is_holder_reward = (
is_holder_reward_raw.get("field_0", False)
if isinstance(is_holder_reward_raw, dict)
else bool(is_holder_reward_raw)
if is_holder_reward_raw is not None
else False
)
# `creator_fee_bps` is deliberately left at TokenInfo's default
# (0) rather than decoded from args here. Upstream documents it
# as ignored on SOL- and USDC-paired coins, where the standard
# fee schedule applies regardless of what the creator passed --
# so a non-zero arg would not reflect the coin's actual fee for
# the common case, and nothing in this codebase reads the field
# to act on it (informational only). The CreateEvent path above
# gets it from the canonical on-chain event instead.
# create_v2 passes a non-native quote mint as optional remaining
# account 17 (index 16). Absent means the coin is SOL-paired.
quote_mint = normalize_quote_mint(
@@ -452,8 +483,9 @@ class PumpFunEventParser(EventParser):
token_program_id=token_program_id,
is_mayhem_mode=bool(args.get("is_mayhem_mode", False)),
is_cashback_coin=is_cashback,
is_holder_reward=bool(is_holder_reward),
quote_mint=quote_mint,
quote_token_program_id=quote_token_program(quote_mint),
quote_token_program_id=cached_quote_token_program(quote_mint),
creation_timestamp=monotonic(),
)
+15 -8
View File
@@ -537,7 +537,11 @@ class PumpFunInstructionBuilder(InstructionBuilder):
),
]
# Remaining accounts (after fee_program) for cashback + bonding_curve_v2
# Remaining accounts (after fee_program) for cashback + bonding_curve_v2.
# Cashback was deprecated 2026-09-15 (create_v2 now rejects new
# cashback coins with 6082 CashbackDeprecated), but existing cashback
# coins still trade and still accrue cashback, so this branch stays
# live for them — do not remove it.
if token_info.is_cashback_coin:
# Cashback sell: user_volume_accumulator (mutable) + bonding_curve_v2 (readonly)
sell_accounts.append(
@@ -714,10 +718,11 @@ class PumpFunInstructionBuilder(InstructionBuilder):
# Buy operations: ATA creation + buy instruction
return 100_000
# buy_v2 touches 27 accounts, so it costs more than the legacy
# 18-account buy. Mainnet simulation of a SOL-paired Token-2022 buy
# (including base ATA creation) consumed ~125k CU; a non-SOL quote adds
# another ATA init on top. Re-measure with
# learning-examples/simulate_v2_trades.py after any program upgrade.
# 18-account buy. Mainnet simulation measured 106,677 CU on
# 2026-09-15 (`uv run learning-examples/simulate_v2_trades.py
# <MINT>`), so 180k keeps headroom for a non-SOL quote's extra ATA
# init. Re-measure with that script after any program upgrade — the
# exact figure varies with account state.
return 180_000
def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int:
@@ -734,7 +739,9 @@ class PumpFunInstructionBuilder(InstructionBuilder):
if self._use_legacy_instructions:
# Sell operations: typically just sell instruction (ATA exists)
return 60_000
# sell_v2 touches 26 accounts. Measured at ~85k CU by simulating a
# buy+sell in one transaction on mainnet (the combined tx consumed
# ~211k against ~126k for the buy alone).
# sell_v2 touches 26 accounts. Measured at ~77,196 CU on 2026-09-15
# (`uv run learning-examples/simulate_v2_trades.py <MINT>`, via its
# buy+sell combined estimate), so 120k keeps headroom. The exact
# figure varies with account state — re-measure after any program
# upgrade.
return 120_000
+8
View File
@@ -14,6 +14,7 @@ from core.pubkeys import (
TOKEN_DECIMALS,
WSOL_MINT,
SystemAddresses,
cached_quote_token_program,
is_sol_paired,
normalize_quote_mint,
quote_units_per_token,
@@ -108,6 +109,12 @@ def _refresh_quote_mint(token_info: TokenInfo, pool_state: dict) -> Pubkey:
Listeners do not all carry quote_mint (pumpportal carries none of the
per-coin flags), and the curve is authoritative, so prefer its value.
`quote_token_program_id` is re-derived alongside it: it was set from the
*previous* quote_mint at TokenInfo construction (or left None), and a
stale non-None value would otherwise win the `or` in
AddressProvider.resolve_quote, so the corrected program never gets
looked up. `cached_quote_token_program` is a synchronous dict read, so
this stays a zero-RPC-call operation on the extreme_fast_mode path.
Args:
token_info: Token information, mutated in place
@@ -120,6 +127,7 @@ def _refresh_quote_mint(token_info: TokenInfo, pool_state: dict) -> Pubkey:
pool_state.get("quote_mint", token_info.quote_mint)
)
token_info.quote_mint = quote_mint
token_info.quote_token_program_id = cached_quote_token_program(quote_mint)
return quote_mint
+38
View File
@@ -22,8 +22,10 @@ from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import (
WSOL_MINT,
normalize_quote_mint,
quote_decimals,
resolve_quote_amounts,
resolve_quote_mint,
resolve_quote_token_program,
)
from core.wallet import Wallet
from interfaces.core import Platform, TokenInfo
@@ -277,6 +279,36 @@ class UniversalTrader:
self.processed_tokens: set[str] = set()
self.token_timestamps: dict[str, float] = {}
async def _resolve_quote_token_programs(self) -> None:
"""Warm the quote-mint token-program and decimals caches at startup.
`extreme_fast_mode` submits a buy with zero RPC calls between
detecting a token and sending the transaction, so the token program
that owns a coin's quote mint -- and its decimals, which size
`max_sol_cost`/`min_sol_output` -- have to already be known by then.
The set of quote mints the bot can ever buy is fixed once at startup
-- `self.quote_amounts` comes from `_resolve_quote_config` -- so this
resolves and caches both facts for each of them once here, from the
single mint-account fetch `resolve_quote_token_program` already
makes. The hot path only ever reads those caches (see
`cached_quote_token_program` and `quote_decimals` in core.pubkeys).
Raises:
ValueError: If a configured quote mint's owner is neither SPL
Token nor Token-2022, or its decimals cannot be read off the
mint account. Left uncaught deliberately: trading a quote
mint the bot cannot correctly size or derive accounts for is
worse than refusing to start.
"""
for quote_mint in self.quote_amounts:
token_program = await resolve_quote_token_program(
quote_mint, self.solana_client.get_account_info
)
logger.info(
f"Resolved quote mint {quote_mint} -> token program "
f"{token_program}, decimals {quote_decimals(quote_mint)}"
)
async def start(self) -> None:
"""Start the trading bot and listen for new tokens."""
logger.info(f"Starting Universal Trader for {self.platform.value}")
@@ -310,6 +342,12 @@ class UniversalTrader:
except Exception as e:
logger.warning(f"RPC warm-up failed: {e!s}")
# Deliberately outside the try/except above: an RPC health check is
# best-effort, but a quote mint we cannot resolve to a token program
# would go on to derive a wrong (non-existent) ATA and revert every
# trade against it -- fail startup instead of trading blind.
await self._resolve_quote_token_programs()
try:
# Choose operating mode based on yolo_mode
if not self.yolo_mode:
+24 -3
View File
@@ -352,6 +352,25 @@ class IDLParser:
print(f"Error decoding account data for {account_type_name}: {e}")
return None
# --------------------------------------------------------------------------
# Public Methods (External API) - Errors
# --------------------------------------------------------------------------
def describe_error_code(self, code: int) -> str | None:
"""Render an Anchor custom error code as "<code> <Name>: <msg>".
Args:
code: The Custom(N) value from meta.err.
Returns:
A human-readable description, or None if this IDL's error table
has no such code.
"""
for err in self.idl.get("errors", []):
if err.get("code") == code:
return f"{code} {err['name']}: {err.get('msg', '')}".rstrip(": ")
return None
# --------------------------------------------------------------------------
# Internal Helper Methods
# --------------------------------------------------------------------------
@@ -425,15 +444,17 @@ class IDLParser:
def _is_optional_type(self, type_def: str | dict) -> bool:
"""Whether a type may legally be absent when it trails instruction data.
Covers Anchor's native ``option`` wrapper and pump.fun's ``OptionBool``
defined type — both are observed omitted from the wire when trailing.
Covers Anchor's native ``option`` wrapper and pump.fun's ``Option*``
defined types — ``OptionBool`` (is_cashback_enabled, is_holder_reward)
and ``OptionU64`` (creator_fee_bps). All are observed omitted from the
wire when trailing; see issue #184 and the 2026-09-15 program upgrade.
"""
if not isinstance(type_def, dict):
return False
if "option" in type_def:
return True
if "defined" in type_def:
return self._get_defined_type_name(type_def) == "OptionBool"
return self._get_defined_type_name(type_def).startswith("Option")
return False
def _get_primitive_size(self, type_name: str) -> int: