From 02343b775bedd76bfe539bfb81a4136d516a7002 Mon Sep 17 00:00:00 2001 From: Anton Sauchyk <43762166+smypmsa@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:58:33 +0200 Subject: [PATCH] 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) --- CLAUDE.md | 102 +- README.md | 52 +- bots/bot-sniper-1-geyser.yaml | 14 + idl/pump_fees.json | 3150 +++++++++- idl/pump_fun_idl.json | 5507 ++++++++--------- idl/pump_swap_idl.json | 1124 +++- learning-examples/live_listener_matrix.py | 409 ++ learning-examples/live_v2_round_trip.py | 160 + learning-examples/manual_buy.py | 337 +- learning-examples/manual_buy_cu_optimized.py | 196 +- learning-examples/manual_buy_geyser.py | 186 +- learning-examples/manual_sell.py | 293 +- learning-examples/mint_and_buy.py | 150 +- learning-examples/mint_and_buy_v2.py | 156 +- learning-examples/pump_v2.py | 502 ++ .../pumpswap/manual_buy_pumpswap.py | 61 +- .../pumpswap/manual_sell_pumpswap.py | 61 +- learning-examples/simulate_bot_buy_path.py | 230 + learning-examples/simulate_v2_trades.py | 288 + learning-examples/verify_v2_account_layout.py | 556 ++ src/bot_runner.py | 3 + src/cleanup/manager.py | 11 +- src/config_loader.py | 33 + src/core/client.py | 109 +- src/core/pubkeys.py | 158 + src/interfaces/core.py | 7 + src/monitoring/universal_block_listener.py | 10 +- src/monitoring/universal_logs_listener.py | 10 +- src/platforms/pumpfun/address_provider.py | 227 +- src/platforms/pumpfun/curve_manager.py | 73 +- src/platforms/pumpfun/event_parser.py | 59 +- src/platforms/pumpfun/instruction_builder.py | 338 +- src/trading/platform_aware.py | 259 +- src/trading/universal_trader.py | 105 +- 34 files changed, 10949 insertions(+), 3987 deletions(-) create mode 100644 learning-examples/live_listener_matrix.py create mode 100644 learning-examples/live_v2_round_trip.py create mode 100644 learning-examples/pump_v2.py create mode 100644 learning-examples/simulate_bot_buy_path.py create mode 100644 learning-examples/simulate_v2_trades.py create mode 100644 learning-examples/verify_v2_account_layout.py diff --git a/CLAUDE.md b/CLAUDE.md index 70fbaa4..ca7bf24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 + +# 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 diff --git a/README.md b/README.md index 3ba85dc..da98d0d 100644 --- a/README.md +++ b/README.md @@ -124,15 +124,59 @@ This helps ensure reliable operation within your node provider's rate limits wit The IDLs under [`idl/`](idl/) are vendored from [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs). To refresh, copy `pump.json`, `pump_amm.json`, `pump_fees.json` from that repo into `pump_fun_idl.json`, `pump_swap_idl.json`, `pump_fees.json` respectively, and reference the upstream commit hash in your commit message. -> **The IDL is incomplete.** It doesn't list two PDAs that the on-chain program actually requires: -> - `bonding-curve-v2` — required on every BC `buy` (18 accounts) and `sell` (16/17 accounts). Seed: `["bonding-curve-v2", mint]` under the pump program. +Currently vendored from upstream commit `9c82f61`. + +> **The IDL under-reports the legacy instructions.** It omits two PDAs that the on-chain program requires on the pre-v2 path: +> - `bonding-curve-v2` — required on every legacy BC `buy` (18 accounts) and `sell` (16/17 accounts). Seed: `["bonding-curve-v2", mint]` under the pump program. > - `pool-v2` — required on every PumpSwap `buy`/`sell`. Seed: `["pool-v2", base_mint]` under the pump-amm program. **Without it, pump-amm throws `AnchorError 6023 (Overflow)` after the trade transfers complete** — a misleading error code for a missing-account issue. > -> Always cross-check your account lists against a recent successful on-chain tx (`getSignaturesForAddress` + `getTransaction`) before trusting the IDL. +> The `buy_v2` / `sell_v2` account lists *are* complete in the IDL — that's the point of the v2 interface. For anything else, cross-check against a recent successful on-chain tx (`getSignaturesForAddress` + `getTransaction`) before trusting the IDL. + +## Quote assets: SOL and USDC (v2 trade instructions) + +Pump.fun added support for quote assets other than SOL, with USDC first. The bonding curve carries a `quote_mint` field (`Pubkey::default()` for SOL-paired coins), and trading non-SOL-paired coins **requires** the newer `buy_v2` / `sell_v2` instructions — the legacy `buy` / `sell` cannot do it at all. + +The bot uses `buy_v2` (27 accounts) and `sell_v2` (26 accounts) for every pump.fun trade. Every account is mandatory and the order is identical for all coins, regardless of quote asset, mayhem mode, or cashback — no more conditional account lists. + +To trade a non-SOL quote asset, give it a spend amount. Amounts are in that mint's own whole units, so `usdc: 1.0` is one USDC and is **not** comparable to `buy_amount`: + +```yaml +trade: + buy_amount: 0.0001 # SOL-paired coins + quote_amounts: + usdc: 1.0 # USDC-paired coins + +filters: + allowed_quote_mints: ["sol", "usdc"] # omit to allow any configured quote +``` + +Keys accept the aliases `sol` / `wsol` / `usdc` or a raw base58 mint address. A coin whose quote mint has no configured amount is skipped with a log line rather than bought with a wrongly-scaled amount. SOL always falls back to `buy_amount`, so existing configs keep working untouched. + +Buying a USDC-paired coin requires USDC in the wallet plus a little SOL for fees and ATA rent. + +Verify the v2 wiring after any program upgrade: + +```bash +uv run learning-examples/verify_v2_account_layout.py # offline: layouts, PDAs, encoding +uv run learning-examples/simulate_v2_trades.py # mainnet simulation, no funds moved +uv run learning-examples/simulate_bot_buy_path.py # whole bot buy path, simulated +``` + +## PumpSwap: quote against effective reserves + +The PumpSwap `Pool` account gained a trailing `virtual_quote_reserves` field (an **`i128`** at offset 245; fields end at 261, live accounts are 301 bytes). Price must be computed from **effective** quote reserves: + +``` +effective_quote_reserves = pool_quote_token_account.amount + Pool::virtual_quote_reserves +``` + +> ⚠️ Upstream's release note says `virtual_quote_reserves` is 0 on every pool. **That is out of date.** Verified on mainnet: pool `6Bv1JM1deBPeEeovhoRajFtbMVDKQidC9mXYRQWfGoXz` holds 17.584505433 SOL of virtual reserves against a 148.455 SOL vault — quoting off the raw vault balance under-prices by **~10.6%**. Note it is `i128`, not `u64`; an 8-byte read only works while the high half happens to be zero. + +`learning-examples/pumpswap/manual_buy_pumpswap.py` and `manual_sell_pumpswap.py` read and apply it. Note that pump-amm has **no** `buy_v2`/`sell_v2` — the AMM instruction names are unchanged; only the pool layout and quoting moved. ## 2026-04-28 program upgrade -Pump.fun shipped a breaking program upgrade on **2026-04-28 16:00 UTC** ([BREAKING_FEE_RECIPIENT.md](https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md)). The bot is updated for it: +Pump.fun shipped a breaking program upgrade on **2026-04-28 16:00 UTC** ([BREAKING_FEE_RECIPIENT.md](https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md)). This section describes the **legacy** instruction path, which the bot retains only as a fallback (`PumpFunInstructionBuilder(..., use_legacy_instructions=True)`) — see the quote-assets section above for the v2 path used by default: - BC `buy` ix is now **18 accounts** (was 17). Trailing account is one of 8 `BREAKING_FEE_RECIPIENTS` (mutable), AFTER `bonding-curve-v2`. - BC `sell` ix is now **16 accounts non-cashback / 17 cashback** (was 15/16). Same trailing fee recipient. diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml index cceb543..8ca4d86 100644 --- a/bots/bot-sniper-1-geyser.yaml +++ b/bots/bot-sniper-1-geyser.yaml @@ -27,6 +27,16 @@ trade: buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) sell_slippage: 0.3 + # Spend amounts for coins paired against a non-SOL quote asset. + # pump.fun now supports quote mints other than SOL (USDC first). Amounts are + # in that mint's own whole units, so "usdc: 1.0" means one USDC — it is NOT + # comparable to buy_amount above. Keys accept the aliases "sol"/"usdc" or a + # raw base58 mint address. A coin whose quote mint has no entry here is + # skipped rather than bought with a SOL-sized amount. + # SOL always uses buy_amount above and needs no entry. + #quote_amounts: + # usdc: 1.0 + # Exit strategy configuration exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" #take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%) @@ -69,6 +79,10 @@ compute_units: filters: match_string: null # Only process tokens with this string in name/symbol bro_address: null # Only trade tokens created by this user address + # Restrict which quote assets to trade. Omit (or set null) to allow any quote + # mint that has an amount configured under trade.quote_amounts. + # Accepts "sol"/"usdc" aliases or raw mint addresses. + #allowed_quote_mints: ["sol"] listener_type: "geyser" # Method for detecting new tokens: "logs", "blocks", or "geyser" max_token_age: 0.001 # Maximum token age in seconds for processing marry_mode: false # Only buy tokens, skip selling diff --git a/idl/pump_fees.json b/idl/pump_fees.json index 8b45bbb..900a99f 100644 --- a/idl/pump_fees.json +++ b/idl/pump_fees.json @@ -116,6 +116,681 @@ } } }, + { + "name": "claim_social_fee_pda_v2", + "discriminator": [ + 17, + 77, + 240, + 134, + 58, + 188, + 53, + 149 + ], + "accounts": [ + { + "name": "recipient", + "writable": true + }, + { + "name": "social_fee_pda", + "writable": true + }, + { + "name": "quote_mint", + "docs": [ + "Quote mint for claim" + ], + "writable": true + }, + { + "name": "associated_social_fee_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "social_fee_pda" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "associated_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_program_global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 45, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 45, + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "social_claim_authority", + "signer": true, + "relations": [ + "fee_program_global" + ] + }, + { + "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": "user_id", + "type": "string" + }, + { + "name": "platform", + "type": "u8" + } + ], + "returns": { + "option": { + "defined": { + "name": "SocialFeePdaClaimed" + } + } + } + }, + { + "name": "crank_donation_fee_pda", + "discriminator": [ + 220, + 10, + 189, + 167, + 169, + 17, + 25, + 69 + ], + "accounts": [ + { + "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", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "payer", + "docs": [ + "Pays rent when [`temp_wsol_token_account`] is created (`init_if_needed`); receives rent when it is closed after the relay CPI." + ], + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "fee_program_global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 45, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 45, + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "donation_fee_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 100, + 111, + 110, + 97, + 116, + 105, + 111, + 110, + 45, + 102, + 101, + 101, + 45, + 112, + 100, + 97 + ] + }, + { + "kind": "account", + "path": "donation_fee_pda.base_mint", + "account": "DonationFeePda" + }, + { + "kind": "account", + "path": "donation_fee_pda.config_id", + "account": "DonationFeePda" + } + ] + } + }, + { + "name": "quote_mint", + "docs": [ + "Quote mint from donation fee pda." + ], + "writable": true + }, + { + "name": "donation_fee_pda_ata", + "docs": [ + "WSOL ATA owned by `donation_fee_pda`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "donation_fee_pda" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "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 + ] + } + } + }, + { + "name": "donation_relay_program", + "address": "RLAYHr9TRFcKB2ubYQhspcnXiaGpaVzNQvHytt47RZu" + }, + { + "name": "donation_relay_event_authority" + }, + { + "name": "mint_whitelist" + }, + { + "name": "epoch_tracker", + "writable": true + }, + { + "name": "debouncer", + "writable": true + }, + { + "name": "debouncer_ata", + "writable": true + } + ], + "args": [] + }, + { + "name": "create_donation_fee_pda", + "discriminator": [ + 244, + 139, + 16, + 88, + 14, + 255, + 122, + 26 + ], + "accounts": [ + { + "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", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "fee_program_global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 45, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 45, + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "donation_fee_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 100, + 111, + 110, + 97, + 116, + 105, + 111, + 110, + 45, + 102, + 101, + 101, + 45, + 112, + 100, + 97 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "config_id" + } + ] + } + }, + { + "name": "config_id", + "docs": [ + "stored on the PDA, so distinct `config_id`s for the same `base_mint` derive distinct addresses." + ] + }, + { + "name": "base_mint" + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "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": "pool" + }, + { + "name": "sharing_config", + "docs": [ + "(derived from `[SHARING_CONFIG_SEED, base_mint]`)" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 104, + 97, + 114, + 105, + 110, + 103, + 45, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + } + ], + "args": [] + }, { "name": "create_fee_sharing_config", "docs": [ @@ -580,6 +1255,72 @@ } ] }, + { + "name": "extend_fee_config", + "docs": [ + "Realloc the fee_config PDA to [`FeeConfig::CURRENT_SIZE`] (signer pays rent delta)." + ], + "discriminator": [ + 68, + 179, + 244, + 90, + 173, + 56, + 17, + 217 + ], + "accounts": [ + { + "name": "fee_config", + "writable": true + }, + { + "name": "user", + "signer": true + }, + { + "name": "config_program_id" + }, + { + "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": "get_fees", "docs": [ @@ -638,6 +1379,14 @@ { "name": "trade_size_lamports", "type": "u64" + }, + { + "name": "is_new_quote_mint", + "type": { + "defined": { + "name": "OptionBool" + } + } } ], "returns": { @@ -646,6 +1395,139 @@ } } }, + { + "name": "initialize_buyback", + "discriminator": [ + 250, + 129, + 236, + 160, + 227, + 36, + 103, + 134 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "buyback_vault", + "writable": true + }, + { + "name": "buyback_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_vault" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "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 + ] + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "mint" + }, + { + "name": "token_program" + }, + { + "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": "index", + "type": "u8" + } + ] + }, { "name": "initialize_fee_config", "docs": [ @@ -1402,7 +2284,582 @@ }, { "name": "coin_creator_vault_ata", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "wsol_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + } + ], + "args": [] + }, + { + "name": "reset_fee_sharing_config_v2", + "docs": [ + "Reset Fee Sharing Config and distribute pending fees first" + ], + "discriminator": [ + 169, + 245, + 17, + 209, + 94, + 91, + 248, + 128 + ], + "accounts": [ + { + "name": "new_admin" + }, + { + "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", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "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": "mint", + "relations": [ + "sharing_config" + ] + }, + { + "name": "sharing_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 104, + 97, + 114, + 105, + 110, + 103, + 45, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "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": "pump_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "sharing_config" + } + ], + "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": "pump_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pump_creator_vault" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "pump_program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "pump_event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "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": "pump_amm_program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "amm_event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + } + }, + { + "name": "quote_mint" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "coin_creator_vault_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "sharing_config" + } + ], + "program": { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + } + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } } ], "args": [] @@ -1793,6 +3250,203 @@ } ] }, + { + "name": "sweep_buyback", + "discriminator": [ + 138, + 33, + 204, + 38, + 207, + 161, + 159, + 226 + ], + "accounts": [ + { + "name": "destination", + "writable": true + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "buyback_vault" + ] + }, + { + "name": "buyback_vault", + "writable": true + }, + { + "name": "buyback_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_vault" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "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 + ] + } + } + }, + { + "name": "destination_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "destination" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "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 + ] + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "mint" + }, + { + "name": "token_program" + }, + { + "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": "index", + "type": "u8" + } + ] + }, { "name": "transfer_fee_sharing_authority", "docs": [ @@ -1902,6 +3556,206 @@ ], "args": [] }, + { + "name": "update_buyback_authority", + "discriminator": [ + 66, + 98, + 113, + 202, + 121, + 37, + 219, + 107 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "fee_program_global" + ] + }, + { + "name": "fee_program_global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 45, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 45, + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "buyback_vault", + "writable": true + }, + { + "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": "index", + "type": "u8" + }, + { + "name": "new_authority", + "type": "pubkey" + } + ] + }, + { + "name": "update_buyback_claim_rate_limit", + "discriminator": [ + 186, + 95, + 135, + 190, + 255, + 199, + 137, + 170 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "fee_program_global" + ] + }, + { + "name": "fee_program_global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 45, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 45, + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "buyback_vault", + "writable": true + }, + { + "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": "index", + "type": "u8" + }, + { + "name": "claim_rate_limit", + "type": "i64" + } + ] + }, { "name": "update_fee_config", "docs": [ @@ -2528,6 +4382,667 @@ } ] }, + { + "name": "update_fee_shares_v2", + "docs": [ + "Update Fee Shares, make sure to distribute all the fees before calling this" + ], + "discriminator": [ + 111, + 251, + 49, + 6, + 78, + 78, + 106, + 18 + ], + "accounts": [ + { + "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", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "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": "mint", + "relations": [ + "sharing_config" + ] + }, + { + "name": "sharing_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 104, + 97, + 114, + 105, + 110, + 103, + 45, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "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": "pump_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "sharing_config" + } + ], + "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": "pump_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pump_creator_vault" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "pump_program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "pump_event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "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": "pump_amm_program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "amm_event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + } + }, + { + "name": "quote_mint" + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "coin_creator_vault_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "sharing_config" + } + ], + "program": { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + } + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + } + ], + "args": [ + { + "name": "shareholders", + "type": { + "vec": { + "defined": { + "name": "Shareholder" + } + } + } + } + ] + }, + { + "name": "update_stable_fee_config", + "docs": [ + "Set/Replace fee parameters entirely (only callable by admin)" + ], + "discriminator": [ + 107, + 169, + 100, + 179, + 134, + 155, + 146, + 221 + ], + "accounts": [ + { + "name": "fee_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "config_program_id" + } + ] + } + }, + { + "name": "admin", + "signer": true, + "relations": [ + "fee_config" + ] + }, + { + "name": "config_program_id" + }, + { + "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": "fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + } + ] + }, { "name": "upsert_fee_tiers", "docs": [ @@ -2630,6 +5145,109 @@ "type": "u8" } ] + }, + { + "name": "upsert_stable_fee_tiers", + "docs": [ + "Update or expand fee tiers (only callable by admin)" + ], + "discriminator": [ + 181, + 160, + 162, + 252, + 74, + 76, + 224, + 221 + ], + "accounts": [ + { + "name": "fee_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "config_program_id" + } + ] + } + }, + { + "name": "admin", + "signer": true, + "relations": [ + "fee_config" + ] + }, + { + "name": "config_program_id" + }, + { + "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": "fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + }, + { + "name": "offset", + "type": "u8" + } + ] } ], "accounts": [ @@ -2646,6 +5264,32 @@ 96 ] }, + { + "name": "BuybackVault", + "discriminator": [ + 153, + 166, + 71, + 144, + 179, + 189, + 137, + 251 + ] + }, + { + "name": "DonationFeePda", + "discriminator": [ + 246, + 197, + 96, + 9, + 193, + 30, + 93, + 115 + ] + }, { "name": "FeeConfig", "discriminator": [ @@ -2739,6 +5383,45 @@ 88 ] }, + { + "name": "DonationFeePdaCranked", + "discriminator": [ + 30, + 208, + 107, + 93, + 177, + 0, + 223, + 78 + ] + }, + { + "name": "DonationFeePdaCreated", + "discriminator": [ + 94, + 20, + 137, + 239, + 35, + 77, + 225, + 235 + ] + }, + { + "name": "ExtendFeeConfigEvent", + "discriminator": [ + 226, + 203, + 224, + 35, + 153, + 10, + 88, + 51 + ] + }, { "name": "InitializeFeeConfigEvent", "discriminator": [ @@ -2856,6 +5539,19 @@ 169 ] }, + { + "name": "SweepBuybackEvent", + "discriminator": [ + 43, + 56, + 42, + 214, + 153, + 57, + 166, + 137 + ] + }, { "name": "UpdateAdminEvent", "discriminator": [ @@ -2895,6 +5591,19 @@ 203 ] }, + { + "name": "UpdateStableFeeConfigEvent", + "discriminator": [ + 94, + 5, + 43, + 237, + 103, + 147, + 232, + 245 + ] + }, { "name": "UpsertFeeTiersEvent", "discriminator": [ @@ -2907,6 +5616,19 @@ 33, 204 ] + }, + { + "name": "UpsertStableFeeTiersEvent", + "discriminator": [ + 232, + 237, + 237, + 52, + 98, + 146, + 73, + 243 + ] } ], "errors": [ @@ -3034,6 +5756,41 @@ "code": 6024, "name": "FeeSharesAlreadyUpdated", "msg": "Reward split can only be updated once" + }, + { + "code": 6025, + "name": "MathOverflow", + "msg": "Math overflow" + }, + { + "code": 6026, + "name": "InvalidBuybackIndex", + "msg": "Invalid buybackindex" + }, + { + "code": 6027, + "name": "ClaimRateLimitExceeded", + "msg": "Claim rate limit exceeded" + }, + { + "code": 6028, + "name": "InvalidFeeConfigAccount", + "msg": "Account is not a valid FeeConfig for this instruction" + }, + { + "code": 6029, + "name": "AccountTypeNotSupported", + "msg": "Account type not supported" + }, + { + "code": 6030, + "name": "InvalidMint", + "msg": "Mint does not match quote mint" + }, + { + "code": 6031, + "name": "UnsupportedQuoteMint", + "msg": "Unsupported quote mint" } ], "types": [ @@ -3047,7 +5804,7 @@ "type": "u64" }, { - "name": "virtual_sol_reserves", + "name": "virtual_quote_reserves", "type": "u64" }, { @@ -3055,7 +5812,7 @@ "type": "u64" }, { - "name": "real_sol_reserves", + "name": "real_quote_reserves", "type": "u64" }, { @@ -3077,6 +5834,47 @@ ] } }, + { + "name": "BuybackVault", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "total_claimed", + "type": "u64" + }, + { + "name": "total_claimed_token1", + "type": "u64" + }, + { + "name": "total_claimed_token2", + "type": "u64" + }, + { + "name": "last_claimed", + "type": "i64" + }, + { + "name": "claim_rate_limit", + "type": "i64" + }, + { + "name": "_reserved", + "type": { + "array": [ + "u8", + 128 + ] + } + } + ] + } + }, { "name": "ConfigStatus", "type": { @@ -3143,6 +5941,162 @@ ] } }, + { + "name": "DonationFeePda", + "docs": [ + "Escrow PDA for donation relay: one per (mint, donation campaign `config_id`)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "version", + "type": "u8" + }, + { + "name": "config_id", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "total_donated", + "type": "u64" + }, + { + "name": "last_crank_ts", + "type": "i64" + }, + { + "name": "_reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "DonationFeePdaCranked", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "donation_fee_pda", + "type": "pubkey" + }, + { + "name": "config_id", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "DonationFeePdaCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "created_by", + "type": "pubkey" + }, + { + "name": "donation_fee_pda", + "type": "pubkey" + }, + { + "name": "config_id", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "ExtendFeeConfigEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "fee_config", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "current_size", + "type": "u64" + }, + { + "name": "new_size", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, { "name": "FeeConfig", "type": { @@ -3185,6 +6139,19 @@ } } } + }, + { + "name": "stable_fee_tiers", + "docs": [ + "The fee tiers" + ], + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } } ] } @@ -3412,6 +6379,15 @@ ] } }, + { + "name": "OptionBool", + "type": { + "kind": "struct", + "fields": [ + "bool" + ] + } + }, { "name": "Pool", "type": { @@ -3686,12 +6662,16 @@ "name": "last_claimed", "type": "u64" }, + { + "name": "total_stable_claimed", + "type": "u64" + }, { "name": "_reserved", "type": { "array": [ "u8", - 128 + 120 ] } } @@ -3746,6 +6726,14 @@ { "name": "recipient_balance_after", "type": "u64" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "lifetime_stable_claimed", + "type": "u64" } ] } @@ -3778,6 +6766,38 @@ ] } }, + { + "name": "SweepBuybackEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "u8" + }, + { + "name": "sol_amount", + "type": "u64" + }, + { + "name": "token_amount", + "type": "u64" + }, + { + "name": "destination", + "type": "pubkey" + }, + { + "name": "buyback_vault", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + } + ] + } + }, { "name": "UpdateAdminEvent", "type": { @@ -3874,6 +6894,44 @@ ] } }, + { + "name": "UpdateStableFeeConfigEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "fee_config", + "type": "pubkey" + }, + { + "name": "stable_fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + }, + { + "name": "flat_fees", + "type": { + "defined": { + "name": "Fees" + } + } + } + ] + } + }, { "name": "UpsertFeeTiersEvent", "type": { @@ -3907,6 +6965,40 @@ } ] } + }, + { + "name": "UpsertStableFeeTiersEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "fee_config", + "type": "pubkey" + }, + { + "name": "stable_fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + }, + { + "name": "offset", + "type": "u8" + } + ] + } } ], "constants": [ @@ -3920,6 +7012,36 @@ }, "value": "[99, 114, 101, 97, 116, 111, 114, 95, 118, 97, 117, 108, 116]" }, + { + "name": "BUYBACK_VAULT_SEED", + "type": { + "array": [ + "u8", + 13 + ] + }, + "value": "[98, 117, 121, 98, 97, 99, 107, 45, 118, 97, 117, 108, 116]" + }, + { + "name": "DEBOUNCER_V1", + "type": "bytes", + "value": "[100, 101, 98, 111, 117, 110, 99, 101, 114, 95, 118, 49]" + }, + { + "name": "DONATION_FEE_PDA_SEED", + "type": { + "array": [ + "u8", + 16 + ] + }, + "value": "[100, 111, 110, 97, 116, 105, 111, 110, 45, 102, 101, 101, 45, 112, 100, 97]" + }, + { + "name": "EPOCH_TRACKER_V1", + "type": "bytes", + "value": "[101, 112, 111, 99, 104, 95, 116, 114, 97, 99, 107, 101, 114, 95, 118, 49]" + }, { "name": "FEE_CONFIG_SEED", "type": "bytes", @@ -3935,6 +7057,26 @@ }, "value": "[102, 101, 101, 45, 112, 114, 111, 103, 114, 97, 109, 45, 103, 108, 111, 98, 97, 108]" }, + { + "name": "IX_DONATE_PUBKEY_CONFIG_ID_WITH_PAYER_V1", + "type": { + "array": [ + "u8", + 8 + ] + }, + "value": "[120, 217, 57, 241, 135, 104, 139, 184]" + }, + { + "name": "MAX_BUYBACK_INDEX", + "type": "u8", + "value": "8" + }, + { + "name": "MINT_WHITELIST_V1", + "type": "bytes", + "value": "[109, 105, 110, 116, 95, 119, 104, 105, 116, 101, 108, 105, 115, 116, 95, 118, 49]" + }, { "name": "PUMP_CREATOR_VAULT_SEED", "type": { diff --git a/idl/pump_fun_idl.json b/idl/pump_fun_idl.json index 9977ae3..062e66f 100644 --- a/idl/pump_fun_idl.json +++ b/idl/pump_fun_idl.json @@ -7,28 +7,64 @@ "description": "Created with Anchor" }, "instructions": [ + { + "name": "add_quote_mint", + "discriminator": [111, 121, 21, 56, 40, 24, 94, 209], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "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": "quote_mint", + "type": "pubkey" + } + ] + }, { "name": "admin_set_creator", "docs": [ "Allows Global::admin_set_creator_authority to override the bonding curve creator" ], - "discriminator": [ - 69, - 25, - 171, - 142, - 57, - 239, - 13, - 4 - ], + "discriminator": [69, 25, 171, 142, 57, 239, 13, 4], "accounts": [ { "name": "admin_set_creator_authority", "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "global", @@ -36,14 +72,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -59,19 +88,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -88,23 +105,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -123,23 +125,12 @@ }, { "name": "admin_set_idl_authority", - "discriminator": [ - 8, - 217, - 96, - 231, - 144, - 104, - 192, - 5 - ], + "discriminator": [8, 217, 96, 231, 144, 104, 192, 5], "accounts": [ { "name": "authority", "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "global", @@ -147,14 +138,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -180,23 +164,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -215,24 +184,13 @@ }, { "name": "admin_update_token_incentives", - "discriminator": [ - 209, - 11, - 115, - 87, - 213, - 23, - 124, - 204 - ], + "discriminator": [209, 11, 115, 87, 213, 23, 124, 204], "accounts": [ { "name": "authority", "writable": true, "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "global", @@ -240,14 +198,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -260,31 +211,8 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] } ] @@ -314,38 +242,9 @@ "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 + 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 ] } } @@ -368,23 +267,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -419,19 +303,8 @@ }, { "name": "buy", - "docs": [ - "Buys tokens from a bonding curve." - ], - "discriminator": [ - 102, - 6, - 61, - 18, - 1, - 218, - 235, - 234 - ], + "docs": ["Buys tokens from a bonding curve."], + "discriminator": [102, 6, 61, 18, 1, 218, 235, 234], "accounts": [ { "name": "global", @@ -439,14 +312,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -466,19 +332,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -509,38 +363,9 @@ "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 + 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 ] } } @@ -569,19 +394,7 @@ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, - 111, - 114, - 45, - 118, - 97, - 117, - 108, - 116 + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 ] }, { @@ -599,23 +412,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -632,31 +430,8 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] } ] @@ -670,29 +445,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -708,54 +462,14 @@ "seeds": [ { "kind": "const", - "value": [ - 102, - 101, - 101, - 95, - 99, - 111, - 110, - 102, - 105, - 103 - ] + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] }, { "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 + 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 ] } ], @@ -789,6 +503,412 @@ } ] }, + { + "name": "buy_exact_quote_in_v2", + "discriminator": [194, 171, 28, 70, 104, 77, 91, 47], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "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 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "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 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_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 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "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 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "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 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "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 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "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 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "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 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "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", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [ + { + "name": "spendable_quote_in", + "type": "u64" + }, + { + "name": "min_tokens_out", + "type": "u64" + } + ] + }, { "name": "buy_exact_sol_in", "docs": [ @@ -818,16 +938,7 @@ "- creator_vault: rent.minimum_balance(0)", "- user_volume_accumulator: rent.minimum_balance(UserVolumeAccumulator::LEN)" ], - "discriminator": [ - 56, - 252, - 116, - 8, - 158, - 223, - 205, - 95 - ], + "discriminator": [56, 252, 116, 8, 158, 223, 205, 95], "accounts": [ { "name": "global", @@ -835,14 +946,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -862,19 +966,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -905,38 +997,9 @@ "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 + 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 ] } } @@ -965,19 +1028,7 @@ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, - 111, - 114, - 45, - 118, - 97, - 117, - 108, - 116 + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 ] }, { @@ -995,23 +1046,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -1028,31 +1064,8 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] } ] @@ -1066,29 +1079,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -1104,54 +1096,14 @@ "seeds": [ { "kind": "const", - "value": [ - 102, - 101, - 101, - 95, - 99, - 111, - 110, - 102, - 105, - 103 - ] + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] }, { "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 + 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 ] } ], @@ -1186,17 +1138,414 @@ ] }, { - "name": "claim_cashback", - "discriminator": [ - 37, - 58, - 35, - 126, - 190, - 53, - 228, - 197 + "name": "buy_v2", + "discriminator": [184, 23, 238, 97, 103, 197, 211, 61], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "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 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "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 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_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 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "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 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "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 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "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 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "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 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "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 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "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", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "max_sol_cost", + "type": "u64" + } + ] + }, + { + "name": "claim_cashback", + "discriminator": [37, 58, 35, 126, 190, 53, 228, 197], "accounts": [ { "name": "user", @@ -1210,29 +1559,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -1253,23 +1581,118 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [] + }, + { + "name": "claim_cashback_v2", + "discriminator": [122, 243, 204, 65, 94, 116, 29, 55], + "accounts": [ + { + "name": "user", + "writable": true + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "associated_quote_user", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "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 ] } ] @@ -1284,16 +1707,7 @@ }, { "name": "claim_token_incentives", - "discriminator": [ - 16, - 4, - 71, - 28, - 204, - 1, - 40, - 27 - ], + "discriminator": [16, 4, 71, 28, 204, 1, 40, 27], "accounts": [ { "name": "user" @@ -1319,38 +1733,9 @@ "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 + 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 ] } } @@ -1362,31 +1747,8 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] } ] @@ -1413,38 +1775,9 @@ "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 + 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 ] } } @@ -1457,29 +1790,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -1491,9 +1803,7 @@ }, { "name": "mint", - "relations": [ - "global_volume_accumulator" - ] + "relations": ["global_volume_accumulator"] }, { "name": "token_program" @@ -1513,23 +1823,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -1549,16 +1844,7 @@ }, { "name": "close_user_volume_accumulator", - "discriminator": [ - 249, - 69, - 164, - 218, - 150, - 103, - 84, - 138 - ], + "discriminator": [249, 69, 164, 218, 150, 103, 84, 138], "accounts": [ { "name": "user", @@ -1573,29 +1859,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -1612,23 +1877,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -1645,16 +1895,7 @@ "docs": [ "Collects creator_fee from creator_vault to the coin creator account" ], - "discriminator": [ - 20, - 22, - 86, - 123, - 198, - 28, - 219, - 132 - ], + "discriminator": [20, 22, 86, 123, 198, 28, 219, 132], "accounts": [ { "name": "creator", @@ -1668,19 +1909,7 @@ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, - 111, - 114, - 45, - 118, - 97, - 117, - 108, - 116 + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 ] }, { @@ -1701,23 +1930,119 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "collect_creator_fee_v2", + "docs": [ + "Collects creator_fee from creator_vault to the coin creator account" + ], + "discriminator": [207, 17, 138, 242, 4, 34, 19, 56], + "accounts": [ + { + "name": "creator", + "writable": true + }, + { + "name": "creator_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "creator" + } + ] + } + }, + { + "name": "creator_vault_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "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 ] } ] @@ -1731,19 +2056,8 @@ }, { "name": "create", - "docs": [ - "Creates a new coin and bonding curve." - ], - "discriminator": [ - 24, - 30, - 200, - 40, - 5, - 28, - 7, - 119 - ], + "docs": ["Creates a new coin and bonding curve."], + "discriminator": [24, 30, 200, 40, 5, 28, 7, 119], "accounts": [ { "name": "mint", @@ -1757,19 +2071,7 @@ { "kind": "const", "value": [ - 109, - 105, - 110, - 116, - 45, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, + 109, 105, 110, 116, 45, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] } @@ -1784,19 +2086,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -1818,38 +2108,9 @@ { "kind": "const", "value": [ - 6, - 221, - 246, - 225, - 215, - 101, - 161, - 147, - 217, - 203, - 225, - 70, - 206, - 235, - 121, - 172, - 28, - 180, - 133, - 237, - 95, - 91, - 55, - 145, - 58, - 140, - 245, - 133, - 126, - 255, - 0, - 169 + 6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, + 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, + 245, 133, 126, 255, 0, 169 ] }, { @@ -1860,38 +2121,9 @@ "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 + 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 ] } } @@ -1902,14 +2134,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -1925,52 +2150,14 @@ "seeds": [ { "kind": "const", - "value": [ - 109, - 101, - 116, - 97, - 100, - 97, - 116, - 97 - ] + "value": [109, 101, 116, 97, 100, 97, 116, 97] }, { "kind": "const", "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, + 4, 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, + 209, 188, 3, 248, 41, 70 ] }, { @@ -2012,23 +2199,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -2059,19 +2231,8 @@ }, { "name": "create_v2", - "docs": [ - "Creates a new spl-22 coin and bonding curve." - ], - "discriminator": [ - 214, - 144, - 76, - 236, - 95, - 139, - 49, - 180 - ], + "docs": ["Creates a new spl-22 coin and bonding curve."], + "discriminator": [214, 144, 76, 236, 95, 139, 49, 180], "accounts": [ { "name": "mint", @@ -2085,19 +2246,7 @@ { "kind": "const", "value": [ - 109, - 105, - 110, - 116, - 45, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, + 109, 105, 110, 116, 45, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] } @@ -2112,19 +2261,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -2155,38 +2292,9 @@ "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 + 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 ] } } @@ -2197,14 +2305,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -2238,57 +2339,16 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 45, - 112, - 97, - 114, - 97, - 109, - 115 + 103, 108, 111, 98, 97, 108, 45, 112, 97, 114, 97, 109, 115 ] } ], "program": { "kind": "const", "value": [ - 5, - 42, - 229, - 215, - 167, - 218, - 167, - 36, - 166, - 234, - 176, - 167, - 41, - 84, - 145, - 133, - 90, - 212, - 160, - 103, - 22, - 96, - 103, - 76, - 78, - 3, - 69, - 89, - 128, - 61, - 101, - 163 + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 ] } } @@ -2300,54 +2360,15 @@ "seeds": [ { "kind": "const", - "value": [ - 115, - 111, - 108, - 45, - 118, - 97, - 117, - 108, - 116 - ] + "value": [115, 111, 108, 45, 118, 97, 117, 108, 116] } ], "program": { "kind": "const", "value": [ - 5, - 42, - 229, - 215, - 167, - 218, - 167, - 36, - 166, - 234, - 176, - 167, - 41, - 84, - 145, - 133, - 90, - 212, - 160, - 103, - 22, - 96, - 103, - 76, - 78, - 3, - 69, - 89, - 128, - 61, - 101, - 163 + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 ] } } @@ -2360,18 +2381,7 @@ { "kind": "const", "value": [ - 109, - 97, - 121, - 104, - 101, - 109, - 45, - 115, - 116, - 97, - 116, - 101 + 109, 97, 121, 104, 101, 109, 45, 115, 116, 97, 116, 101 ] }, { @@ -2382,38 +2392,9 @@ "program": { "kind": "const", "value": [ - 5, - 42, - 229, - 215, - 167, - 218, - 167, - 36, - 166, - 234, - 176, - 167, - 41, - 84, - 145, - 133, - 90, - 212, - 160, - 103, - 22, - 96, - 103, - 76, - 78, - 3, - 69, - 89, - 128, - 61, - 101, - 163 + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 ] } } @@ -2429,23 +2410,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -2493,22 +2459,11 @@ "The creator vault needs to have at least the minimum distributable amount to distribute fees", "This can be checked with the get_minimum_distributable_fee instruction" ], - "discriminator": [ - 165, - 114, - 103, - 0, - 121, - 206, - 247, - 81 - ], + "discriminator": [165, 114, 103, 0, 121, 206, 247, 81], "accounts": [ { "name": "mint", - "relations": [ - "sharing_config" - ] + "relations": ["sharing_config"] }, { "name": "bonding_curve", @@ -2517,19 +2472,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -2546,19 +2489,7 @@ { "kind": "const", "value": [ - 115, - 104, - 97, - 114, - 105, - 110, - 103, - 45, - 99, - 111, - 110, - 102, - 105, + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, 103 ] }, @@ -2570,38 +2501,9 @@ "program": { "kind": "const", "value": [ - 12, - 53, - 255, - 169, - 5, - 90, - 142, - 86, - 141, - 168, - 247, - 188, - 7, - 86, - 21, - 39, - 76, - 241, - 201, - 44, - 164, - 31, - 64, - 0, - 156, - 81, - 106, - 164, - 20, - 194, - 124, - 112 + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 ] } } @@ -2614,19 +2516,7 @@ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, - 111, - 114, - 45, - 118, - 97, - 117, - 108, - 116 + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 ] }, { @@ -2648,23 +2538,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -2682,21 +2557,157 @@ } } }, + { + "name": "distribute_creator_fees_v2", + "discriminator": [255, 203, 19, 79, 244, 68, 8, 159], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "mint", + "relations": ["sharing_config"] + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "sharing_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "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", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "creator_vault_quote_token_account", + "docs": [ + "Deserialized manually in the handler for non-legacy quote mints." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "initialize_ata", + "type": "bool" + } + ], + "returns": { + "defined": { + "name": "DistributeCreatorFeesEvent" + } + } + }, { "name": "extend_account", - "docs": [ - "Extends the size of program-owned accounts" - ], - "discriminator": [ - 234, - 102, - 194, - 203, - 150, - 72, - 62, - 229 - ], + "docs": ["Extends the size of program-owned accounts"], + "discriminator": [234, 102, 194, 203, 150, 72, 62, 229], "accounts": [ { "name": "account", @@ -2717,23 +2728,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -2751,22 +2747,11 @@ "Permissionless instruction to check the minimum required fees for distribution", "Returns the minimum required balance from the creator_vault and whether distribution can proceed" ], - "discriminator": [ - 117, - 225, - 127, - 202, - 134, - 95, - 68, - 35 - ], + "discriminator": [117, 225, 127, 202, 134, 95, 68, 35], "accounts": [ { "name": "mint", - "relations": [ - "sharing_config" - ] + "relations": ["sharing_config"] }, { "name": "bonding_curve", @@ -2775,19 +2760,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -2804,19 +2777,7 @@ { "kind": "const", "value": [ - 115, - 104, - 97, - 114, - 105, - 110, - 103, - 45, - 99, - 111, - 110, - 102, - 105, + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, 103 ] }, @@ -2828,38 +2789,9 @@ "program": { "kind": "const", "value": [ - 12, - 53, - 255, - 169, - 5, - 90, - 142, - 86, - 141, - 168, - 247, - 188, - 7, - 86, - 21, - 39, - 76, - 241, - 201, - 44, - 164, - 31, - 64, - 0, - 156, - 81, - 106, - 164, - 20, - 194, - 124, - 112 + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 ] } } @@ -2871,19 +2803,7 @@ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, - 111, - 114, - 45, - 118, - 97, - 117, - 108, - 116 + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 ] }, { @@ -2904,16 +2824,7 @@ }, { "name": "init_user_volume_accumulator", - "discriminator": [ - 94, - 6, - 202, - 115, - 255, - 96, - 232, - 183 - ], + "discriminator": [94, 6, 202, 115, 255, 96, 232, 183], "accounts": [ { "name": "payer", @@ -2931,29 +2842,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -2974,23 +2864,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -3004,19 +2879,8 @@ }, { "name": "initialize", - "docs": [ - "Creates the global state." - ], - "discriminator": [ - 175, - 175, - 109, - 31, - 13, - 152, - 155, - 237 - ], + "docs": ["Creates the global state."], + "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], "accounts": [ { "name": "global", @@ -3025,14 +2889,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -3054,16 +2911,7 @@ "docs": [ "Migrates liquidity to pump_amm if the bonding curve is complete" ], - "discriminator": [ - 155, - 234, - 231, - 146, - 236, - 158, - 162, - 30 - ], + "discriminator": [155, 234, 231, 146, 236, 158, 162, 30], "accounts": [ { "name": "global", @@ -3071,14 +2919,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -3086,9 +2927,7 @@ { "name": "withdraw_authority", "writable": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "mint" @@ -3101,19 +2940,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -3144,38 +2971,9 @@ "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 + 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 ] } } @@ -3203,19 +3001,11 @@ "seeds": [ { "kind": "const", - "value": [ - 112, - 111, - 111, - 108 - ] + "value": [112, 111, 111, 108] }, { "kind": "const", - "value": [ - 0, - 0 - ] + "value": [0, 0] }, { "kind": "account", @@ -3244,19 +3034,7 @@ { "kind": "const", "value": [ - 112, - 111, - 111, - 108, - 45, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, + 112, 111, 111, 108, 45, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] }, @@ -3322,19 +3100,7 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 99, - 111, - 110, - 102, - 105, - 103 + 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ], @@ -3356,18 +3122,7 @@ { "kind": "const", "value": [ - 112, - 111, - 111, - 108, - 95, - 108, - 112, - 95, - 109, - 105, - 110, - 116 + 112, 111, 111, 108, 95, 108, 112, 95, 109, 105, 110, 116 ] }, { @@ -3468,23 +3223,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ], @@ -3501,52 +3241,31 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] } }, { - "name": "program" + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" } ], "args": [] }, { "name": "migrate_bonding_curve_creator", - "discriminator": [ - 87, - 124, - 52, - 191, - 52, - 38, - 214, - 232 - ], + "discriminator": [87, 124, 52, 191, 52, 38, 214, 232], "accounts": [ { "name": "mint", - "relations": [ - "sharing_config" - ] + "relations": ["sharing_config"] }, { "name": "bonding_curve", @@ -3556,19 +3275,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -3585,19 +3292,7 @@ { "kind": "const", "value": [ - 115, - 104, - 97, - 114, - 105, - 110, - 103, - 45, - 99, - 111, - 110, - 102, - 105, + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, 103 ] }, @@ -3609,38 +3304,9 @@ "program": { "kind": "const", "value": [ - 12, - 53, - 255, - 169, - 5, - 90, - 142, - 86, - 141, - 168, - 247, - 188, - 7, - 86, - 21, - 39, - 76, - 241, - 201, - 44, - 164, - 31, - 64, - 0, - 156, - 81, - 106, - 164, - 20, - 194, - 124, - 112 + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 ] } } @@ -3652,23 +3318,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -3681,38 +3332,431 @@ "args": [] }, { - "name": "sell", + "name": "migrate_v2", "docs": [ - "Sells tokens into a bonding curve.", - "For cashback coins, optionally pass user_volume_accumulator as remaining_accounts[0].", - "If provided and valid, creator_fee goes to user_volume_accumulator.", - "Otherwise, falls back to transferring creator_fee to creator_vault." - ], - "discriminator": [ - 51, - 230, - 133, - 164, - 1, - 127, - 131, - 173 + "Migrates liquidity to pump_amm if the bonding curve is complete" ], + "discriminator": [187, 203, 18, 31, 206, 237, 254, 41], "accounts": [ { "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "withdraw_authority", + "writable": true, + "relations": ["global"] + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "bonding_curve", + "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_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 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "pump_amm", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 111, 108] + }, + { + "kind": "const", + "value": [0, 0] + }, + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "pool_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, 111, 111, 108, 45, 97, 117, 116, 104, 111, 114, 105, 116, + 121 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "pool_authority_mint_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_authority_quote_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "amm_global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 + ] + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, 111, 111, 108, 95, 108, 112, 95, 109, 105, 110, 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "user_pool_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "token_2022_program" + }, + { + "kind": "account", + "path": "lp_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_base_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_quote_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "pump_amm_event_authority" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "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": "remove_quote_mint", + "discriminator": [177, 65, 223, 38, 88, 209, 158, 155], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "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": "quote_mint", + "type": "pubkey" + } + ] + }, + { + "name": "sell", + "docs": [ + "Sells tokens into a bonding curve.", + "For cashback coins, pass as remaining_accounts: [0] user_volume_accumulator,", + "[1] bonding_curve_v2. If provided and valid, creator_fee goes to user_volume_accumulator.", + "Otherwise, falls back to transferring creator_fee to creator_vault." + ], + "discriminator": [51, 230, 133, 164, 1, 127, 131, 173], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -3732,19 +3776,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -3775,38 +3807,9 @@ "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 + 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 ] } } @@ -3832,19 +3835,7 @@ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, - 111, - 114, - 45, - 118, - 97, - 117, - 108, - 116 + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 ] }, { @@ -3865,23 +3856,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -3897,54 +3873,14 @@ "seeds": [ { "kind": "const", - "value": [ - 102, - 101, - 101, - 95, - 99, - 111, - 110, - 102, - 105, - 103 - ] + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] }, { "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 + 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 ] } ], @@ -3970,28 +3906,409 @@ } ] }, + { + "name": "sell_v2", + "discriminator": [93, 246, 130, 60, 231, 233, 64, 178], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "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 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "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 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_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 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "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 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "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 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "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 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "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 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "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 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "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", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "min_sol_output", + "type": "u64" + } + ] + }, { "name": "set_creator", "docs": [ "Allows Global::set_creator_authority to set the bonding curve creator from Metaplex metadata or input argument" ], - "discriminator": [ - 254, - 148, - 255, - 112, - 207, - 142, - 170, - 165 - ], + "discriminator": [254, 148, 255, 112, 207, 142, 170, 165], "accounts": [ { "name": "set_creator_authority", "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "global", @@ -3999,14 +4316,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -4020,52 +4330,14 @@ "seeds": [ { "kind": "const", - "value": [ - 109, - 101, - 116, - 97, - 100, - 97, - 116, - 97 - ] + "value": [109, 101, 116, 97, 100, 97, 116, 97] }, { "kind": "const", "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, + 4, 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, + 209, 188, 3, 248, 41, 70 ] }, { @@ -4076,38 +4348,9 @@ "program": { "kind": "const", "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, 4, + 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, 209, + 188, 3, 248, 41, 70 ] } } @@ -4120,19 +4363,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -4149,23 +4380,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4184,16 +4400,7 @@ }, { "name": "set_mayhem_virtual_params", - "discriminator": [ - 61, - 169, - 188, - 191, - 153, - 149, - 42, - 97 - ], + "discriminator": [61, 169, 188, 191, 153, 149, 42, 97], "accounts": [ { "name": "sol_vault_authority", @@ -4203,54 +4410,15 @@ "seeds": [ { "kind": "const", - "value": [ - 115, - 111, - 108, - 45, - 118, - 97, - 117, - 108, - 116 - ] + "value": [115, 111, 108, 45, 118, 97, 117, 108, 116] } ], "program": { "kind": "const", "value": [ - 5, - 42, - 229, - 215, - 167, - 218, - 167, - 36, - 166, - 234, - 176, - 167, - 41, - 84, - 145, - 133, - 90, - 212, - 160, - 103, - 22, - 96, - 103, - 76, - 78, - 3, - 69, - 89, - 128, - 61, - 101, - 163 + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 ] } } @@ -4276,38 +4444,9 @@ "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 + 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 ] } } @@ -4321,14 +4460,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -4341,19 +4473,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -4374,23 +4494,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4407,16 +4512,7 @@ "docs": [ "Syncs the bonding curve creator with the Metaplex metadata creator if it exists" ], - "discriminator": [ - 138, - 96, - 174, - 217, - 48, - 85, - 197, - 246 - ], + "discriminator": [138, 96, 174, 217, 48, 85, 197, 246], "accounts": [ { "name": "mint" @@ -4427,52 +4523,14 @@ "seeds": [ { "kind": "const", - "value": [ - 109, - 101, - 116, - 97, - 100, - 97, - 116, - 97 - ] + "value": [109, 101, 116, 97, 100, 97, 116, 97] }, { "kind": "const", "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, + 4, 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, + 209, 188, 3, 248, 41, 70 ] }, { @@ -4483,38 +4541,9 @@ "program": { "kind": "const", "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, 4, + 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, 209, + 188, 3, 248, 41, 70 ] } } @@ -4527,19 +4556,7 @@ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 ] }, { @@ -4556,23 +4573,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4586,19 +4588,8 @@ }, { "name": "set_params", - "docs": [ - "Sets the global state parameters." - ], - "discriminator": [ - 27, - 234, - 178, - 52, - 147, - 2, - 187, - 141 - ], + "docs": ["Sets the global state parameters."], + "discriminator": [27, 234, 178, 52, 147, 2, 187, 141], "accounts": [ { "name": "global", @@ -4607,14 +4598,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -4623,9 +4607,7 @@ "name": "authority", "writable": true, "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "event_authority", @@ -4634,23 +4616,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4709,16 +4676,7 @@ }, { "name": "set_reserved_fee_recipients", - "discriminator": [ - 111, - 172, - 162, - 232, - 114, - 89, - 213, - 142 - ], + "discriminator": [111, 172, 162, 232, 114, 89, 213, 142], "accounts": [ { "name": "global", @@ -4727,14 +4685,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -4742,9 +4693,7 @@ { "name": "authority", "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "event_authority", @@ -4753,23 +4702,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4787,17 +4721,55 @@ ] }, { - "name": "sync_user_volume_accumulator", - "discriminator": [ - 86, - 31, - 192, - 87, - 163, - 87, - 79, - 238 + "name": "set_virtual_quote_reserves", + "discriminator": [101, 135, 191, 104, 9, 88, 20, 96], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "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": "initial_virtual_quote_reserves", + "type": "u64" + } + ] + }, + { + "name": "sync_user_volume_accumulator", + "discriminator": [86, 31, 192, 87, 163, 87, 79, 238], "accounts": [ { "name": "user" @@ -4809,31 +4781,8 @@ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] } ] @@ -4847,29 +4796,8 @@ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 ] }, { @@ -4886,23 +4814,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4916,16 +4829,7 @@ }, { "name": "toggle_cashback_enabled", - "discriminator": [ - 115, - 103, - 224, - 255, - 189, - 89, - 86, - 195 - ], + "discriminator": [115, 103, 224, 255, 189, 89, 86, 195], "accounts": [ { "name": "global", @@ -4934,14 +4838,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -4950,9 +4847,7 @@ "name": "authority", "writable": true, "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "event_authority", @@ -4961,23 +4856,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -4996,16 +4876,7 @@ }, { "name": "toggle_create_v2", - "discriminator": [ - 28, - 255, - 230, - 240, - 172, - 107, - 203, - 171 - ], + "discriminator": [28, 255, 230, 240, 172, 107, 203, 171], "accounts": [ { "name": "global", @@ -5014,14 +4885,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -5030,9 +4894,7 @@ "name": "authority", "writable": true, "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "event_authority", @@ -5041,23 +4903,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -5076,16 +4923,7 @@ }, { "name": "toggle_mayhem_mode", - "discriminator": [ - 1, - 9, - 111, - 208, - 100, - 31, - 255, - 163 - ], + "discriminator": [1, 9, 111, 208, 100, 31, 255, 163], "accounts": [ { "name": "global", @@ -5094,14 +4932,7 @@ "seeds": [ { "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "value": [103, 108, 111, 98, 97, 108] } ] } @@ -5110,9 +4941,7 @@ "name": "authority", "writable": true, "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "event_authority", @@ -5121,23 +4950,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -5155,43 +4969,74 @@ ] }, { - "name": "update_global_authority", - "discriminator": [ - 227, - 181, - 74, - 196, - 208, - 21, - 97, - 213 - ], + "name": "update_buyback_config", + "discriminator": [251, 224, 171, 146, 160, 26, 113, 233], "accounts": [ { "name": "global", "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", "pda": { "seeds": [ { "kind": "const", "value": [ - 103, - 108, - 111, - 98, - 97, - 108 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] } }, + { + "name": "program" + } + ], + "args": [ + { + "name": "buyback_basis_points", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "update_global_authority", + "discriminator": [227, 181, 74, 196, 208, 21, 97, 213], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, { "name": "authority", "signer": true, - "relations": [ - "global" - ] + "relations": ["global"] }, { "name": "new_authority" @@ -5203,23 +5048,8 @@ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, - 111, - 114, - 105, - 116, - 121 + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 ] } ] @@ -5235,382 +5065,121 @@ "accounts": [ { "name": "BondingCurve", - "discriminator": [ - 23, - 183, - 248, - 55, - 96, - 216, - 172, - 96 - ] + "discriminator": [23, 183, 248, 55, 96, 216, 172, 96] }, { "name": "FeeConfig", - "discriminator": [ - 143, - 52, - 146, - 187, - 219, - 123, - 76, - 155 - ] + "discriminator": [143, 52, 146, 187, 219, 123, 76, 155] }, { "name": "Global", - "discriminator": [ - 167, - 232, - 232, - 177, - 200, - 108, - 114, - 127 - ] + "discriminator": [167, 232, 232, 177, 200, 108, 114, 127] }, { "name": "GlobalVolumeAccumulator", - "discriminator": [ - 202, - 42, - 246, - 43, - 142, - 190, - 30, - 255 - ] + "discriminator": [202, 42, 246, 43, 142, 190, 30, 255] }, { "name": "SharingConfig", - "discriminator": [ - 216, - 74, - 9, - 0, - 56, - 140, - 93, - 75 - ] + "discriminator": [216, 74, 9, 0, 56, 140, 93, 75] }, { "name": "UserVolumeAccumulator", - "discriminator": [ - 86, - 255, - 112, - 14, - 102, - 53, - 154, - 250 - ] + "discriminator": [86, 255, 112, 14, 102, 53, 154, 250] } ], "events": [ { "name": "AdminSetCreatorEvent", - "discriminator": [ - 64, - 69, - 192, - 104, - 29, - 30, - 25, - 107 - ] + "discriminator": [64, 69, 192, 104, 29, 30, 25, 107] }, { "name": "AdminSetIdlAuthorityEvent", - "discriminator": [ - 245, - 59, - 70, - 34, - 75, - 185, - 109, - 92 - ] + "discriminator": [245, 59, 70, 34, 75, 185, 109, 92] }, { "name": "AdminUpdateTokenIncentivesEvent", - "discriminator": [ - 147, - 250, - 108, - 120, - 247, - 29, - 67, - 222 - ] + "discriminator": [147, 250, 108, 120, 247, 29, 67, 222] }, { "name": "ClaimCashbackEvent", - "discriminator": [ - 226, - 214, - 246, - 33, - 7, - 242, - 147, - 229 - ] + "discriminator": [226, 214, 246, 33, 7, 242, 147, 229] }, { "name": "ClaimTokenIncentivesEvent", - "discriminator": [ - 79, - 172, - 246, - 49, - 205, - 91, - 206, - 232 - ] + "discriminator": [79, 172, 246, 49, 205, 91, 206, 232] }, { "name": "CloseUserVolumeAccumulatorEvent", - "discriminator": [ - 146, - 159, - 189, - 172, - 146, - 88, - 56, - 244 - ] + "discriminator": [146, 159, 189, 172, 146, 88, 56, 244] }, { "name": "CollectCreatorFeeEvent", - "discriminator": [ - 122, - 2, - 127, - 1, - 14, - 191, - 12, - 175 - ] + "discriminator": [122, 2, 127, 1, 14, 191, 12, 175] }, { "name": "CompleteEvent", - "discriminator": [ - 95, - 114, - 97, - 156, - 212, - 46, - 152, - 8 - ] + "discriminator": [95, 114, 97, 156, 212, 46, 152, 8] }, { "name": "CompletePumpAmmMigrationEvent", - "discriminator": [ - 189, - 233, - 93, - 185, - 92, - 148, - 234, - 148 - ] + "discriminator": [189, 233, 93, 185, 92, 148, 234, 148] }, { "name": "CreateEvent", - "discriminator": [ - 27, - 114, - 169, - 77, - 222, - 235, - 99, - 118 - ] + "discriminator": [27, 114, 169, 77, 222, 235, 99, 118] }, { "name": "DistributeCreatorFeesEvent", - "discriminator": [ - 165, - 55, - 129, - 112, - 4, - 179, - 202, - 40 - ] + "discriminator": [165, 55, 129, 112, 4, 179, 202, 40] }, { "name": "ExtendAccountEvent", - "discriminator": [ - 97, - 97, - 215, - 144, - 93, - 146, - 22, - 124 - ] + "discriminator": [97, 97, 215, 144, 93, 146, 22, 124] }, { "name": "InitUserVolumeAccumulatorEvent", - "discriminator": [ - 134, - 36, - 13, - 72, - 232, - 101, - 130, - 216 - ] + "discriminator": [134, 36, 13, 72, 232, 101, 130, 216] }, { "name": "MigrateBondingCurveCreatorEvent", - "discriminator": [ - 155, - 167, - 104, - 220, - 213, - 108, - 243, - 3 - ] + "discriminator": [155, 167, 104, 220, 213, 108, 243, 3] }, { "name": "MinimumDistributableFeeEvent", - "discriminator": [ - 168, - 216, - 132, - 239, - 235, - 182, - 49, - 52 - ] + "discriminator": [168, 216, 132, 239, 235, 182, 49, 52] }, { "name": "ReservedFeeRecipientsEvent", - "discriminator": [ - 43, - 188, - 250, - 18, - 221, - 75, - 187, - 95 - ] + "discriminator": [43, 188, 250, 18, 221, 75, 187, 95] }, { "name": "SetCreatorEvent", - "discriminator": [ - 237, - 52, - 123, - 37, - 245, - 251, - 72, - 210 - ] + "discriminator": [237, 52, 123, 37, 245, 251, 72, 210] }, { "name": "SetMetaplexCreatorEvent", - "discriminator": [ - 142, - 203, - 6, - 32, - 127, - 105, - 191, - 162 - ] + "discriminator": [142, 203, 6, 32, 127, 105, 191, 162] }, { "name": "SetParamsEvent", - "discriminator": [ - 223, - 195, - 159, - 246, - 62, - 48, - 143, - 131 - ] + "discriminator": [223, 195, 159, 246, 62, 48, 143, 131] }, { "name": "SyncUserVolumeAccumulatorEvent", - "discriminator": [ - 197, - 122, - 167, - 124, - 116, - 81, - 91, - 255 - ] + "discriminator": [197, 122, 167, 124, 116, 81, 91, 255] }, { "name": "TradeEvent", - "discriminator": [ - 189, - 219, - 127, - 211, - 78, - 230, - 97, - 238 - ] + "discriminator": [189, 219, 127, 211, 78, 230, 97, 238] }, { "name": "UpdateGlobalAuthorityEvent", - "discriminator": [ - 182, - 195, - 137, - 42, - 35, - 206, - 207, - 247 - ] + "discriminator": [182, 195, 137, 42, 35, 206, 207, 247] }, { "name": "UpdateMayhemVirtualParamsEvent", - "discriminator": [ - 117, - 123, - 228, - 182, - 161, - 168, - 220, - 214 - ] + "discriminator": [117, 123, 228, 182, 161, 168, 220, 214] } ], "errors": [ @@ -5883,6 +5452,78 @@ "code": 6056, "name": "CashbackNotEnabled", "msg": "Cashback is not enabled" + }, + { + "code": 6057, + "name": "BuybackFeeRecipientNotAuthorized", + "msg": "Buyback fee recipient not authorized" + }, + { + "code": 6058, + "name": "AllBuybackFeeRecipientsShouldBeNonZero" + }, + { + "code": 6059, + "name": "NotUniqueBuybackFeeRecipients" + }, + { + "code": 6060, + "name": "BuybackBasisPointsOutOfRange", + "msg": "buyback_basis_points must be <= 10_000" + }, + { + "code": 6061, + "name": "WrongBuybackFeeRecipientsCount", + "msg": "buyback fee recipients require exactly 8 remaining accounts (or none)" + }, + { + "code": 6062, + "name": "BuybackFeeRecipientMissing" + }, + { + "code": 6063, + "name": "UnsupportedQuoteMint", + "msg": "Unsupported quote mint" + }, + { + "code": 6064, + "name": "InvalidQuoteTokenProgram", + "msg": "Create v2: quote token program must be legacy SPL Token" + }, + { + "code": 6065, + "name": "InvalidAssociatedQuoteBondingCurve", + "msg": "Create v2: associated quote bonding curve address does not match derivation" + }, + { + "code": 6066, + "name": "QuoteMintWhitelistFull", + "msg": "Quote mint whitelist is full" + }, + { + "code": 6067, + "name": "QuoteMintAlreadyWhitelisted", + "msg": "Quote mint is already whitelisted" + }, + { + "code": 6068, + "name": "QuoteMintNotWhitelisted", + "msg": "Quote mint is not in the whitelist" + }, + { + "code": 6069, + "name": "QuoteMintNotEligibleForWhitelist", + "msg": "Quote mint cannot be added or removed via whitelist (default or native SOL mint)" + }, + { + "code": 6070, + "name": "UnableToDistributeCreatorFeesToUninitializedAccount", + "msg": "Unable to distribute creator fees to uninitialized account" + }, + { + "code": 6071, + "name": "MayhemModeQuoteMintNotAllowed", + "msg": "Mayhem mode quote mint not allowed" } ], "types": [ @@ -5976,7 +5617,7 @@ "type": "u64" }, { - "name": "virtual_sol_reserves", + "name": "virtual_quote_reserves", "type": "u64" }, { @@ -5984,7 +5625,7 @@ "type": "u64" }, { - "name": "real_sol_reserves", + "name": "real_quote_reserves", "type": "u64" }, { @@ -6006,6 +5647,10 @@ { "name": "is_cashback_coin", "type": "bool" + }, + { + "name": "quote_mint", + "type": "pubkey" } ] } @@ -6118,6 +5763,10 @@ { "name": "creator_fee", "type": "u64" + }, + { + "name": "quote_mint", + "type": "pubkey" } ] } @@ -6142,6 +5791,10 @@ { "name": "timestamp", "type": "i64" + }, + { + "name": "quote_mint", + "type": "pubkey" } ] } @@ -6182,6 +5835,10 @@ { "name": "pool", "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" } ] } @@ -6264,6 +5921,14 @@ { "name": "is_cashback_enabled", "type": "bool" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "virtual_quote_reserves", + "type": "u64" } ] } @@ -6306,6 +5971,10 @@ { "name": "distributed", "type": "u64" + }, + { + "name": "quote_mint", + "type": "pubkey" } ] } @@ -6368,6 +6037,16 @@ } } } + }, + { + "name": "stable_fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } } ] } @@ -6419,9 +6098,7 @@ "fields": [ { "name": "initialized", - "docs": [ - "Unused" - ], + "docs": ["Unused"], "type": "bool" }, { @@ -6458,9 +6135,7 @@ }, { "name": "enable_migrate", - "docs": [ - "Unused" - ], + "docs": ["Unused"], "type": "bool" }, { @@ -6474,10 +6149,7 @@ { "name": "fee_recipients", "type": { - "array": [ - "pubkey", - 7 - ] + "array": ["pubkey", 7] } }, { @@ -6507,15 +6179,32 @@ { "name": "reserved_fee_recipients", "type": { - "array": [ - "pubkey", - 7 - ] + "array": ["pubkey", 7] } }, { "name": "is_cashback_enabled", "type": "bool" + }, + { + "name": "buyback_fee_recipients", + "type": { + "array": ["pubkey", 8] + } + }, + { + "name": "buyback_basis_points", + "type": "u64" + }, + { + "name": "initial_virtual_quote_reserves", + "type": "u64" + }, + { + "name": "whitelisted_quote_mints", + "type": { + "array": ["pubkey", 1] + } } ] } @@ -6544,19 +6233,13 @@ { "name": "total_token_supply", "type": { - "array": [ - "u64", - 30 - ] + "array": ["u64", 30] } }, { "name": "sol_volumes", "type": { - "array": [ - "u64", - 30 - ] + "array": ["u64", 30] } } ] @@ -6638,9 +6321,7 @@ "name": "OptionBool", "type": { "kind": "struct", - "fields": [ - "bool" - ] + "fields": ["bool"] } }, { @@ -6659,10 +6340,7 @@ { "name": "reserved_fee_recipients", "type": { - "array": [ - "pubkey", - 7 - ] + "array": ["pubkey", 7] } } ] @@ -6768,10 +6446,7 @@ { "name": "fee_recipients", "type": { - "array": [ - "pubkey", - 8 - ] + "array": ["pubkey", 8] } }, { @@ -6877,9 +6552,7 @@ }, { "name": "TradeEvent", - "docs": [ - "ix_name: \"buy\" | \"sell\" | \"buy_exact_sol_in\"" - ], + "docs": ["ix_name: \"buy\" | \"sell\" | \"buy_exact_sol_in\""], "type": { "kind": "struct", "fields": [ @@ -6982,6 +6655,40 @@ { "name": "cashback", "type": "u64" + }, + { + "name": "buyback_fee_basis_points", + "type": "u64" + }, + { + "name": "buyback_fee", + "type": "u64" + }, + { + "name": "shareholders", + "type": { + "vec": { + "defined": { + "name": "Shareholder" + } + } + } + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "quote_amount", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "u64" + }, + { + "name": "real_quote_reserves", + "type": "u64" } ] } @@ -7090,9 +6797,17 @@ { "name": "total_cashback_claimed", "type": "u64" + }, + { + "name": "stable_cashback_earned", + "type": "u64" + }, + { + "name": "total_stable_cashback_claimed", + "type": "u64" } ] } } ] -} \ No newline at end of file +} diff --git a/idl/pump_swap_idl.json b/idl/pump_swap_idl.json index 47d7b95..a654b6f 100644 --- a/idl/pump_swap_idl.json +++ b/idl/pump_swap_idl.json @@ -288,6 +288,213 @@ } ] }, + { + "name": "boost_buy_and_burn", + "discriminator": [ + 105, + 68, + 6, + 175, + 0, + 7, + 35, + 162 + ], + "accounts": [ + { + "name": "pool" + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "base_mint", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "boost_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 111, + 115, + 116, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "boost_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "boost_vault_authority" + }, + { + "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 + ] + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "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": "quote_amount_in", + "type": "u64" + }, + { + "name": "min_base_amount_burned", + "type": "u64" + } + ] + }, { "name": "buy", "docs": [ @@ -2587,6 +2794,208 @@ ], "args": [] }, + { + "name": "init_boost", + "discriminator": [ + 140, + 233, + 33, + 94, + 132, + 90, + 194, + 143 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "pool_base_token_account", + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "boost_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 111, + 115, + 116, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "boost_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "boost_vault_authority" + }, + { + "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 + ] + } + } + }, + { + "name": "quote_token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "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": "init_user_volume_accumulator", "discriminator": [ @@ -3175,6 +3584,72 @@ } ] }, + { + "name": "set_boost_authority", + "discriminator": [ + 227, + 149, + 76, + 42, + 130, + 39, + 234, + 205 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "boost_authority" + }, + { + "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": "set_coin_creator", "docs": [ @@ -3610,6 +4085,38 @@ ], "args": [] }, + { + "name": "toggle_boost", + "discriminator": [ + 117, + 161, + 160, + 74, + 223, + 137, + 118, + 99 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, { "name": "toggle_cashback_enabled", "discriminator": [ @@ -3965,6 +4472,254 @@ ], "args": [] }, + { + "name": "transfer_creator_fees_to_pump_v2", + "discriminator": [ + 1, + 33, + 78, + 185, + 33, + 67, + 44, + 92 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "quote_mint" + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "coin_creator" + }, + { + "name": "coin_creator_vault_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ] + } + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "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 + ] + } + } + }, + { + "name": "pump_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ], + "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": "pump_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pump_creator_vault" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "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": "update_admin", "discriminator": [ @@ -4027,6 +4782,72 @@ ], "args": [] }, + { + "name": "update_buyback_config", + "discriminator": [ + 251, + 224, + 171, + 146, + 160, + 26, + 113, + 233 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "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": "buyback_basis_points", + "type": { + "option": "u64" + } + } + ] + }, { "name": "update_fee_config", "discriminator": [ @@ -4358,6 +5179,19 @@ 222 ] }, + { + "name": "BoostBuyAndBurnEvent", + "discriminator": [ + 63, + 69, + 28, + 22, + 48, + 92, + 194, + 185 + ] + }, { "name": "BuyEvent", "discriminator": [ @@ -4488,6 +5322,19 @@ 124 ] }, + { + "name": "InitBoostEvent", + "discriminator": [ + 174, + 124, + 74, + 249, + 4, + 81, + 246, + 17 + ] + }, { "name": "InitUserVolumeAccumulatorEvent", "discriminator": [ @@ -4553,6 +5400,19 @@ 211 ] }, + { + "name": "SetBoostAuthorityEvent", + "discriminator": [ + 89, + 128, + 240, + 141, + 91, + 202, + 71, + 105 + ] + }, { "name": "SetMetaplexCoinCreatorEvent", "discriminator": [ @@ -4835,7 +5695,79 @@ }, { "code": 6052, - "name": "CashbackEarnedDoesNotMatchTokenInVault" + "name": "TokensInVaultLessThanCashbackEarned" + }, + { + "code": 6053, + "name": "BuybackFeeRecipientNotAuthorized", + "msg": "Buyback fee recipient not authorized" + }, + { + "code": 6054, + "name": "AllBuybackFeeRecipientsShouldBeNonZero" + }, + { + "code": 6055, + "name": "NotUniqueBuybackFeeRecipients" + }, + { + "code": 6056, + "name": "BuybackBasisPointsOutOfRange", + "msg": "buyback_basis_points must be <= 10_000" + }, + { + "code": 6057, + "name": "WrongBuybackFeeRecipientsCount", + "msg": "buyback fee recipients require exactly 8 remaining accounts (or none)" + }, + { + "code": 6058, + "name": "BuybackFeeRecipientMissing" + }, + { + "code": 6059, + "name": "MissingCashbackAccounts", + "msg": "Cashback trade is missing the required remaining accounts" + }, + { + "code": 6060, + "name": "InvalidCashbackAccumulator", + "msg": "Cashback user_volume_accumulator account is invalid" + }, + { + "code": 6061, + "name": "InvalidCashbackAccumulatorAta", + "msg": "Cashback user_volume_accumulator ATA is missing or invalid" + }, + { + "code": 6062, + "name": "InvalidPoolV2", + "msg": "pool_v2 remaining account is missing or invalid" + }, + { + "code": 6063, + "name": "InsufficientRealQuoteReserves", + "msg": "BOOST: sell output exceeds the real quote vault. effective = real + virtual is pricing-only; payout is capped at real_vault, so quote min(out, real_vault)" + }, + { + "code": 6064, + "name": "BoostPoolLiquidityUnsupported", + "msg": "BOOST: deposit/withdraw don't apply to boost pools" + }, + { + "code": 6065, + "name": "PoolCannotBoost", + "msg": "BOOST: pool cannot be boosted (no virtual reserves)" + }, + { + "code": 6066, + "name": "BoostDisabled", + "msg": "BOOST: boost is disabled" + }, + { + "code": 6067, + "name": "SeedLockViolation", + "msg": "BOOST: lp_supply must never drop below the circulating LP mint supply" } ], "types": [ @@ -4951,6 +5883,62 @@ ] } }, + { + "name": "BoostBuyAndBurnEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "quote_amount_in_requested", + "type": "u64" + }, + { + "name": "quote_amount_in_used", + "type": "u64" + }, + { + "name": "base_amount_burned", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "real_quote_reserves_after", + "type": "u64" + }, + { + "name": "base_reserves_after", + "type": "u64" + }, + { + "name": "boost_vault_remaining", + "type": "u64" + } + ] + } + }, { "name": "BuyEvent", "docs": [ @@ -5086,6 +6074,26 @@ { "name": "cashback", "type": "u64" + }, + { + "name": "buyback_fee_basis_points", + "type": "u64" + }, + { + "name": "buyback_fee", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "can_boost", + "type": "bool" + }, + { + "name": "base_supply", + "type": "u64" } ] } @@ -5523,6 +6531,16 @@ } } } + }, + { + "name": "stable_fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } } ] } @@ -5646,6 +6664,27 @@ { "name": "is_cashback_enabled", "type": "bool" + }, + { + "name": "buyback_fee_recipients", + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "buyback_basis_points", + "type": "u64" + }, + { + "name": "boost_authority", + "type": "pubkey" + }, + { + "name": "boost_enabled", + "type": "bool" } ] } @@ -5692,6 +6731,38 @@ ] } }, + { + "name": "InitBoostEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "real_quote_reserves_after", + "type": "u64" + } + ] + } + }, { "name": "InitUserVolumeAccumulatorEvent", "type": { @@ -5808,6 +6879,13 @@ { "name": "is_cashback_coin", "type": "bool" + }, + { + "name": "virtual_quote_reserves", + "docs": [ + "For non-boost pools, value is 0, so the behavior is identical to legacy pools." + ], + "type": "i128" } ] } @@ -5941,6 +7019,26 @@ { "name": "cashback", "type": "u64" + }, + { + "name": "buyback_fee_basis_points", + "type": "u64" + }, + { + "name": "buyback_fee", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "can_boost", + "type": "bool" + }, + { + "name": "base_supply", + "type": "u64" } ] } @@ -5973,6 +7071,30 @@ ] } }, + { + "name": "SetBoostAuthorityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "old_boost_authority", + "type": "pubkey" + }, + { + "name": "new_boost_authority", + "type": "pubkey" + } + ] + } + }, { "name": "SetMetaplexCoinCreatorEvent", "type": { diff --git a/learning-examples/live_listener_matrix.py b/learning-examples/live_listener_matrix.py new file mode 100644 index 0000000..523a909 --- /dev/null +++ b/learning-examples/live_listener_matrix.py @@ -0,0 +1,409 @@ +"""Verify every listener with a real buy_v2 + sell_v2 + ATA cleanup on mainnet. + +WARNING: this submits real transactions and spends real funds. + +For each listener (geyser, logs, blocks, pumpportal) it detects a live token, +buys it, sells it, then closes the base-token ATA so the ~0.002 SOL of rent is +reclaimed rather than stranded. Tests run sequentially so the same funds are +recycled across listeners. + +Each listener exercises a different event-parsing path into the same v2 trade +code, which is the point: pumpportal in particular carries no mayhem/cashback/ +quote_mint flags, so it relies entirely on the on-chain curve refresh. + +Usage: + uv run learning-examples/live_listener_matrix.py --yes + uv run learning-examples/live_listener_matrix.py --yes --listeners geyser,logs + uv run learning-examples/live_listener_matrix.py --cleanup-only [...] +""" + +import asyncio +import os +import sys +from dataclasses import dataclass, field +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from dotenv import load_dotenv # noqa: E402 +from solders.pubkey import Pubkey # noqa: E402 + +from cleanup.manager import AccountCleanupManager # noqa: E402 +from core.client import SolanaClient # noqa: E402 +from core.priority_fee.manager import PriorityFeeManager # noqa: E402 +from core.pubkeys import LAMPORTS_PER_SOL, SystemAddresses # 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 +from trading.platform_aware import PlatformAwareBuyer, PlatformAwareSeller # noqa: E402 + +load_dotenv(PROJECT_ROOT / ".env") + +BUY_AMOUNT_SOL = 0.0001 +EXTREME_FAST_TOKEN_AMOUNT = 20 +HOLD_SECONDS = 5 +PRIORITY_FEE = 1_000_000 +# This harness verifies plumbing, not profitability. Brand-new coins routinely +# lose 90% of their curve SOL within seconds of creation, and a realistic +# sell_slippage of 0.3 then makes the program reject the sell with +# TooLittleSolReceived (6003) — a correct guard, but it stops us proving the +# 26-account sell_v2 actually lands. Accept almost any payout instead; the +# position is worth well under a lamport-thousandth of a SOL either way. +SELL_SLIPPAGE = 0.95 +BUY_SLIPPAGE = 0.3 +DETECT_TIMEOUT_SECONDS = 150.0 +# Time to let a close/sell finalize before reading balances or account state. +SETTLE_SECONDS = 20 +ALL_LISTENERS = ("geyser", "logs", "blocks", "pumpportal") + + +@dataclass +class LegResult: + """Outcome of one listener's buy/sell/cleanup cycle.""" + + listener: str + detected: str | None = None + symbol: str | None = None + quote_mint: str | None = None + buy_ok: bool = False + buy_sig: str | None = None + sell_ok: bool = False + sell_sig: str | None = None + cleanup_ok: bool = False + start_sol: float = 0.0 + end_sol: float = 0.0 + notes: list[str] = field(default_factory=list) + + @property + def passed(self) -> bool: + """Whether the whole cycle succeeded.""" + return self.buy_ok and self.sell_ok and self.cleanup_ok + + +def make_listener(listener_type: str): + """Build a listener of the given type wired to this project's endpoints. + + Args: + listener_type: One of geyser, logs, blocks, pumpportal + + Returns: + Configured listener instance + """ + return ListenerFactory.create_listener( + listener_type=listener_type, + wss_endpoint=os.environ.get("SOLANA_NODE_WSS_ENDPOINT"), + geyser_endpoint=os.environ.get("GEYSER_ENDPOINT"), + geyser_api_token=os.environ.get("GEYSER_API_TOKEN"), + geyser_auth_type=os.environ.get("GEYSER_AUTH_TYPE", "x-token"), + platforms=[Platform.PUMP_FUN], + ) + + +async def detect(listener_type: str) -> TokenInfo | None: + """Wait for one new pump.fun token from the given listener. + + Args: + listener_type: Listener to use + + Returns: + TokenInfo, or None on timeout + """ + listener = make_listener(listener_type) + seen: list[TokenInfo] = [] + + async def on_token(token_info: TokenInfo) -> None: + seen.append(token_info) + + task = asyncio.create_task(listener.listen_for_tokens(on_token)) + try: + for _ in range(int(DETECT_TIMEOUT_SECONDS / 0.5)): + if seen: + break + await asyncio.sleep(0.5) + finally: + task.cancel() + await asyncio.sleep(0) + + return seen[0] if seen else None + + +async def sol_balance(client: SolanaClient, wallet: Wallet) -> float: + """Read the wallet's SOL balance at finalized commitment. + + Reads immediately after a transaction confirms can still serve a slot that + predates it, which silently produces "net change: 0" style nonsense. Ask for + finalized explicitly so the reported numbers mean something. + + Args: + client: RPC client + wallet: Wallet to query + + Returns: + Balance in SOL + """ + response = await client.post_rpc( + { + "jsonrpc": "2.0", + "id": 1, + "method": "getBalance", + "params": [str(wallet.pubkey), {"commitment": "finalized"}], + } + ) + lamports = (response or {}).get("result", {}).get("value") + if lamports is None: + raise RuntimeError(f"getBalance failed for {wallet.pubkey}: {response}") + return lamports / LAMPORTS_PER_SOL + + +async def ata_is_closed(client: SolanaClient, ata: Pubkey) -> bool: + """Check whether an ATA is closed, at finalized commitment. + + Args: + client: RPC client + ata: Associated token account address + + Returns: + True if the account no longer exists + """ + response = await client.post_rpc( + { + "jsonrpc": "2.0", + "id": 1, + "method": "getAccountInfo", + "params": [str(ata), {"encoding": "base64", "commitment": "finalized"}], + } + ) + return (response or {}).get("result", {}).get("value") is None + + +async def run_leg( + listener_type: str, + client: SolanaClient, + wallet: Wallet, + priority_fee_manager: PriorityFeeManager, +) -> LegResult: + """Detect, buy, sell and clean up for one listener. + + Args: + listener_type: Listener under test + client: RPC client + wallet: Trading wallet + priority_fee_manager: Priority fee strategy + + Returns: + LegResult describing the cycle + """ + result = LegResult(listener=listener_type) + result.start_sol = await sol_balance(client, wallet) + + print( + f"\n{'=' * 62}\n{listener_type.upper()} (start {result.start_sol:.9f} SOL)\n{'=' * 62}" + ) + + print( + f"[{listener_type}] waiting for a token (up to {DETECT_TIMEOUT_SECONDS:.0f}s)..." + ) + token_info = await detect(listener_type) + if token_info is None: + result.notes.append("no token detected before timeout") + print(f"[{listener_type}] no token detected") + return result + + result.detected = str(token_info.mint) + result.symbol = token_info.symbol + result.quote_mint = str(token_info.quote_mint) + print(f"[{listener_type}] detected {token_info.symbol} ({token_info.mint})") + print(f"[{listener_type}] quote_mint from listener: {token_info.quote_mint}") + + buyer = PlatformAwareBuyer( + client, + wallet, + priority_fee_manager, + BUY_AMOUNT_SOL, + slippage=BUY_SLIPPAGE, + max_retries=1, + extreme_fast_token_amount=EXTREME_FAST_TOKEN_AMOUNT, + extreme_fast_mode=True, + ) + seller = PlatformAwareSeller( + client, wallet, priority_fee_manager, slippage=SELL_SLIPPAGE, max_retries=1 + ) + + buy = await buyer.execute(token_info) + result.buy_ok = buy.success + result.buy_sig = str(buy.tx_signature) if buy.tx_signature else None + print(f"[{listener_type}] BUY ok={buy.success} tx={buy.tx_signature}") + if not buy.success: + result.notes.append(f"buy failed: {buy.error_message}") + else: + await asyncio.sleep(HOLD_SECONDS) + sell = await seller.execute(token_info, buy.amount, buy.price) + result.sell_ok = sell.success + result.sell_sig = str(sell.tx_signature) if sell.tx_signature else None + print(f"[{listener_type}] SELL ok={sell.success} tx={sell.tx_signature}") + if not sell.success: + result.notes.append(f"sell failed: {sell.error_message}") + + # Always attempt cleanup, even after a failed buy: a partially-created ATA + # still holds rent. + token_program = token_info.token_program_id or SystemAddresses.TOKEN_2022_PROGRAM + manager = AccountCleanupManager( + client, wallet, priority_fee_manager, use_priority_fee=False, force_burn=True + ) + print(f"[{listener_type}] cleaning up ATA for {token_info.mint}...") + await manager.cleanup_ata(token_info.mint, token_program) + + # Let the close finalize before judging it; otherwise the check races the + # very transaction it is verifying. + await asyncio.sleep(SETTLE_SECONDS) + + ata = wallet.get_associated_token_address(token_info.mint, token_program) + result.cleanup_ok = await ata_is_closed(client, ata) + if not result.cleanup_ok: + result.notes.append(f"ATA {ata} still open after cleanup") + + result.end_sol = await sol_balance(client, wallet) + print( + f"[{listener_type}] cleanup ok={result.cleanup_ok} " + f"end {result.end_sol:.9f} SOL " + f"(net {result.end_sol - result.start_sol:+.9f})" + ) + return result + + +async def cleanup_only(mints: list[str]) -> int: + """Close ATAs for the given mints and exit. + + Args: + mints: Base mint addresses whose ATAs should be closed + + Returns: + Process exit code + """ + client = SolanaClient(os.environ["SOLANA_NODE_RPC_ENDPOINT"]) + wallet = Wallet(os.environ["SOLANA_PRIVATE_KEY"]) + priority_fee_manager = PriorityFeeManager( + client=client, + enable_dynamic_fee=False, + enable_fixed_fee=True, + fixed_fee=PRIORITY_FEE, + extra_fee=0.0, + hard_cap=PRIORITY_FEE, + ) + try: + before = await sol_balance(client, wallet) + print(f"balance before cleanup: {before:.9f} SOL") + manager = AccountCleanupManager( + client, + wallet, + priority_fee_manager, + use_priority_fee=False, + force_burn=True, + ) + for mint_str in mints: + mint = Pubkey.from_string(mint_str) + for program in ( + SystemAddresses.TOKEN_2022_PROGRAM, + SystemAddresses.TOKEN_PROGRAM, + ): + ata = wallet.get_associated_token_address(mint, program) + if await ata_is_closed(client, ata): + continue + print(f"closing {ata} (mint {mint_str}, program {program})") + await manager.cleanup_ata(mint, program) + await asyncio.sleep(SETTLE_SECONDS) + print(f" closed: {await ata_is_closed(client, ata)}") + after = await sol_balance(client, wallet) + print(f"balance after cleanup: {after:.9f} SOL (net {after - before:+.9f})") + return 0 + finally: + await client.close() + + +async def main() -> int: + """Run the listener matrix. + + Returns: + Process exit code (0 if every requested listener passed) + """ + if "--cleanup-only" in sys.argv: + index = sys.argv.index("--cleanup-only") + mints = sys.argv[index + 1 :] + if not mints: + print("--cleanup-only needs at least one mint") + return 2 + return await cleanup_only(mints) + + if "--yes" not in sys.argv: + print("This spends real funds. Re-run with --yes to proceed.") + return 2 + + listeners = list(ALL_LISTENERS) + if "--listeners" in sys.argv: + listeners = sys.argv[sys.argv.index("--listeners") + 1].split(",") + + client = SolanaClient(os.environ["SOLANA_NODE_RPC_ENDPOINT"]) + wallet = Wallet(os.environ["SOLANA_PRIVATE_KEY"]) + priority_fee_manager = PriorityFeeManager( + client=client, + enable_dynamic_fee=False, + enable_fixed_fee=True, + fixed_fee=PRIORITY_FEE, + extra_fee=0.0, + hard_cap=PRIORITY_FEE, + ) + + results: list[LegResult] = [] + try: + opening = await sol_balance(client, wallet) + print(f"wallet: {wallet.pubkey}\nopening balance: {opening:.9f} SOL") + print(f"listeners under test: {', '.join(listeners)}") + + for listener_type in listeners: + try: + results.append( + await run_leg(listener_type, client, wallet, priority_fee_manager) + ) + except Exception as error: # noqa: BLE001 + failed = LegResult(listener=listener_type) + failed.notes.append(f"crashed: {error}") + results.append(failed) + print(f"[{listener_type}] CRASHED: {error}") + + closing = await sol_balance(client, wallet) + finally: + await client.close() + + print(f"\n{'=' * 62}\nSUMMARY\n{'=' * 62}") + header = ( + f"{'listener':<12} {'detect':<7} {'buy':<5} {'sell':<5} {'cleanup':<8} symbol" + ) + print(header) + for item in results: + print( + f"{item.listener:<12} " + f"{'yes' if item.detected else 'no':<7} " + f"{'ok' if item.buy_ok else '-':<5} " + f"{'ok' if item.sell_ok else '-':<5} " + f"{'ok' if item.cleanup_ok else '-':<8} " + f"{item.symbol or ''}" + ) + for note in item.notes: + print(f"{'':<12} note: {note}") + + print(f"\nopening {opening:.9f} SOL -> closing {closing:.9f} SOL") + print(f"total net change: {closing - opening:+.9f} SOL") + for item in results: + if item.buy_sig: + print(f" {item.listener} buy: {item.buy_sig}") + if item.sell_sig: + print(f" {item.listener} sell: {item.sell_sig}") + + passed = [item for item in results if item.passed] + print(f"\n{len(passed)}/{len(results)} listeners passed the full cycle") + return 0 if len(passed) == len(results) else 1 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/learning-examples/live_v2_round_trip.py b/learning-examples/live_v2_round_trip.py new file mode 100644 index 0000000..e975857 --- /dev/null +++ b/learning-examples/live_v2_round_trip.py @@ -0,0 +1,160 @@ +"""Execute one real buy_v2 + sell_v2 round trip on mainnet. + +WARNING: this submits real transactions and spends real funds. It exists to +cover the last gap the simulation scripts cannot reach — actual submission, +confirmation, and the post-trade accounting that parses a confirmed +transaction. Keep BUY_AMOUNT_SOL tiny. + +Uses the bot's own PlatformAwareBuyer / PlatformAwareSeller, so a pass here +means the production path works end to end. + +Usage: + uv run learning-examples/live_v2_round_trip.py # needs confirmation + uv run learning-examples/live_v2_round_trip.py --yes # skip the prompt +""" + +import asyncio +import os +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from dotenv import load_dotenv # noqa: E402 + +from core.client import SolanaClient # noqa: E402 +from core.priority_fee.manager import PriorityFeeManager # noqa: E402 +from core.pubkeys import LAMPORTS_PER_SOL # 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 +from trading.platform_aware import PlatformAwareBuyer, PlatformAwareSeller # noqa: E402 + +load_dotenv(PROJECT_ROOT / ".env") + +BUY_AMOUNT_SOL = 0.0001 +EXTREME_FAST_TOKEN_AMOUNT = 20 +HOLD_SECONDS = 5 +PRIORITY_FEE = 1_000_000 + + +async def wait_for_token(timeout_seconds: float = 120.0) -> TokenInfo | None: + """Wait for the bot's geyser listener to report a new coin. + + Args: + timeout_seconds: How long to wait + + Returns: + First TokenInfo seen, or None on timeout + """ + listener = ListenerFactory.create_listener( + listener_type="geyser", + geyser_endpoint=os.environ["GEYSER_ENDPOINT"], + geyser_api_token=os.environ["GEYSER_API_TOKEN"], + geyser_auth_type=os.environ.get("GEYSER_AUTH_TYPE", "x-token"), + platforms=[Platform.PUMP_FUN], + ) + + seen: list[TokenInfo] = [] + + async def on_token(token_info: TokenInfo) -> None: + seen.append(token_info) + + task = asyncio.create_task(listener.listen_for_tokens(on_token)) + try: + for _ in range(int(timeout_seconds / 0.5)): + if seen: + break + await asyncio.sleep(0.5) + finally: + task.cancel() + + return seen[0] if seen else None + + +async def main() -> int: + """Run one live buy/sell round trip. + + Returns: + Process exit code (0 if both legs confirmed) + """ + if "--yes" not in sys.argv: + print( + f"This spends real funds ({BUY_AMOUNT_SOL} SOL plus fees). " + f"Re-run with --yes to proceed." + ) + return 2 + + client = SolanaClient(os.environ["SOLANA_NODE_RPC_ENDPOINT"]) + wallet = Wallet(os.environ["SOLANA_PRIVATE_KEY"]) + priority_fee_manager = PriorityFeeManager( + client=client, + enable_dynamic_fee=False, + enable_fixed_fee=True, + fixed_fee=PRIORITY_FEE, + extra_fee=0.0, + hard_cap=PRIORITY_FEE, + ) + + try: + start_lamports = (await client.get_account_info(wallet.pubkey)).lamports + print(f"wallet: {wallet.pubkey}") + print(f"start balance: {start_lamports / LAMPORTS_PER_SOL:.9f} SOL\n") + + print("Waiting for a fresh pump.fun coin...") + token_info = await wait_for_token() + if token_info is None: + print("No coin detected before timeout.") + return 2 + + print(f"detected: {token_info.symbol} ({token_info.mint})") + print(f"quote: {token_info.quote_mint}\n") + + buyer = PlatformAwareBuyer( + client, + wallet, + priority_fee_manager, + BUY_AMOUNT_SOL, + slippage=0.3, + max_retries=1, + extreme_fast_token_amount=EXTREME_FAST_TOKEN_AMOUNT, + extreme_fast_mode=True, + ) + seller = PlatformAwareSeller( + client, wallet, priority_fee_manager, slippage=0.3, max_retries=1 + ) + + print("--- BUY (buy_v2) ---") + buy = await buyer.execute(token_info) + print(f"success={buy.success} tx={buy.tx_signature}") + if not buy.success: + print(f"error: {buy.error_message}") + return 1 + print(f"tokens={buy.amount} price={buy.price:.10f}\n") + + print(f"holding {HOLD_SECONDS}s...") + await asyncio.sleep(HOLD_SECONDS) + + print("\n--- SELL (sell_v2) ---") + sell = await seller.execute(token_info, buy.amount, buy.price) + print(f"success={sell.success} tx={sell.tx_signature}") + if not sell.success: + print(f"error: {sell.error_message}") + + end_lamports = (await client.get_account_info(wallet.pubkey)).lamports + delta = (end_lamports - start_lamports) / LAMPORTS_PER_SOL + print(f"\nend balance: {end_lamports / LAMPORTS_PER_SOL:.9f} SOL") + print(f"net change: {delta:+.9f} SOL") + print( + "\nNote: the base-token ATA still holds rent (~0.002 SOL) until a " + "cleanup run closes it." + ) + + return 0 if (buy.success and sell.success) else 1 + finally: + await client.close() + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index 1622d91..9a3380d 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -3,28 +3,25 @@ import base64 import hashlib import json import os -import random import struct import base58 +import pump_v2 import websockets -from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solders.compute_budget import set_compute_unit_price -from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction, VersionedTransaction from spl.token.instructions import ( create_idempotent_associated_token_account, - get_associated_token_address, ) # Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py -EXPECTED_DISCRIMINATOR = struct.pack(" None: - """Parse bonding curve data progressively based on available bytes. - - Args: - data: Raw account data including discriminator - - Raises: - ValueError: If discriminator is invalid or data is too short - """ - if len(data) < 8: - raise ValueError("Data too short to contain discriminator") - - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - # Parse base fields (always present) - offset = 8 - base_data = data[offset:] - parsed = self._BASE_STRUCT.parse(base_data) - self.__dict__.update(parsed) - - # Calculate offset after base struct - offset += self._BASE_STRUCT.sizeof() - - # Parse creator if bytes remaining (added in V2) - if len(data) >= offset + 32: - creator_bytes = data[offset : offset + 32] - self.creator = Pubkey.from_bytes(creator_bytes) - offset += 32 - else: - self.creator = None - - # Parse mayhem mode flag if bytes remaining (added in V3) - if len(data) >= offset + 1: - self.is_mayhem_mode = bool(data[offset]) - else: - self.is_mayhem_mode = False +# The bonding curve account and the v2 instruction layout live in pump_v2 so +# every example shares one copy. See learning-examples/pump_v2.py. +BondingCurveState = pump_v2.BondingCurveState async def get_pump_curve_state( conn: AsyncClient, curve_address: Pubkey -) -> BondingCurveState: +) -> pump_v2.BondingCurveState: + """Fetch and parse a bonding curve account. + + Args: + conn: Solana RPC client + curve_address: Bonding curve address + + Returns: + Parsed curve state + + Raises: + ValueError: If the account is missing or not a bonding curve + """ response = await conn.get_account_info(curve_address, encoding="base64") if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") - data = response.value.data - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - return BondingCurveState(data) + return pump_v2.BondingCurveState(response.value.data) -def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: - if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: - raise ValueError("Invalid reserve state") - - return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( - curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS - ) - - -def _find_creator_vault(creator: Pubkey) -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PUMP_PROGRAM, - ) - return derived_address - - -def _find_global_volume_accumulator() -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"global_volume_accumulator"], - PUMP_PROGRAM, - ) - return derived_address - - -def _find_user_volume_accumulator(user: Pubkey) -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"user_volume_accumulator", bytes(user)], - PUMP_PROGRAM, - ) - return derived_address - - -def _find_fee_config() -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"fee_config", bytes(PUMP_PROGRAM)], - PUMP_FEE_PROGRAM, - ) - return derived_address - - -def _find_bonding_curve_v2(mint: Pubkey) -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"bonding-curve-v2", bytes(mint)], - PUMP_PROGRAM, - ) - return derived_address - - -async def get_fee_recipient( - client: AsyncClient, curve_state: BondingCurveState -) -> Pubkey: - """Determine the correct fee recipient based on mayhem mode. - - Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account) - instead of the standard fee recipient. This function checks the bonding curve state - and returns the appropriate fee recipient. +def calculate_pump_curve_price(curve_state: pump_v2.BondingCurveState) -> float: + """Price of one whole token in whole quote units. Args: - client: Solana RPC client to fetch Global account data - curve_state: Parsed bonding curve state containing is_mayhem_mode flag + curve_state: Parsed curve state Returns: - Appropriate fee recipient pubkey (mayhem or standard) + Price in the curve's quote asset + + Raises: + ValueError: If reserves are empty """ - if not curve_state.is_mayhem_mode: - return PUMP_FEE - - # Fetch Global account to get reserved_fee_recipient for mayhem mode tokens - response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") - if not response.value or not response.value.data: - # Fallback to standard fee if Global account cannot be fetched - return PUMP_FEE - - data = response.value.data - - # Parse reserved_fee_recipient from Global account - # Offset calculation based on pump_fun_idl.json Global struct: - # discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) + - # initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) + - # initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) + - # withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) + - # creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) + - # admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483 - RESERVED_FEE_RECIPIENT_OFFSET = 483 - - if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: - # Fallback if account data is too short - return PUMP_FEE - - reserved_fee_recipient_bytes = data[ - RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 - ] - return Pubkey.from_bytes(reserved_fee_recipient_bytes) + price = curve_state.price_per_token() + if price <= 0: + raise ValueError("Invalid reserve state") + return price async def buy_token( @@ -243,97 +110,54 @@ async def buy_token( payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address( - payer.pubkey(), mint, token_program_id=token_program - ) - amount_lamports = int(amount * LAMPORTS_PER_SOL) - - # Fetch bonding curve state to calculate price and determine fee recipient + # Fetch bonding curve state for price, mayhem mode and quote asset. curve_state = await get_pump_curve_state(client, bonding_curve) token_price_sol = calculate_pump_curve_price(curve_state) + + # Amounts are denominated in the curve's quote asset, which is not + # necessarily SOL any more. + quote_mint = pump_v2.normalize_quote_mint( + getattr(curve_state, "quote_mint", None) + ) + quote_unit = pump_v2.quote_units(quote_mint) token_amount = amount / token_price_sol + max_quote_cost = int(amount * quote_unit * (1 + slippage)) - # Calculate maximum SOL to spend with slippage - max_amount_lamports = int(amount_lamports * (1 + slippage)) + print(f"Quote asset: {quote_mint}") + print(f"Buying {token_amount:.6f} tokens, max cost {max_quote_cost} raw units") - # Determine fee recipient based on whether token uses mayhem mode - fee_recipient = await get_fee_recipient(client, curve_state) + # buy_v2 takes 27 mandatory accounts in a fixed order for every coin. + buy_ix = pump_v2.build_buy_v2_instruction( + base_mint=mint, + creator=curve_state.creator, + user=payer.pubkey(), + token_amount_raw=int(token_amount * 10**TOKEN_DECIMALS), + max_quote_cost_raw=max_quote_cost, + quote_mint=quote_mint, + base_token_program=token_program, + is_mayhem_mode=curve_state.is_mayhem_mode, + ) - accounts = [ - AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta( - pubkey=associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, - is_signer=False, - is_writable=True, - ), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=token_program, is_signer=False, is_writable=False), - AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), - AccountMeta( - pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta( - pubkey=_find_global_volume_accumulator(), - is_signer=False, - is_writable=False, - ), - AccountMeta( - pubkey=_find_user_volume_accumulator(payer.pubkey()), - is_signer=False, - is_writable=True, - ), - # Index 14: fee_config (readonly) - AccountMeta( - pubkey=_find_fee_config(), - is_signer=False, - is_writable=False, - ), - # Index 15: fee_program (readonly) - AccountMeta( - pubkey=PUMP_FEE_PROGRAM, - is_signer=False, - is_writable=False, - ), - # Remaining account: bonding_curve_v2 (readonly, required for all coins) - AccountMeta( - pubkey=_find_bonding_curve_v2(mint), - is_signer=False, - is_writable=False, - ), - # 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28 - AccountMeta( - pubkey=random.choice(BREAKING_FEE_RECIPIENTS), - is_signer=False, - is_writable=True, + instructions = [ + set_compute_unit_price(1_000), + create_idempotent_associated_token_account( + payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program ), ] + # SOL-paired coins settle in native SOL and only seed-check the quote + # ATA, so creating it would waste rent. Other quotes need a real account. + if not pump_v2.is_sol_paired(quote_mint): + instructions.append( + create_idempotent_associated_token_account( + payer.pubkey(), + payer.pubkey(), + quote_mint, + token_program_id=pump_v2.quote_token_program(quote_mint), + ) + ) + instructions.append(buy_ix) - discriminator = struct.pack("= len(static_keys) for idx in ix.accounts): + static_keys = ( + transaction.message.account_keys + ) + if any( + idx >= len(static_keys) + for idx in ix.accounts + ): continue account_keys = [ str(static_keys[index]) @@ -524,7 +355,9 @@ async def main(): mint = Pubkey.from_string(token_data["mint"]) bonding_curve = Pubkey.from_string(token_data["bondingCurve"]) associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"]) - creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"])) + creator_vault = pump_v2.find_creator_vault( + Pubkey.from_string(token_data["creator"]) + ) token_program = Pubkey.from_string(token_data["token_program"]) # Fetch the token price diff --git a/learning-examples/manual_buy_cu_optimized.py b/learning-examples/manual_buy_cu_optimized.py index 6af2faf..6aad16d 100644 --- a/learning-examples/manual_buy_cu_optimized.py +++ b/learning-examples/manual_buy_cu_optimized.py @@ -22,28 +22,26 @@ import base64 import hashlib import json import os -import random import struct import base58 +import pump_v2 import websockets -from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solders.compute_budget import set_compute_unit_price -from solders.instruction import AccountMeta, Instruction +from solders.instruction import Instruction from solders.keypair import Keypair from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction, VersionedTransaction from spl.token.instructions import ( create_idempotent_associated_token_account, - get_associated_token_address, ) # Discriminators -EXPECTED_DISCRIMINATOR = struct.pack(" None: - """Parse bonding curve data progressively based on available bytes. - - Args: - data: Raw account data including discriminator - - Raises: - ValueError: If discriminator is invalid or data is too short - """ - if len(data) < 8: - raise ValueError("Data too short to contain discriminator") - - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - # Parse base fields (always present) - offset = 8 - base_data = data[offset:] - parsed = self._BASE_STRUCT.parse(base_data) - self.__dict__.update(parsed) - - # Calculate offset after base struct - offset += self._BASE_STRUCT.sizeof() - - # Parse creator if bytes remaining (added in V2) - if len(data) >= offset + 32: - creator_bytes = data[offset : offset + 32] - self.creator = Pubkey.from_bytes(creator_bytes) - offset += 32 - else: - self.creator = None - - # Parse mayhem mode flag if bytes remaining (added in V3) - if len(data) >= offset + 1: - self.is_mayhem_mode = bool(data[offset]) - else: - self.is_mayhem_mode = False +BondingCurveState = pump_v2.BondingCurveState async def get_pump_curve_state( @@ -151,16 +100,25 @@ async def get_pump_curve_state( if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - return BondingCurveState(data) + return pump_v2.BondingCurveState(data) -def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: - if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: +def calculate_pump_curve_price(curve_state: pump_v2.BondingCurveState) -> float: + """Price of one whole token in whole units of the curve's quote asset. + + Args: + curve_state: Parsed curve state + + Returns: + Price in the quote asset + + Raises: + ValueError: If reserves are empty + """ + price = curve_state.price_per_token() + if price <= 0: raise ValueError("Invalid reserve state") - - return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( - curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS - ) + return price def _find_creator_vault(creator: Pubkey) -> Pubkey: @@ -281,89 +239,30 @@ async def buy_token( payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address( - payer.pubkey(), mint, token_program_id=token_program - ) - amount_lamports = int(amount * LAMPORTS_PER_SOL) - # Fetch bonding curve state to calculate price and determine fee recipient curve_state = await get_pump_curve_state(client, bonding_curve) token_price_sol = calculate_pump_curve_price(curve_state) token_amount = amount / token_price_sol - # Calculate maximum SOL to spend with slippage - max_amount_lamports = int(amount_lamports * (1 + slippage)) - # Determine fee recipient based on whether token uses mayhem mode - fee_recipient = await get_fee_recipient(client, curve_state) - accounts = [ - AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta( - pubkey=associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, - is_signer=False, - is_writable=True, - ), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=token_program, is_signer=False, is_writable=False), - AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), - AccountMeta( - pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta( - pubkey=_find_global_volume_accumulator(), - is_signer=False, - is_writable=False, - ), - AccountMeta( - pubkey=_find_user_volume_accumulator(payer.pubkey()), - is_signer=False, - is_writable=True, - ), - # Index 14: fee_config (readonly) - AccountMeta( - pubkey=_find_fee_config(), - is_signer=False, - is_writable=False, - ), - # Index 15: fee_program (readonly) - AccountMeta( - pubkey=PUMP_FEE_PROGRAM, - is_signer=False, - is_writable=False, - ), - # Remaining account: bonding_curve_v2 (readonly, required for all coins) - AccountMeta( - pubkey=_find_bonding_curve_v2(mint), - is_signer=False, - is_writable=False, - ), - # 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28 - AccountMeta( - pubkey=random.choice(BREAKING_FEE_RECIPIENTS), - is_signer=False, - is_writable=True, - ), - ] - - discriminator = struct.pack("= len(static_keys) for idx in ix.accounts): + static_keys = ( + transaction.message.account_keys + ) + if any( + idx >= len(static_keys) + for idx in ix.accounts + ): continue account_keys = [ str( @@ -569,7 +475,9 @@ async def main(): mint = Pubkey.from_string(token_data["mint"]) bonding_curve = Pubkey.from_string(token_data["bondingCurve"]) associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"]) - creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"])) + creator_vault = pump_v2.find_creator_vault( + Pubkey.from_string(token_data["creator"]) + ) token_program = Pubkey.from_string(token_data["token_program"]) # Fetch the token price diff --git a/learning-examples/manual_buy_geyser.py b/learning-examples/manual_buy_geyser.py index a43ec99..1752c22 100644 --- a/learning-examples/manual_buy_geyser.py +++ b/learning-examples/manual_buy_geyser.py @@ -1,25 +1,22 @@ import asyncio import json import os -import random import struct import sys import base58 import grpc -from construct import Flag, Int64ul, Struct +import pump_v2 from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solders.compute_budget import set_compute_unit_price -from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction from spl.token.instructions import ( create_idempotent_associated_token_account, - get_associated_token_address, ) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) @@ -29,7 +26,7 @@ from src.geyser.generated import ( ) # Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py -EXPECTED_DISCRIMINATOR = struct.pack(" None: - """Parse bonding curve data progressively based on available bytes. - - Args: - data: Raw account data including discriminator - - Raises: - ValueError: If discriminator is invalid or data is too short - """ - if len(data) < 8: - raise ValueError("Data too short to contain discriminator") - - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - # Parse base fields (always present) - offset = 8 - base_data = data[offset:] - parsed = self._BASE_STRUCT.parse(base_data) - self.__dict__.update(parsed) - - # Calculate offset after base struct - offset += self._BASE_STRUCT.sizeof() - - # Parse creator if bytes remaining (added in V2) - if len(data) >= offset + 32: - creator_bytes = data[offset : offset + 32] - self.creator = Pubkey.from_bytes(creator_bytes) - offset += 32 - else: - self.creator = None - - # Parse mayhem mode flag if bytes remaining (added in V3) - if len(data) >= offset + 1: - self.is_mayhem_mode = bool(data[offset]) - else: - self.is_mayhem_mode = False +BondingCurveState = pump_v2.BondingCurveState async def get_pump_curve_state( @@ -143,16 +87,25 @@ async def get_pump_curve_state( if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - return BondingCurveState(data) + return pump_v2.BondingCurveState(data) -def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: - if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: +def calculate_pump_curve_price(curve_state: pump_v2.BondingCurveState) -> float: + """Price of one whole token in whole units of the curve's quote asset. + + Args: + curve_state: Parsed curve state + + Returns: + Price in the quote asset + + Raises: + ValueError: If reserves are empty + """ + price = curve_state.price_per_token() + if price <= 0: raise ValueError("Invalid reserve state") - - return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( - curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS - ) + return price def _find_creator_vault(creator: Pubkey) -> Pubkey: @@ -380,87 +333,28 @@ async def buy_token( payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address( - payer.pubkey(), mint, token_program - ) - amount_lamports = int(amount * LAMPORTS_PER_SOL) - - # Fetch bonding curve state for price + mayhem-mode-aware fee recipient. + # Fetch bonding curve state for price, mayhem mode and quote asset. curve_state = await get_pump_curve_state(client, bonding_curve) token_price_sol = calculate_pump_curve_price(curve_state) token_amount = amount / token_price_sol - fee_recipient = await get_fee_recipient(client, curve_state) - # Calculate maximum SOL to spend with slippage - max_amount_lamports = int(amount_lamports * (1 + slippage)) - - accounts = [ - AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta( - pubkey=associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, - is_signer=False, - is_writable=True, - ), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=token_program, is_signer=False, is_writable=False), - AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), - AccountMeta( - pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta( - pubkey=_find_global_volume_accumulator(), - is_signer=False, - is_writable=False, - ), - AccountMeta( - pubkey=_find_user_volume_accumulator(payer.pubkey()), - is_signer=False, - is_writable=True, - ), - # Index 14: fee_config (readonly) - AccountMeta( - pubkey=_find_fee_config(), - is_signer=False, - is_writable=False, - ), - # Index 15: fee_program (readonly) - AccountMeta( - pubkey=PUMP_FEE_PROGRAM, - is_signer=False, - is_writable=False, - ), - # Remaining account: bonding_curve_v2 (readonly, required for all coins) - AccountMeta( - pubkey=_find_bonding_curve_v2(mint), - is_signer=False, - is_writable=False, - ), - # 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28 - AccountMeta( - pubkey=random.choice(BREAKING_FEE_RECIPIENTS), - is_signer=False, - is_writable=True, - ), - ] - - discriminator = struct.pack(" 1 else "...") # Pass mint as argv[1] or hardcode here +TOKEN_MINT = Pubkey.from_string( + sys.argv[1] if len(sys.argv) > 1 else "..." +) # Pass mint as argv[1] or hardcode here # Global constants PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") @@ -44,82 +46,8 @@ UNIT_BUDGET = 100_000 RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") -# 8 breaking-upgrade fee recipients (pump.fun program upgrade 2026-04-28). -# One must be appended (mutable) AFTER bonding-curve-v2 on every buy/sell. -# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md -BREAKING_FEE_RECIPIENTS = [ - Pubkey.from_string("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"), - Pubkey.from_string("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"), - Pubkey.from_string("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"), - Pubkey.from_string("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"), - Pubkey.from_string("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"), - Pubkey.from_string("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"), - Pubkey.from_string("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"), - Pubkey.from_string("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"), -] - -class BondingCurveState: - """Bonding curve state parser with progressive field parsing. - - Parses bonding curve account data progressively based on available bytes, - making it forward-compatible with future schema versions. - """ - - # Base struct present in all versions - _BASE_STRUCT = Struct( - "virtual_token_reserves" / Int64ul, - "virtual_sol_reserves" / Int64ul, - "real_token_reserves" / Int64ul, - "real_sol_reserves" / Int64ul, - "token_total_supply" / Int64ul, - "complete" / Flag, - ) - - def __init__(self, data: bytes) -> None: - """Parse bonding curve data progressively based on available bytes. - - Args: - data: Raw account data including discriminator - - Raises: - ValueError: If discriminator is invalid or data is too short - """ - if len(data) < 8: - raise ValueError("Data too short to contain discriminator") - - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - # Parse base fields (always present) - offset = 8 - base_data = data[offset:] - parsed = self._BASE_STRUCT.parse(base_data) - self.__dict__.update(parsed) - - # Calculate offset after base struct - offset += self._BASE_STRUCT.sizeof() - - # Parse creator if bytes remaining (added in V2) - if len(data) >= offset + 32: - creator_bytes = data[offset : offset + 32] - self.creator = Pubkey.from_bytes(creator_bytes) - offset += 32 - else: - self.creator = None - - # Parse mayhem mode flag if bytes remaining (added in V3) - if len(data) >= offset + 1: - self.is_mayhem_mode = bool(data[offset]) - offset += 1 - else: - self.is_mayhem_mode = False - - # Parse cashback flag if bytes remaining (added in V4 — late-Feb 2026 cashback upgrade) - if len(data) >= offset + 1: - self.is_cashback_coin = bool(data[offset]) - else: - self.is_cashback_coin = False +BondingCurveState = pump_v2.BondingCurveState async def get_pump_curve_state( @@ -133,7 +61,7 @@ async def get_pump_curve_state( if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - return BondingCurveState(data) + return pump_v2.BondingCurveState(data) def get_bonding_curve_address(mint: Pubkey) -> tuple[Pubkey, int]: @@ -162,84 +90,22 @@ def find_creator_vault(creator: Pubkey) -> Pubkey: return derived_address -def _find_fee_config() -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"fee_config", bytes(PUMP_PROGRAM)], - PUMP_FEE_PROGRAM, - ) - return derived_address - - -def _find_bonding_curve_v2(mint: Pubkey) -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"bonding-curve-v2", bytes(mint)], - PUMP_PROGRAM, - ) - return derived_address - - -def _find_user_volume_accumulator(user: Pubkey) -> Pubkey: - derived_address, _ = Pubkey.find_program_address( - [b"user_volume_accumulator", bytes(user)], - PUMP_PROGRAM, - ) - return derived_address - - -async def get_fee_recipient( - client: AsyncClient, curve_state: BondingCurveState -) -> Pubkey: - """Determine the correct fee recipient based on mayhem mode. - - Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account) - instead of the standard fee recipient. This function checks the bonding curve state - and returns the appropriate fee recipient. +def calculate_pump_curve_price(curve_state: pump_v2.BondingCurveState) -> float: + """Price of one whole token in whole units of the curve's quote asset. Args: - client: Solana RPC client to fetch Global account data - curve_state: Parsed bonding curve state containing is_mayhem_mode flag + curve_state: Parsed curve state Returns: - Appropriate fee recipient pubkey (mayhem or standard) + Price in the quote asset + + Raises: + ValueError: If reserves are empty """ - if not curve_state.is_mayhem_mode: - return PUMP_FEE - - # Fetch Global account to get reserved_fee_recipient for mayhem mode tokens - response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") - if not response.value or not response.value.data: - # Fallback to standard fee if Global account cannot be fetched - return PUMP_FEE - - data = response.value.data - - # Parse reserved_fee_recipient from Global account - # Offset calculation based on pump_fun_idl.json Global struct: - # discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) + - # initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) + - # initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) + - # withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) + - # creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) + - # admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483 - RESERVED_FEE_RECIPIENT_OFFSET = 483 - - if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: - # Fallback if account data is too short - return PUMP_FEE - - reserved_fee_recipient_bytes = data[ - RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 - ] - return Pubkey.from_bytes(reserved_fee_recipient_bytes) - - -def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: - if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + price = curve_state.price_per_token() + if price <= 0: raise ValueError("Invalid reserve state") - - return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( - curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS - ) + return price async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey): @@ -298,97 +164,48 @@ async def sell_token( token_price_sol = calculate_pump_curve_price(curve_state) print(f"Price per Token: {token_price_sol:.20f} SOL") - # Calculate minimum SOL output + # Minimum payout, in the curve's quote asset raw units. + quote_mint = pump_v2.normalize_quote_mint( + getattr(curve_state, "quote_mint", None) + ) + quote_unit = pump_v2.quote_units(quote_mint) amount = token_balance - min_sol_output = float(token_balance_decimal) * float(token_price_sol) - slippage_factor = 1 - slippage - min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) + expected_output = float(token_balance_decimal) * float(token_price_sol) + min_quote_output = max(1, int(expected_output * (1 - slippage) * quote_unit)) print(f"Selling {token_balance_decimal} tokens") - print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") + print(f"Quote asset: {quote_mint}") + print( + f"Minimum output: {min_quote_output / quote_unit:.10f} ({min_quote_output} raw)" + ) - # Determine fee recipient based on whether token uses mayhem mode - fee_recipient = await get_fee_recipient(client, curve_state) + # sell_v2 takes the same 26 mandatory accounts for every coin — no + # cashback/mayhem branching on the account list any more. + sell_ix = pump_v2.build_sell_v2_instruction( + base_mint=mint, + creator=curve_state.creator, + user=payer.pubkey(), + token_amount_raw=amount, + min_quote_output_raw=min_quote_output, + quote_mint=quote_mint, + base_token_program=token_program_id, + is_mayhem_mode=curve_state.is_mayhem_mode, + ) - accounts = [ - AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta( - pubkey=associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, - is_signer=False, - is_writable=True, - ), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta( - pubkey=creator_vault, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=token_program_id, is_signer=False, is_writable=False - ), # Use dynamic token_program_id - AccountMeta( - pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - # Index 12: fee_config (readonly) - AccountMeta( - pubkey=_find_fee_config(), - is_signer=False, - is_writable=False, - ), - # Index 13: fee_program (readonly) - AccountMeta( - pubkey=PUMP_FEE_PROGRAM, - is_signer=False, - is_writable=False, - ), - ] - # For cashback coins, insert user_volume_accumulator before bonding-curve-v2 (17 accounts total). - if curve_state.is_cashback_coin: - accounts.append( - AccountMeta( - pubkey=_find_user_volume_accumulator(payer.pubkey()), - is_signer=False, - is_writable=True, + instructions = [set_compute_unit_price(1_000)] + # Non-SOL proceeds land in the seller's quote ATA, which must exist. + if not pump_v2.is_sol_paired(quote_mint): + instructions.append( + create_idempotent_associated_token_account( + payer.pubkey(), + payer.pubkey(), + quote_mint, + token_program_id=pump_v2.quote_token_program(quote_mint), ) ) - accounts.extend([ - # bonding_curve_v2 (readonly, required for all coins) - AccountMeta( - pubkey=_find_bonding_curve_v2(mint), - is_signer=False, - is_writable=False, - ), - # Breaking-upgrade fee recipient (mutable) — required from 2026-04-28. - # 16 accounts non-cashback / 17 accounts cashback. - AccountMeta( - pubkey=random.choice(BREAKING_FEE_RECIPIENTS), - is_signer=False, - is_writable=True, - ), - ]) + instructions.append(sell_ix) - discriminator = struct.pack(" Pubkey: return derived_address +async def assert_transaction_succeeded(client: AsyncClient, signature) -> None: + """Raise if a confirmed transaction actually failed on-chain. + + `confirm_transaction` only waits for the transaction to land — a landed + transaction can still have reverted. Without this check a failed buy prints + as a success, which is exactly how a wrong fee recipient (NotAuthorized, + 6000) can look like a passing test. + + Args: + client: Solana RPC client + signature: Transaction signature to inspect + + Raises: + RuntimeError: If the transaction reverted + """ + result = await client.get_transaction( + signature, commitment="confirmed", max_supported_transaction_version=0 + ) + value = result.value + if value is None: + raise RuntimeError(f"Transaction {signature} not found after confirmation") + err = value.transaction.meta.err if value.transaction.meta else None + if err: + raise RuntimeError(f"Transaction {signature} landed but failed on-chain: {err}") + + def create_pump_create_instruction( mint: Pubkey, mint_authority: Pubkey, @@ -251,68 +277,46 @@ def create_buy_instruction( token_amount: int, max_sol_cost: int, track_volume: bool = True, + is_mayhem_mode: bool = False, ) -> Instruction: - """Create the buy instruction.""" - accounts = [ - AccountMeta(pubkey=global_state, is_signer=False, is_writable=False), - AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_user, is_signer=False, is_writable=True), - AccountMeta(pubkey=user, is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), - AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta( - pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=_find_user_volume_accumulator(user), - is_signer=False, - is_writable=True, - ), - # Index 14: fee_config (readonly) - AccountMeta( - pubkey=_find_fee_config(), - is_signer=False, - is_writable=False, - ), - # Index 15: fee_program (readonly) - AccountMeta( - pubkey=PUMP_FEE_PROGRAM, - is_signer=False, - is_writable=False, - ), - # Remaining account: bonding_curve_v2 (readonly, required for all coins) - AccountMeta( - pubkey=_find_bonding_curve_v2(mint), - is_signer=False, - is_writable=False, - ), - # 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28 - AccountMeta( - pubkey=random.choice(BREAKING_FEE_RECIPIENTS), - is_signer=False, - is_writable=True, - ), - ] + """Create the buy instruction (buy_v2). - # Encode OptionBool for track_volume - # OptionBool: [0] = None, [1, 0] = Some(false), [1, 1] = Some(true) - track_volume_bytes = bytes([1, 1 if track_volume else 0]) + The signature is unchanged for callers, but this builds `buy_v2` with its 27 + mandatory accounts. Several parameters are accepted only for backwards + compatibility and are derived or dropped internally: buy_v2 takes no + track_volume argument, and pump_v2 selects the fee recipient from the + documented set. This script mints the coin with `creator = payer`, so the + buyer is also the creator. - data = ( - BUY_DISCRIMINATOR - + struct.pack(" None: + """Raise if a confirmed transaction actually failed on-chain. + + `confirm_transaction` only waits for the transaction to land — a landed + transaction can still have reverted. Without this check a failed buy prints + as a success, which is exactly how a wrong fee recipient (NotAuthorized, + 6000) can look like a passing test. + + Args: + client: Solana RPC client + signature: Transaction signature to inspect + + Raises: + RuntimeError: If the transaction reverted + """ + result = await client.get_transaction( + signature, commitment="confirmed", max_supported_transaction_version=0 + ) + value = result.value + if value is None: + raise RuntimeError(f"Transaction {signature} not found after confirmation") + err = value.transaction.meta.err if value.transaction.meta else None + if err: + raise RuntimeError(f"Transaction {signature} landed but failed on-chain: {err}") + + def create_buy_instruction( global_state: Pubkey, fee_recipient: Pubkey, @@ -295,68 +321,49 @@ def create_buy_instruction( token_amount: int, max_sol_cost: int, track_volume: bool = True, + is_mayhem_mode: bool = False, ) -> Instruction: - """Create the buy instruction.""" - accounts = [ - AccountMeta(pubkey=global_state, is_signer=False, is_writable=False), - AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_user, is_signer=False, is_writable=True), - AccountMeta(pubkey=user, is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=TOKEN_2022_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), - AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta( - pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=_find_user_volume_accumulator(user), - is_signer=False, - is_writable=True, - ), - # Index 14: fee_config (readonly) - AccountMeta( - pubkey=_find_fee_config(), - is_signer=False, - is_writable=False, - ), - # Index 15: fee_program (readonly) - AccountMeta( - pubkey=PUMP_FEE_PROGRAM, - is_signer=False, - is_writable=False, - ), - # Remaining account: bonding_curve_v2 (readonly, required for all coins) - AccountMeta( - pubkey=_find_bonding_curve_v2(mint), - is_signer=False, - is_writable=False, - ), - # 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28 - AccountMeta( - pubkey=random.choice(BREAKING_FEE_RECIPIENTS), - is_signer=False, - is_writable=True, - ), - ] + """Create the buy instruction (buy_v2). - # Encode OptionBool for track_volume - # OptionBool: [0] = None, [1, 0] = Some(false), [1, 1] = Some(true) - track_volume_bytes = bytes([1, 1 if track_volume else 0]) + Signature is unchanged for the caller, but this now builds `buy_v2` with its + 27 mandatory accounts. `global_state`, `fee_recipient`, + `associated_bonding_curve`, `associated_user`, `creator_vault` and + `track_volume` are accepted for backwards compatibility and derived or + dropped internally — buy_v2 has no track_volume argument, and pump_v2 picks + the fee recipient from the documented set. - data = ( - BUY_DISCRIMINATOR - + struct.pack(" Pubkey: """Get the appropriate fee recipient based on mayhem mode. @@ -474,14 +481,19 @@ async def main(): bonding_curve=bonding_curve, user=payer.pubkey(), ), - # Create user ATA + ] + + # buy_v2's 27 accounts push a combined create+buy message past Solana's + # 1232-byte packet limit (measured 1972 bytes), so the buy goes in a + # second transaction. The legacy 18-account buy used to fit; recovering + # atomicity would need an address lookup table. + buy_instructions = [ create_idempotent_associated_token_account( payer.pubkey(), payer.pubkey(), mint_keypair.pubkey(), TOKEN_2022_PROGRAM, ), - # Buy tokens create_buy_instruction( global_state=PUMP_GLOBAL, fee_recipient=fee_recipient, @@ -494,6 +506,7 @@ async def main(): token_amount=expected_tokens, max_sol_cost=max_sol_cost, track_volume=True, + is_mayhem_mode=ENABLE_MAYHEM_MODE, ), ] @@ -503,18 +516,31 @@ async def main(): [payer, mint_keypair], message, recent_blockhash.value.blockhash ) - print("\nSending transaction...") + print("\nSending create transaction...") opts = TxOpts(skip_preflight=True, preflight_commitment=Confirmed) try: response = await client.send_transaction(transaction, opts) tx_hash = response.value - - print(f"Transaction sent: https://solscan.io/tx/{tx_hash}") - + print(f"Create sent: https://solscan.io/tx/{tx_hash}") print("Waiting for confirmation...") await client.confirm_transaction(tx_hash, commitment="confirmed") - print("Transaction confirmed!") + await assert_transaction_succeeded(client, tx_hash) + print("Create confirmed!") + + buy_blockhash = await client.get_latest_blockhash() + buy_tx = Transaction( + [payer], + Message(buy_instructions, payer.pubkey()), + buy_blockhash.value.blockhash, + ) + print("\nSending buy transaction (buy_v2)...") + buy_response = await client.send_transaction(buy_tx, opts) + buy_hash = buy_response.value + print(f"Buy sent: https://solscan.io/tx/{buy_hash}") + await client.confirm_transaction(buy_hash, commitment="confirmed") + await assert_transaction_succeeded(client, buy_hash) + print("Buy confirmed!") return tx_hash diff --git a/learning-examples/pump_v2.py b/learning-examples/pump_v2.py new file mode 100644 index 0000000..cf390c7 --- /dev/null +++ b/learning-examples/pump_v2.py @@ -0,0 +1,502 @@ +"""Self-contained pump.fun v2 trade helpers for the learning examples. + +The bonding-curve program's `buy_v2` / `sell_v2` instructions take 27 and 26 +mandatory accounts in a fixed order, identical for every coin — SOL-paired or +USDC-paired, mayhem or not, cashback or not. Encoding that once here keeps the +example scripts from each carrying their own copy of the list, which is how they +drift out of sync with the program. + +Deliberately standalone: it imports nothing from `src/`, so the examples stay +readable on their own. `learning-examples/verify_v2_account_layout.py` checks the +layout below against `idl/pump_fun_idl.json`. + +Docs: BUY.md, SELL.md and COIN_CREATION.md under docs/instructions in +github.com/pump-fun/pump-public-docs +""" + +import secrets +import struct + +from construct import Flag, Int64ul, Struct +from solders.instruction import AccountMeta, Instruction +from solders.pubkey import Pubkey + +# Programs and well-known accounts +PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf") +PUMP_EVENT_AUTHORITY = Pubkey.from_string( + "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" +) +PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") +SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") +TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") +ASSOCIATED_TOKEN_PROGRAM = Pubkey.from_string( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" +) + +# Quote mints. bonding_curve.quote_mint is Pubkey::default() for SOL-paired +# coins, but the v2 instructions expect wrapped SOL to be passed explicitly. +DEFAULT_PUBKEY = Pubkey.from_string("11111111111111111111111111111111") +WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112") +USDC_MINT = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") +QUOTE_DECIMALS = {WSOL_MINT: 9, USDC_MINT: 6} + +TOKEN_DECIMALS = 6 +LAMPORTS_PER_SOL = 1_000_000_000 + +# Account discriminator for the BondingCurve account. +BONDING_CURVE_DISCRIMINATOR = struct.pack("")). +BUY_V2_DISCRIMINATOR = bytes([184, 23, 238, 97, 103, 197, 211, 61]) +SELL_V2_DISCRIMINATOR = bytes([93, 246, 130, 60, 231, 233, 64, 178]) + +# Fee recipients: 8 normal (non-mayhem coins), 8 reserved (mayhem coins), +# 8 buyback (every coin). See FEE_RECIPIENTS.md in the pump-fun public docs. +NORMAL_FEE_RECIPIENTS = [ + Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"), + Pubkey.from_string("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"), + Pubkey.from_string("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"), + Pubkey.from_string("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"), + Pubkey.from_string("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"), + Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"), + Pubkey.from_string("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"), + Pubkey.from_string("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"), +] +RESERVED_FEE_RECIPIENTS = [ + Pubkey.from_string("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"), + Pubkey.from_string("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"), + Pubkey.from_string("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"), + Pubkey.from_string("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"), + Pubkey.from_string("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"), + Pubkey.from_string("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"), + Pubkey.from_string("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"), + Pubkey.from_string("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"), +] +BUYBACK_FEE_RECIPIENTS = [ + Pubkey.from_string("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"), + Pubkey.from_string("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"), + Pubkey.from_string("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"), + Pubkey.from_string("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"), + Pubkey.from_string("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"), + Pubkey.from_string("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"), + Pubkey.from_string("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"), + Pubkey.from_string("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"), +] + + +def normalize_quote_mint(quote_mint: Pubkey | None) -> Pubkey: + """Map a curve's raw quote_mint onto the mint to pass to the instruction. + + Args: + quote_mint: Raw value read from the bonding curve, or None + + Returns: + Wrapped SOL for SOL-paired coins, otherwise the mint unchanged + """ + if quote_mint is None or quote_mint == DEFAULT_PUBKEY: + return WSOL_MINT + return quote_mint + + +def is_sol_paired(quote_mint: Pubkey | None) -> bool: + """Whether a coin settles in native SOL. + + Args: + quote_mint: Raw or normalized quote mint + + Returns: + True if the coin is SOL-paired + """ + return normalize_quote_mint(quote_mint) == WSOL_MINT + + +def quote_units(quote_mint: Pubkey) -> int: + """Raw units per whole unit of a quote mint. + + Args: + quote_mint: Quote mint address + + Returns: + 1e9 for SOL, 1e6 for USDC + """ + return 10 ** QUOTE_DECIMALS.get(quote_mint, 9) + + +def quote_token_program(quote_mint: Pubkey) -> Pubkey: + """Token program owning a quote mint. + + Args: + quote_mint: Quote mint address + + Returns: + Token program id (both current quote mints are legacy SPL Token) + """ + return TOKEN_PROGRAM + + +def find_bonding_curve(mint: Pubkey) -> Pubkey: + """Derive the bonding curve PDA. + + Args: + mint: Base token mint + + Returns: + Bonding curve address + """ + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM)[0] + + +def find_creator_vault(creator: Pubkey) -> Pubkey: + """Derive the creator vault PDA. + + Args: + creator: Coin creator + + Returns: + Creator vault address + """ + return Pubkey.find_program_address( + [b"creator-vault", bytes(creator)], PUMP_PROGRAM + )[0] + + +def find_global_volume_accumulator() -> Pubkey: + """Derive the global volume accumulator PDA. + + Returns: + Global volume accumulator address + """ + return Pubkey.find_program_address([b"global_volume_accumulator"], PUMP_PROGRAM)[0] + + +def find_user_volume_accumulator(user: Pubkey) -> Pubkey: + """Derive a user's volume accumulator PDA. + + Args: + user: User wallet + + Returns: + User volume accumulator address + """ + return Pubkey.find_program_address( + [b"user_volume_accumulator", bytes(user)], PUMP_PROGRAM + )[0] + + +def find_fee_config() -> Pubkey: + """Derive the fee config PDA (under the pump fees program). + + Returns: + Fee config address + """ + return Pubkey.find_program_address( + [b"fee_config", bytes(PUMP_PROGRAM)], PUMP_FEE_PROGRAM + )[0] + + +def find_sharing_config(base_mint: Pubkey) -> Pubkey: + """Derive the creator-fee sharing config PDA (under the pump fees program). + + Mandatory on buy_v2/sell_v2. + + Args: + base_mint: Base token mint + + Returns: + Sharing config address + """ + return Pubkey.find_program_address( + [b"sharing-config", bytes(base_mint)], PUMP_FEE_PROGRAM + )[0] + + +def find_associated_token_account( + owner: Pubkey, mint: Pubkey, token_program: Pubkey +) -> Pubkey: + """Derive an associated token account address. + + Args: + owner: ATA owner (may be a PDA) + mint: Token mint + token_program: Token program owning the mint + + Returns: + Associated token account address + """ + return Pubkey.find_program_address( + [bytes(owner), bytes(token_program), bytes(mint)], + ASSOCIATED_TOKEN_PROGRAM, + )[0] + + +def pick_fee_recipient(*, is_mayhem_mode: bool) -> Pubkey: + """Pick a fee_recipient from the set the program expects for this coin. + + Args: + is_mayhem_mode: Whether the coin is in mayhem mode + + Returns: + A fee recipient address + """ + pool = RESERVED_FEE_RECIPIENTS if is_mayhem_mode else NORMAL_FEE_RECIPIENTS + return secrets.choice(pool) + + +def pick_buyback_fee_recipient() -> Pubkey: + """Pick a buyback fee recipient, required on every v2 trade. + + Returns: + A buyback fee recipient address + """ + return secrets.choice(BUYBACK_FEE_RECIPIENTS) + + +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. + """ + + _STRUCT = Struct( + "virtual_token_reserves" / Int64ul, + "virtual_quote_reserves" / Int64ul, + "real_token_reserves" / Int64ul, + "real_quote_reserves" / Int64ul, + "token_total_supply" / Int64ul, + "complete" / Flag, + ) + # Byte offsets from the start of the account, discriminator included. + _CREATOR_OFFSET = 49 + _MAYHEM_OFFSET = 81 + _CASHBACK_OFFSET = 82 + _QUOTE_MINT_OFFSET = 83 + + def __init__(self, data: bytes) -> None: + """Parse bonding curve account data. + + Args: + data: Raw account data including the 8-byte discriminator + + Raises: + ValueError: If the discriminator is wrong or the data is truncated + """ + if len(data) < 8: + raise ValueError("Data too short to contain discriminator") + if data[:8] != BONDING_CURVE_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + self.__dict__.update(self._STRUCT.parse(data[8:])) + + # Aliases for the pre-rename field names. + self.virtual_sol_reserves = self.virtual_quote_reserves + self.real_sol_reserves = self.real_quote_reserves + + self.creator = self._read_pubkey(data, self._CREATOR_OFFSET) + self.is_mayhem_mode = self._read_flag(data, self._MAYHEM_OFFSET) + self.is_cashback_coin = self._read_flag(data, self._CASHBACK_OFFSET) + raw_quote_mint = self._read_pubkey(data, self._QUOTE_MINT_OFFSET) + self.quote_mint = normalize_quote_mint(raw_quote_mint) + self.is_sol_paired = is_sol_paired(raw_quote_mint) + + @staticmethod + def _read_pubkey(data: bytes, offset: int) -> Pubkey | None: + """Read a 32-byte pubkey if the data extends that far. + + Args: + data: Raw account data + offset: Byte offset + + Returns: + Pubkey, or None if the field is absent + """ + if len(data) < offset + 32: + return None + return Pubkey.from_bytes(data[offset : offset + 32]) + + @staticmethod + def _read_flag(data: bytes, offset: int) -> bool: + """Read a single-byte bool if the data extends that far. + + Args: + data: Raw account data + offset: Byte offset + + Returns: + Flag value, or False if the field is absent + """ + return bool(data[offset]) if len(data) > offset else False + + def price_per_token(self) -> float: + """Current price in whole quote units per whole token. + + Returns: + Price, or 0.0 if reserves are empty + """ + 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) + + +def build_v2_accounts( + *, + base_mint: Pubkey, + creator: Pubkey, + user: Pubkey, + quote_mint: Pubkey, + base_token_program: Pubkey, + is_mayhem_mode: bool, + include_global_volume_accumulator: bool, +) -> list[AccountMeta]: + """Build the ordered account list shared by buy_v2 and sell_v2. + + Args: + base_mint: Coin being traded + creator: Coin creator, from bonding_curve.creator + user: Signer / trader + quote_mint: Normalized quote mint (wrapped SOL for SOL-paired coins) + 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 + + Returns: + Ordered AccountMeta list (27 entries for buy_v2, 26 for sell_v2) + """ + quote_program = 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) + fee_recipient = pick_fee_recipient(is_mayhem_mode=is_mayhem_mode) + buyback_fee_recipient = pick_buyback_fee_recipient() + + def ata(owner: Pubkey, mint: Pubkey, program: Pubkey) -> Pubkey: + return find_associated_token_account(owner, mint, program) + + accounts = [ + (PUMP_GLOBAL, False), + (base_mint, False), + (quote_mint, False), + (base_token_program, False), + (quote_program, False), + (ASSOCIATED_TOKEN_PROGRAM, False), + (fee_recipient, True), + (ata(fee_recipient, quote_mint, quote_program), True), + (buyback_fee_recipient, True), + (ata(buyback_fee_recipient, quote_mint, quote_program), True), + (bonding_curve, True), + (ata(bonding_curve, base_mint, base_token_program), True), + (ata(bonding_curve, quote_mint, quote_program), True), + (user, True), + (ata(user, base_mint, base_token_program), True), + (ata(user, quote_mint, quote_program), True), + (creator_vault, True), + (ata(creator_vault, quote_mint, quote_program), True), + (find_sharing_config(base_mint), False), + ] + if include_global_volume_accumulator: + accounts.append((find_global_volume_accumulator(), False)) + accounts += [ + (user_volume_accumulator, True), + (ata(user_volume_accumulator, quote_mint, quote_program), True), + (find_fee_config(), False), + (PUMP_FEE_PROGRAM, False), + (SYSTEM_PROGRAM, False), + (PUMP_EVENT_AUTHORITY, False), + (PUMP_PROGRAM, False), + ] + + return [ + AccountMeta(pubkey=pubkey, is_signer=pubkey == user, is_writable=writable) + for pubkey, writable in accounts + ] + + +def build_buy_v2_instruction( + *, + base_mint: Pubkey, + creator: Pubkey, + user: Pubkey, + token_amount_raw: int, + max_quote_cost_raw: int, + quote_mint: Pubkey = WSOL_MINT, + base_token_program: Pubkey = TOKEN_2022_PROGRAM, + is_mayhem_mode: bool = False, +) -> Instruction: + """Build a buy_v2 instruction. + + Args: + base_mint: Coin to buy + creator: Coin creator, from bonding_curve.creator + user: Buyer / signer + token_amount_raw: Base tokens to buy, in raw units + max_quote_cost_raw: Spend cap in the quote mint's raw units + quote_mint: Normalized quote mint + base_token_program: Token program owning base_mint + is_mayhem_mode: Whether the coin is in mayhem mode + + Returns: + The buy_v2 instruction + """ + return Instruction( + program_id=PUMP_PROGRAM, + data=BUY_V2_DISCRIMINATOR + + struct.pack(" Instruction: + """Build a sell_v2 instruction. + + Args: + base_mint: Coin to sell + creator: Coin creator, from bonding_curve.creator + user: Seller / signer + token_amount_raw: Base tokens to sell, in raw units + min_quote_output_raw: Minimum acceptable payout in raw quote units + quote_mint: Normalized quote mint + base_token_program: Token program owning base_mint + is_mayhem_mode: Whether the coin is in mayhem mode + + Returns: + The sell_v2 instruction + """ + return Instruction( + program_id=PUMP_PROGRAM, + data=SELL_V2_DISCRIMINATOR + + struct.pack(" dict: ("pool_quote_token_account", "pubkey"), ("lp_supply", "u64"), ("coin_creator", "pubkey"), + # Appended after coin_creator: is_mayhem_mode (243), is_cashback_coin + # (244), then virtual_quote_reserves as an i128 at 245..261. Live pool + # accounts are 301 bytes (fields end at 261, rest is padding). + ("is_mayhem_mode", "u8"), + ("is_cashback_coin", "u8"), ] for field_name, field_type in fields: @@ -364,23 +373,54 @@ async def get_pumpswap_fee_recipients( # ============================================================================ +async def read_virtual_quote_reserves(client: AsyncClient, pool: Pubkey) -> int: + """Read Pool::virtual_quote_reserves, the field appended after the flags. + + Args: + client: Solana RPC client + pool: Pool (market) address + + Returns: + Raw virtual quote reserves, or 0 if the account predates the field + """ + response = await client.get_account_info(pool, encoding="base64") + if not response.value or not response.value.data: + return 0 + data = response.value.data + end = POOL_VIRTUAL_QUOTE_RESERVES_OFFSET + POOL_VIRTUAL_QUOTE_RESERVES_SIZE + if len(data) < end: + return 0 + return int.from_bytes( + data[POOL_VIRTUAL_QUOTE_RESERVES_OFFSET : end], "little", signed=True + ) + + async def calculate_token_pool_price( client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey, + virtual_quote_reserves: int = 0, ) -> float: - """Calculate current token price from AMM pool balances. + """Calculate current token price from AMM pool reserves. - AMM price is determined by the ratio of tokens in the pool: - price = quote_balance / base_balance + Price is the ratio of *effective* quote reserves to base reserves: + + effective_quote_reserves = + pool_quote_token_account.amount + Pool::virtual_quote_reserves + + PumpSwap added `virtual_quote_reserves` to the Pool account. Upstream's + release note says it is 0 on every pool, but that is out of date: live pools + carry non-zero values (17.58 SOL observed on a 148 SOL pool, i.e. quoting + off the raw vault balance under-prices by ~10.6%). Always add it. Args: client: Solana RPC client pool_base_token_account: Pool's token account (the token being priced) - pool_quote_token_account: Pool's SOL account (the quote currency) + pool_quote_token_account: Pool's quote account + virtual_quote_reserves: Pool::virtual_quote_reserves, in raw quote units Returns: - Price in SOL per token + Price in quote asset per token """ base_balance_resp = await client.get_token_account_balance(pool_base_token_account) quote_balance_resp = await client.get_token_account_balance( @@ -388,7 +428,9 @@ async def calculate_token_pool_price( ) base_amount = float(base_balance_resp.value.ui_amount) - quote_amount = float(quote_balance_resp.value.ui_amount) + quote_decimals = int(quote_balance_resp.value.decimals) + quote_raw = int(quote_balance_resp.value.amount) + int(virtual_quote_reserves) + quote_amount = quote_raw / 10**quote_decimals return quote_amount / base_amount @@ -462,7 +504,10 @@ async def buy_pump_swap( """ token_program_id = await get_token_program_id(client, base_mint) token_price_sol = await calculate_token_pool_price( - client, pool_base_token_account, pool_quote_token_account + client, + pool_base_token_account, + pool_quote_token_account, + await read_virtual_quote_reserves(client, market), ) print(f"Token price in SOL: {token_price_sol:.10f} SOL") diff --git a/learning-examples/pumpswap/manual_sell_pumpswap.py b/learning-examples/pumpswap/manual_sell_pumpswap.py index 7067bca..2a44b36 100644 --- a/learning-examples/pumpswap/manual_sell_pumpswap.py +++ b/learning-examples/pumpswap/manual_sell_pumpswap.py @@ -92,7 +92,11 @@ BREAKING_FEE_RECIPIENTS = [ POOL_DISCRIMINATOR_SIZE = 8 POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored -POOL_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin flag is stored +POOL_IS_CASHBACK_OFFSET = 244 +# virtual_quote_reserves is an i128 appended after the flags. Pool fields +# end at 261; live accounts are 301 bytes with trailing padding. +POOL_VIRTUAL_QUOTE_RESERVES_OFFSET = 245 +POOL_VIRTUAL_QUOTE_RESERVES_SIZE = 16 POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag # GlobalConfig structure offsets @@ -176,6 +180,11 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: ("pool_quote_token_account", "pubkey"), ("lp_supply", "u64"), ("coin_creator", "pubkey"), + # Appended after coin_creator: is_mayhem_mode (243), is_cashback_coin + # (244), then virtual_quote_reserves as an i128 at 245..261. Live pool + # accounts are 301 bytes (fields end at 261, rest is padding). + ("is_mayhem_mode", "u8"), + ("is_cashback_coin", "u8"), ] for field_name, field_type in fields: @@ -328,23 +337,54 @@ async def get_pumpswap_fee_recipients( # ============================================================================ +async def read_virtual_quote_reserves(client: AsyncClient, pool: Pubkey) -> int: + """Read Pool::virtual_quote_reserves, the field appended after the flags. + + Args: + client: Solana RPC client + pool: Pool (market) address + + Returns: + Raw virtual quote reserves, or 0 if the account predates the field + """ + response = await client.get_account_info(pool, encoding="base64") + if not response.value or not response.value.data: + return 0 + data = response.value.data + end = POOL_VIRTUAL_QUOTE_RESERVES_OFFSET + POOL_VIRTUAL_QUOTE_RESERVES_SIZE + if len(data) < end: + return 0 + return int.from_bytes( + data[POOL_VIRTUAL_QUOTE_RESERVES_OFFSET : end], "little", signed=True + ) + + async def calculate_token_pool_price( client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey, + virtual_quote_reserves: int = 0, ) -> float: - """Calculate current token price from AMM pool balances. + """Calculate current token price from AMM pool reserves. - AMM price is determined by the ratio of tokens in the pool: - price = quote_balance / base_balance + Price is the ratio of *effective* quote reserves to base reserves: + + effective_quote_reserves = + pool_quote_token_account.amount + Pool::virtual_quote_reserves + + PumpSwap added `virtual_quote_reserves` to the Pool account. Upstream's + release note says it is 0 on every pool, but that is out of date: live pools + carry non-zero values (17.58 SOL observed on a 148 SOL pool, i.e. quoting + off the raw vault balance under-prices by ~10.6%). Always add it. Args: client: Solana RPC client pool_base_token_account: Pool's token account (the token being priced) - pool_quote_token_account: Pool's SOL account (the quote currency) + pool_quote_token_account: Pool's quote account + virtual_quote_reserves: Pool::virtual_quote_reserves, in raw quote units Returns: - Price in SOL per token + Price in quote asset per token """ base_balance_resp = await client.get_token_account_balance(pool_base_token_account) quote_balance_resp = await client.get_token_account_balance( @@ -352,7 +392,9 @@ async def calculate_token_pool_price( ) base_amount = float(base_balance_resp.value.ui_amount) - quote_amount = float(quote_balance_resp.value.ui_amount) + quote_decimals = int(quote_balance_resp.value.decimals) + quote_raw = int(quote_balance_resp.value.amount) + int(virtual_quote_reserves) + quote_amount = quote_raw / 10**quote_decimals return quote_amount / base_amount @@ -469,7 +511,10 @@ async def sell_pump_swap( return None token_price_sol = await calculate_token_pool_price( - client, pool_base_token_account, pool_quote_token_account + client, + pool_base_token_account, + pool_quote_token_account, + await read_virtual_quote_reserves(client, market), ) print(f"Price per Token: {token_price_sol:.20f} SOL") diff --git a/learning-examples/simulate_bot_buy_path.py b/learning-examples/simulate_bot_buy_path.py new file mode 100644 index 0000000..696ddae --- /dev/null +++ b/learning-examples/simulate_bot_buy_path.py @@ -0,0 +1,230 @@ +"""Dry-run the bot's real buy path against a freshly created coin. + +Detects a new pump.fun coin with the bot's own listener, runs the same +PlatformAwareBuyer code the bot uses (including the extreme_fast_mode curve +refresh), but intercepts the transaction just before submission and simulates it +instead. This exercises the listener -> event parser -> curve manager -> +address provider -> instruction builder chain as a unit. + +No funds move: `build_and_send_transaction` is monkeypatched to simulate. + +Usage: + uv run learning-examples/simulate_bot_buy_path.py + uv run learning-examples/simulate_bot_buy_path.py --no-extreme-fast +""" + +import asyncio +import os +import sys +from base64 import b64encode +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from dotenv import load_dotenv # noqa: E402 +from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price # noqa: E402 +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.wallet import Wallet # noqa: E402 +from interfaces.core import Platform, TokenInfo # noqa: E402 +from monitoring.listener_factory import ListenerFactory # noqa: E402 +from trading.platform_aware import PlatformAwareBuyer # noqa: E402 + +load_dotenv(PROJECT_ROOT / ".env") + +BUY_AMOUNT_SOL = 0.0001 +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 + + +async def wait_for_token(timeout_seconds: float = 90.0) -> TokenInfo | None: + """Wait for the bot's geyser listener to report a new coin. + + Args: + timeout_seconds: How long to wait + + Returns: + The first TokenInfo seen, or None on timeout + """ + listener = ListenerFactory.create_listener( + listener_type="geyser", + geyser_endpoint=os.environ["GEYSER_ENDPOINT"], + geyser_api_token=os.environ["GEYSER_API_TOKEN"], + geyser_auth_type=os.environ.get("GEYSER_AUTH_TYPE", "x-token"), + platforms=[Platform.PUMP_FUN], + ) + + seen: list[TokenInfo] = [] + + async def on_token(token_info: TokenInfo) -> None: + seen.append(token_info) + + task = asyncio.create_task(listener.listen_for_tokens(on_token)) + try: + for _ in range(int(timeout_seconds / 0.5)): + if seen: + break + await asyncio.sleep(0.5) + finally: + task.cancel() + + return seen[0] if seen else None + + +def install_simulation_hook(client: SolanaClient) -> dict: + """Replace transaction submission with simulation. + + Args: + client: Client whose send path should be intercepted + + Returns: + Dict that will be populated with the simulation outcome + """ + outcome: dict = {} + + async def simulate_instead( + instructions, + signer_keypair, + skip_preflight=True, + max_retries=3, + priority_fee=None, + compute_unit_limit=None, + account_data_size_limit=None, + ): + preamble = [] + if compute_unit_limit: + preamble.append(set_compute_unit_limit(compute_unit_limit)) + if priority_fee: + preamble.append(set_compute_unit_price(priority_fee)) + + blockhash = await client.get_latest_blockhash() + message = Message.new_with_blockhash( + [*preamble, *instructions], signer_keypair.pubkey(), blockhash + ) + transaction = Transaction([signer_keypair], message, blockhash) + + response = await client.post_rpc( + { + "jsonrpc": "2.0", + "id": 1, + "method": "simulateTransaction", + "params": [ + b64encode(bytes(transaction)).decode(), + { + "encoding": "base64", + "sigVerify": False, + "replaceRecentBlockhash": True, + "commitment": "processed", + }, + ], + } + ) + value = (response or {}).get("result", {}).get("value", {}) + outcome.update( + { + "err": value.get("err"), + "units": value.get("unitsConsumed"), + "logs": value.get("logs") or [], + "cu_limit": compute_unit_limit, + "priority_fee": priority_fee, + "instruction_count": len(instructions), + "account_count": len(instructions[-1].accounts), + } + ) + # Returning a sentinel signature: confirm_transaction is stubbed below. + return "SIMULATED" + + async def never_confirm(_signature, **_kwargs): + return False + + client.build_and_send_transaction = simulate_instead + client.confirm_transaction = never_confirm + return outcome + + +async def main() -> int: + """Run the bot's buy path in simulation mode. + + Returns: + Process exit code (0 if the simulated buy had no program error) + """ + extreme_fast = "--no-extreme-fast" not in sys.argv + + print("Waiting for a fresh pump.fun coin via the bot's geyser listener...") + token_info = await wait_for_token() + if token_info is None: + print("No coin detected before timeout.") + return 2 + + print(f"\ndetected: {token_info.symbol} ({token_info.mint})") + print(f"quote_mint (from CreateEvent): {token_info.quote_mint}") + print(f"token program: {token_info.token_program_id}") + print(f"mayhem={token_info.is_mayhem_mode} cashback={token_info.is_cashback_coin}") + print(f"extreme_fast_mode={extreme_fast}\n") + + client = SolanaClient(os.environ["SOLANA_NODE_RPC_ENDPOINT"]) + wallet = Wallet(os.environ["SOLANA_PRIVATE_KEY"]) + priority_fee_manager = PriorityFeeManager( + client=client, + enable_dynamic_fee=False, + enable_fixed_fee=True, + fixed_fee=1_000_000, + extra_fee=0.0, + hard_cap=1_000_000, + ) + + outcome = install_simulation_hook(client) + buyer = PlatformAwareBuyer( + client, + wallet, + priority_fee_manager, + BUY_AMOUNT_SOL, + slippage=0.3, + max_retries=1, + extreme_fast_token_amount=EXTREME_FAST_TOKEN_AMOUNT, + extreme_fast_mode=extreme_fast, + ) + + if not extreme_fast: + # Mirror the bot's retries.wait_after_creation pause. Without it the + # curve read races the account's confirmation and fails before any + # instruction is built. + print(f"Waiting {CURVE_STABILIZE_SECONDS}s for the curve to stabilize...") + await asyncio.sleep(CURVE_STABILIZE_SECONDS) + + try: + result = await buyer.execute(token_info) + finally: + await client.close() + + if not outcome: + print(f"Buy path never reached transaction submission: {result.error_message}") + return 1 + + print("simulated buy:") + print(f" instructions: {outcome['instruction_count']}") + print(f" trade accounts: {outcome['account_count']}") + print(f" cu_limit: {outcome['cu_limit']}") + print(f" unitsConsumed: {outcome['units']}") + print(f" err: {outcome['err']}") + + if outcome["err"]: + for line in outcome["logs"]: + if "Error" in line or "failed" in line or "Instruction:" in line: + print(f" {line}") + return 1 + + headroom = outcome["cu_limit"] - (outcome["units"] or 0) + print(f"\nCU headroom: {headroom} ({headroom / outcome['cu_limit']:.0%})") + print("Buy path validated end to end against live mainnet state.") + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/learning-examples/simulate_v2_trades.py b/learning-examples/simulate_v2_trades.py new file mode 100644 index 0000000..2901977 --- /dev/null +++ b/learning-examples/simulate_v2_trades.py @@ -0,0 +1,288 @@ +"""Simulate pump.fun buy_v2 / sell_v2 against mainnet without spending funds. + +Builds both instructions through the bot's real code path (address provider, +instruction builder, curve manager) and runs them through +`simulateTransaction`, which executes the program against live state but never +submits. Reports the program error (if any) and the compute units consumed — +use the latter to tune `get_buy_compute_unit_limit` / `get_sell_compute_unit_limit`. + +The wallet is only used to derive a pubkey and sign nothing: simulation runs +with sigVerify disabled. + +Usage: + # simulate against a specific coin + uv run learning-examples/simulate_v2_trades.py + + # discover a fresh coin via geyser, then simulate against it + uv run learning-examples/simulate_v2_trades.py +""" + +import asyncio +import os +import sys +from base64 import b64encode +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from dotenv import load_dotenv # noqa: E402 +from solders.compute_budget import set_compute_unit_limit # noqa: E402 +from solders.message import Message # noqa: E402 +from solders.pubkey import Pubkey # noqa: E402 +from solders.transaction import Transaction # noqa: E402 + +from core.client import SolanaClient # noqa: E402 +from core.pubkeys import ( # noqa: E402 + TOKEN_DECIMALS, + SystemAddresses, + normalize_quote_mint, + quote_token_program, + quote_units_per_token, +) +from core.wallet import Wallet # noqa: E402 +from interfaces.core import Platform, TokenInfo # noqa: E402 +from platforms import get_platform_implementations # noqa: E402 + +load_dotenv(PROJECT_ROOT / ".env") + +# Buy this many whole tokens in the simulation. Small enough that the quote cost +# stays well under a test wallet's balance on a fresh curve. +SIMULATED_TOKEN_AMOUNT = 20 +# Generous ceiling so simulation reports true consumption rather than hitting +# the limit. The real bot uses the builder's tuned values. +SIMULATION_CU_LIMIT = 400_000 + + +async def discover_mint(timeout_seconds: float = 45.0) -> Pubkey | None: + """Listen for a freshly created pump.fun coin via geyser. + + Args: + timeout_seconds: How long to wait for a creation event + + Returns: + Mint of the first coin seen, or None on timeout + """ + from monitoring.listener_factory import ListenerFactory + + listener = ListenerFactory.create_listener( + listener_type="geyser", + geyser_endpoint=os.environ["GEYSER_ENDPOINT"], + geyser_api_token=os.environ["GEYSER_API_TOKEN"], + geyser_auth_type=os.environ.get("GEYSER_AUTH_TYPE", "x-token"), + platforms=[Platform.PUMP_FUN], + ) + + seen: list[TokenInfo] = [] + + async def on_token(token_info: TokenInfo) -> None: + seen.append(token_info) + + task = asyncio.create_task(listener.listen_for_tokens(on_token)) + try: + deadline = timeout_seconds / 0.5 + for _ in range(int(deadline)): + if seen: + break + await asyncio.sleep(0.5) + finally: + task.cancel() + + return seen[0].mint if seen else None + + +async def build_token_info( + mint: Pubkey, client: SolanaClient +) -> tuple[TokenInfo, dict]: + """Assemble TokenInfo for an existing coin from its on-chain curve state. + + Args: + mint: Base token mint + client: Solana RPC client + + Returns: + Tuple of (TokenInfo, decoded curve state) + """ + implementations = get_platform_implementations(Platform.PUMP_FUN, client) + provider = implementations.address_provider + curve_manager = implementations.curve_manager + + bonding_curve = provider.derive_pool_address(mint) + state = await curve_manager.get_pool_state(bonding_curve, commitment="processed") + + creator = state["creator"] + creator = Pubkey.from_string(creator) if isinstance(creator, str) else creator + quote_mint = normalize_quote_mint(state["quote_mint"]) + + # Which token program owns the base mint decides the base ATA derivation. + account = await client.get_account_info(mint) + base_token_program = ( + SystemAddresses.TOKEN_2022_PROGRAM + if str(account.owner) == str(SystemAddresses.TOKEN_2022_PROGRAM) + else SystemAddresses.TOKEN_PROGRAM + ) + + token_info = TokenInfo( + name="simulation", + symbol="SIM", + uri="", + mint=mint, + platform=Platform.PUMP_FUN, + bonding_curve=bonding_curve, + associated_bonding_curve=provider.derive_associated_bonding_curve( + mint, bonding_curve, base_token_program + ), + creator=creator, + creator_vault=provider.derive_creator_vault(creator), + token_program_id=base_token_program, + is_mayhem_mode=state["is_mayhem_mode"], + is_cashback_coin=state["is_cashback_coin"], + quote_mint=quote_mint, + quote_token_program_id=quote_token_program(quote_mint), + ) + return token_info, state + + +async def simulate( + client: SolanaClient, wallet: Wallet, instructions: list, label: str +) -> bool: + """Run a set of instructions through simulateTransaction and report. + + Args: + client: Solana RPC client + wallet: Wallet whose pubkey pays and signs + instructions: Instructions to simulate + label: Human-readable name for output + + Returns: + True if the simulation reported no program error + """ + blockhash = await client.get_latest_blockhash() + message = Message.new_with_blockhash( + [set_compute_unit_limit(SIMULATION_CU_LIMIT), *instructions], + wallet.pubkey, + blockhash, + ) + transaction = Transaction([wallet.keypair], message, blockhash) + + response = await client.post_rpc( + { + "jsonrpc": "2.0", + "id": 1, + "method": "simulateTransaction", + "params": [ + b64encode(bytes(transaction)).decode(), + { + "encoding": "base64", + "sigVerify": False, + "replaceRecentBlockhash": True, + "commitment": "processed", + }, + ], + } + ) + + if not response or "result" not in response: + print(f" {label}: RPC call failed: {response}") + return False + + value = response["result"]["value"] + err = value.get("err") + units = value.get("unitsConsumed") + account_count = len(instructions[-1].accounts) + + print(f" {label}: accounts={account_count} unitsConsumed={units} err={err}") + if err: + for line in value.get("logs") or []: + if "Error" in line or "failed" in line or "Instruction:" in line: + print(f" {line}") + return err is None + + +async def main() -> int: + """Simulate a buy_v2 and sell_v2 for one coin. + + Returns: + Process exit code (0 if the buy simulation succeeded) + """ + mint_arg = sys.argv[1] if len(sys.argv) > 1 else None + + if mint_arg: + mint = Pubkey.from_string(mint_arg) + else: + print("No mint given; listening for a fresh pump.fun coin via geyser...") + mint = await discover_mint() + if mint is None: + print("No coin seen before timeout. Pass a mint explicitly.") + return 2 + + client = SolanaClient(os.environ["SOLANA_NODE_RPC_ENDPOINT"]) + wallet = Wallet(os.environ["SOLANA_PRIVATE_KEY"]) + + try: + token_info, state = await build_token_info(mint, client) + quote_mint = token_info.quote_mint + quote_unit = quote_units_per_token(quote_mint) + + print(f"\nmint: {mint}") + print(f"bonding_curve: {token_info.bonding_curve}") + print(f"quote_mint: {quote_mint}") + print(f"base program: {token_info.token_program_id}") + print( + f"mayhem={token_info.is_mayhem_mode} " + f"cashback={token_info.is_cashback_coin} " + f"complete={state['complete']}" + ) + print(f"price: {state['price_per_token']:.10f} quote/token") + print(f"wallet: {wallet.pubkey}\n") + + implementations = get_platform_implementations(Platform.PUMP_FUN, client) + provider = implementations.address_provider + builder = implementations.instruction_builder + + token_raw = SIMULATED_TOKEN_AMOUNT * 10**TOKEN_DECIMALS + # Cap the quote spend generously; simulation only needs it affordable. + max_quote_raw = int( + SIMULATED_TOKEN_AMOUNT * state["price_per_token"] * quote_unit * 2 + ) or int(0.001 * quote_unit) + + print("simulating:") + buy_instructions = await builder.build_buy_v2_instruction( + token_info, wallet.pubkey, max_quote_raw, token_raw, provider + ) + buy_ok = await simulate(client, wallet, buy_instructions, "buy_v2 ") + + sell_instructions = await builder.build_sell_v2_instruction( + token_info, wallet.pubkey, token_raw, 1, provider + ) + await simulate(client, wallet, sell_instructions, "sell_v2") + print( + " ^ expected to fail with AccountNotInitialized on " + "associated_base_user unless\n the wallet already holds this " + "coin. That error means all 26 accounts validated." + ) + + # Buy and sell in one transaction so the position exists mid-tx. This is + # the only way to get a representative sell_v2 CU number without + # actually holding the coin. Sell slightly less than bought to stay + # inside the realised balance. + combined = [*buy_instructions, *sell_instructions[:-1]] + resell_raw = int(token_raw * 0.9) + combined.append( + ( + await builder.build_sell_v2_instruction( + token_info, wallet.pubkey, resell_raw, 1, provider + ) + )[-1] + ) + combined_ok = await simulate(client, wallet, combined, "buy+sell") + if combined_ok: + print(" ^ subtract the buy_v2 figure above to estimate sell_v2 CU") + + return 0 if buy_ok else 1 + finally: + await client.close() + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/learning-examples/verify_v2_account_layout.py b/learning-examples/verify_v2_account_layout.py new file mode 100644 index 0000000..f35b64a --- /dev/null +++ b/learning-examples/verify_v2_account_layout.py @@ -0,0 +1,556 @@ +"""Cross-check the hardcoded buy_v2/sell_v2 account layouts against the IDL. + +The v2 instructions take 27 and 26 mandatory accounts in a fixed order. Getting +one position or writability flag wrong produces an on-chain failure that is +awkward to debug, so this script diffs the layouts in +`platforms.pumpfun.instruction_builder` against `idl/pump_fun_idl.json` and also +recomputes every PDA/ATA the address provider derives, comparing each against +the seeds declared in the IDL. + +Runs entirely offline — no RPC, no keys, no transactions. + +Usage: + uv run learning-examples/verify_v2_account_layout.py +""" + +import json +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(PROJECT_ROOT / "learning-examples")) +sys.path.insert(0, str(PROJECT_ROOT / "src")) + +from solders.pubkey import Pubkey # noqa: E402 +from spl.token.instructions import get_associated_token_address # noqa: E402 + +from core.pubkeys import USDC_MINT, WSOL_MINT, SystemAddresses # noqa: E402 +from interfaces.core import Platform, TokenInfo # noqa: E402 +from platforms.pumpfun.address_provider import ( # noqa: E402 + PumpFunAddresses, + PumpFunAddressProvider, +) +from platforms.pumpfun.instruction_builder import ( # noqa: E402 + _BUY_V2_ACCOUNTS, + _SELL_V2_ACCOUNTS, +) + +IDL_PATH = PROJECT_ROOT / "idl" / "pump_fun_idl.json" + + +def load_idl_accounts(name: str) -> list[dict]: + """Get the IDL account list for an instruction. + + Args: + name: Instruction name + + Returns: + List of IDL account definitions + """ + idl = json.loads(IDL_PATH.read_text()) + for instruction in idl["instructions"]: + if instruction["name"] == name: + return instruction["accounts"] + raise KeyError(f"Instruction {name} not present in {IDL_PATH}") + + +def check_layout(name: str, layout: list[tuple[str, bool]]) -> list[str]: + """Compare a hardcoded layout against the IDL's account list. + + Args: + name: Instruction name + layout: Ordered (account name, is_writable) pairs from our builder + + Returns: + List of human-readable problems (empty if the layout matches) + """ + idl_accounts = load_idl_accounts(name) + problems = [] + + if len(layout) != len(idl_accounts): + problems.append( + f"{name}: account count {len(layout)} != IDL {len(idl_accounts)}" + ) + + for index, (idl_account, ours) in enumerate(zip(idl_accounts, layout), start=1): + our_name, our_writable = ours + if idl_account["name"] != our_name: + problems.append( + f"{name}[{index}]: name {our_name!r} != IDL {idl_account['name']!r}" + ) + idl_writable = bool(idl_account.get("writable")) + if idl_writable != our_writable: + problems.append( + f"{name}[{index}] {our_name}: writable={our_writable} " + f"!= IDL writable={idl_writable}" + ) + if idl_account.get("signer") and our_name != "user": + problems.append( + f"{name}[{index}] {our_name}: IDL marks this a signer but only " + f"`user` is expected to sign" + ) + + return problems + + +def build_token_info(quote_mint: Pubkey, *, mayhem: bool) -> TokenInfo: + """Construct a TokenInfo for a synthetic coin. + + Args: + quote_mint: Quote mint to pair the coin against + mayhem: Whether the coin is in mayhem mode + + Returns: + TokenInfo suitable for driving the address provider + """ + provider = PumpFunAddressProvider() + # Fixed, arbitrary mint/creator so results are reproducible. + mint = Pubkey.from_string("CU7nUQaJ4beyYjC3xAUrh5RiSjw14fhU6oWTwRBse8gj") + creator = Pubkey.from_string("5wyFsNExysbXf2hTtcn8Tqd3urs9Nv85Zx1zNdAfTMmX") + bonding_curve = provider.derive_pool_address(mint) + + return TokenInfo( + name="layout-check", + symbol="CHK", + uri="", + mint=mint, + platform=Platform.PUMP_FUN, + bonding_curve=bonding_curve, + associated_bonding_curve=provider.derive_associated_bonding_curve( + mint, bonding_curve, SystemAddresses.TOKEN_2022_PROGRAM + ), + creator=creator, + creator_vault=provider.derive_creator_vault(creator), + token_program_id=SystemAddresses.TOKEN_2022_PROGRAM, + is_mayhem_mode=mayhem, + quote_mint=quote_mint, + ) + + +def check_derivations(quote_mint: Pubkey, *, mayhem: bool) -> list[str]: + """Independently recompute every derived v2 account and compare. + + Args: + quote_mint: Quote mint to pair the coin against + mayhem: Whether the coin is in mayhem mode + + Returns: + List of mismatches (empty if all derivations agree) + """ + provider = PumpFunAddressProvider() + token_info = build_token_info(quote_mint, mayhem=mayhem) + user = Pubkey.from_string("Ba99j1dYxidfQZvuNGMaXGxJsUeWXu6VNW8damkrdLVd") + accounts = provider.get_buy_v2_instruction_accounts(token_info, user) + + pump = PumpFunAddresses.PROGRAM + fee_program = PumpFunAddresses.FEE_PROGRAM + quote_program = SystemAddresses.TOKEN_PROGRAM + base_program = SystemAddresses.TOKEN_2022_PROGRAM + mint = token_info.mint + bonding_curve = token_info.bonding_curve + creator_vault = token_info.creator_vault + uva, _ = Pubkey.find_program_address( + [b"user_volume_accumulator", bytes(user)], pump + ) + + expected = { + "bonding_curve": Pubkey.find_program_address( + [b"bonding-curve", bytes(mint)], pump + )[0], + "creator_vault": Pubkey.find_program_address( + [b"creator-vault", bytes(token_info.creator)], pump + )[0], + "sharing_config": Pubkey.find_program_address( + [b"sharing-config", bytes(mint)], fee_program + )[0], + "global_volume_accumulator": Pubkey.find_program_address( + [b"global_volume_accumulator"], pump + )[0], + "user_volume_accumulator": uva, + "fee_config": Pubkey.find_program_address( + [b"fee_config", bytes(pump)], fee_program + )[0], + "event_authority": Pubkey.find_program_address([b"__event_authority"], pump)[0], + "associated_base_bonding_curve": get_associated_token_address( + bonding_curve, mint, base_program + ), + "associated_quote_bonding_curve": get_associated_token_address( + bonding_curve, quote_mint, quote_program + ), + "associated_base_user": get_associated_token_address(user, mint, base_program), + "associated_quote_user": get_associated_token_address( + user, quote_mint, quote_program + ), + "associated_creator_vault": get_associated_token_address( + creator_vault, quote_mint, quote_program + ), + "associated_quote_fee_recipient": get_associated_token_address( + accounts["fee_recipient"], quote_mint, quote_program + ), + "associated_quote_buyback_fee_recipient": get_associated_token_address( + accounts["buyback_fee_recipient"], quote_mint, quote_program + ), + "associated_user_volume_accumulator": get_associated_token_address( + uva, quote_mint, quote_program + ), + "quote_mint": quote_mint, + "base_mint": mint, + "user": user, + "program": pump, + "fee_program": fee_program, + "system_program": SystemAddresses.SYSTEM_PROGRAM, + "associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + "base_token_program": base_program, + "quote_token_program": quote_program, + "global": PumpFunAddresses.GLOBAL, + } + + problems = [ + f"{key}: provider={accounts[key]} != expected={value}" + for key, value in expected.items() + if accounts[key] != value + ] + + # The fee recipient must come from the right documented set. + recipient_set = ( + PumpFunAddresses.RESERVED_FEE_RECIPIENTS + if mayhem + else PumpFunAddresses.NORMAL_FEE_RECIPIENTS + ) + if accounts["fee_recipient"] not in recipient_set: + problems.append( + f"fee_recipient {accounts['fee_recipient']} not in " + f"{'reserved' if mayhem else 'normal'} fee recipient set" + ) + if accounts["buyback_fee_recipient"] not in PumpFunAddresses.BUYBACK_FEE_RECIPIENTS: + problems.append( + f"buyback_fee_recipient {accounts['buyback_fee_recipient']} not in " + f"buyback fee recipient set" + ) + + # Every account must be distinct except where the program expects reuse. + if len(set(accounts.values())) != len(accounts): + duplicates = [ + key + for key, value in accounts.items() + if list(accounts.values()).count(value) > 1 + ] + problems.append(f"duplicate account addresses for: {sorted(duplicates)}") + + return problems + + +def check_instruction_encoding() -> list[str]: + """Build real v2 instructions and assert their shape and data encoding. + + Covers the USDC path, which cannot be exercised on-chain until a + USDC-paired coin exists. + + Returns: + List of problems (empty if the instructions are well-formed) + """ + import asyncio + import struct + + from interfaces.core import Platform as _Platform # noqa: F401 + from platforms.pumpfun.instruction_builder import PumpFunInstructionBuilder + from utils.idl_manager import get_idl_manager + + provider = PumpFunAddressProvider() + builder = PumpFunInstructionBuilder(get_idl_manager().get_parser(Platform.PUMP_FUN)) + user = Pubkey.from_string("Ba99j1dYxidfQZvuNGMaXGxJsUeWXu6VNW8damkrdLVd") + problems = [] + + for quote_mint, label, expect_quote_ata in ( + (WSOL_MINT, "SOL-paired", False), + (USDC_MINT, "USDC-paired", True), + ): + token_info = build_token_info(quote_mint, mayhem=False) + + buy = asyncio.run( + builder.build_buy_v2_instruction( + token_info, user, 1_500_000, 20_000_000, provider + ) + ) + sell = asyncio.run( + builder.build_sell_v2_instruction( + token_info, user, 20_000_000, 900_000, provider + ) + ) + + # Instruction counts: base ATA always, quote ATA only for non-SOL. + expected_buy_ix = 3 if expect_quote_ata else 2 + if len(buy) != expected_buy_ix: + problems.append( + f"{label}: buy produced {len(buy)} instructions, " + f"expected {expected_buy_ix}" + ) + expected_sell_ix = 2 if expect_quote_ata else 1 + if len(sell) != expected_sell_ix: + problems.append( + f"{label}: sell produced {len(sell)} instructions, " + f"expected {expected_sell_ix}" + ) + + if len(buy[-1].accounts) != 27: + problems.append( + f"{label}: buy_v2 has {len(buy[-1].accounts)} accounts != 27" + ) + if len(sell[-1].accounts) != 26: + problems.append( + f"{label}: sell_v2 has {len(sell[-1].accounts)} accounts != 26" + ) + + # buy_v2 data: 8-byte discriminator + amount (tokens) + max_sol_cost. + # No trailing track_volume OptionBool, unlike the legacy buy. + buy_data = bytes(buy[-1].data) + if len(buy_data) != 24: + problems.append( + f"{label}: buy_v2 data is {len(buy_data)} bytes, expected 24 " + f"(discriminator + 2 u64, no track_volume)" + ) + else: + amount, max_cost = struct.unpack(" list[str]: + """Assert quote-amount config resolution and alias handling. + + Returns: + List of problems (empty if config resolution behaves correctly) + """ + from core.pubkeys import resolve_quote_amounts, resolve_quote_mint + from trading.universal_trader import _resolve_quote_config + + problems = [] + + if resolve_quote_mint("usdc") != USDC_MINT: + problems.append("alias 'usdc' did not resolve to the USDC mint") + if resolve_quote_mint("sol") != WSOL_MINT: + problems.append("alias 'sol' did not resolve to wrapped SOL") + if resolve_quote_mint(str(USDC_MINT)) != USDC_MINT: + problems.append("raw USDC mint string did not resolve") + + try: + resolve_quote_amounts({"usdc": 0}) + problems.append("resolve_quote_amounts accepted a zero amount") + except ValueError: + pass + + try: + resolve_quote_mint("not-a-mint") + problems.append("resolve_quote_mint accepted an invalid mint") + except ValueError: + pass + + # SOL always present from buy_amount; USDC only when configured. + amounts, allowed = _resolve_quote_config(0.0001, None, None) + if amounts.get(WSOL_MINT) != 0.0001: + problems.append("SOL amount did not fall back to buy_amount") + if USDC_MINT in amounts: + problems.append("USDC present in amounts without being configured") + if allowed is not None: + problems.append("allowed_quote_mints should be None when unset") + + amounts, allowed = _resolve_quote_config(0.0001, {"usdc": 5.0}, ["sol", "usdc"]) + if amounts.get(USDC_MINT) != 5.0: + problems.append("configured USDC amount not resolved") + if allowed != {WSOL_MINT, USDC_MINT}: + problems.append(f"allowed_quote_mints resolved to {allowed}") + + return problems + + +def check_examples_toolkit() -> list[str]: + """Check learning-examples/pump_v2.py agrees with the IDL and with src/. + + The examples carry their own standalone copy of the v2 layout so they stay + readable without importing src/. That copy is exactly the kind of thing that + silently drifts, so diff it against both sources of truth. + + Returns: + List of problems (empty if the toolkit agrees) + """ + import pump_v2 + + from platforms.pumpfun.address_provider import PumpFunAddresses + + problems = [] + user = Pubkey.from_string("Ba99j1dYxidfQZvuNGMaXGxJsUeWXu6VNW8damkrdLVd") + mint = Pubkey.from_string("CU7nUQaJ4beyYjC3xAUrh5RiSjw14fhU6oWTwRBse8gj") + creator = Pubkey.from_string("5wyFsNExysbXf2hTtcn8Tqd3urs9Nv85Zx1zNdAfTMmX") + + # Fee recipient sets must match src/ exactly. + for label, theirs, ours in ( + ( + "normal", + pump_v2.NORMAL_FEE_RECIPIENTS, + PumpFunAddresses.NORMAL_FEE_RECIPIENTS, + ), + ( + "reserved", + pump_v2.RESERVED_FEE_RECIPIENTS, + PumpFunAddresses.RESERVED_FEE_RECIPIENTS, + ), + ( + "buyback", + pump_v2.BUYBACK_FEE_RECIPIENTS, + PumpFunAddresses.BUYBACK_FEE_RECIPIENTS, + ), + ): + if theirs != ours: + problems.append(f"pump_v2 {label} fee recipients differ from src/") + + # Discriminators must match the IDL. + idl = json.loads(IDL_PATH.read_text()) + by_name = {i["name"]: i for i in idl["instructions"]} + for name, disc in ( + ("buy_v2", pump_v2.BUY_V2_DISCRIMINATOR), + ("sell_v2", pump_v2.SELL_V2_DISCRIMINATOR), + ): + expected = bytes(by_name[name]["discriminator"]) + if disc != expected: + problems.append( + f"pump_v2 {name} discriminator {list(disc)} != IDL {list(expected)}" + ) + + # Account lists must match the IDL in order and writability, for both + # quote assets and both mayhem states. + for quote_mint, label in ( + (pump_v2.WSOL_MINT, "SOL"), + (pump_v2.USDC_MINT, "USDC"), + ): + for mayhem in (False, True): + for name, builder in ( + ("buy_v2", pump_v2.build_buy_v2_instruction), + ("sell_v2", pump_v2.build_sell_v2_instruction), + ): + kwargs = { + "base_mint": mint, + "creator": creator, + "user": user, + "quote_mint": quote_mint, + "is_mayhem_mode": mayhem, + } + if name == "buy_v2": + instruction = builder( + token_amount_raw=1, max_quote_cost_raw=2, **kwargs + ) + else: + instruction = builder( + token_amount_raw=1, min_quote_output_raw=2, **kwargs + ) + + idl_accounts = by_name[name]["accounts"] + if len(instruction.accounts) != len(idl_accounts): + problems.append( + f"pump_v2 {name} ({label}, mayhem={mayhem}): " + f"{len(instruction.accounts)} accounts != IDL " + f"{len(idl_accounts)}" + ) + continue + for index, (meta, idl_account) in enumerate( + zip(instruction.accounts, idl_accounts), start=1 + ): + if meta.is_writable != bool(idl_account.get("writable")): + problems.append( + f"pump_v2 {name}[{index}] {idl_account['name']} " + f"({label}, mayhem={mayhem}): writable " + f"{meta.is_writable} != IDL " + f"{bool(idl_account.get('writable'))}" + ) + + # Cross-check every address against the src/ provider, which the + # checks above already validated. + provider = PumpFunAddressProvider() + token_info = build_token_info(quote_mint, mayhem=mayhem) + resolved = ( + provider.get_buy_v2_instruction_accounts(token_info, user) + if name == "buy_v2" + else provider.get_sell_v2_instruction_accounts(token_info, user) + ) + layout = _BUY_V2_ACCOUNTS if name == "buy_v2" else _SELL_V2_ACCOUNTS + for (account_name, _), meta in zip(layout, instruction.accounts): + # Fee recipients are picked at random from a set, so compare + # membership rather than identity. + if "fee_recipient" in account_name: + continue + if resolved[account_name] != meta.pubkey: + problems.append( + f"pump_v2 {name} {account_name} ({label}, " + f"mayhem={mayhem}): {meta.pubkey} != src/ " + f"{resolved[account_name]}" + ) + + return problems + + +def main() -> int: + """Run all layout and derivation checks. + + Returns: + Process exit code (0 on success) + """ + all_problems = [] + + for name, layout in (("buy_v2", _BUY_V2_ACCOUNTS), ("sell_v2", _SELL_V2_ACCOUNTS)): + problems = check_layout(name, layout) + status = "OK" if not problems else f"{len(problems)} PROBLEM(S)" + print(f"{name}: {len(layout)} accounts vs IDL -> {status}") + all_problems.extend(problems) + + for quote_mint, label in ((WSOL_MINT, "SOL-paired"), (USDC_MINT, "USDC-paired")): + for mayhem in (False, True): + problems = check_derivations(quote_mint, mayhem=mayhem) + tag = f"{label}, mayhem={mayhem}" + status = "OK" if not problems else f"{len(problems)} PROBLEM(S)" + print(f"derivations ({tag}) -> {status}") + all_problems.extend(problems) + + for name, check in ( + ("instruction encoding", check_instruction_encoding), + ("quote config resolution", check_quote_config), + ("learning-examples pump_v2 toolkit", check_examples_toolkit), + ): + problems = check() + status = "OK" if not problems else f"{len(problems)} PROBLEM(S)" + print(f"{name} -> {status}") + all_problems.extend(problems) + + if all_problems: + print("\nProblems found:") + for problem in all_problems: + print(f" - {problem}") + return 1 + + print("\nAll v2 account layouts and derivations match the IDL.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/bot_runner.py b/src/bot_runner.py index 18fe997..ebf4133 100644 --- a/src/bot_runner.py +++ b/src/bot_runner.py @@ -98,6 +98,9 @@ async def start_bot(config_path: str): # Extreme fast mode settings extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), + # Quote asset configuration (pump.fun non-SOL pairs) + quote_amounts=cfg["trade"].get("quote_amounts"), + allowed_quote_mints=cfg["filters"].get("allowed_quote_mints"), # Exit strategy configuration exit_strategy=cfg["trade"].get("exit_strategy", "time_based"), take_profit_percentage=cfg["trade"].get("take_profit_percentage"), diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index b545f56..3cbbf14 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -69,7 +69,16 @@ class AccountCleanupManager: balance = await self.client.get_token_account_balance(ata) instructions = [] - if balance > 0 and self.close_with_force_burn: + # Never burn wrapped SOL: closing a WSOL account already returns + # both the wrapped lamports and the rent to the owner, so burning + # first would destroy real value. + if balance > 0 and mint == SystemAddresses.WSOL_MINT: + logger.info( + f"Unwrapping {balance} lamports of wrapped SOL from {ata} " + f"by closing it (burn skipped)" + ) + + elif balance > 0 and self.close_with_force_burn: logger.info( f"Burning {balance} tokens from ATA {ata} (mint: {mint})..." ) diff --git a/src/config_loader.py b/src/config_loader.py index 2b9eea3..8719336 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -188,6 +188,10 @@ def validate_config(config: dict) -> None: if "Missing required config key" not in str(e): raise + # Quote-asset configuration must resolve before the bot starts trading, + # otherwise a bad mint alias only surfaces on the first non-SOL coin. + validate_quote_config(config) + # Platform-specific validation platform_str = config.get("platform", "pump_fun") try: @@ -201,6 +205,35 @@ def validate_config(config: dict) -> None: raise +def validate_quote_config(config: dict) -> None: + """Validate trade.quote_amounts and filters.allowed_quote_mints. + + Args: + config: Loaded bot configuration + + Raises: + ValueError: If a quote mint alias/address or amount is invalid + """ + from core.pubkeys import resolve_quote_amounts, resolve_quote_mint + + quote_amounts = config.get("trade", {}).get("quote_amounts") + if quote_amounts is not None: + if not isinstance(quote_amounts, dict): + raise ValueError( + "trade.quote_amounts must be a mapping of quote mint to amount" + ) + resolve_quote_amounts(quote_amounts) + + allowed = config.get("filters", {}).get("allowed_quote_mints") + if allowed is not None: + if not isinstance(allowed, list) or not allowed: + raise ValueError( + "filters.allowed_quote_mints must be a non-empty list of quote mints" + ) + for mint in allowed: + resolve_quote_mint(mint) + + def validate_platform_config(config: dict, platform: Platform) -> None: """Validate platform-specific configuration requirements.""" # Check if platform is supported diff --git a/src/core/client.py b/src/core/client.py index 0d57067..01e40ed 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -19,6 +19,7 @@ from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction +from core.pubkeys import is_sol_paired from core.rpc_rate_limiter import TokenBucketRateLimiter from utils.logger import get_logger @@ -359,21 +360,30 @@ class SolanaClient: return None async def get_buy_transaction_details( - self, signature: str, mint: Pubkey, sol_destination: Pubkey + self, + signature: str, + mint: Pubkey, + sol_destination: Pubkey, + quote_mint: Pubkey | None = None, ) -> tuple[int | None, int | None]: - """Get actual tokens received and SOL spent from a buy transaction. + """Get actual tokens received and quote spent from a buy transaction. Uses preBalances/postBalances to find exact SOL transferred to the pool/curve and pre/post token balance diff to find tokens received. + For coins paired against an SPL quote asset (e.g. USDC) the quote spend + does not show up in lamport balances, so it is read from the quote + mint's token balance deltas instead. Args: signature: Transaction signature mint: Token mint address sol_destination: Address where SOL is sent (bonding curve for pump.fun, quote_vault for letsbonk) + quote_mint: Quote mint of the coin. Pass None or wrapped SOL for + SOL-paired coins. Returns: - Tuple of (tokens_received_raw, sol_spent_lamports), or (None, None) + Tuple of (tokens_received_raw, quote_spent_raw), or (None, None) """ result = await self._get_transaction_result(signature) if not result: @@ -384,47 +394,29 @@ 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}" - ) + logger.error(f"Transaction {signature[:16]}... failed with error: {tx_err}") return None, None - mint_str = str(mint) - # Get tokens received from pre/post token balance diff # This works for Token2022 where owner might be different - tokens_received = None - pre_token_balances = meta.get("preTokenBalances", []) - post_token_balances = meta.get("postTokenBalances", []) + tokens_received = self._extract_positive_token_diff(meta, str(mint)) + if tokens_received is not None: + logger.info(f"Tokens received from tx: {tokens_received}") - # Build lookup by account index - pre_by_idx = {b.get("accountIndex"): b for b in pre_token_balances} - post_by_idx = {b.get("accountIndex"): b for b in post_token_balances} - - # Find positive token diff for our mint (user receiving tokens) - all_indices = set(pre_by_idx.keys()) | set(post_by_idx.keys()) - for idx in all_indices: - pre = pre_by_idx.get(idx) - post = post_by_idx.get(idx) - - # Check if this is our mint - balance_mint = (post or pre).get("mint", "") - if balance_mint != mint_str: - continue - - pre_amount = ( - int(pre.get("uiTokenAmount", {}).get("amount", 0)) if pre else 0 - ) - post_amount = ( - int(post.get("uiTokenAmount", {}).get("amount", 0)) if post else 0 - ) - diff = post_amount - pre_amount - - # Positive diff means tokens received (not the bonding curve's negative) - if diff > 0: - tokens_received = diff - logger.info(f"Tokens received from tx: {tokens_received}") - break + # Non-SOL quote assets move as SPL token transfers, so the lamport + # deltas below would report only rent/fees. Read the quote spend from + # the quote mint's token balance deltas: the positive diff is the + # curve's quote vault receiving what the buyer paid. + if quote_mint is not None and not is_sol_paired(quote_mint): + quote_spent = self._extract_positive_token_diff(meta, str(quote_mint)) + if quote_spent is None: + logger.warning( + f"No positive {quote_mint} balance diff found in tx " + f"{signature[:16]}...; cannot determine quote spent" + ) + else: + logger.info(f"Quote spent from tx: {quote_spent} (mint {quote_mint})") + return tokens_received, quote_spent # Get SOL spent from preBalances/postBalances at sol_destination sol_destination_str = str(sol_destination) @@ -451,6 +443,45 @@ class SolanaClient: return tokens_received, sol_spent + @staticmethod + def _extract_positive_token_diff(meta: dict, mint_str: str) -> int | None: + """Find the largest positive token balance change for a mint in a tx. + + Args: + meta: Transaction meta containing pre/postTokenBalances + mint_str: Mint address to look for + + Returns: + Raw positive balance delta, or None if no account gained this mint + """ + pre_by_idx = { + b.get("accountIndex"): b for b in meta.get("preTokenBalances", []) + } + post_by_idx = { + b.get("accountIndex"): b for b in meta.get("postTokenBalances", []) + } + + best: int | None = None + for idx in set(pre_by_idx) | set(post_by_idx): + pre = pre_by_idx.get(idx) + post = post_by_idx.get(idx) + + if (post or pre).get("mint", "") != mint_str: + continue + + pre_amount = ( + int(pre.get("uiTokenAmount", {}).get("amount", 0)) if pre else 0 + ) + post_amount = ( + int(post.get("uiTokenAmount", {}).get("amount", 0)) if post else 0 + ) + diff = post_amount - pre_amount + + if diff > 0 and (best is None or diff > best): + best = diff + + return best + async def _get_transaction_result(self, signature: str) -> dict | None: """Fetch transaction result from RPC. diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py index ad7182e..312b773 100644 --- a/src/core/pubkeys.py +++ b/src/core/pubkeys.py @@ -38,6 +38,161 @@ SOL_MINT: Final[Pubkey] = Pubkey.from_string( "So11111111111111111111111111111111111111112" ) +# Quote mints supported by pump.fun's v2 trade instructions. +# `bonding_curve.quote_mint` is Pubkey::default() (all zeros) for SOL-paired +# coins; the v2 instructions still expect wrapped SOL to be passed explicitly. +# Doc: pump-public-docs README, "New Bonding Curve Trade Instructions". +DEFAULT_PUBKEY: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") +WSOL_MINT: Final[Pubkey] = SOL_MINT +USDC_MINT: Final[Pubkey] = Pubkey.from_string( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" +) + +# Decimals per quote mint. SOL uses 9, USDC uses 6. +QUOTE_DECIMALS: Final[dict[Pubkey, int]] = { + WSOL_MINT: 9, + 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. +QUOTE_TOKEN_PROGRAMS: Final[dict[Pubkey, Pubkey]] = { + WSOL_MINT: TOKEN_PROGRAM, + USDC_MINT: TOKEN_PROGRAM, +} + + +def quote_decimals(quote_mint: Pubkey) -> int: + """Get the decimal count for a quote mint, defaulting to SOL's 9. + + Args: + quote_mint: Quote mint address + + Returns: + Number of decimals used by the quote mint + """ + return QUOTE_DECIMALS.get(quote_mint, 9) + + +def quote_units_per_token(quote_mint: Pubkey) -> int: + """Get the raw-units-per-whole-unit factor for a quote mint. + + Args: + quote_mint: Quote mint address + + Returns: + 10 ** decimals for the quote mint (1e9 for SOL, 1e6 for USDC) + """ + return 10 ** quote_decimals(quote_mint) + + +def quote_token_program(quote_mint: Pubkey) -> Pubkey: + """Get the token program owning a quote mint, defaulting to SPL Token. + + Args: + quote_mint: Quote mint address + + Returns: + Token program id for the quote mint + """ + return QUOTE_TOKEN_PROGRAMS.get(quote_mint, TOKEN_PROGRAM) + + +def normalize_quote_mint(quote_mint: Pubkey | None) -> Pubkey: + """Resolve a bonding curve's quote_mint into a concrete mint address. + + SOL-paired coins carry Pubkey::default() on-chain but must be traded with + wrapped SOL passed as the quote mint. + + Args: + quote_mint: Raw quote_mint from the bonding curve, or None + + Returns: + Wrapped SOL for SOL-paired coins, otherwise the quote mint unchanged + """ + if quote_mint is None or quote_mint == DEFAULT_PUBKEY: + return WSOL_MINT + return quote_mint + + +# Friendly aliases accepted in bot YAML so configs don't need raw mints. +QUOTE_MINT_ALIASES: Final[dict[str, Pubkey]] = { + "sol": WSOL_MINT, + "wsol": WSOL_MINT, + "usdc": USDC_MINT, +} + + +def resolve_quote_mint(value: str | Pubkey) -> Pubkey: + """Resolve a config value into a quote mint address. + + Accepts the aliases "sol"/"wsol"/"usdc" or a raw base58 mint address. + + Args: + value: Alias or mint address from configuration + + Returns: + Resolved quote mint + + Raises: + ValueError: If the value is neither a known alias nor a valid address + """ + if isinstance(value, Pubkey): + return normalize_quote_mint(value) + + alias = QUOTE_MINT_ALIASES.get(str(value).strip().lower()) + if alias is not None: + return alias + + try: + return normalize_quote_mint(Pubkey.from_string(str(value))) + except (ValueError, TypeError) as exc: + raise ValueError( + f"Unknown quote mint {value!r}. Use one of " + f"{sorted(QUOTE_MINT_ALIASES)} or a base58 mint address." + ) from exc + + +def resolve_quote_amounts( + amounts: dict[str, float] | None, +) -> dict[Pubkey, float]: + """Resolve a config map of quote mint -> spend amount. + + Args: + amounts: Mapping of alias/mint address to amount in whole quote units + + Returns: + Mapping keyed by resolved Pubkey (empty if amounts is None) + + Raises: + ValueError: If a key is not a valid quote mint or an amount is not positive + """ + if not amounts: + return {} + + resolved: dict[Pubkey, float] = {} + for key, amount in amounts.items(): + mint = resolve_quote_mint(key) + if not isinstance(amount, int | float) or amount <= 0: + raise ValueError( + f"quote_amounts[{key!r}] must be a positive number, got {amount!r}" + ) + resolved[mint] = float(amount) + return resolved + + +def is_sol_paired(quote_mint: Pubkey | None) -> bool: + """Check whether a coin is SOL-paired (native SOL transfers). + + Args: + quote_mint: Raw or normalized quote mint + + Returns: + True if the coin trades against native/wrapped SOL + """ + return normalize_quote_mint(quote_mint) == WSOL_MINT + class SystemAddresses: """System-level Solana addresses shared across all platforms.""" @@ -49,6 +204,9 @@ class SystemAddresses: ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM RENT = RENT SOL_MINT = SOL_MINT + WSOL_MINT = WSOL_MINT + USDC_MINT = USDC_MINT + DEFAULT_PUBKEY = DEFAULT_PUBKEY @classmethod def get_all_system_addresses(cls) -> dict[str, Pubkey]: diff --git a/src/interfaces/core.py b/src/interfaces/core.py index aab92a9..d79eda2 100644 --- a/src/interfaces/core.py +++ b/src/interfaces/core.py @@ -49,6 +49,13 @@ class TokenInfo: is_mayhem_mode: bool = False # pump.fun mayhem mode flag is_cashback_coin: bool = False # pump.fun cashback coin flag + # Quote asset (pump.fun v2 instructions). SOL-paired coins carry + # Pubkey::default() on-chain; normalize_quote_mint() maps that to wrapped + # SOL, which is what buy_v2/sell_v2 expect to be passed. + quote_mint: Pubkey | None = None + quote_token_program_id: Pubkey | None = None + virtual_quote_reserves: int | None = None + # Metadata creation_timestamp: float | None = None additional_data: dict[str, Any] | None = None diff --git a/src/monitoring/universal_block_listener.py b/src/monitoring/universal_block_listener.py index a538f8f..a3874eb 100644 --- a/src/monitoring/universal_block_listener.py +++ b/src/monitoring/universal_block_listener.py @@ -18,6 +18,12 @@ from utils.logger import get_logger logger = get_logger(__name__) +# Solana logsSubscribe / blockSubscribe frames routinely exceed the websockets +# library's 1 MiB default, which closes the connection with code 1009 +# ("message too big"). Reconnecting recovers, but every dropped frame is a +# missed token, so raise the ceiling instead of eating the disconnects. +WEBSOCKET_MAX_MESSAGE_BYTES = 32 * 1024 * 1024 + class UniversalBlockListener(BaseTokenListener): """Universal block listener that works with any platform.""" @@ -97,7 +103,9 @@ class UniversalBlockListener(BaseTokenListener): while True: try: - async with websockets.connect(self.wss_endpoint) as websocket: + async with websockets.connect( + self.wss_endpoint, max_size=WEBSOCKET_MAX_MESSAGE_BYTES + ) as websocket: await self._subscribe_to_programs(websocket) ping_task = asyncio.create_task(self._ping_loop(websocket)) diff --git a/src/monitoring/universal_logs_listener.py b/src/monitoring/universal_logs_listener.py index 325916e..c4f9830 100644 --- a/src/monitoring/universal_logs_listener.py +++ b/src/monitoring/universal_logs_listener.py @@ -14,6 +14,12 @@ from utils.logger import get_logger logger = get_logger(__name__) +# Solana logsSubscribe / blockSubscribe frames routinely exceed the websockets +# library's 1 MiB default, which closes the connection with code 1009 +# ("message too big"). Reconnecting recovers, but every dropped frame is a +# missed token, so raise the ceiling instead of eating the disconnects. +WEBSOCKET_MAX_MESSAGE_BYTES = 32 * 1024 * 1024 + class UniversalLogsListener(BaseTokenListener): """Universal logs listener that works with any platform.""" @@ -96,7 +102,9 @@ class UniversalLogsListener(BaseTokenListener): while True: try: - async with websockets.connect(self.wss_endpoint) as websocket: + async with websockets.connect( + self.wss_endpoint, max_size=WEBSOCKET_MAX_MESSAGE_BYTES + ) as websocket: await self._subscribe_to_logs(websocket) ping_task = asyncio.create_task(self._ping_loop(websocket)) diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py index 2cb825d..bb4de1c 100644 --- a/src/platforms/pumpfun/address_provider.py +++ b/src/platforms/pumpfun/address_provider.py @@ -5,13 +5,14 @@ This module provides all pump.fun-specific addresses and PDA derivations by implementing the AddressProvider interface. """ +import secrets from dataclasses import dataclass from typing import ClassVar, Final from solders.pubkey import Pubkey from spl.token.instructions import get_associated_token_address -from core.pubkeys import SystemAddresses +from core.pubkeys import SystemAddresses, normalize_quote_mint, quote_token_program from interfaces.core import AddressProvider, Platform, TokenInfo @@ -43,10 +44,35 @@ class PumpFunAddresses: FEE_PROGRAM: Final[Pubkey] = Pubkey.from_string( "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" ) - # 8 breaking-upgrade fee recipients (pump.fun program upgrade 2026-04-28). - # One must be appended (mutable) AFTER bonding-curve-v2 on every buy/sell. - # Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md - BREAKING_FEE_RECIPIENTS: ClassVar[list[Pubkey]] = [ + # 8 normal fee recipients — use one as fee_recipient for non-mayhem coins. + # See FEE_RECIPIENTS.md in the pump-fun public docs repository. + NORMAL_FEE_RECIPIENTS: ClassVar[list[Pubkey]] = [ + Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"), + Pubkey.from_string("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"), + Pubkey.from_string("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"), + Pubkey.from_string("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"), + Pubkey.from_string("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"), + Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"), + Pubkey.from_string("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"), + Pubkey.from_string("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"), + ] + # 8 reserved fee recipients — use one as fee_recipient for mayhem coins. + RESERVED_FEE_RECIPIENTS: ClassVar[list[Pubkey]] = [ + Pubkey.from_string("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"), + Pubkey.from_string("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"), + Pubkey.from_string("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"), + Pubkey.from_string("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"), + Pubkey.from_string("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"), + Pubkey.from_string("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"), + Pubkey.from_string("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"), + Pubkey.from_string("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"), + ] + # 8 buyback fee recipients — one is required on every buy/sell, for every + # coin. On the legacy buy/sell these are appended (mutable) after + # bonding-curve-v2; on buy_v2/sell_v2 they are the buyback_fee_recipient + # account. Introduced by the 2026-04-28 program upgrade. + # See FEE_RECIPIENTS.md in the pump-fun public docs repository. + BUYBACK_FEE_RECIPIENTS: ClassVar[list[Pubkey]] = [ Pubkey.from_string("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"), Pubkey.from_string("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"), Pubkey.from_string("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"), @@ -56,16 +82,40 @@ class PumpFunAddresses: Pubkey.from_string("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"), Pubkey.from_string("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"), ] + # Back-compat alias for the pre-v2 naming used by the legacy buy/sell path. + BREAKING_FEE_RECIPIENTS: ClassVar[list[Pubkey]] = BUYBACK_FEE_RECIPIENTS @staticmethod - def pick_breaking_fee_recipient() -> Pubkey: - """Pick one of the 8 breaking-upgrade fee recipients at random. + def pick_buyback_fee_recipient() -> Pubkey: + """Pick one of the 8 buyback fee recipients at random. Spreads load across recipients per pump.fun's recommendation. """ - import random + return secrets.choice(PumpFunAddresses.BUYBACK_FEE_RECIPIENTS) - return random.choice(PumpFunAddresses.BREAKING_FEE_RECIPIENTS) + @staticmethod + def pick_breaking_fee_recipient() -> Pubkey: + """Deprecated alias for :meth:`pick_buyback_fee_recipient`.""" + return PumpFunAddresses.pick_buyback_fee_recipient() + + @staticmethod + def find_sharing_config(base_mint: Pubkey) -> Pubkey: + """Derive the creator-fee sharing config PDA for a coin. + + Mandatory on buy_v2/sell_v2. Lives under the pump fees program, not + the pump program. + + Args: + base_mint: Base token mint address + + Returns: + Pubkey of the derived sharing config account + """ + derived_address, _ = Pubkey.find_program_address( + [b"sharing-config", bytes(base_mint)], + PumpFunAddresses.FEE_PROGRAM, + ) + return derived_address @staticmethod def find_global_volume_accumulator() -> Pubkey: @@ -237,7 +287,10 @@ class PumpFunAddressProvider(AddressProvider): return accounts def derive_associated_bonding_curve( - self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None + self, + mint: Pubkey, + bonding_curve: Pubkey, + token_program_id: Pubkey | None = None, ) -> Pubkey: """Derive the associated bonding curve (ATA of bonding curve for the token). @@ -314,6 +367,160 @@ class PumpFunAddressProvider(AddressProvider): """ return PumpFunAddresses.find_fee_config() + def derive_sharing_config(self, base_mint: Pubkey) -> Pubkey: + """Derive the creator-fee sharing config PDA for a coin. + + Args: + base_mint: Base token mint address + + Returns: + Sharing config address + """ + return PumpFunAddresses.find_sharing_config(base_mint) + + def resolve_quote(self, token_info: TokenInfo) -> tuple[Pubkey, Pubkey]: + """Resolve the quote mint and its token program for a coin. + + SOL-paired coins store Pubkey::default() in bonding_curve.quote_mint + but must pass wrapped SOL to the v2 instructions. + + Args: + token_info: Token information + + Returns: + 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_mint + ) + return quote_mint, quote_program + + def derive_quote_token_account( + self, owner: Pubkey, quote_mint: Pubkey, quote_token_program_id: Pubkey + ) -> Pubkey: + """Derive an associated token account for the quote mint. + + Args: + owner: Account that owns the ATA (may be a PDA) + quote_mint: Quote mint address + quote_token_program_id: Token program owning the quote mint + + Returns: + Associated token account address + """ + return get_associated_token_address(owner, quote_mint, quote_token_program_id) + + def _get_v2_common_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: + """Build the account set shared by buy_v2 and sell_v2. + + 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. + + Args: + token_info: Token information + user: User's wallet address + + Returns: + Dictionary of account addresses keyed by IDL account name + """ + additional_accounts = self.get_additional_accounts(token_info) + + base_mint = token_info.mint + base_token_program = ( + token_info.token_program_id or SystemAddresses.TOKEN_2022_PROGRAM + ) + quote_mint, quote_program = self.resolve_quote(token_info) + + bonding_curve = additional_accounts.get( + "bonding_curve", token_info.bonding_curve + ) + creator_vault = additional_accounts.get( + "creator_vault", token_info.creator_vault + ) + fee_recipient = self.get_fee_recipient(token_info) + buyback_fee_recipient = PumpFunAddresses.pick_buyback_fee_recipient() + user_volume_accumulator = self.derive_user_volume_accumulator(user) + + return { + "global": PumpFunAddresses.GLOBAL, + "base_mint": base_mint, + "quote_mint": quote_mint, + "base_token_program": base_token_program, + "quote_token_program": quote_program, + "associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + "fee_recipient": fee_recipient, + "associated_quote_fee_recipient": self.derive_quote_token_account( + fee_recipient, quote_mint, quote_program + ), + "buyback_fee_recipient": buyback_fee_recipient, + "associated_quote_buyback_fee_recipient": self.derive_quote_token_account( + buyback_fee_recipient, quote_mint, quote_program + ), + "bonding_curve": bonding_curve, + "associated_base_bonding_curve": additional_accounts.get( + "associated_bonding_curve", token_info.associated_bonding_curve + ), + "associated_quote_bonding_curve": self.derive_quote_token_account( + bonding_curve, quote_mint, quote_program + ), + "user": user, + "associated_base_user": self.derive_user_token_account( + user, base_mint, base_token_program + ), + "associated_quote_user": self.derive_quote_token_account( + user, quote_mint, quote_program + ), + "creator_vault": creator_vault, + "associated_creator_vault": self.derive_quote_token_account( + creator_vault, quote_mint, quote_program + ), + "sharing_config": self.derive_sharing_config(base_mint), + "user_volume_accumulator": user_volume_accumulator, + "associated_user_volume_accumulator": self.derive_quote_token_account( + user_volume_accumulator, quote_mint, quote_program + ), + "fee_config": self.derive_fee_config(), + "fee_program": PumpFunAddresses.FEE_PROGRAM, + "system_program": SystemAddresses.SYSTEM_PROGRAM, + "event_authority": PumpFunAddresses.EVENT_AUTHORITY, + "program": PumpFunAddresses.PROGRAM, + } + + def get_buy_v2_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: + """Get all 27 accounts needed for a buy_v2 instruction. + + Args: + token_info: Token information + user: User's wallet address + + Returns: + Dictionary of account addresses for the buy_v2 instruction + """ + accounts = self._get_v2_common_accounts(token_info, user) + accounts["global_volume_accumulator"] = self.derive_global_volume_accumulator() + return accounts + + def get_sell_v2_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: + """Get all 26 accounts needed for a sell_v2 instruction. + + Args: + token_info: Token information + user: User's wallet address + + Returns: + Dictionary of account addresses for the sell_v2 instruction + """ + return self._get_v2_common_accounts(token_info, user) + def get_fee_recipient(self, token_info: TokenInfo) -> Pubkey: """Get the correct fee recipient based on mayhem mode. diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py index 74fd188..5eace49 100644 --- a/src/platforms/pumpfun/curve_manager.py +++ b/src/platforms/pumpfun/curve_manager.py @@ -10,7 +10,13 @@ from typing import Any from solders.pubkey import Pubkey from core.client import SolanaClient -from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from core.pubkeys import ( + LAMPORTS_PER_SOL, + TOKEN_DECIMALS, + is_sol_paired, + normalize_quote_mint, + quote_units_per_token, +) from interfaces.core import CurveManager, Platform from utils.idl_parser import IDLParser from utils.logger import get_logger @@ -75,20 +81,17 @@ class PumpFunCurveManager(CurveManager): pool_address: Address of the bonding curve Returns: - Current token price in SOL + Current token price denominated in the curve's quote asset + (SOL for SOL-paired coins, USDC for USDC-paired coins) """ pool_state = await self.get_pool_state(pool_address) - # Use virtual reserves for price calculation - virtual_token_reserves = pool_state["virtual_token_reserves"] - virtual_sol_reserves = pool_state["virtual_sol_reserves"] - - if virtual_token_reserves <= 0: + if pool_state["virtual_token_reserves"] <= 0: return 0.0 - # Price = sol_reserves / token_reserves - price_lamports = virtual_sol_reserves / virtual_token_reserves - return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL + # _decode_curve_state_with_idl already scales by the quote mint's + # decimals, so don't re-derive the price with a hardcoded 1e9 here. + return pool_state["price_per_token"] async def calculate_buy_amount_out( self, pool_address: Pubkey, amount_in: int @@ -185,51 +188,77 @@ class PumpFunCurveManager(CurveManager): if not decoded_curve_state: raise ValueError("Failed to decode bonding curve state with IDL parser") - # Extract the fields we need for trading calculations - # Based on the BondingCurve structure from the IDL + # Extract the fields we need for trading calculations. + # The BondingCurve struct renamed its SOL fields to quote fields when + # pump.fun added non-SOL quote assets, and appended quote_mint. The + # old names are kept as aliases below so callers written against the + # SOL-only layout keep working for SOL-paired coins. + raw_quote_mint = decoded_curve_state.get("quote_mint") + quote_mint = normalize_quote_mint( + Pubkey.from_string(raw_quote_mint) + if isinstance(raw_quote_mint, str) + else raw_quote_mint + ) + quote_unit = quote_units_per_token(quote_mint) + curve_data = { "virtual_token_reserves": decoded_curve_state.get( "virtual_token_reserves", 0 ), - "virtual_sol_reserves": decoded_curve_state.get("virtual_sol_reserves", 0), + "virtual_quote_reserves": decoded_curve_state.get( + "virtual_quote_reserves", 0 + ), "real_token_reserves": decoded_curve_state.get("real_token_reserves", 0), - "real_sol_reserves": decoded_curve_state.get("real_sol_reserves", 0), + "real_quote_reserves": decoded_curve_state.get("real_quote_reserves", 0), "token_total_supply": decoded_curve_state.get("token_total_supply", 0), "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_curve_state.get("is_cashback_coin", False), + "quote_mint": quote_mint, + "is_sol_paired": is_sol_paired(quote_mint), } + # Back-compat aliases for the pre-quote-mint field names. + curve_data["virtual_sol_reserves"] = curve_data["virtual_quote_reserves"] + curve_data["real_sol_reserves"] = curve_data["real_quote_reserves"] + # Calculate additional metrics # Validate reserves are positive before calculating price if curve_data["virtual_token_reserves"] <= 0: raise ValueError( f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price" ) - if curve_data["virtual_sol_reserves"] <= 0: + if curve_data["virtual_quote_reserves"] <= 0: raise ValueError( - f"Invalid virtual_sol_reserves: {curve_data['virtual_sol_reserves']} - cannot calculate price" + f"Invalid virtual_quote_reserves: {curve_data['virtual_quote_reserves']} - cannot calculate price" ) + # Price is denominated in the curve's quote asset, so scale by that + # mint's decimals (1e9 for SOL, 1e6 for USDC) rather than assuming SOL. curve_data["price_per_token"] = ( - (curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"]) + ( + curve_data["virtual_quote_reserves"] + / curve_data["virtual_token_reserves"] + ) * (10**TOKEN_DECIMALS) - / LAMPORTS_PER_SOL + / quote_unit ) # Add convenience decimal fields curve_data["token_reserves_decimal"] = ( curve_data["virtual_token_reserves"] / 10**TOKEN_DECIMALS ) - curve_data["sol_reserves_decimal"] = ( - curve_data["virtual_sol_reserves"] / LAMPORTS_PER_SOL + curve_data["quote_reserves_decimal"] = ( + curve_data["virtual_quote_reserves"] / quote_unit ) + curve_data["sol_reserves_decimal"] = curve_data["quote_reserves_decimal"] logger.debug( f"Decoded curve state: virtual_token_reserves={curve_data['virtual_token_reserves']}, " - f"virtual_sol_reserves={curve_data['virtual_sol_reserves']}, " - f"price={curve_data['price_per_token']:.8f} SOL" + f"virtual_quote_reserves={curve_data['virtual_quote_reserves']}, " + f"quote_mint={quote_mint}, " + f"price={curve_data['price_per_token']:.8f} quote/token" ) return curve_data diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index 678244e..3493df5 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -13,7 +13,11 @@ from typing import Any from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction -from core.pubkeys import SystemAddresses +from core.pubkeys import ( + SystemAddresses, + normalize_quote_mint, + quote_token_program, +) from interfaces.core import EventParser, Platform, TokenInfo from platforms.pumpfun.address_provider import PumpFunAddresses from utils.idl_parser import IDLParser @@ -21,6 +25,38 @@ from utils.logger import get_logger logger = get_logger(__name__) +# Index of the optional `quote_mint` remaining account on create_v2. Accounts +# 1-16 are in the IDL; 17-19 are optional remaining accounts appended only when +# creating a coin with a non-native quote mint. See COIN_CREATION.md in the +# pump-fun public docs repository. +_CREATE_V2_QUOTE_MINT_ACCOUNT_INDEX = 16 + +# Length of a Solana public key in bytes. +PUBKEY_BYTE_LENGTH = 32 + + +def _coerce_pubkey(value: object) -> Pubkey | None: + """Coerce a decoded IDL pubkey field into a Pubkey. + + Args: + value: Decoded field, which may be a str, Pubkey, bytes or None + + Returns: + Pubkey, or None if the value cannot be interpreted + """ + if value is None: + return None + if isinstance(value, Pubkey): + return value + if isinstance(value, str): + try: + return Pubkey.from_string(value) + except (ValueError, TypeError): + return None + if isinstance(value, bytes | bytearray) and len(value) == PUBKEY_BYTE_LENGTH: + return Pubkey.from_bytes(bytes(value)) + return None + class PumpFunEventParser(EventParser): """Pump.Fun implementation of EventParser interface with IDL-based event parsing.""" @@ -262,6 +298,14 @@ class PumpFunEventParser(EventParser): f"✅ Successfully parsed CreateEvent for token: {fields.get('symbol', 'Unknown')}" ) + # CreateEvent gained quote_mint and virtual_quote_reserves + # when pump.fun added non-SOL quote assets. Carry them on + # TokenInfo so extreme_fast_mode — which skips the curve + # fetch — still knows which quote asset to trade against. + quote_mint = normalize_quote_mint( + _coerce_pubkey(fields.get("quote_mint")) + ) + return TokenInfo( name=fields["name"], symbol=fields["symbol"], @@ -276,6 +320,9 @@ 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), + quote_mint=quote_mint, + quote_token_program_id=quote_token_program(quote_mint), + virtual_quote_reserves=fields.get("virtual_quote_reserves"), creation_timestamp=monotonic(), ) @@ -373,6 +420,14 @@ class PumpFunEventParser(EventParser): else False ) + # 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( + get_account_key(_CREATE_V2_QUOTE_MINT_ACCOUNT_INDEX) + if is_create_v2 + else None + ) + return TokenInfo( name=args.get("name", ""), symbol=args.get("symbol", ""), @@ -387,6 +442,8 @@ class PumpFunEventParser(EventParser): token_program_id=token_program_id, is_mayhem_mode=bool(args.get("is_mayhem_mode", False)), is_cashback_coin=is_cashback, + quote_mint=quote_mint, + quote_token_program_id=quote_token_program(quote_mint), creation_timestamp=monotonic(), ) diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index a18ca9d..e3d9ee3 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -11,37 +11,119 @@ from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey from spl.token.instructions import create_idempotent_associated_token_account -from core.pubkeys import TOKEN_DECIMALS +from core.pubkeys import TOKEN_DECIMALS, is_sol_paired from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo from utils.idl_parser import IDLParser from utils.logger import get_logger logger = get_logger(__name__) +# Account order for buy_v2 (27 accounts) and sell_v2 (26 accounts). +# Every account is mandatory and the order is identical for every coin type — +# SOL-paired or USDC-paired, mayhem or not, cashback or not. This is the whole +# point of the v2 interface. See BUY.md and SELL.md under docs/instructions in +# the pump-fun public docs repository. +_BUY_V2_ACCOUNTS: list[tuple[str, bool]] = [ + ("global", False), + ("base_mint", False), + ("quote_mint", False), + ("base_token_program", False), + ("quote_token_program", False), + ("associated_token_program", False), + ("fee_recipient", True), + ("associated_quote_fee_recipient", True), + ("buyback_fee_recipient", True), + ("associated_quote_buyback_fee_recipient", True), + ("bonding_curve", True), + ("associated_base_bonding_curve", True), + ("associated_quote_bonding_curve", True), + ("user", True), + ("associated_base_user", True), + ("associated_quote_user", True), + ("creator_vault", True), + ("associated_creator_vault", True), + ("sharing_config", False), + ("global_volume_accumulator", False), + ("user_volume_accumulator", True), + ("associated_user_volume_accumulator", True), + ("fee_config", False), + ("fee_program", False), + ("system_program", False), + ("event_authority", False), + ("program", False), +] + +# sell_v2 is buy_v2 without global_volume_accumulator. +_SELL_V2_ACCOUNTS: list[tuple[str, bool]] = [ + entry for entry in _BUY_V2_ACCOUNTS if entry[0] != "global_volume_accumulator" +] + class PumpFunInstructionBuilder(InstructionBuilder): """Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators.""" - def __init__(self, idl_parser: IDLParser): + def __init__(self, idl_parser: IDLParser, *, use_legacy_instructions: bool = False): """Initialize pump.fun instruction builder with injected IDL parser. Args: idl_parser: Pre-loaded IDL parser for pump.fun platform + use_legacy_instructions: Build the pre-v2 buy/sell instructions + instead of buy_v2/sell_v2. Legacy instructions cannot trade + coins paired with anything other than SOL. """ self._idl_parser = idl_parser + self._use_legacy_instructions = use_legacy_instructions # Get discriminators from injected IDL parser discriminators = self._idl_parser.get_instruction_discriminators() self._buy_discriminator = discriminators["buy"] self._sell_discriminator = discriminators["sell"] + self._buy_v2_discriminator = discriminators["buy_v2"] + self._sell_v2_discriminator = discriminators["sell_v2"] - logger.info("Pump.Fun instruction builder initialized with injected IDL parser") + logger.info( + "Pump.Fun instruction builder initialized with injected IDL parser " + f"(instruction set: {'legacy' if use_legacy_instructions else 'v2'})" + ) @property def platform(self) -> Platform: """Get the platform this builder serves.""" return Platform.PUMP_FUN + @staticmethod + def _build_account_metas( + layout: list[tuple[str, bool]], + accounts_info: dict[str, Pubkey], + signer: Pubkey, + ) -> list[AccountMeta]: + """Turn a v2 account layout into ordered AccountMetas. + + Args: + layout: Ordered (account name, is_writable) pairs + accounts_info: Resolved account addresses keyed by IDL name + signer: The account that signs the transaction + + Returns: + Ordered list of AccountMeta + + Raises: + KeyError: If the address provider did not supply a required account + """ + metas = [] + for name, is_writable in layout: + pubkey = accounts_info[name] + if pubkey is None: + raise KeyError(f"Missing required account for v2 instruction: {name}") + metas.append( + AccountMeta( + pubkey=pubkey, + is_signer=pubkey == signer, + is_writable=is_writable, + ) + ) + return metas + async def build_buy_instruction( self, token_info: TokenInfo, @@ -52,6 +134,169 @@ class PumpFunInstructionBuilder(InstructionBuilder): ) -> list[Instruction]: """Build buy instruction(s) for pump.fun. + Dispatches to buy_v2 unless the builder was constructed with + ``use_legacy_instructions=True``. + + Args: + token_info: Token information + user: User's wallet address + amount_in: Maximum quote amount to spend (raw quote units) + minimum_amount_out: Minimum tokens expected (raw token units) + address_provider: Platform address provider + + Returns: + List of instructions needed for the buy operation + """ + if not self._use_legacy_instructions: + return await self.build_buy_v2_instruction( + token_info, user, amount_in, minimum_amount_out, address_provider + ) + return await self.build_buy_legacy_instruction( + token_info, user, amount_in, minimum_amount_out, address_provider + ) + + async def build_buy_v2_instruction( + self, + token_info: TokenInfo, + user: Pubkey, + amount_in: int, + minimum_amount_out: int, + address_provider: AddressProvider, + ) -> list[Instruction]: + """Build a buy_v2 instruction plus the ATAs it needs. + + Args: + token_info: Token information + user: User's wallet address + amount_in: Maximum quote amount to spend (raw quote units: + lamports for SOL-paired coins, 1e-6 USDC for USDC-paired) + minimum_amount_out: Base tokens to buy (raw token units) + address_provider: Platform address provider + + Returns: + List of instructions needed for the buy operation + """ + instructions = [] + + accounts_info = address_provider.get_buy_v2_instruction_accounts( + token_info, user + ) + + # Base-token ATA for the buyer. buy_v2 does not create this for us. + instructions.append( + create_idempotent_associated_token_account( + user, + user, + accounts_info["base_mint"], + accounts_info["base_token_program"], + ) + ) + + # Quote ATA for the buyer. For SOL-paired coins the program transfers + # native SOL and only seed-checks this account, so creating it would + # burn ~0.002 SOL of rent for nothing. Non-SOL quotes need a real, + # funded token account. + if not is_sol_paired(accounts_info["quote_mint"]): + instructions.append( + create_idempotent_associated_token_account( + user, + user, + accounts_info["quote_mint"], + accounts_info["quote_token_program"], + ) + ) + + # buy_v2 args: amount (base tokens out), max_sol_cost (max quote in). + # Unlike legacy buy there is no track_volume OptionBool — volume + # tracking is unconditional now that user_volume_accumulator is + # mandatory. + instruction_data = ( + self._buy_v2_discriminator + + struct.pack(" list[Instruction]: + """Build a sell_v2 instruction plus the ATAs it needs. + + Args: + token_info: Token information + user: User's wallet address + amount_in: Base tokens to sell (raw token units) + minimum_amount_out: Minimum quote amount to receive (raw quote units) + address_provider: Platform address provider + + Returns: + List of instructions needed for the sell operation + """ + instructions = [] + + accounts_info = address_provider.get_sell_v2_instruction_accounts( + token_info, user + ) + + # Proceeds of a non-SOL sale land in the seller's quote ATA, which must + # exist. SOL-paired sales pay out in native SOL. + if not is_sol_paired(accounts_info["quote_mint"]): + instructions.append( + create_idempotent_associated_token_account( + user, + user, + accounts_info["quote_mint"], + accounts_info["quote_token_program"], + ) + ) + + # sell_v2 args: amount (base tokens in), min_sol_output (min quote out). + instruction_data = ( + self._sell_v2_discriminator + + struct.pack(" list[Instruction]: + """Build the pre-v2 18-account buy instruction. + + Only works for SOL-paired coins. + Args: token_info: Token information user: User's wallet address @@ -186,6 +431,39 @@ class PumpFunInstructionBuilder(InstructionBuilder): ) -> list[Instruction]: """Build sell instruction(s) for pump.fun. + Dispatches to sell_v2 unless the builder was constructed with + ``use_legacy_instructions=True``. + + Args: + token_info: Token information + user: User's wallet address + amount_in: Amount of tokens to sell (raw token units) + minimum_amount_out: Minimum quote amount expected (raw quote units) + address_provider: Platform address provider + + Returns: + List of instructions needed for the sell operation + """ + if not self._use_legacy_instructions: + return await self.build_sell_v2_instruction( + token_info, user, amount_in, minimum_amount_out, address_provider + ) + return await self.build_sell_legacy_instruction( + token_info, user, amount_in, minimum_amount_out, address_provider + ) + + async def build_sell_legacy_instruction( + self, + token_info: TokenInfo, + user: Pubkey, + amount_in: int, + minimum_amount_out: int, + address_provider: AddressProvider, + ) -> list[Instruction]: + """Build the pre-v2 16/17-account sell instruction. + + Only works for SOL-paired coins. + Args: token_info: Token information user: User's wallet address @@ -318,6 +596,12 @@ class PumpFunInstructionBuilder(InstructionBuilder): Returns: List of account addresses that will be accessed """ + if not self._use_legacy_instructions: + accounts_info = address_provider.get_buy_v2_instruction_accounts( + token_info, user + ) + return self._writable_accounts(_BUY_V2_ACCOUNTS, accounts_info) + accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) return [ @@ -336,6 +620,28 @@ class PumpFunInstructionBuilder(InstructionBuilder): accounts_info["breaking_fee_recipient"], ] + @staticmethod + def _writable_accounts( + layout: list[tuple[str, bool]], accounts_info: dict[str, Pubkey] + ) -> list[Pubkey]: + """Collect the writable accounts from a v2 layout. + + getRecentPrioritizationFees is only meaningful for accounts that are + write-locked, so program ids and sysvars are dropped. + + Args: + layout: Ordered (account name, is_writable) pairs + accounts_info: Resolved account addresses keyed by IDL name + + Returns: + List of writable account addresses + """ + return [ + accounts_info[name] + for name, is_writable in layout + if is_writable and accounts_info.get(name) is not None + ] + def get_required_accounts_for_sell( self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider ) -> list[Pubkey]: @@ -349,6 +655,12 @@ class PumpFunInstructionBuilder(InstructionBuilder): Returns: List of account addresses that will be accessed """ + if not self._use_legacy_instructions: + accounts_info = address_provider.get_sell_v2_instruction_accounts( + token_info, user + ) + return self._writable_accounts(_SELL_V2_ACCOUNTS, accounts_info) + accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) return [ @@ -398,8 +710,15 @@ class PumpFunInstructionBuilder(InstructionBuilder): """ if config_override is not None: return config_override - # Buy operations: ATA creation + buy instruction - return 100_000 + if self._use_legacy_instructions: + # 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. + return 180_000 def get_sell_compute_unit_limit(self, config_override: int | None = None) -> int: """Get the recommended compute unit limit for pump.fun sell operations. @@ -412,5 +731,10 @@ class PumpFunInstructionBuilder(InstructionBuilder): """ if config_override is not None: return config_override - # Sell operations: typically just sell instruction (ATA exists) - return 60_000 + 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). + return 120_000 diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 1ac8ea8..af66208 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -9,7 +9,13 @@ from solders.pubkey import Pubkey from core.client import SolanaClient from core.priority_fee.manager import PriorityFeeManager -from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from core.pubkeys import ( + TOKEN_DECIMALS, + WSOL_MINT, + is_sol_paired, + normalize_quote_mint, + quote_units_per_token, +) from core.wallet import Wallet from interfaces.core import AddressProvider, Platform, TokenInfo from platforms import get_platform_implementations @@ -19,6 +25,79 @@ from utils.logger import get_logger logger = get_logger(__name__) +def _quote_symbol(quote_mint: Pubkey) -> str: + """Human-readable label for a quote mint, for logging only. + + Args: + quote_mint: Quote mint address + + Returns: + "SOL" for wrapped SOL, otherwise a truncated mint address + """ + if is_sol_paired(quote_mint): + return "SOL" + mint_str = str(quote_mint) + return f"{mint_str[:4]}..{mint_str[-4:]}" + + +async def _read_pool_state_with_retry( + curve_manager: object, + pool_address: Pubkey, + attempts: int = 4, + delay_seconds: float = 0.15, +) -> dict: + """Read bonding curve state, retrying briefly on a lagging RPC node. + + A freshly created curve may not be visible at `confirmed` yet, and a node + can momentarily serve a slot that predates it — both surface as "account + not found". Reading at `processed` and retrying a few times costs a handful + of RPC calls, which is far cheaper than trading on stale account data. + + Args: + curve_manager: Platform curve manager + pool_address: Bonding curve / pool address + attempts: How many reads to try before giving up + delay_seconds: Pause between attempts + + Returns: + Decoded pool state + + Raises: + Exception: The last read error if every attempt fails + """ + last_error: Exception | None = None + for _ in range(attempts): + try: + return await curve_manager.get_pool_state( + pool_address, commitment="processed" + ) + except Exception as error: # noqa: BLE001 + last_error = error + await asyncio.sleep(delay_seconds) + + raise last_error or RuntimeError("pool_state unavailable after retries") + + +def _refresh_quote_mint(token_info: TokenInfo, pool_state: dict) -> Pubkey: + """Sync token_info's quote asset from freshly-read curve state. + + Listeners do not all carry quote_mint (pumpportal carries none of the + per-coin flags), and the curve is authoritative, so prefer its value. + + Args: + token_info: Token information, mutated in place + pool_state: Decoded bonding curve state + + Returns: + The resolved quote mint + """ + quote_mint = normalize_quote_mint( + pool_state.get("quote_mint", token_info.quote_mint) + ) + token_info.quote_mint = quote_mint + return quote_mint + + class PlatformAwareBuyer(Trader): """Platform-aware token buyer that works with any supported platform.""" @@ -33,8 +112,25 @@ class PlatformAwareBuyer(Trader): extreme_fast_token_amount: int = 0, extreme_fast_mode: bool = False, compute_units: dict | None = None, + quote_amounts: dict[Pubkey, float] | None = None, ): - """Initialize platform-aware token buyer.""" + """Initialize platform-aware token buyer. + + Args: + client: Solana RPC client + wallet: Trading wallet + priority_fee_manager: Priority fee strategy + amount: Amount of SOL to spend per buy on SOL-paired coins + slippage: Acceptable price deviation + max_retries: Transaction submission attempts + extreme_fast_token_amount: Tokens to buy when skipping price checks + extreme_fast_mode: Skip curve stabilization and price check + compute_units: Optional CU overrides + quote_amounts: Per-quote-mint spend amounts in whole quote units, + for coins paired against something other than SOL. A coin whose + quote mint is absent from this map is skipped rather than + traded with a SOL-denominated amount. + """ self.client = client self.wallet = wallet self.priority_fee_manager = priority_fee_manager @@ -44,6 +140,23 @@ class PlatformAwareBuyer(Trader): self.extreme_fast_mode = extreme_fast_mode self.extreme_fast_token_amount = extreme_fast_token_amount self.compute_units = compute_units or {} + # SOL-paired coins always use `amount`; other quotes need an explicit + # per-mint amount because 0.0001 USDC and 0.0001 SOL are not comparable. + self.quote_amounts: dict[Pubkey, float] = { + WSOL_MINT: amount, + **(quote_amounts or {}), + } + + def _resolve_quote_amount(self, quote_mint: Pubkey) -> float | None: + """Get the configured spend amount for a quote mint. + + Args: + quote_mint: Normalized quote mint + + Returns: + Amount in whole quote units, or None if this quote is not configured + """ + return self.quote_amounts.get(quote_mint) async def execute(self, token_info: TokenInfo) -> TradeResult: """Execute buy operation using platform-specific implementations.""" @@ -56,13 +169,13 @@ class PlatformAwareBuyer(Trader): instruction_builder = implementations.instruction_builder curve_manager = implementations.curve_manager - # Convert amount to lamports - amount_lamports = int(self.amount * LAMPORTS_PER_SOL) + # Quote asset is resolved from the curve below; start from whatever + # the listener gave us so extreme_fast_mode has a usable default. + quote_mint = normalize_quote_mint(token_info.quote_mint) if self.extreme_fast_mode: - # Skip the wait and directly calculate the amount - token_amount = self.extreme_fast_token_amount - token_price_sol = self.amount / token_amount if token_amount > 0 else 0 + # Skip the price check; the token count is fixed by config and + # sizing is finished once the quote amount is resolved below. # Even in extreme_fast_mode, refresh mayhem/cashback/creator from # chain — listeners (especially pumpportal) often don't carry # these, and the program rejects with NotAuthorized (0x1770) / @@ -71,34 +184,23 @@ class PlatformAwareBuyer(Trader): # readable, so retry briefly. One handful of RPC calls is cheap # relative to a failed buy. try: - pool_address = self._get_pool_address( - token_info, address_provider + pool_address = self._get_pool_address(token_info, address_provider) + # Geyser/logs fire on processed, so the BC is typically + # readable in the same slot; pumpportal occasionally races + # the on-chain commit, hence the retries. + pool_state = await _read_pool_state_with_retry( + curve_manager, pool_address ) - pool_state = None - last_err: Exception | None = None - # Use processed commitment — geyser/logs fire on processed - # so the BC is typically readable in the same slot. Most - # listeners only need 1 attempt; pumpportal occasionally - # races the on-chain commit, so allow a few quick retries. - for attempt in range(4): - try: - pool_state = await curve_manager.get_pool_state( - pool_address, commitment="processed" - ) - break - except Exception as inner: # noqa: BLE001 - last_err = inner - await asyncio.sleep(0.15) - if pool_state is None: - raise last_err or RuntimeError( - "pool_state unavailable after retries" - ) token_info.is_mayhem_mode = pool_state.get( "is_mayhem_mode", token_info.is_mayhem_mode ) token_info.is_cashback_coin = pool_state.get( "is_cashback_coin", token_info.is_cashback_coin ) + # The quote asset decides which balance we spend and how + # amounts are scaled, so it must come from the curve rather + # than a listener guess. + quote_mint = _refresh_quote_mint(token_info, pool_state) fresh_creator = pool_state.get("creator") if fresh_creator and hasattr( address_provider, "derive_creator_vault" @@ -142,20 +244,48 @@ class PlatformAwareBuyer(Trader): token_info.is_cashback_coin = pool_state.get( "is_cashback_coin", token_info.is_cashback_coin ) - token_amount = self.amount / token_price_sol + quote_mint = _refresh_quote_mint(token_info, pool_state) + + # A coin paired against a quote asset we have no configured amount + # for cannot be traded — spending `amount` of it would be a + # different order of magnitude entirely. + quote_amount = self._resolve_quote_amount(quote_mint) + if quote_amount is None: + return TradeResult( + success=False, + platform=token_info.platform, + error_message=( + f"No configured buy amount for quote mint {quote_mint}; " + f"set trade.quote_amounts for this mint to trade it" + ), + ) + + quote_unit = quote_units_per_token(quote_mint) + quote_label = _quote_symbol(quote_mint) + + # Both branches need the resolved quote amount to finish sizing the + # trade: extreme_fast_mode fixes the token count and back-derives an + # implied price, while the regular path fixes the spend and derives + # the token count from the curve price. + if self.extreme_fast_mode: + token_amount = self.extreme_fast_token_amount + token_price_sol = quote_amount / token_amount if token_amount > 0 else 0 + else: + token_amount = quote_amount / token_price_sol # Calculate minimum token amount with slippage minimum_token_amount = token_amount * (1 - self.slippage) minimum_token_amount_raw = int(minimum_token_amount * 10**TOKEN_DECIMALS) - # Calculate maximum SOL to spend with slippage - max_amount_lamports = int(amount_lamports * (1 + self.slippage)) + # Calculate maximum quote to spend with slippage, in the quote + # mint's own raw units (lamports for SOL, 1e-6 for USDC). + max_quote_amount_raw = int(quote_amount * quote_unit * (1 + self.slippage)) # Build buy instructions using platform-specific builder instructions = await instruction_builder.build_buy_instruction( token_info, self.wallet.pubkey, - max_amount_lamports, # amount_in (SOL) + max_quote_amount_raw, # amount_in (raw quote units) minimum_token_amount_raw, # minimum_amount_out (tokens) address_provider, ) @@ -166,10 +296,12 @@ class PlatformAwareBuyer(Trader): ) logger.info( - f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token on {token_info.platform.value}" + f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} " + f"{quote_label} per token on {token_info.platform.value}" ) logger.info( - f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" + f"Total cost: {quote_amount:.6f} {quote_label} " + f"(max: {max_quote_amount_raw / quote_unit:.6f} {quote_label})" ) # Send transaction @@ -199,27 +331,33 @@ class PlatformAwareBuyer(Trader): sol_destination = self._get_sol_destination( token_info, address_provider ) - tokens_raw, sol_spent = await self.client.get_buy_transaction_details( - str(tx_signature), token_info.mint, sol_destination + tokens_raw, quote_spent = await self.client.get_buy_transaction_details( + str(tx_signature), + token_info.mint, + sol_destination, + quote_mint=quote_mint, ) - if tokens_raw is not None and sol_spent is not None: + if tokens_raw is not None and quote_spent is not None: actual_amount = tokens_raw / 10**TOKEN_DECIMALS - actual_price = (sol_spent / LAMPORTS_PER_SOL) / actual_amount + actual_price = (quote_spent / quote_unit) / actual_amount logger.info( f"Actual tokens received: {actual_amount:.6f} " f"(expected: {token_amount:.6f})" ) logger.info( - f"Actual SOL spent: {sol_spent / LAMPORTS_PER_SOL:.10f} SOL" + f"Actual {quote_label} spent: " + f"{quote_spent / quote_unit:.10f} {quote_label}" + ) + logger.info( + f"Actual price: {actual_price:.10f} {quote_label}/token" ) - logger.info(f"Actual price: {actual_price:.10f} SOL/token") token_amount = actual_amount token_price_sol = actual_price else: raise ValueError( f"Failed to parse transaction details: tokens={tokens_raw}, " - f"sol_spent={sol_spent} (tx: {tx_signature}). " + f"quote_spent={quote_spent} (tx: {tx_signature}). " f"The transaction may have failed on-chain — check explorer." ) @@ -369,6 +507,9 @@ class PlatformAwareSeller(Trader): instruction_builder = implementations.instruction_builder curve_manager = implementations.curve_manager + # Fall back to the listener's quote asset if the refresh below fails. + quote_mint = normalize_quote_mint(token_info.quote_mint) + # Refresh mayhem-mode and cashback flags from curve state. # The sell account list is 16 (non-cashback) vs 17 (cashback), and # fee_recipient differs in mayhem mode — both can change between @@ -376,13 +517,20 @@ class PlatformAwareSeller(Trader): # flags carried in token_info. try: pool_address = self._get_pool_address(token_info, address_provider) - pool_state = await curve_manager.get_pool_state(pool_address) + # Retry rather than reading once at `confirmed`: a node serving a + # slightly stale slot reports the curve as missing, and silently + # falling back to create-time values risks a wrong creator_vault + # (ConstraintSeeds 0x7d6) or wrong mayhem fee_recipient. + pool_state = await _read_pool_state_with_retry( + curve_manager, pool_address + ) token_info.is_mayhem_mode = pool_state.get( "is_mayhem_mode", token_info.is_mayhem_mode ) token_info.is_cashback_coin = pool_state.get( "is_cashback_coin", token_info.is_cashback_coin ) + quote_mint = _refresh_quote_mint(token_info, pool_state) # Refresh creator/creator_vault from current BC state. Post # 2026-04-28 the program may delegate BC.creator to a PFEE-owned # PDA after the initial creator buy, so the create-time vault @@ -408,13 +556,18 @@ class PlatformAwareSeller(Trader): f"is_cashback_coin={token_info.is_cashback_coin}" ) + quote_unit = quote_units_per_token(quote_mint) + quote_label = _quote_symbol(quote_mint) + # Use pre-known amount and price (no RPC delay) token_balance_decimal = token_amount token_balance = int(token_amount * 10**TOKEN_DECIMALS) token_price_sol = token_price logger.info(f"Token balance: {token_balance_decimal:.6f}") - logger.info(f"Price per Token (from buy): {token_price_sol:.8f} SOL") + logger.info( + f"Price per Token (from buy): {token_price_sol:.8f} {quote_label}" + ) if token_balance == 0: logger.info("No tokens to sell.") @@ -424,19 +577,23 @@ class PlatformAwareSeller(Trader): error_message="No tokens to sell", ) - # Calculate expected SOL output with slippage protection - expected_sol_output = token_balance_decimal * token_price_sol - min_sol_output = max( + # Calculate expected quote output with slippage protection, in the + # quote mint's raw units. + expected_quote_output = token_balance_decimal * token_price_sol + min_quote_output = max( 1, - int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL), + int((expected_quote_output * (1 - self.slippage)) * quote_unit), ) logger.info( f"Selling {token_balance_decimal} tokens on {token_info.platform.value}" ) - logger.info(f"Expected SOL output: {expected_sol_output:.10f} SOL") logger.info( - f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): " - f"{min_sol_output / LAMPORTS_PER_SOL:.10f} SOL ({min_sol_output} lamports)" + f"Expected {quote_label} output: {expected_quote_output:.10f} {quote_label}" + ) + logger.info( + f"Minimum {quote_label} output (with {self.slippage * 100:.1f}% slippage): " + f"{min_quote_output / quote_unit:.10f} {quote_label} " + f"({min_quote_output} raw units)" ) # Build sell instructions using platform-specific builder @@ -444,7 +601,7 @@ class PlatformAwareSeller(Trader): token_info, self.wallet.pubkey, token_balance, # amount_in (tokens) - min_sol_output, # minimum_amount_out (SOL) + min_quote_output, # minimum_amount_out (raw quote units) address_provider, ) diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index 739fc6b..7b5577e 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -19,6 +19,12 @@ from cleanup.modes import ( ) from core.client import SolanaClient from core.priority_fee.manager import PriorityFeeManager +from core.pubkeys import ( + WSOL_MINT, + normalize_quote_mint, + resolve_quote_amounts, + resolve_quote_mint, +) from core.wallet import Wallet from interfaces.core import Platform, TokenInfo from monitoring.listener_factory import ListenerFactory @@ -46,6 +52,34 @@ except ImportError: logger = get_logger(__name__) +def _resolve_quote_config( + buy_amount: float, + quote_amounts: dict[str, float] | None, + allowed_quote_mints: list[str] | None, +) -> tuple[dict[Pubkey, float], set[Pubkey] | None]: + """Resolve quote-asset configuration into per-mint amounts and an allowlist. + + Keys may be mint addresses or the aliases "sol"/"usdc". SOL always falls + back to trade.buy_amount, so a config that never mentions quote assets + keeps its existing SOL-only behaviour. + + Args: + buy_amount: SOL amount per buy from trade.buy_amount + quote_amounts: Optional map of quote mint -> amount in whole units + allowed_quote_mints: Optional list of quote mints permitted to trade + + Returns: + Tuple of (amount per quote mint, allowed quote mints or None for any) + """ + amounts = {WSOL_MINT: buy_amount, **resolve_quote_amounts(quote_amounts)} + allowed = ( + {resolve_quote_mint(mint) for mint in allowed_quote_mints} + if allowed_quote_mints + else None + ) + return amounts, allowed + + class UniversalTrader: """Universal trading coordinator that works with any supported platform.""" @@ -68,6 +102,9 @@ class UniversalTrader: # Trading configuration extreme_fast_mode: bool = False, extreme_fast_token_amount: int = 30, + # Quote asset configuration (pump.fun non-SOL pairs) + quote_amounts: dict[str, float] | None = None, + allowed_quote_mints: list[str] | None = None, # Exit strategy configuration exit_strategy: str = "time_based", take_profit_percentage: float | None = None, @@ -137,29 +174,34 @@ class UniversalTrader: self.platform, self.solana_client ) - # Store compute unit configuration + # Store compute unit and quote-asset configuration self.compute_units = compute_units or {} - - # Create platform-aware traders - self.buyer = PlatformAwareBuyer( - self.solana_client, - self.wallet, - self.priority_fee_manager, - buy_amount, - buy_slippage, - max_retries, - extreme_fast_token_amount, - extreme_fast_mode, - compute_units=self.compute_units, + self.quote_amounts, self.allowed_quote_mints = _resolve_quote_config( + buy_amount, quote_amounts, allowed_quote_mints ) - self.seller = PlatformAwareSeller( - self.solana_client, - self.wallet, - self.priority_fee_manager, - sell_slippage, - max_retries, - compute_units=self.compute_units, + # Create platform-aware traders + self.buyer, self.seller = ( + PlatformAwareBuyer( + self.solana_client, + self.wallet, + self.priority_fee_manager, + buy_amount, + buy_slippage, + max_retries, + extreme_fast_token_amount, + extreme_fast_mode, + compute_units=self.compute_units, + quote_amounts=self.quote_amounts, + ), + PlatformAwareSeller( + self.solana_client, + self.wallet, + self.priority_fee_manager, + sell_slippage, + max_retries, + compute_units=self.compute_units, + ), ) # Initialize the appropriate listener with platform filtering @@ -427,6 +469,25 @@ class UniversalTrader: ) return + # Skip coins paired against a quote asset we are not set up to + # trade. Cheaper to drop here than to fail a buy on-chain. + token_quote_mint = normalize_quote_mint(token_info.quote_mint) + if ( + self.allowed_quote_mints is not None + and token_quote_mint not in self.allowed_quote_mints + ): + logger.info( + f"Skipping {token_info.symbol} - quote mint {token_quote_mint} " + f"not in allowed_quote_mints" + ) + return + if token_quote_mint not in self.quote_amounts: + logger.info( + f"Skipping {token_info.symbol} - no buy amount configured for " + f"quote mint {token_quote_mint}" + ) + return + # Wait for pool/curve to stabilize (unless in extreme fast mode) if not self.extreme_fast_mode: await self._save_token_info(token_info) @@ -437,7 +498,9 @@ class UniversalTrader: # Buy token logger.info( - f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol} on {token_info.platform.value}..." + f"Buying {self.quote_amounts[token_quote_mint]:.6f} of quote " + f"{token_quote_mint} worth of {token_info.symbol} " + f"on {token_info.platform.value}..." ) buy_result: TradeResult = await self.buyer.execute(token_info)