mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-15 00:08:04 +00:00
feat(pumpfun): migrate to buy_v2/sell_v2 and support non-SOL quote assets (#176)
Refresh the vendored IDLs from pump-fun/pump-public-docs @ 9c82f61 and move all pump.fun trading onto the v2 instruction interface. This is required, not optional: legacy buy/sell cannot trade coins paired against a quote asset other than SOL, and USDC is already whitelisted in the on-chain Global account. Protocol changes absorbed: - buy_v2 (27 accounts) / sell_v2 (26 accounts) replace the legacy instructions. Every account is mandatory and the order is identical for all coins, so the conditional cashback/mayhem account lists are gone. Legacy remains available via PumpFunInstructionBuilder(use_legacy_instructions=True). - BondingCurve is 151 bytes: virtual_sol_reserves -> virtual_quote_reserves, real_sol_reserves -> real_quote_reserves, plus quote_mint at offset 83. Old field names are kept as aliases so existing callers keep working. - v2 instruction data drops the track_volume OptionBool; amounts are in the quote mint's raw units rather than always lamports. - create_v2 carries a non-SOL quote mint as optional remaining accounts 17-19, and CreateEvent gained quote_mint, so extreme_fast_mode can resolve the quote asset without an extra fetch. USDC support: new trade.quote_amounts and filters.allowed_quote_mints config, accepting "sol"/"usdc" aliases or raw mints. Amounts are per-quote-mint because 1 USDC and 1 SOL are not interchangeable. A coin whose quote mint has no configured amount is skipped rather than traded at the wrong size, so SOL-only configs are unaffected. Bug fixes found while verifying: - The logs and blocks listeners set no websocket max_size, so any frame over 1 MiB closed the connection with 1009 and the token in it was lost. Raised to 32 MiB. - PumpSwap priced against the raw quote vault balance, ignoring the new Pool.virtual_quote_reserves (i128 at offset 245; live pools are 301 bytes). Upstream's note that this field is 0 everywhere is out of date: a live pool carries 17.58 SOL against a 148 SOL vault, a 10.15% price error. - The seller read curve state once at confirmed commitment and silently fell back to create-time values, risking a stale creator_vault and ConstraintSeeds. It now retries at processed, matching the buyer. - Account cleanup would burn wrapped SOL when force_burn was set, destroying value that closing the account returns. WSOL is now closed without burning. - The mint scripts treated a landed transaction as a successful one, so a reverted buy printed as success. They now assert the on-chain result. Compute unit limits retuned from mainnet measurements: buy 100k -> 180k, sell 60k -> 120k. Mint-and-buy is no longer atomic, because create_v2 plus buy_v2 exceeds the 1232-byte transaction limit; both mint scripts send two transactions. Adds learning-examples/pump_v2.py as one shared, standalone v2 toolkit for the example scripts, and three verification scripts: an offline layout check against the IDL, a no-funds mainnet simulation, and a live listener matrix that buys, sells and closes the ATA per listener. Verified on mainnet: all four listeners (geyser, logs, blocks, pumpportal) and all eight example scripts completed a real buy, sell and ATA close, each confirmed by reading the transaction result back rather than trusting confirmation alone. The USDC path is verified structurally only; no USDC-paired coin could be found on-chain to exercise it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b88a06d9d
commit
02343b775b
@@ -50,6 +50,23 @@ uv run learning-examples/listen-new-tokens/listen_logsubscribe_abc.py
|
||||
uv run learning-examples/listen-new-tokens/compare_listeners.py
|
||||
```
|
||||
|
||||
### Verifying pump.fun v2 trade instructions
|
||||
```bash
|
||||
# Offline: cross-check buy_v2/sell_v2 account layouts, PDA/ATA derivations,
|
||||
# instruction encoding and quote-asset config against idl/pump_fun_idl.json
|
||||
uv run learning-examples/verify_v2_account_layout.py
|
||||
|
||||
# Mainnet, no funds moved: simulate buy_v2/sell_v2 for one coin, report CU
|
||||
uv run learning-examples/simulate_v2_trades.py <MINT>
|
||||
|
||||
# Mainnet, no funds moved: run the bot's whole buy path against a fresh coin
|
||||
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
|
||||
`unitsConsumed`; use it to retune `get_buy_compute_unit_limit` /
|
||||
`get_sell_compute_unit_limit` in `platforms/pumpfun/instruction_builder.py`.
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Format code
|
||||
@@ -64,12 +81,85 @@ ruff check --fix
|
||||
|
||||
## Pump.fun protocol notes (gotchas)
|
||||
|
||||
- The vendored IDL at `idl/pump_fun_idl.json` is **incomplete**: it does not list `bonding-curve-v2` (BC trades) or `pool-v2` (PumpSwap trades). Both are required on-chain. `bonding-curve-v2` PDA seed is `["bonding-curve-v2", mint]` under the pump program; `pool-v2` is `["pool-v2", base_mint]` under the pump-amm program. Always cross-check account lists against a recent successful on-chain tx — IDL alone will produce broken code.
|
||||
- BC `buy` ix is **18 accounts** (post 2026-04-28 upgrade). The trailing account is one of 8 `BREAKING_FEE_RECIPIENTS` (mutable), AFTER `bonding-curve-v2`.
|
||||
- BC `sell` ix is **16 accounts non-cashback / 17 cashback**. Cashback path inserts `user_volume_accumulator` (PDA seed `["user_volume_accumulator", user]`) BEFORE `bonding-curve-v2`. Detect cashback from byte 82 of the bonding-curve account data, or via the `is_cashback_coin` field returned by the curve manager.
|
||||
- The BondingCurve account is **83 bytes** (was 81): trailing fields are `is_mayhem_mode: bool` then `is_cashback_coin: bool`. PumpSwap Pool is **245 bytes** with the same `is_cashback_coin` byte at offset 244.
|
||||
- The IDL instruction is `create_v2` (snake_case). `create_v2` 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.
|
||||
- `extreme_fast_mode` skips the curve-state RPC fetch — make sure event parsers populate `is_mayhem_mode` and `is_cashback_coin` on `TokenInfo` from the `CreateEvent` payload (otherwise mayhem coins use the wrong fee_recipient and cashback coins use the wrong sell account count).
|
||||
The IDLs under `idl/` are vendored verbatim from `github.com/pump-fun/pump-public-docs`
|
||||
(`idl/pump.json` → `pump_fun_idl.json`, `pump_amm.json` → `pump_swap_idl.json`,
|
||||
`pump_fees.json`). Refresh them from upstream rather than hand-editing.
|
||||
|
||||
### 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.**
|
||||
- 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` /
|
||||
`_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 +
|
||||
two u64). Volume tracking is unconditional now that `user_volume_accumulator`
|
||||
is mandatory. `max_sol_cost`/`min_sol_output` are in the **quote mint's** raw
|
||||
units — lamports for SOL, 1e-6 for USDC.
|
||||
- Even for SOL-paired coins you must pass **wrapped SOL** as `quote_mint`, not
|
||||
`Pubkey::default()`. Transfers still happen in native SOL, and the
|
||||
`associated_quote_*` accounts are only seed-constrained — do **not** create the
|
||||
user's WSOL ATA, it would burn ~0.002 SOL of rent for nothing.
|
||||
- Fee recipients: 24 total, in three sets of 8 (`NORMAL_FEE_RECIPIENTS`,
|
||||
`RESERVED_FEE_RECIPIENTS` for mayhem coins, `BUYBACK_FEE_RECIPIENTS`). Every
|
||||
v2 buy/sell needs a `fee_recipient` **and** a `buyback_fee_recipient`.
|
||||
- `sharing_config` (PDA `["sharing-config", base_mint]`) lives under the **pump
|
||||
fees program**, not the pump program. Easy to derive against the wrong program.
|
||||
|
||||
### 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.
|
||||
- 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
|
||||
keep working for SOL-paired coins — but anything doing arithmetic must scale by
|
||||
the quote mint's decimals (`quote_units_per_token`), not a hardcoded 1e9.
|
||||
- PumpSwap `Pool` gained a trailing **`virtual_quote_reserves: i128`** (16 bytes,
|
||||
offset 245). Pool fields end at 261; live accounts are **301 bytes** with
|
||||
trailing padding. Quote against **effective** reserves:
|
||||
`pool_quote_token_account.amount + virtual_quote_reserves`.
|
||||
**Upstream's release note claims it is 0 on all pools — that is out of date.**
|
||||
Verified on mainnet: pool `6Bv1JM1deBPe…` carries 17.584505433 SOL of virtual
|
||||
reserves against a 148.455 SOL vault, so quoting off the raw vault balance
|
||||
under-prices by ~10.6%. It is `i128`, not `u64` — reading 8 bytes happens to
|
||||
work only while the high half is zero.
|
||||
|
||||
### 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.
|
||||
- `create_v2` accounts 1-16 are in the IDL; accounts **17-19 are optional
|
||||
remaining accounts** (`quote_mint`, `associated_quote_bonding_curve`,
|
||||
`quote_token_program`) appended only for a non-SOL quote mint. All three or
|
||||
none. This is the only way to read a new coin's quote asset from the
|
||||
instruction rather than the event.
|
||||
- `extreme_fast_mode` skips the curve-state price fetch but still refreshes
|
||||
mayhem/cashback/creator/**quote_mint** from chain, because the wrong quote mint
|
||||
means spending the wrong balance entirely. Event parsers also populate
|
||||
`quote_mint` from `CreateEvent` (which gained `quote_mint` and
|
||||
`virtual_quote_reserves` as trailing fields).
|
||||
|
||||
### Legacy instructions (fallback only)
|
||||
|
||||
Retained behind `PumpFunInstructionBuilder(..., use_legacy_instructions=True)`.
|
||||
The IDL under-reports these: `buy` is **18 accounts** on-chain (IDL lists 16) and
|
||||
`sell` is **16 non-cashback / 17 cashback** (IDL lists 14). The extras are
|
||||
`bonding-curve-v2` (PDA `["bonding-curve-v2", mint]`) followed by a buyback fee
|
||||
recipient (mutable); the cashback sell path also inserts
|
||||
`user_volume_accumulator` before `bonding-curve-v2`. Prefer v2 — it is the
|
||||
interface pump.fun maintains.
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user