mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
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:
co-authored by
Claude Opus 5
parent
a0540fdc9e
commit
55ae2afd82
@@ -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)`.
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+6157
-583
File diff suppressed because it is too large
Load Diff
+377
-86
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user