diff --git a/Cargo.toml b/Cargo.toml index b1f94b1..643980d 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.22" +version = "4.0.23" edition = "2021" authors = [ "William ", diff --git a/README.md b/README.md index d636d81..d9415fb 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,9 @@ This SDK is available in multiple languages: ## 🔖 Current Release -**Rust crate:** `sol-trade-sdk = "4.0.21"` +**Rust crate:** `sol-trade-sdk = "4.0.23"` -This release refreshes PumpFun native-SOL quote handling so SOL/WSOL sentinels prefer the smaller V1 hot path, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data. +This release updates PumpSwap for the July 2026 virtual quote reserve rollout. Pool and event schemas include `virtual_quote_reserves`, and all PumpSwap buy, sell, pricing, and dynamic-fee calculations use `quote_vault_balance + virtual_quote_reserves`. ## ✨ Features @@ -126,14 +126,14 @@ Add the dependency to your `Cargo.toml`: ```toml # Add to your Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.21" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" } ``` ### Use crates.io ```toml # Add to your Cargo.toml -sol-trade-sdk = "4.0.21" +sol-trade-sdk = "4.0.23" ``` ## 🛠️ Usage Examples @@ -506,6 +506,14 @@ For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_au - **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`. - **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event's `coin_creator_vault_ata` and `coin_creator_vault_authority`. +#### PumpSwap: virtual quote reserves + +PumpSwap quotes must use `effective_quote_reserves = pool_quote_token_account.amount + virtual_quote_reserves`. The Pool account and BuyEvent/SellEvent encode `virtual_quote_reserves` as `i128`. + +- RPC constructors such as `PumpSwapParams::from_pool_address_by_rpc` read and apply the Pool field automatically. +- Event fast paths must pass the event's raw `pool_quote_token_reserves` and `virtual_quote_reserves` separately to `PumpSwapParams::from_trade(...)` or `from_trade_with_fee_basis_points(...)`. Do not add them before calling the constructor. +- The SDK uses effective reserves for buys, sells, prices, and dynamic fee-tier selection. Invalid signed sums return an error instead of wrapping. + ## 🛡️ MEV Protection Services You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos) diff --git a/README_CN.md b/README_CN.md index 91ea4ed..5f54526 100755 --- a/README_CN.md +++ b/README_CN.md @@ -92,9 +92,9 @@ ## 🔖 当前版本 -**Rust crate:** `sol-trade-sdk = "4.0.21"` +**Rust crate:** `sol-trade-sdk = "4.0.23"` -本版本刷新 PumpFun native SOL quote 处理逻辑,SOL/WSOL sentinel 默认优先走更小的 V1 热路径,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。 +本版本适配 PumpSwap 2026 年 7 月的虚拟 quote 储备升级。Pool 与事件 schema 新增 `virtual_quote_reserves`,PumpSwap 买入、卖出、报价和动态费率计算统一使用 `quote_vault_balance + virtual_quote_reserves`。 ## ✨ 项目特性 @@ -126,14 +126,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.21" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" } ``` ### 使用 crates.io ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = "4.0.21" +sol-trade-sdk = "4.0.23" ``` ## 🛠️ 使用示例 @@ -503,6 +503,14 @@ legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quo | 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) / `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | 优先旧版 `buy`/`sell`/`buy_exact_sol_in` | native SOL 配对;普通 SOL 结算走 V1,显式 WSOL 结算才走 V2 | | `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) | +#### PumpSwap:虚拟 quote 储备 + +PumpSwap 报价必须使用 `effective_quote_reserves = pool_quote_token_account.amount + virtual_quote_reserves`。Pool 账户以及 BuyEvent/SellEvent 中的 `virtual_quote_reserves` 类型均为 `i128`。 + +- `PumpSwapParams::from_pool_address_by_rpc` 等 RPC 构造器会自动读取并应用 Pool 字段。 +- 事件热路径必须把事件中的原始 `pool_quote_token_reserves` 和 `virtual_quote_reserves` 分别传给 `PumpSwapParams::from_trade(...)` 或 `from_trade_with_fee_basis_points(...)`,不要在调用前自行相加。 +- SDK 在买入、卖出、报价和动态费率分层中统一使用有效储备;无效的有符号结果会返回错误,不会发生整数回绕。 + ## 🛡️ MEV 保护服务 可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos) diff --git a/examples/pumpswap_trading/README.md b/examples/pumpswap_trading/README.md index ac58549..b68d5b0 100644 --- a/examples/pumpswap_trading/README.md +++ b/examples/pumpswap_trading/README.md @@ -21,6 +21,7 @@ cargo run --release --package pumpswap_trading - The buy uses `BuyAmount::WithMaxInput`, which applies slippage to maximum quote cost and is appropriate when fill priority matters. - Buy parameters use post-trade reserves and LP/protocol/creator fee bps from the event. +- `solana-streamer-sdk 0.5.0` predates the appended `virtual_quote_reserves` event field, so this compatibility example reads that Pool field once before quoting. When the parser exposes the field, pass it directly to `PumpSwapParams::from_trade_with_fee_basis_points` or keep it in a Pool account cache to remove this RPC from the hot path. - The example records the pre-buy balance and sells only the confirmed balance increase. It refreshes pool state and blockhash before selling. - Use `BuyAmount::ExactInput` when the quote spend must be exact. That mode protects minimum output and can fail more often in an active pool. diff --git a/examples/pumpswap_trading/README_CN.md b/examples/pumpswap_trading/README_CN.md index c799c83..1a879b6 100644 --- a/examples/pumpswap_trading/README_CN.md +++ b/examples/pumpswap_trading/README_CN.md @@ -19,6 +19,7 @@ cargo run --release --package pumpswap_trading - 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。 - 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。 +- `solana-streamer-sdk 0.5.0` 尚未暴露追加的 `virtual_quote_reserves` 事件字段,因此该兼容示例会在报价前读取一次 Pool 字段。解析器暴露该字段后,应直接传给 `PumpSwapParams::from_trade_with_fee_basis_points`,或维护 Pool 账户缓存,以移除热路径中的这次 RPC。 - 示例记录买前余额,只卖出确认后的余额增量;卖出前重新获取池状态和 blockhash。 - 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。 diff --git a/examples/pumpswap_trading/src/main.rs b/examples/pumpswap_trading/src/main.rs index 6fde037..37c1759 100644 --- a/examples/pumpswap_trading/src/main.rs +++ b/examples/pumpswap_trading/src/main.rs @@ -1,4 +1,5 @@ use sol_trade_sdk::common::{clock::now_micros, SolanaRpcClient, TradeConfig}; +use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool; use sol_trade_sdk::TradeTokenType; use sol_trade_sdk::{ common::AnyResult, @@ -279,6 +280,10 @@ async fn pumpswap_trade_with_grpc_buy_event( blockhash_cache: BlockhashCache, trade_info: PumpSwapBuyEvent, ) -> AnyResult<()> { + // solana-streamer-sdk 0.5.0 predates the appended event field. Read the + // Pool value so this compatibility example still prices effective reserves. + let virtual_quote_reserves = + fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?.virtual_quote_reserves; let params = PumpSwapParams::from_trade_with_fee_basis_points( trade_info.pool, trade_info.base_mint, @@ -287,6 +292,7 @@ async fn pumpswap_trade_with_grpc_buy_event( trade_info.pool_quote_token_account, trade_info.pool_base_token_reserves, trade_info.pool_quote_token_reserves, + virtual_quote_reserves, trade_info.coin_creator_vault_ata, trade_info.coin_creator_vault_authority, trade_info.base_token_program, @@ -317,6 +323,8 @@ async fn pumpswap_trade_with_grpc_sell_event( blockhash_cache: BlockhashCache, trade_info: PumpSwapSellEvent, ) -> AnyResult<()> { + let virtual_quote_reserves = + fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?.virtual_quote_reserves; let params = PumpSwapParams::from_trade_with_fee_basis_points( trade_info.pool, trade_info.base_mint, @@ -325,6 +333,7 @@ async fn pumpswap_trade_with_grpc_sell_event( trade_info.pool_quote_token_account, trade_info.pool_base_token_reserves, trade_info.pool_quote_token_reserves, + virtual_quote_reserves, trade_info.coin_creator_vault_ata, trade_info.coin_creator_vault_authority, trade_info.base_token_program, diff --git a/idl/pump_amm.json b/idl/pump_amm.json index f73cf11..a654b6f 100644 --- a/idl/pump_amm.json +++ b/idl/pump_amm.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": [ @@ -4672,6 +5179,19 @@ 222 ] }, + { + "name": "BoostBuyAndBurnEvent", + "discriminator": [ + 63, + 69, + 28, + 22, + 48, + 92, + 194, + 185 + ] + }, { "name": "BuyEvent", "discriminator": [ @@ -4802,6 +5322,19 @@ 124 ] }, + { + "name": "InitBoostEvent", + "discriminator": [ + 174, + 124, + 74, + 249, + 4, + 81, + 246, + 17 + ] + }, { "name": "InitUserVolumeAccumulatorEvent", "discriminator": [ @@ -4867,6 +5400,19 @@ 211 ] }, + { + "name": "SetBoostAuthorityEvent", + "discriminator": [ + 89, + 128, + 240, + 141, + 91, + 202, + 71, + 105 + ] + }, { "name": "SetMetaplexCoinCreatorEvent", "discriminator": [ @@ -5177,6 +5723,51 @@ { "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": [ @@ -5292,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": [ @@ -5435,6 +6082,18 @@ { "name": "buyback_fee", "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "can_boost", + "type": "bool" + }, + { + "name": "base_supply", + "type": "u64" } ] } @@ -6018,6 +6677,14 @@ { "name": "buyback_basis_points", "type": "u64" + }, + { + "name": "boost_authority", + "type": "pubkey" + }, + { + "name": "boost_enabled", + "type": "bool" } ] } @@ -6064,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": { @@ -6180,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" } ] } @@ -6321,6 +7027,18 @@ { "name": "buyback_fee", "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "can_boost", + "type": "bool" + }, + { + "name": "base_supply", + "type": "u64" } ] } @@ -6353,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/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index a72b816..943190f 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -54,7 +54,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { let base_mint = protocol_params.base_mint; let quote_mint = protocol_params.quote_mint; let pool_base_token_reserves = protocol_params.pool_base_token_reserves; - let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves; + let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?; let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata; let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority; let create_input_ata = params.create_input_mint_ata; @@ -283,7 +283,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { let base_mint = protocol_params.base_mint; let quote_mint = protocol_params.quote_mint; let pool_base_token_reserves = protocol_params.pool_base_token_reserves; - let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves; + let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?; let pool_base_token_account = protocol_params.pool_base_token_account; let pool_quote_token_account = protocol_params.pool_quote_token_account; let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata; @@ -535,6 +535,7 @@ mod tests { pk(4), 1_000_000_000, 2_000_000_000, + 0, pk(5), accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY, crate::constants::TOKEN_PROGRAM, @@ -627,6 +628,7 @@ mod tests { pk(4), 1_000_000_000, 2_000_000_000, + 0, pk(5), accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY, crate::constants::TOKEN_PROGRAM, @@ -653,8 +655,9 @@ mod tests { let mut params = swap_params(TradeType::Buy, None); params.input_amount = Some(1_000_000); params.use_exact_sol_amount = Some(false); - params.protocol_params = - DexParamEnum::PumpSwap(pumpswap_params().with_fee_basis_points(20, 5, 75)); + let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 75); + protocol_params.virtual_quote_reserves = 500_000_000; + params.protocol_params = DexParamEnum::PumpSwap(protocol_params); let instructions = PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap(); @@ -667,10 +670,33 @@ mod tests { 1_000_000, 100, 1_000_000_000, - 2_000_000_000, + 2_500_000_000, &crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0), ) .unwrap(); assert_eq!(base_amount_out, expected.base); } + + #[tokio::test] + async fn pumpswap_sell_prices_with_effective_quote_reserves() { + let mut params = swap_params(TradeType::Sell, None); + let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 0); + protocol_params.virtual_quote_reserves = 500_000_000; + params.protocol_params = DexParamEnum::PumpSwap(protocol_params); + + let instructions = + PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap(); + let ix = instructions.last().unwrap(); + let min_quote_amount_out = u64::from_le_bytes(ix.data[16..24].try_into().unwrap()); + + let expected = crate::utils::calc::pumpswap::sell_base_input_internal_with_fees( + 100_000, + 100, + 1_000_000_000, + 2_500_000_000, + &crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0), + ) + .unwrap(); + assert_eq!(min_quote_amount_out, expected.min_quote); + } } diff --git a/src/instruction/utils/pumpswap.rs b/src/instruction/utils/pumpswap.rs index 57e60ea..6602f6a 100644 --- a/src/instruction/utils/pumpswap.rs +++ b/src/instruction/utils/pumpswap.rs @@ -18,7 +18,7 @@ use std::sync::{ use std::time::{Duration, Instant}; use tracing::warn; -// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22) +// Pool account sizes are handled by find_by_base_mint/find_by_quote_mint. /// Constants used as seeds for deriving PDAs (Program Derived Addresses) pub mod seeds { @@ -740,12 +740,14 @@ pub async fn fetch_pool( Ok(pool) } -/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022) -const POOL_DATA_LEN_SPL: u64 = 8 + 244; -const POOL_DATA_LEN_T22: u64 = 643; +/// Known allocated Pool account sizes. The July 2026 layout carrying +/// `virtual_quote_reserves` is allocated to 300 bytes on-chain. +const POOL_DATA_LEN_LEGACY: u64 = 8 + 244; +const POOL_DATA_LEN_CURRENT: u64 = 300; +const POOL_DATA_LEN_EXTENDED: u64 = 643; -/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel. -async fn get_program_accounts_both_sizes( +/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel. +async fn get_program_accounts_known_sizes( rpc: &SolanaRpcClient, memcmp_offset: usize, mint: &Pubkey, @@ -768,12 +770,14 @@ async fn get_program_accounts_both_sizes( }; let program_id = accounts::AMM_PROGRAM; #[allow(deprecated)] - let (spl_result, t22_result) = tokio::join!( - rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)), - rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)), + let (legacy_result, current_result, extended_result) = tokio::join!( + rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_LEGACY)), + rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_CURRENT)), + rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)), ); - let mut all = spl_result.unwrap_or_default(); - all.extend(t22_result.unwrap_or_default()); + let mut all = legacy_result.unwrap_or_default(); + all.extend(current_result.unwrap_or_default()); + all.extend(extended_result.unwrap_or_default()); Ok(all) } @@ -797,7 +801,7 @@ pub async fn find_by_base_mint( base_mint: &Pubkey, ) -> Result<(Pubkey, Pool), anyhow::Error> { // base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43 - let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?; + let accounts = get_program_accounts_known_sizes(rpc, 43, base_mint).await?; if accounts.is_empty() { return Err(anyhow!("No pool found for mint {}", base_mint)); } @@ -814,7 +818,7 @@ pub async fn find_by_quote_mint( quote_mint: &Pubkey, ) -> Result<(Pubkey, Pool), anyhow::Error> { // quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75 - let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?; + let accounts = get_program_accounts_known_sizes(rpc, 75, quote_mint).await?; if accounts.is_empty() { return Err(anyhow!("No pool found for mint {}", quote_mint)); } diff --git a/src/instruction/utils/pumpswap_types.rs b/src/instruction/utils/pumpswap_types.rs index b44eaec..06bcc7d 100644 --- a/src/instruction/utils/pumpswap_types.rs +++ b/src/instruction/utils/pumpswap_types.rs @@ -17,16 +17,121 @@ pub struct Pool { pub is_mayhem_mode: bool, /// Whether this pool's coin has cashback enabled pub is_cashback_coin: bool, - /// Reserved for future fields (pump-public-docs: pool structure = 244 bytes total) - pub _reserved: [u8; 7], + /// Virtual quote reserves appended to the Pool account. + /// + /// Quotes must use `quote_vault_balance + virtual_quote_reserves`. + pub virtual_quote_reserves: i128, } -/// Borsh 解码用的 Pool 长度。链上池为 244 字节(pump-public-docs Breaking Change),与 POOL_SIZE 一致。 -pub const POOL_SIZE: usize = 244; +/// Minimum Borsh payload length for the current Pool layout, excluding the +/// 8-byte Anchor account discriminator. +pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 + 16; +const LEGACY_POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1; + +#[derive(BorshDeserialize)] +struct LegacyPool { + pool_bump: u8, + index: u16, + creator: Pubkey, + base_mint: Pubkey, + quote_mint: Pubkey, + lp_mint: Pubkey, + pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, + lp_supply: u64, + coin_creator: Pubkey, + is_mayhem_mode: bool, + is_cashback_coin: bool, +} + +impl From for Pool { + fn from(pool: LegacyPool) -> Self { + Self { + pool_bump: pool.pool_bump, + index: pool.index, + creator: pool.creator, + base_mint: pool.base_mint, + quote_mint: pool.quote_mint, + lp_mint: pool.lp_mint, + pool_base_token_account: pool.pool_base_token_account, + pool_quote_token_account: pool.pool_quote_token_account, + lp_supply: pool.lp_supply, + coin_creator: pool.coin_creator, + is_mayhem_mode: pool.is_mayhem_mode, + is_cashback_coin: pool.is_cashback_coin, + virtual_quote_reserves: 0, + } + } +} pub fn pool_decode(data: &[u8]) -> Option { - if data.len() < POOL_SIZE { - return None; + if data.len() >= POOL_SIZE { + return borsh::from_slice::(&data[..POOL_SIZE]).ok(); + } + + if data.len() >= LEGACY_POOL_SIZE { + return borsh::from_slice::(&data[..LEGACY_POOL_SIZE]).ok().map(Into::into); + } + + None +} + +/// Compute the quote reserves used by PumpSwap pricing. +/// +/// Returns `None` when the signed sum is negative or cannot fit in a `u64`. +#[inline] +pub fn effective_quote_reserves( + quote_vault_balance: u64, + virtual_quote_reserves: i128, +) -> Option { + i128::from(quote_vault_balance) + .checked_add(virtual_quote_reserves) + .and_then(|reserves| u64::try_from(reserves).ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pool_payload(virtual_quote_reserves: i128) -> Vec { + let mut data = Vec::with_capacity(POOL_SIZE); + data.push(7); + data.extend_from_slice(&42u16.to_le_bytes()); + for seed in 1..=6 { + data.extend_from_slice(Pubkey::new_from_array([seed; 32]).as_ref()); + } + data.extend_from_slice(&123_456u64.to_le_bytes()); + data.extend_from_slice(Pubkey::new_from_array([7; 32]).as_ref()); + data.push(1); + data.push(0); + data.extend_from_slice(&virtual_quote_reserves.to_le_bytes()); + data + } + + #[test] + fn decodes_current_pool_virtual_quote_reserves() { + let pool = pool_decode(&pool_payload(987_654_321)).unwrap(); + + assert_eq!(pool.virtual_quote_reserves, 987_654_321); + assert!(pool.is_mayhem_mode); + assert!(!pool.is_cashback_coin); + } + + #[test] + fn decodes_legacy_pool_with_zero_virtual_quote_reserves() { + let mut data = pool_payload(0); + data.truncate(LEGACY_POOL_SIZE); + data.extend_from_slice(&[0; 7]); + + let pool = pool_decode(&data).unwrap(); + assert_eq!(pool.virtual_quote_reserves, 0); + } + + #[test] + fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() { + assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250)); + assert_eq!(effective_quote_reserves(1_000, -250), Some(750)); + assert_eq!(effective_quote_reserves(100, -101), None); + assert_eq!(effective_quote_reserves(u64::MAX, 1), None); } - borsh::from_slice::(&data[..POOL_SIZE]).ok() } diff --git a/src/trading/core/params/pumpswap.rs b/src/trading/core/params/pumpswap.rs index ef19ff6..83e660b 100644 --- a/src/trading/core/params/pumpswap.rs +++ b/src/trading/core/params/pumpswap.rs @@ -32,8 +32,10 @@ pub struct PumpSwapParams { pub pool_quote_token_account: Pubkey, /// Base token reserves in the pool pub pool_base_token_reserves: u64, - /// Quote token reserves in the pool + /// Raw quote-vault token balance. Pricing uses this plus [`Self::virtual_quote_reserves`]. pub pool_quote_token_reserves: u64, + /// Signed virtual quote reserves from the PumpSwap Pool account or trade event. + pub virtual_quote_reserves: i128, /// Coin creator vault ATA pub coin_creator_vault_ata: Pubkey, /// Coin creator vault authority @@ -75,6 +77,7 @@ impl PumpSwapParams { pool_quote_token_account: Pubkey, pool_base_token_reserves: u64, pool_quote_token_reserves: u64, + virtual_quote_reserves: i128, coin_creator_vault_ata: Pubkey, coin_creator_vault_authority: Pubkey, base_token_program: Pubkey, @@ -99,6 +102,7 @@ impl PumpSwapParams { pool_quote_token_account, pool_base_token_reserves, pool_quote_token_reserves, + virtual_quote_reserves, coin_creator_vault_ata, coin_creator_vault_authority, base_token_program, @@ -127,6 +131,21 @@ impl PumpSwapParams { self } + /// Quote reserves used by PumpSwap pricing and fee-tier selection. + pub fn effective_quote_reserves(&self) -> Result { + crate::instruction::utils::pumpswap_types::effective_quote_reserves( + self.pool_quote_token_reserves, + self.virtual_quote_reserves, + ) + .ok_or_else(|| { + anyhow::anyhow!( + "Invalid PumpSwap effective quote reserves: vault={} virtual={}", + self.pool_quote_token_reserves, + self.virtual_quote_reserves + ) + }) + } + pub fn with_fee_basis_points( mut self, lp_fee_basis_points: u64, @@ -161,6 +180,7 @@ impl PumpSwapParams { pool_quote_token_account: Pubkey, pool_base_token_reserves: u64, pool_quote_token_reserves: u64, + virtual_quote_reserves: i128, coin_creator_vault_ata: Pubkey, coin_creator_vault_authority: Pubkey, base_token_program: Pubkey, @@ -178,6 +198,7 @@ impl PumpSwapParams { pool_quote_token_account, pool_base_token_reserves, pool_quote_token_reserves, + virtual_quote_reserves, coin_creator_vault_ata, coin_creator_vault_authority, base_token_program, @@ -202,6 +223,7 @@ impl PumpSwapParams { pool_quote_token_account: Pubkey, pool_base_token_reserves: u64, pool_quote_token_reserves: u64, + virtual_quote_reserves: i128, coin_creator_vault_ata: Pubkey, coin_creator_vault_authority: Pubkey, base_token_program: Pubkey, @@ -223,6 +245,7 @@ impl PumpSwapParams { pool_quote_token_account, pool_base_token_reserves, pool_quote_token_reserves, + virtual_quote_reserves, coin_creator_vault_ata, coin_creator_vault_authority, base_token_program, @@ -277,6 +300,18 @@ impl PumpSwapParams { ) -> Result { let (pool_base_token_reserves, pool_quote_token_reserves) = crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?; + let effective_quote_token_reserves = + crate::instruction::utils::pumpswap_types::effective_quote_reserves( + pool_quote_token_reserves, + pool_data.virtual_quote_reserves, + ) + .ok_or_else(|| { + anyhow::anyhow!( + "Invalid PumpSwap effective quote reserves: vault={} virtual={}", + pool_quote_token_reserves, + pool_data.virtual_quote_reserves + ) + })?; let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok(); let fee_config = crate::instruction::utils::pumpswap::fetch_fee_config(rpc).await; let raw_fee_basis_points = crate::instruction::utils::pumpswap::compute_fee_basis_points( @@ -285,7 +320,7 @@ impl PumpSwapParams { pool_data.base_mint, base_mint_supply, pool_base_token_reserves, - pool_quote_token_reserves, + effective_quote_token_reserves, ); let creator_fee_basis_points = if pool_data.coin_creator == Pubkey::default() { 0 @@ -319,6 +354,7 @@ impl PumpSwapParams { pool_quote_token_account: pool_data.pool_quote_token_account, pool_base_token_reserves, pool_quote_token_reserves, + virtual_quote_reserves: pool_data.virtual_quote_reserves, coin_creator_vault_ata, coin_creator_vault_authority, base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {