Compare commits

..

7 Commits

Author SHA1 Message Date
0xfnzero 22c1a149e7 Release v4.0.21 2026-06-18 01:26:54 +08:00
Wood 8657273ba6 Merge pull request #105 from civa/main
feat(swqos): update Solami endpoints + tip accounts
2026-06-17 13:01:17 +08:00
civa 99fb4ee604 feat(swqos): update Solami endpoints + tip accounts 2026-06-16 18:45:43 +00:00
0xfnzero 3719d4a9f4 Merge pull request #86 from civa/feat/add-solami
# Conflicts:
#	src/constants/swqos.rs
#	src/swqos/mod.rs
2026-06-17 01:16:07 +08:00
0xfnzero d7326b55f4 fix pumpfun native sol quote routing 2026-06-08 03:13:04 +08:00
0xfnzero 7471bcba09 Release v4.0.19 2026-06-08 00:00:32 +08:00
civa f19f8a5d1c feat(swqos): add Solami 2026-03-02 03:00:49 +00:00
14 changed files with 776 additions and 121 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.18"
version = "4.0.21"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+37 -15
View File
@@ -83,11 +83,11 @@ This SDK is available in multiple languages:
**Rust crate:** `sol-trade-sdk = "4.0.17"`
This release refreshes PumpFun V2 WSOL quote-pool handling, 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 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.
## ✨ Features
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, selecting legacy or V2 on-chain instructions as needed (SOL/WSOL + USDC)
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, preferring V1 for native SOL and selecting V2 for USDC/non-native quote mints or explicit WSOL settlement
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
3. **Bonk Trading**: Support for Bonk trading operations
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
@@ -259,7 +259,8 @@ Optional builder methods:
| `.wait_for_all_submits(true)` | In fast-submit mode, wait for all SWQoS lane responses and return all signatures. |
| `.simulate(true)` | Build and simulate the transaction instead of sending it. |
| `.grpc_recv_us(ts)` | Attach upstream receive timestamp for latency tracing. |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Use durable nonce instead of `recent_blockhash`. |
| `.durable_nonce(nonce_info)` | Use durable nonce and clear `recent_blockhash`. Recommended when you start from `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)`. |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Construct params directly with durable nonce instead of `recent_blockhash`. |
| `SimpleSellParams::with_tip(false)` | Disable relay tips for sells. Buys use the gas fee strategy/tip settings. |
`TradeBuyParams` and `TradeSellParams` remain available as advanced low-level APIs. See the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
@@ -323,7 +324,29 @@ let temporal_config = SwqosConfig::Temporal(
- If no custom URL is provided (`None`), the system will use the default endpoint for the specified `SwqosRegion`
- This allows for maximum flexibility while maintaining backward compatibility
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
When using multiple MEV services, you need to use `Durable Nonce`. Fetch the latest nonce value and attach it to the high-level buy/sell params:
```rust
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
DexParamEnum::PumpFun(pumpfun_params),
recent_blockhash, // will be cleared by `.durable_nonce(...)`
gas_fee_strategy.clone(),
)
.durable_nonce(nonce_info)
.account_policy(AccountPolicy::HotPathMinimal);
client.buy_simple(buy_params).await?;
```
#### Astralane (Binary / Plain HTTP / QUIC)
@@ -403,15 +426,15 @@ PumpFun has two instruction sets for bonding-curve trading:
|---|---|---|
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
| Account metas | 18 | 27 |
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
| Quote mint | Native SOL (`default`, Solscan SOL sentinel, or WSOL sentinel) | Non-native quote mint, or explicit WSOL settlement |
| Transaction size | Smaller (preferred hot path) | Larger (may require LUT for nonce/tip/ATA-heavy transactions) |
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` selects the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` plus the requested settlement token (`pay_with` / `receive_as`) select the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
**Default: V1**. When `quote_mint` is `Pubkey::default()` or the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), the SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. Passing `WSOL_TOKEN_ACCOUNT` selects SOL V2; passing USDC selects USDC V2.
**Default: V1**. When `quote_mint` is `Pubkey::default()`, the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), or `WSOL_TOKEN_ACCOUNT` (`So11111111111111111111111111111111111111112`), the SDK treats the curve as native SOL-paired and uses V1 instructions when `pay_with` / `receive_as` is `SOL`. This is the preferred hot path because it avoids the 27-account V2 layout. Passing USDC or another real quote mint selects V2. Passing `WSOL` as the buy input or sell output selects V2 only when you intentionally want to settle through an existing WSOL ATA.
**Key changes in v2 instructions:**
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
- `quote_mint` parameter — native SOL-paired curves may appear as default, Solscan SOL, or WSOL; USDC/non-native quote mints select V2
- 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts**
- `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory
- Same pricing and cost as legacy instructions for SOL-paired coins
@@ -419,13 +442,12 @@ The SDK-side builder is version-neutral: callers use the normal buy/sell flow, a
**Pass `quote_mint` into `PumpFunParams::from_trade`**:
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()` and Solscan SOL (`So11111111111111111111111111111111111111111`) mean the legacy SOL layout; `WSOL_TOKEN_ACCOUNT` means SOL V2; USDC means USDC V2.
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()`, Solscan SOL (`So11111111111111111111111111111111111111111`), and `WSOL_TOKEN_ACCOUNT` all mean a native SOL-paired curve and default to V1 for normal SOL settlement. USDC means USDC V2.
```rust
// quote_mint is not a PDA. It is the quote SPL mint carried by parser/gRPC events:
// - Legacy SOL pool: Pubkey::default() or Solscan SOL from parser data
// - SOL V2 pool: WSOL_TOKEN_ACCOUNT
// - USDC V2 pool: USDC mint
// - Native SOL pool: Pubkey::default(), Solscan SOL, or WSOL sentinel from parser data
// - USDC/non-native pool: actual quote SPL mint
let quote_mint = e.quote_mint;
let params = PumpFunParams::from_trade(
e.bonding_curve,
@@ -446,11 +468,11 @@ let params = PumpFunParams::from_trade(
);
```
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves.
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves. For SOL-paired curves, use `SOL` for the normal fast path; use `WSOL` only if you intentionally want V2 settlement through an existing WSOL ATA.
When consuming parser events, map `quoteMint`, `virtualQuoteReserves`, and `realQuoteReserves` into `PumpFunParams::from_trade(...)`; USDC pools use `4_292_000_000` as the initial virtual quote reserve.
For legacy SOL events where `quote_mint` is `Pubkey::default()` or Solscan SOL, use `virtual_sol_reserves` / `real_sol_reserves` when the quote-reserve fields are absent or zero.
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
> **Note**: V2 transactions with ATA creation + durable nonce/tip may exceed `PACKET_DATA_SIZE`. The SDK reports this locally and does not remove compute-budget or tip instructions because that changes priority semantics. Use V1 when the curve is native SOL-paired, pre-create ATAs, or enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
#### PumpSwap: coin_creator_vault from events (no RPC)
+35 -13
View File
@@ -83,11 +83,11 @@
**Rust crate:** `sol-trade-sdk = "4.0.17"`
本版本刷新 PumpFun V2 WSOL quote 处理逻辑,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
本版本刷新 PumpFun native SOL quote 处理逻辑,SOL/WSOL sentinel 默认优先走更小的 V1 热路径,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
## ✨ 项目特性
1. **PumpFun 交易**: SDK 侧统一为 `buy``sell``buy_exact_quote_in` 流程,内部按需选择旧版或 V2 链上指令(SOL/WSOL + USDC
1. **PumpFun 交易**: SDK 侧统一为 `buy``sell``buy_exact_quote_in` 流程,native SOL 优先走 V1USDC/非 SOL quote 或显式 WSOL 结算才走 V2
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
3. **Bonk 交易**: 支持 Bonk 的交易操作
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
@@ -258,7 +258,8 @@ client.buy_simple(buy_params).await?;
| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 |
| `.simulate(true)` | 只构建并模拟交易,不真正发送。 |
| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 使用 durable nonce不使用 `recent_blockhash`。 |
| `.durable_nonce(nonce_info)` | 使用 durable nonce并清空 `recent_blockhash`如果你从 `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)` 开始构造,推荐用这个。 |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 直接用 durable nonce 构造参数,不使用 `recent_blockhash`。 |
| `SimpleSellParams::with_tip(false)` | 关闭卖出交易 relay tip。买入的 tip 使用 gas fee strategy 控制。 |
`TradeBuyParams``TradeSellParams` 仍保留为高级低层接口。详细说明见 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
@@ -322,7 +323,29 @@ let temporal_config = SwqosConfig::Temporal(
- 如果没有提供自定义 URL`None`),系统将使用指定 `SwqosRegion` 的默认端点
- 这提供了最大的灵活性,同时保持向后兼容性
当使用多个MEV服务时,需要使用`Durable Nonce`你需要使用`fetch_nonce_info`函数获取最新`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
当使用多个 MEV 服务时,需要使用 `Durable Nonce`获取最新 nonce,再挂到新的 buy/sell 参数上:
```rust
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
DexParamEnum::PumpFun(pumpfun_params),
recent_blockhash, // 会被 `.durable_nonce(...)` 清空
gas_fee_strategy.clone(),
)
.durable_nonce(nonce_info)
.account_policy(AccountPolicy::HotPathMinimal);
client.buy_simple(buy_params).await?;
```
#### AstralaneBinary / Plain / QUIC
@@ -405,22 +428,21 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 自动选择正确的链上 discriminator 和账户布局。
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 和买/卖的结算 mint 自动选择正确的链上 discriminator 和账户布局。能用 V1 的 native SOL 池会优先用 V1。
**v2 指令关键变化:**
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL`So11111111111111111111111111111111111111112`USDC 配对传 USDC mint
- 新增 `quote_mint` 参数 — native SOL 配对可能表现为默认值、Solscan SOL sentinel`So11111111111111111111111111111111111111111`或 WSOL sentinel`So11111111111111111111111111111111111111112`);USDC/其他真实 quote mint 才选择 V2
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
- `buyback_fee_recipient``sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
- SOL 配对币的报价和成本与旧版一致,无额外开销
**使用方式:**
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade``quote_mint` 不是 PDA,它就是 quote SPL mint`Pubkey::default()`Solscan SOL`So11111111111111111111111111111111111111111`表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2USDC 表示 USDC V2
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade``quote_mint` 不是 PDA,它就是 quote SPL mint 或 native SOL sentinel`Pubkey::default()`Solscan SOL`So11111111111111111111111111111111111111111``WSOL_TOKEN_ACCOUNT` 表示 native SOL 配对,正常用 SOL 结算时默认走旧版 V1;USDC 表示 USDC V2
```rust
// legacy SOL 池:log 事件里可能是 Pubkey::default()parser 数据里是 Solscan SOL sentinel
// SOL V2 池:WSOL_TOKEN_ACCOUNT
// USDC 池:就是 USDC mint
// native SOL 池:可能是 Pubkey::default()、Solscan SOL sentinel 或 WSOL sentinel
// USDC / 非 SOL 池:就是实际 quote SPL mint
let quote_mint = e.quote_mint;
let params = PumpFunParams::from_trade(
@@ -446,14 +468,14 @@ client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SOL 配对的普通热路径请传 `SOL`SDK 会用 V1;只有你明确传 `WSOL` 作为买入输入或卖出输出、希望通过已有 WSOL ATA 结算时,才会选择 V2
SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。
消费 parser 事件时,需要把 `quoteMint``virtualQuoteReserves``realQuoteReserves` 传进 `PumpFunParams::from_trade(...)`USDC 池初始虚拟 quote reserve 是 `4_292_000_000`
legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quote reserve 字段缺失/为 0,应回退使用 `virtual_sol_reserves` / `real_sol_reserves`
| quote_mint | 实际使用的指令 | 说明 |
|-----------|---------|------|
| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
| `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
| 未设置(默认)/ `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 |
## 🛡️ MEV 保护服务
+27 -2
View File
@@ -30,7 +30,7 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
@@ -50,7 +50,7 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
| `with_tip` | `bool` | ❌ | Whether sells include relay tips. Default is `true`; set with `.with_tip(false)`. |
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
@@ -74,6 +74,31 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
| `CreateMissing` | Include ATA creation where possible. | Convenience matters more than smallest transaction size. |
| `AssumePrepared` | Do not create or close token accounts; caller prepared everything. | Advanced deterministic flows. |
### Durable Nonce With Simple Params
`fetch_nonce_info` and `DurableNonceInfo` are re-exported from the crate root:
```rust
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
dex_type,
pay_with,
mint,
amount,
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.durable_nonce(nonce_info);
```
Calling `.durable_nonce(...)` clears `recent_blockhash`; nonce transactions use the nonce value as the transaction blockhash.
## TradeBuyParams
`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags.
+27 -2
View File
@@ -30,7 +30,7 @@
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)``SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
@@ -50,7 +50,7 @@
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)``SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
| `with_tip` | `bool` | ❌ | 卖出交易是否带 relay tip。默认 `true`,可通过 `.with_tip(false)` 关闭。 |
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
@@ -74,6 +74,31 @@
| `CreateMissing` | 尽量在交易内创建缺失 ATA。 | 更重视方便,不追求最小交易体积。 |
| `AssumePrepared` | 不创建也不关闭 token account,调用方保证都已准备好。 | 高级确定性流程。 |
### Simple 参数使用 Durable Nonce
`fetch_nonce_info``DurableNonceInfo` 已从 crate root 重新导出:
```rust
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
.await
.expect("nonce account must be initialized");
let buy_params = SimpleBuyParams::new(
dex_type,
pay_with,
mint,
amount,
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.durable_nonce(nonce_info);
```
调用 `.durable_nonce(...)` 会清空 `recent_blockhash`nonce 交易会使用 nonce value 作为 transaction blockhash。
## TradeBuyParams
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它。
+42 -7
View File
@@ -14,11 +14,11 @@ use sol_trade_sdk::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
AccountPolicy, BuyAmount, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade,
TradeTokenType,
AccountPolicy, BuyAmount, DurableNonceInfo, SellAmount, SimpleBuyParams, SimpleSellParams,
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer};
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -83,7 +83,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// client.buy_simple(buy_params).await?;
let _ = buy_params;
// Durable nonce is supported by the same high-level params. In production,
// fetch it with `sol_trade_sdk::fetch_nonce_info(...)` immediately before
// building the transaction.
let nonce_buy_params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint,
BuyAmount::WithMaxInput { quote_amount: 100_000 },
pumpfun_params.clone(),
recent_blockhash,
gas_fee_strategy.clone(),
)
.durable_nonce(DurableNonceInfo {
nonce_account: Some(Pubkey::new_unique()),
current_nonce: Some(Hash::new_unique()),
})
.account_policy(AccountPolicy::HotPathMinimal);
let _ = nonce_buy_params;
let sell_params = SimpleSellParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint,
SellAmount::ExactInput(1_000_000),
pumpfun_params.clone(),
client.infrastructure.rpc.get_latest_blockhash().await?,
gas_fee_strategy.clone(),
)
.slippage_basis_points(300)
.account_policy(AccountPolicy::HotPathMinimal);
// client.sell_simple(sell_params).await?;
let _ = sell_params;
let nonce_sell_params = SimpleSellParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint,
@@ -92,11 +126,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
client.infrastructure.rpc.get_latest_blockhash().await?,
gas_fee_strategy,
)
.slippage_basis_points(300)
.durable_nonce(DurableNonceInfo {
nonce_account: Some(Pubkey::new_unique()),
current_nonce: Some(Hash::new_unique()),
})
.account_policy(AccountPolicy::HotPathMinimal);
// client.sell_simple(sell_params).await?;
let _ = sell_params;
let _ = nonce_sell_params;
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
Ok(())
+78 -3
View File
@@ -173,9 +173,10 @@ pub struct SimpleBuyParams {
pub dex_type: DexType,
/// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`.
///
/// For PumpFun V2 SOL-paired coins, use `TradeTokenType::SOL` even when the
/// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote
/// account but keeps settlement in native SOL.
/// For PumpFun SOL-paired coins, use `TradeTokenType::SOL` for the normal
/// fast path even when parser data reports WSOL as the quote sentinel. The
/// SDK will prefer V1; choose `WSOL` only when intentionally spending an
/// existing WSOL ATA through V2.
pub pay_with: TradeTokenType,
/// Mint address of the token being bought.
pub mint: Pubkey,
@@ -335,6 +336,16 @@ impl SimpleBuyParams {
self
}
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
///
/// This clears `recent_blockhash` because nonce transactions must use the
/// nonce value as their transaction blockhash.
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
self.durable_nonce = Some(value);
self.recent_blockhash = None;
self
}
/// Wait for confirmation before returning.
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
self.wait_tx_confirmed = value;
@@ -444,6 +455,16 @@ impl SimpleSellParams {
self
}
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
///
/// This clears `recent_blockhash` because nonce transactions must use the
/// nonce value as their transaction blockhash.
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
self.durable_nonce = Some(value);
self.recent_blockhash = None;
self
}
/// Wait for confirmation before returning.
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
self.wait_tx_confirmed = value;
@@ -1910,6 +1931,33 @@ mod tests {
assert!(!low.wait_tx_confirmed);
}
#[test]
fn simple_buy_builder_can_use_durable_nonce() {
let nonce_account = Pubkey::new_unique();
let nonce_hash = Hash::new_unique();
let durable_nonce = DurableNonceInfo {
nonce_account: Some(nonce_account),
current_nonce: Some(nonce_hash),
};
let simple = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
Pubkey::new_unique(),
BuyAmount::ExactInput(10_000),
dummy_pumpfun_params(),
Hash::new_unique(),
GasFeeStrategy::new(),
)
.durable_nonce(durable_nonce.clone());
let low: TradeBuyParams = simple.into();
assert!(low.recent_blockhash.is_none());
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
}
#[test]
fn simple_sell_auto_creates_non_sol_output_ata() {
let simple = SimpleSellParams {
@@ -1939,4 +1987,31 @@ mod tests {
assert!(!low.close_output_token_ata);
assert!(!low.close_mint_token_ata);
}
#[test]
fn simple_sell_builder_can_use_durable_nonce() {
let nonce_account = Pubkey::new_unique();
let nonce_hash = Hash::new_unique();
let durable_nonce = DurableNonceInfo {
nonce_account: Some(nonce_account),
current_nonce: Some(nonce_hash),
};
let simple = SimpleSellParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
Pubkey::new_unique(),
SellAmount::ExactInput(50_000),
dummy_pumpfun_params(),
Hash::new_unique(),
GasFeeStrategy::new(),
)
.durable_nonce(durable_nonce.clone());
let low: TradeSellParams = simple.into();
assert!(low.recent_blockhash.is_none());
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
}
}
+28
View File
@@ -171,6 +171,19 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
];
pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("15qWd4huAkoxvhDsHMfpUn27TW1YBYMMJJ2jkAkbeam"),
pubkey!("9XuGciSwr5wb7dLTQm91JhuBTvj3GG8WjuRDc3obeam"),
pubkey!("kiQioJNyFG7pU36ELLsRKXkeT48kFbk3b6rSgrWbeam"),
pubkey!("kjmVhW1UzJrW2sU5bY5NtZ79jpvjSStsj37Pzmabeam"),
pubkey!("kREnjPWFpt4AHeY5pijPmyXaCrMnbatUQJo7d3Xbeam"),
pubkey!("praRZG6N6MdbsT4EFpKgZJWReZGXQhAMFcH68oCbeam"),
pubkey!("SqoKQKU5uwBxovq3R7yEBxFwptc4z7vwoghU3M9beam"),
pubkey!("sV72TY66T1RfmDSeHPPbwX6wwJ3bBv5hd4ehJ8tbeam"),
pubkey!("swf8MyEeLo7gtRUo27UuJj6naCASUrypU7dbteSbeam"),
pubkey!("uiuaQsxA47JybQAVN4FTfYuoEDkMiXV1r591Aewbeam"),
];
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
//
@@ -434,6 +447,19 @@ pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
];
pub const SWQOS_ENDPOINTS_SOLAMI: [&str; 10] = [
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
"beam.solami.dev:11000",
];
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001;
pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001;
@@ -450,6 +476,7 @@ pub const SWQOS_MIN_TIP_SOYAS: f64 = 0.001; // Soyas requires minimum 0.001 SOL
pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires minimum 0.001 SOL tip
/// Helius Sender: 0.0002 SOL when not swqos_only; use SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY when swqos_only=true.
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
pub const SWQOS_MIN_TIP_SOLAMI: f64 = 0.0001;
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
@@ -476,6 +503,7 @@ mod tests {
&SWQOS_ENDPOINTS_SOYAS,
&SWQOS_ENDPOINTS_SPEEDLANDING,
&SWQOS_ENDPOINTS_HELIUS,
&SWQOS_ENDPOINTS_SOLAMI,
];
#[test]
+138 -25
View File
@@ -1,10 +1,11 @@
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
//!
//! The SDK selects the legacy or V2 on-chain layout from `PumpFunParams.quote_mint`.
//! `Pubkey::default()` and the Solscan SOL sentinel keep the smaller legacy SOL layout;
//! explicit WSOL or USDC uses the V2 27/26-account unified metas.
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
//! Native SOL-paired coins (`Pubkey::default()`, Solscan SOL sentinel, or WSOL sentinel)
//! keep the smaller legacy SOL layout by default. Non-native quote mints such as USDC use
//! the V2 27/26-account unified metas.
use crate::swqos::TradeType;
use crate::{
common::bonding_curve::BondingCurveAccount,
common::spl_token::close_account,
@@ -52,8 +53,8 @@ fn effective_pump_mint_token_program(protocol_params: &PumpFunParams, mint: &Pub
}
/// Resolve quote mint and its token program from PumpFunParams.
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` means legacy SOL-paired; use WSOL mint when a
/// downstream V2 helper needs a concrete SPL quote mint.
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` / `WSOL_TOKEN_ACCOUNT` are native SOL-paired.
/// V2 helpers still need a concrete SPL mint, so native SOL resolves to WSOL internally.
#[inline]
fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) {
let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint();
@@ -73,6 +74,16 @@ fn is_sol_quote_mint(quote_mint: &Pubkey) -> bool {
|| *quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
}
#[inline]
fn is_native_sol_settlement_mint(mint: &Pubkey) -> bool {
*mint == crate::constants::SOL_TOKEN_ACCOUNT || *mint == Pubkey::default()
}
#[inline]
fn is_explicit_wsol_settlement_mint(mint: &Pubkey) -> bool {
*mint == crate::constants::WSOL_TOKEN_ACCOUNT
}
#[inline]
fn validate_v2_buy_quote_mint(input_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> {
if is_sol_quote_mint(quote_mint) {
@@ -130,9 +141,20 @@ fn should_use_v2_layout(params: &SwapParams) -> Result<bool> {
.downcast_ref::<PumpFunParams>()
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
let (quote_mint, _) = effective_quote_mint_and_token_program(protocol_params);
let explicit_v2_quote = protocol_params.quote_mint != Pubkey::default()
&& protocol_params.quote_mint != crate::constants::SOL_TOKEN_ACCOUNT;
Ok(explicit_v2_quote || !is_sol_quote_mint(&quote_mint))
if !is_sol_quote_mint(&quote_mint) {
return Ok(true);
}
// Pump docs treat WSOL quote mint as the native SOL sentinel for SOL-paired curves.
// Keep V1 for native SOL settlement; use V2 only when the caller explicitly wants to
// spend/receive an existing WSOL ATA.
Ok(match params.trade_type {
TradeType::Buy | TradeType::CreateAndBuy => {
is_explicit_wsol_settlement_mint(&params.input_mint)
}
TradeType::Sell => is_explicit_wsol_settlement_mint(&params.output_mint),
TradeType::Create => false,
})
}
#[inline]
@@ -171,6 +193,13 @@ fn build_buy_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
.downcast_ref::<PumpFunParams>()
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
if !is_native_sol_settlement_mint(&params.input_mint) {
return Err(anyhow!(
"PumpFun native SOL buy expects input_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL input only when spending an existing WSOL ATA.",
params.input_mint
));
}
let lamports_in = params.input_amount.unwrap_or(0);
if lamports_in == 0 {
return Err(anyhow!("Amount cannot be zero"));
@@ -299,6 +328,13 @@ fn build_sell_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
.downcast_ref::<PumpFunParams>()
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
if !is_native_sol_settlement_mint(&params.output_mint) {
return Err(anyhow!(
"PumpFun native SOL sell expects output_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL output only when receiving into an existing WSOL ATA.",
params.output_mint
));
}
let token_amount = if let Some(amount) = params.input_amount {
if amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
@@ -1002,14 +1038,15 @@ mod tests {
}
#[test]
fn pumpfun_v2_fixed_output_uses_buy_with_max_input_budget() {
fn pumpfun_v2_usdc_fixed_output_uses_buy_with_max_input_budget() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.create_output_mint_ata = false;
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
params.fixed_output_amount = Some(42);
params.use_exact_sol_amount = Some(true);
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
}
let instructions = build_buy(&params).unwrap();
@@ -1024,7 +1061,7 @@ mod tests {
}
#[test]
fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() {
fn pumpfun_wsol_quote_regular_buy_uses_v1_native_sol() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.create_output_mint_ata = false;
params.create_input_mint_ata = true;
@@ -1043,13 +1080,13 @@ mod tests {
params.slippage_basis_points.unwrap(),
);
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected);
assert_eq!(buy_ix.accounts.len(), 27);
assert_eq!(buy_ix.accounts.len(), 18);
}
#[test]
fn pumpfun_v2_regular_buy_skips_wsol_ata_create_on_hot_path() {
fn pumpfun_wsol_quote_regular_buy_skips_wsol_ata_create_on_hot_path() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.create_output_mint_ata = false;
params.create_input_mint_ata = true;
@@ -1065,13 +1102,15 @@ mod tests {
assert_eq!(instructions.len(), 1);
assert_eq!(
&instructions.last().unwrap().data[..8],
crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR
crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR
);
}
#[test]
fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() {
fn pumpfun_v2_explicit_wsol_input_with_output_ata_create_reports_oversized_without_dropping_priority(
) {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
params.create_input_mint_ata = true;
params.create_output_mint_ata = true;
params.use_exact_sol_amount = Some(false);
@@ -1080,6 +1119,41 @@ mod tests {
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
}
let business_instructions = build_buy(&params).unwrap();
let err = crate::trading::common::transaction_builder::build_transaction(
&params.payer,
150_000,
500_000,
&business_instructions,
None,
Some(solana_hash::Hash::new_unique()),
None,
"PumpFun",
true,
true,
&Pubkey::new_unique(),
0.001,
None,
)
.unwrap_err()
.to_string();
assert!(err.contains("transaction too large"), "{err}");
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
}
#[test]
fn pumpfun_v2_explicit_wsol_input_hot_path_transaction_fits_when_output_ata_prepared() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
params.create_input_mint_ata = true;
params.create_output_mint_ata = false;
params.use_exact_sol_amount = Some(false);
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
}
let business_instructions = build_buy(&params).unwrap();
let transaction = crate::trading::common::transaction_builder::build_transaction(
&params.payer,
@@ -1107,7 +1181,7 @@ mod tests {
}
#[test]
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_buy_v2() {
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_v1() {
let mint = pump_mint();
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
params.create_output_mint_ata = false;
@@ -1135,8 +1209,8 @@ mod tests {
let instructions = build_buy(&params).unwrap();
let buy_ix = instructions.last().unwrap();
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
assert_eq!(buy_ix.accounts.len(), 27);
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
assert_eq!(buy_ix.accounts.len(), 18);
}
#[test]
@@ -1161,6 +1235,44 @@ mod tests {
assert_eq!(instructions[0].accounts.len(), 27);
}
#[test]
fn pumpfun_sell_does_not_select_v2_from_base_mint_wsol() {
let mut params = swap_params_for_buy(crate::constants::WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM);
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
params.create_output_mint_ata = false;
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
}
let instructions = build_sell(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR);
}
#[test]
fn pumpfun_sell_selects_v2_for_explicit_wsol_output_settlement() {
let mint = pump_mint();
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = mint;
params.output_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
params.create_output_mint_ata = false;
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
}
let instructions = build_sell(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR);
assert_eq!(ix.accounts.len(), 26);
}
#[test]
fn pumpfun_usdc_dev_trade_uses_usdc_initial_quote_reserves() {
let mint = pump_mint();
@@ -1372,13 +1484,14 @@ mod tests {
}
#[test]
fn pumpfun_v2_buyback_fee_recipient_is_writable() {
fn pumpfun_v2_usdc_buyback_fee_recipient_is_writable() {
let mint = pump_mint();
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
params.create_output_mint_ata = false;
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
}
let buy_ix = build_buy(&params).unwrap().pop().unwrap();
@@ -1387,7 +1500,7 @@ mod tests {
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = mint;
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
let sell_ix = build_sell(&params).unwrap().pop().unwrap();
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
assert!(sell_ix.accounts[8].is_writable);
@@ -1416,17 +1529,17 @@ mod tests {
}
#[test]
fn pumpfun_v2_sell_fixed_output_uses_min_sol_directly() {
fn pumpfun_v2_usdc_sell_fixed_output_uses_min_quote_directly() {
let mint = pump_mint();
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = mint;
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
params.create_output_mint_ata = false;
params.fixed_output_amount = Some(42);
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
}
let instructions = build_sell(&params).unwrap();
+1
View File
@@ -7,6 +7,7 @@ pub mod swqos;
pub mod trading;
pub mod utils;
pub use crate::common::nonce_cache::{fetch_nonce_info, DurableNonceInfo};
// Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode)
pub use crate::swqos::{AstralaneTransport, SwqosTransport};
pub use client::{
+24 -9
View File
@@ -11,6 +11,7 @@ pub mod nextblock;
pub mod node1;
pub mod node1_quic;
pub mod serialization;
pub mod solami;
pub mod solana_rpc;
pub mod soyas;
pub mod speedlanding;
@@ -33,21 +34,21 @@ use crate::{
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX,
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO,
SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC,
SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
SWQOS_ENDPOINTS_SOLAMI, SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING,
SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT,
SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE,
SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1,
SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM,
SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
SWQOS_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING,
SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
},
swqos::{
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
flashblock::FlashBlockClient, helius::HeliusClient, jito::JitoClient,
lightspeed::LightspeedClient, nextblock::NextBlockClient, node1::Node1Client,
node1_quic::Node1QuicClient, solana_rpc::SolRpcClient, soyas::SoyasClient,
speedlanding::SpeedlandingClient, stellium::StelliumClient, temporal::TemporalClient,
zeroslot::ZeroSlotClient,
node1_quic::Node1QuicClient, solami::SolamiClient, solana_rpc::SolRpcClient,
soyas::SoyasClient, speedlanding::SpeedlandingClient, stellium::StelliumClient,
temporal::TemporalClient, zeroslot::ZeroSlotClient,
},
};
@@ -121,6 +122,7 @@ pub enum SwqosType {
Soyas,
Speedlanding,
Helius,
Solami,
Default,
}
@@ -143,6 +145,7 @@ impl SwqosType {
Self::Soyas => "Soyas",
Self::Speedlanding => "Speedlanding",
Self::Helius => "Helius",
Self::Solami => "Solami",
Self::Default => "Default",
}
}
@@ -163,6 +166,7 @@ impl SwqosType {
Self::Soyas,
Self::Speedlanding,
Self::Helius,
Self::Solami,
Self::Default,
]
}
@@ -204,6 +208,7 @@ pub trait SwqosClientTrait {
SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS,
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
SwqosType::Helius => SWQOS_MIN_TIP_HELIUS,
SwqosType::Solami => SWQOS_MIN_TIP_SOLAMI,
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
}
}
@@ -264,6 +269,8 @@ pub enum SwqosConfig {
/// Helius Sender: dual routing to validators and Jito. API key optional (custom TPS only).
/// (api_key, region, custom_url, swqos_only). swqos_only: None => false (min tip 0.0002 SOL); Some(true) => SWQOS-only (min tip 0.000005 SOL, much lower).
Helius(String, SwqosRegion, Option<String>, Option<bool>),
/// Solami(api_key, region, custom_url)
Solami(String, SwqosRegion, Option<String>),
}
impl SwqosConfig {
@@ -284,6 +291,7 @@ impl SwqosConfig {
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
SwqosConfig::Solami(_, _, _) => SwqosType::Solami,
}
}
@@ -312,6 +320,7 @@ impl SwqosConfig {
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(),
SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(),
SwqosType::Solami => SWQOS_ENDPOINTS_SOLAMI[region as usize].to_string(),
SwqosType::Default => "".to_string(),
}
}
@@ -512,6 +521,12 @@ impl SwqosConfig {
HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only);
Ok(Arc::new(helius_client))
}
SwqosConfig::Solami(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Solami, region, url);
let solami_client =
SolamiClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
Ok(Arc::new(solami_client))
}
SwqosConfig::Default(endpoint) => {
let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment);
let rpc_client = SolRpcClient::new(Arc::new(rpc));
+242
View File
@@ -0,0 +1,242 @@
use anyhow::Context as _;
use anyhow::Result;
use arc_swap::ArcSwap;
use quinn::{
crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout,
TransportConfig,
};
use rand::seq::IndexedRandom as _;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::signer::Signer;
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use std::time::Instant;
use std::{
net::{SocketAddr, ToSocketAddrs as _},
sync::Arc,
time::Duration,
};
use tokio::sync::Mutex;
use tokio::time::timeout;
use crate::common::SolanaRpcClient;
use crate::swqos::common::poll_transaction_confirmation;
use crate::swqos::SwqosClientTrait;
use crate::{
constants::swqos::SOLAMI_TIP_ACCOUNTS,
swqos::{SwqosType, TradeType},
};
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const SOLAMI_SERVER: &str = "solami-beam";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const SEND_TIMEOUT: Duration = Duration::from_secs(5);
pub struct SolamiClient {
pub rpc_client: Arc<SolanaRpcClient>,
endpoint: Endpoint,
client_config: ClientConfig,
addr: SocketAddr,
connection: ArcSwap<Connection>,
reconnect: Mutex<()>,
}
impl SolamiClient {
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
let keypair_bytes = bs58::decode(api_key.trim())
.into_vec()
.map_err(|e| anyhow::anyhow!("Solami api_token base58 decode failed: {}", e))?;
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
anyhow::anyhow!("Solami api_token is not a valid Solana keypair: {}", e)
})?;
let (cert, key) = new_dummy_x509_certificate(&keypair);
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_client_auth_cert(vec![cert], key)
.context("failed to configure client certificate")?;
crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()];
let client_crypto = QuicClientConfig::try_from(crypto)
.context("failed to convert rustls config into quinn crypto config")?;
let mut client_config = ClientConfig::new(Arc::new(client_crypto));
let mut transport = TransportConfig::default();
transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
transport.max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT)?));
client_config.transport_config(Arc::new(transport));
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(client_config.clone());
let addr = endpoint_string
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
let connecting = endpoint.connect(addr, SOLAMI_SERVER)?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Solami QUIC connect timeout")?
.with_context(|| {
format!(
"Solami QUIC handshake failed (verify wallet pubkey {} is registered and UDP {} is reachable)",
keypair.pubkey(),
endpoint_string
)
})?;
Ok(Self {
rpc_client: Arc::new(rpc_client),
endpoint,
client_config,
addr,
connection: ArcSwap::from_pointee(connection),
reconnect: Mutex::new(()),
})
}
async fn ensure_connected(&self) -> Result<Arc<Connection>> {
let current = self.connection.load_full();
if current.close_reason().is_none() {
return Ok(current);
}
let _guard = self.reconnect.lock().await;
let current = self.connection.load_full();
if current.close_reason().is_some() {
let connecting =
self.endpoint.connect_with(self.client_config.clone(), self.addr, SOLAMI_SERVER)?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Solami QUIC reconnect timeout")?
.with_context(|| {
format!(
"Solami QUIC re-handshake failed (peer {} SNI {})",
self.addr, SOLAMI_SERVER
)
})?;
self.connection.store(Arc::new(connection));
return Ok(self.connection.load_full());
}
Ok(current)
}
async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> Result<()> {
let mut stream = connection.open_uni().await?;
stream.write_all(payload).await?;
stream.finish()?;
Ok(())
}
}
#[async_trait::async_trait]
impl SwqosClientTrait for SolamiClient {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let signature = transaction.get_signature();
let serialized_tx = bincode::serialize(transaction)?;
let connection = self.ensure_connected().await?;
let mut send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &serialized_tx)).await;
let need_retry = matches!(&send_result, Ok(Err(_)) | Err(_));
if need_retry {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed(
"Solami",
trade_type,
start_time.elapsed(),
"reconnecting",
);
}
let connection = self.ensure_connected().await?;
send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &serialized_tx)).await;
}
match send_result {
Ok(Ok(())) => {}
Ok(Err(e)) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed(
"Solami",
trade_type,
start_time.elapsed(),
&e,
);
}
return Err(e);
}
Err(_) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed(
"Solami",
trade_type,
start_time.elapsed(),
"timeout",
);
}
anyhow::bail!("Solami QUIC send timeout");
}
}
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("Solami", trade_type, start_time.elapsed());
}
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(
" [{:width$}] {} confirmation failed: {:?}",
"Solami",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(
" [{:width$}] {} confirmed: {:?}",
"Solami",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
Ok(())
}
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *SOLAMI_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| SOLAMI_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
fn get_swqos_type(&self) -> SwqosType {
SwqosType::Solami
}
}
+57 -31
View File
@@ -46,7 +46,7 @@ pub fn build_transaction(
tip_amount: f64,
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<VersionedTransaction, anyhow::Error> {
let transaction = build_transaction_with_compute_budget(
let transaction = build_transaction_inner(
payer,
unit_limit,
unit_price,
@@ -63,44 +63,32 @@ pub fn build_transaction(
)?;
let serialized_len = bincode::serialized_size(&transaction)? as usize;
if serialized_len <= PACKET_DATA_SIZE
|| durable_nonce.is_some()
|| !with_tip
|| tip_amount <= 0.0
|| (unit_limit == 0 && unit_price == 0)
{
if crate::common::sdk_log::sdk_log_enabled() {
println!(
" [SDK][tx-size ] {} {} serialized={} bytes, business_ix={}, nonce={}, tip={}, cu_limit={}, cu_price={}, alt={}",
protocol_name,
if is_buy { "buy" } else { "sell" },
serialized_len,
business_instructions.len(),
durable_nonce.is_some(),
with_tip && tip_amount > 0.0,
unit_limit,
unit_price,
address_lookup_table_account.is_some()
);
}
if serialized_len <= PACKET_DATA_SIZE {
return Ok(transaction);
}
let compact_transaction = build_transaction_with_compute_budget(
payer,
0,
0,
business_instructions,
address_lookup_table_account,
recent_blockhash,
middleware_manager,
protocol_name,
is_buy,
with_tip,
tip_account,
tip_amount,
durable_nonce,
)?;
let compact_len = bincode::serialized_size(&compact_transaction)? as usize;
if compact_len <= PACKET_DATA_SIZE {
return Ok(compact_transaction);
}
Err(anyhow!(
"transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting",
"transaction too large: {} > {}; SDK did not remove compute budget or relay tip because that changes transaction priority semantics. Use an address lookup table or pre-create token ATAs before submitting",
serialized_len,
PACKET_DATA_SIZE,
compact_len
PACKET_DATA_SIZE
))
}
fn build_transaction_with_compute_budget(
fn build_transaction_inner(
payer: &Arc<Keypair>,
unit_limit: u32,
unit_price: u64,
@@ -181,3 +169,41 @@ fn build_versioned_transaction(
Ok(tx)
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::instruction::AccountMeta;
fn oversized_instruction(account_count: usize, data_len: usize) -> Instruction {
let accounts =
(0..account_count).map(|_| AccountMeta::new(Pubkey::new_unique(), false)).collect();
Instruction { program_id: Pubkey::new_unique(), accounts, data: vec![7; data_len] }
}
#[test]
fn oversized_transaction_returns_error_without_dropping_priority_semantics() {
let payer = Arc::new(Keypair::new());
let business_instructions = vec![oversized_instruction(36, 700)];
let err = build_transaction(
&payer,
80_000,
100_000,
&business_instructions,
None,
Some(Hash::new_unique()),
None,
"test",
true,
true,
&Pubkey::new_unique(),
0.001,
None,
)
.unwrap_err()
.to_string();
assert!(err.contains("transaction too large"), "{err}");
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
}
}
+39 -13
View File
@@ -14,9 +14,10 @@ use std::sync::Arc;
/// **Buy/sell**`creator_vault` 及(若可得)**`tradeEvent` / CPI 日志中的 `creator`** 优先于陈旧的曲线快照;
/// ix 组装与链下询价见 [`Self::effective_creator_for_trade`]、[`crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing`]。
///
/// **V2 instructions**: The SDK selects the layout automatically from `quote_mint`.
/// Leave `quote_mint` default, or pass the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), for
/// legacy SOL layout. Pass `WSOL_TOKEN_ACCOUNT` for SOL V2, or USDC for USDC-paired coins.
/// **Instruction layout**: The SDK selects the smallest valid layout automatically from
/// `quote_mint`. Native SOL-paired coins use the legacy SOL layout when `quote_mint`
/// is default, the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), or the WSOL sentinel
/// (`WSOL_TOKEN_ACCOUNT`). Non-native quote mints such as USDC use V2.
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
@@ -41,8 +42,9 @@ pub struct PumpFunParams {
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值。
pub fee_recipient: Pubkey,
/// Quote mint layout selector. Default and `SOL_TOKEN_ACCOUNT` use legacy SOL layout.
/// `WSOL_TOKEN_ACCOUNT` selects SOL v2; USDC selects USDC v2.
/// Quote mint layout selector. Default, `SOL_TOKEN_ACCOUNT`, and `WSOL_TOKEN_ACCOUNT`
/// are native SOL-paired and use the smaller legacy SOL layout by default.
/// USDC and other non-native quote mints select V2.
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
pub quote_mint: Pubkey,
}
@@ -50,7 +52,10 @@ pub struct PumpFunParams {
impl PumpFunParams {
#[inline]
fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
if quote_mint == Pubkey::default()
|| quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|| quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
{
Pubkey::default()
} else {
BondingCurveAccount::normalize_quote_mint(quote_mint)
@@ -59,7 +64,10 @@ impl PumpFunParams {
#[inline]
fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
if quote_mint == Pubkey::default()
|| quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|| quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
{
crate::constants::SOL_TOKEN_ACCOUNT
} else {
BondingCurveAccount::normalize_quote_mint(quote_mint)
@@ -176,8 +184,8 @@ impl PumpFunParams {
}
/// Build PumpFun params from event/parser data. Pass `quote_mint` from the event:
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` for legacy SOL layout,
/// `WSOL_TOKEN_ACCOUNT` for SOL V2, and `USDC_TOKEN_ACCOUNT` for USDC V2.
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` / `WSOL_TOKEN_ACCOUNT` for native SOL
/// layout, and `USDC_TOKEN_ACCOUNT` or another non-native quote mint for V2.
/// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts.
///
/// `mayhem_mode`:
@@ -369,8 +377,9 @@ impl PumpFunParams {
}
/// Sets `quote_mint`. The instruction builder derives V1/V2 layout from this value.
/// For legacy SOL-paired coins, pass `SOL_TOKEN_ACCOUNT` or leave default.
/// Pass `WSOL_TOKEN_ACCOUNT` only when you intentionally want SOL v2 layout.
/// For native SOL-paired coins, pass `Pubkey::default()`, `SOL_TOKEN_ACCOUNT`, or
/// `WSOL_TOKEN_ACCOUNT`; all three select the smaller V1 layout unless buy/sell params
/// explicitly request WSOL settlement. Pass USDC or another non-native quote mint for V2.
#[inline]
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint);
@@ -419,6 +428,10 @@ mod tests {
PumpFunParams::quote_mint_for_rpc_return(crate::constants::SOL_TOKEN_ACCOUNT),
crate::constants::SOL_TOKEN_ACCOUNT
);
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT),
crate::constants::SOL_TOKEN_ACCOUNT
);
}
#[test]
@@ -427,9 +440,22 @@ mod tests {
PumpFunParams::quote_mint_for_rpc_return(crate::constants::USDC_TOKEN_ACCOUNT),
crate::constants::USDC_TOKEN_ACCOUNT
);
}
#[test]
fn quote_mint_for_layout_normalizes_native_sol_sentinels_to_default() {
assert_eq!(PumpFunParams::quote_mint_for_layout(Pubkey::default()), Pubkey::default());
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT),
crate::constants::WSOL_TOKEN_ACCOUNT
PumpFunParams::quote_mint_for_layout(crate::constants::SOL_TOKEN_ACCOUNT),
Pubkey::default()
);
assert_eq!(
PumpFunParams::quote_mint_for_layout(crate::constants::WSOL_TOKEN_ACCOUNT),
Pubkey::default()
);
assert_eq!(
PumpFunParams::quote_mint_for_layout(crate::constants::USDC_TOKEN_ACCOUNT),
crate::constants::USDC_TOKEN_ACCOUNT
);
}
}