feat(pumpswap): support virtual quote reserves

This commit is contained in:
0xfnzero
2026-07-16 20:58:56 +08:00
parent 8bef655abb
commit dd41dd4f87
11 changed files with 976 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.22"
version = "4.0.23"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+12 -4
View File
@@ -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)
+12 -4
View File
@@ -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)
+1
View File
@@ -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.
+1
View File
@@ -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`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
+9
View File
@@ -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,
+742
View File
@@ -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": {
+31 -5
View File
@@ -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(&params).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(&params).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);
}
}
+17 -13
View File
@@ -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));
}
+112 -7
View File
@@ -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<LegacyPool> 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<Pool> {
if data.len() < POOL_SIZE {
return None;
if data.len() >= POOL_SIZE {
return borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok();
}
if data.len() >= LEGACY_POOL_SIZE {
return borsh::from_slice::<LegacyPool>(&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<u64> {
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<u8> {
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::<Pool>(&data[..POOL_SIZE]).ok()
}
+38 -2
View File
@@ -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<u64, anyhow::Error> {
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<Self, anyhow::Error> {
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 {