Compare commits

..

10 Commits

Author SHA1 Message Date
0xfnzero 47cef59d15 fix: align PumpSwap dynamic fee parameters 2026-06-30 21:40:44 +08:00
0xfnzero 9af86c8397 Update SWQOS submit behavior 2026-06-23 04:32:50 +08:00
0xfnzero 30df893c30 docs: clarify SDK scope and current version 2026-06-20 00:56:14 +08:00
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
58 changed files with 51983 additions and 344 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.18"
version = "4.0.22"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+52 -19
View File
@@ -79,15 +79,26 @@ This SDK is available in multiple languages:
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
## What This SDK Is For
`sol-trade-sdk` is the Rust implementation of the FnZero Solana trading SDK family. It focuses on low-latency transaction construction and submission for Solana DEX trading bots, copy-trading systems, sniper bots, arbitrage strategies, and private trading infrastructure.
| Area | Coverage |
|------|----------|
| DEX protocols | PumpFun, PumpSwap, Bonk, Meteora DAMM v2, Raydium AMM v4, Raydium CPMM |
| Submit lanes | Default Solana RPC plus Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, SpeedLanding, and other SWQoS providers |
| Trading workflows | Buy/sell, exact input/output, copy trading, sniper trading, address lookup tables, durable nonce, middleware, shared infrastructure |
| Hot-path design | Caller supplies recent blockhash or durable nonce; trade execution avoids RPC reads for blockhash, account, or balance data |
## 🔖 Current Release
**Rust crate:** `sol-trade-sdk = "4.0.17"`
**Rust crate:** `sol-trade-sdk = "4.0.21"`
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
@@ -115,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.17" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.21" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.17"
sol-trade-sdk = "4.0.21"
```
## 🛠️ Usage Examples
@@ -256,10 +267,11 @@ Optional builder methods:
| `.slippage_basis_points(300)` | Set slippage. `300` means 3%. |
| `.address_lookup_table_account(alt)` | Attach an ALT to reduce transaction size. Useful for large PumpFun V2 transactions. |
| `.wait_tx_confirmed(true)` | Return only after confirmation. Usually disabled for fastest submit paths. |
| `.wait_for_all_submits(true)` | In fast-submit mode, wait for all SWQoS lane responses and return all signatures. |
| `.wait_for_all_submits(true)` | Wait for all SWQoS lane responses and return submitted signatures. Recent-blockhash route variants are not mutually exclusive; durable nonce variants are. |
| `.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 +335,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 +437,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 +453,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 +479,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)
+50 -17
View File
@@ -79,15 +79,26 @@
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
## 这个 SDK 适合什么场景
`sol-trade-sdk` 是 FnZero Solana 交易 SDK 系列的 Rust 版本,重点服务于低延迟交易构建和提交,适合 Solana DEX 交易机器人、跟单系统、狙击机器人、套利策略和私有交易基础设施。
| 方向 | 覆盖范围 |
|------|----------|
| DEX 协议 | PumpFun、PumpSwap、Bonk、Meteora DAMM v2、Raydium AMM v4、Raydium CPMM |
| 提交通道 | 默认 Solana RPC,以及 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、SpeedLanding 等 SWQoS 服务 |
| 交易流程 | 买入/卖出、精确输入/输出、跟单交易、狙击交易、地址查找表、durable nonce、中间件、共享基础设施 |
| 热路径设计 | 调用方传入 recent blockhash 或 durable nonce;交易执行阶段不再查询 RPC 获取 blockhash、账户或余额 |
## 🔖 当前版本
**Rust crate:** `sol-trade-sdk = "4.0.17"`
**Rust crate:** `sol-trade-sdk = "4.0.21"`
本版本刷新 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) 的交易操作
@@ -115,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.17" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.21" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.17"
sol-trade-sdk = "4.0.21"
```
## 🛠️ 使用示例
@@ -255,10 +266,11 @@ client.buy_simple(buy_params).await?;
| `.slippage_basis_points(300)` | 设置滑点。`300` 表示 3%。 |
| `.address_lookup_table_account(alt)` | 传入 ALT 以减少交易体积。PumpFun V2 交易较大时很有用。 |
| `.wait_tx_confirmed(true)` | 等链上确认后再返回。追求最快提交时通常关闭。 |
| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 |
| `.wait_for_all_submits(true)` | 等待所有 SWQoS 通道返回,并拿到已提交签名。recent blockhash 多路交易不互斥;durable nonce 多路交易互斥。 |
| `.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 +334,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 +439,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 +479,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 保护服务
+23 -3
View File
@@ -19,7 +19,7 @@ Include lookup tables in your trade parameters:
```rust
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await.ok();
let alt = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await?;
// Include lookup table in trade parameters
let buy_params = sol_trade_sdk::TradeBuyParams {
@@ -29,7 +29,7 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: address_lookup_table_account, // Include lookup table
address_lookup_table_accounts: vec![alt], // One ALT
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
@@ -41,6 +41,26 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
client.buy(buy_params).await?;
```
Multiple lookup tables are also supported:
```rust
let alt1 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_1).await?;
let alt2 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_2).await?;
let params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::ExactInput(buy_lamports),
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.address_lookup_table_accounts(vec![alt1, alt2]);
```
Use the same `address_lookup_table_accounts` field for one or many ALTs: `vec![alt]` for a single ALT, `vec![alt1, alt2]` for multiple ALTs.
## 📊 Performance Comparison
| Aspect | Without ALT | With ALT | Improvement |
@@ -64,4 +84,4 @@ client.buy(buy_params).await?;
## 📚 External Resources
- [Solana Address Lookup Tables Documentation](https://docs.solana.com/developing/lookup-tables)
- [Solana Address Lookup Tables Documentation](https://docs.solana.com/developing/lookup-tables)
+23 -3
View File
@@ -19,7 +19,7 @@
```rust
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await.ok();
let alt = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key).await?;
// 在交易参数中包含查找表
let buy_params = sol_trade_sdk::TradeBuyParams {
@@ -29,7 +29,7 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: address_lookup_table_account, // 包含查找表
address_lookup_table_accounts: vec![alt], // 1 个 ALT
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
@@ -41,6 +41,26 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
client.buy(buy_params).await?;
```
也支持同时传入多个查找表:
```rust
let alt1 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_1).await?;
let alt2 = fetch_address_lookup_table_account(&client.rpc, &lookup_table_key_2).await?;
let params = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
mint_pubkey,
BuyAmount::ExactInput(buy_lamports),
extension_params,
recent_blockhash,
gas_fee_strategy,
)
.address_lookup_table_accounts(vec![alt1, alt2]);
```
单 ALT 和多 ALT 都使用同一个 `address_lookup_table_accounts` 字段:单 ALT 传 `vec![alt]`,多 ALT 传 `vec![alt1, alt2]`
## 📊 性能对比
| 方面 | 不使用 ALT | 使用 ALT | 改进幅度 |
@@ -64,4 +84,4 @@ client.buy(buy_params).await?;
## 📚 外部资源
- [Solana 地址查找表文档](https://docs.solana.com/developing/lookup-tables)
- [Solana 地址查找表文档](https://docs.solana.com/developing/lookup-tables)
-1
View File
@@ -49,7 +49,6 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
-1
View File
@@ -49,7 +49,6 @@ let buy_params = sol_trade_sdk::TradeBuyParams {
slippage_basis_points: Some(100),
recent_blockhash: Some(recent_blockhash),
extension_params: Box::new(PumpFunParams::from_trade(&trade_info, None)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_wsol_ata: false,
close_wsol_ata: false,
+33 -10
View File
@@ -27,10 +27,10 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | Optional ALT list. Pass one element for a single ALT or multiple elements for multi-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`. |
| `wait_for_all_submits` | `bool` | ❌ | Wait for every SWQoS lane response and return submitted signatures; useful for poll-any confirmation or external monitoring. Recent-blockhash route variants are not mutually exclusive; durable nonce variants are. |
| `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. |
@@ -47,10 +47,10 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | Optional ALT list. Pass one element for a single ALT or multiple elements for multi-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`. |
| `wait_for_all_submits` | `bool` | ❌ | Wait for every SWQoS lane response and return submitted signatures; useful for poll-any confirmation or external monitoring. Recent-blockhash route variants are not mutually exclusive; durable nonce variants are. |
| `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.
@@ -94,7 +119,6 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Address lookup table for transaction optimization |
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `create_input_token_ata` | `bool` | ✅ | Whether to create input token Associated Token Account |
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
@@ -126,7 +150,6 @@ The `TradeSellParams` struct contains all parameters required for executing sell
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | Address lookup table for transaction optimization |
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `create_output_token_ata` | `bool` | ✅ | Whether to create output token Associated Token Account |
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
@@ -167,7 +190,7 @@ These parameters control automatic account creation and management:
These parameters enable advanced optimizations:
- **address_lookup_table_account**: Use address lookup tables for reduced transaction size
- **address_lookup_table_accounts**: Use one or more address lookup tables for reduced transaction size
### 🔄 Token Type Parameters
@@ -211,7 +234,7 @@ The account management parameters provide granular control:
### 🔍 Address Lookup Tables
Before using `address_lookup_table_account`:
Before using `address_lookup_table_accounts`:
- Lookup tables reduce transaction size and improve success rates
- Particularly beneficial for complex transactions with many account references
+33 -10
View File
@@ -27,10 +27,10 @@
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | 可选 ALT 列表。传 1 个元素表示单 ALT,传多个元素表示多 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` 混用。 |
| `wait_for_all_submits` | `bool` | ❌ | 是否等待所有 SWQoS 通道返回并拿到已提交签名;适合 poll-any 确认或外部监控。recent blockhash 多路交易不互斥;durable nonce 多路交易互斥。 |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)``SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
@@ -47,10 +47,10 @@
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
| `address_lookup_table_accounts` | `Vec<AddressLookupTableAccount>` | ❌ | 可选 ALT 列表。传 1 个元素表示单 ALT,传多个元素表示多 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` 混用。 |
| `wait_for_all_submits` | `bool` | ❌ | 是否等待所有 SWQoS 通道返回并拿到已提交签名;适合 poll-any 确认或外部监控。recent blockhash 多路交易不互斥;durable nonce 多路交易互斥。 |
| `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 时再使用它。
@@ -94,7 +119,6 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | 用于交易优化的地址查找表 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_input_token_ata` | `bool` | ✅ | 是否创建输入代币关联代币账户 |
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
@@ -126,7 +150,6 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 用于交易优化的地址查找表 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_output_token_ata` | `bool` | ✅ | 是否创建输出代币关联代币账户 |
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
@@ -167,7 +190,7 @@
这些参数启用高级优化:
- **address_lookup_table_account**: 使用地址查找表减少交易大小
- **address_lookup_table_accounts**: 使用一个或多个地址查找表减少交易大小
### 🔄 代币类型参数
@@ -211,7 +234,7 @@ let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
### 🔍 地址查找表
使用 `address_lookup_table_account` 之前:
使用 `address_lookup_table_accounts` 之前:
- 查找表减少交易大小并提高成功率
- 对于有许多账户引用的复杂交易特别有益
+4 -5
View File
@@ -144,10 +144,9 @@ async fn pumpfun_copy_trade_with_grpc(
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account =
fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key)
.await
.ok();
let alt = fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key)
.await
.ok();
let gas_fee_strategy = GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
@@ -177,7 +176,7 @@ async fn pumpfun_copy_trade_with_grpc(
trade_info.is_cashback_coin,
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account,
address_lookup_table_accounts: alt.into_iter().collect(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
+2 -2
View File
@@ -169,7 +169,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -221,7 +221,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
with_tip: false,
+2 -2
View File
@@ -137,7 +137,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -182,7 +182,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.creator_associated_account,
trade_info.global_config,
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
+10 -10
View File
@@ -626,7 +626,7 @@ async fn handle_buy_pumpfun(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
@@ -683,7 +683,7 @@ async fn handle_buy_pumpswap(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -740,7 +740,7 @@ async fn handle_buy_bonk(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -801,7 +801,7 @@ async fn handle_buy_raydium_v4(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -863,7 +863,7 @@ async fn handle_buy_raydium_cpmm(
slippage_basis_points: slippage,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -1034,7 +1034,7 @@ async fn handle_sell_pumpfun(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
@@ -1094,7 +1094,7 @@ async fn handle_sell_pumpswap(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
@@ -1154,7 +1154,7 @@ async fn handle_sell_bonk(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
@@ -1217,7 +1217,7 @@ async fn handle_sell_raydium_v4(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
@@ -1281,7 +1281,7 @@ async fn handle_sell_raydium_cpmm(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
@@ -41,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
.await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
@@ -83,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
.await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
+1 -1
View File
@@ -102,7 +102,7 @@ async fn test_middleware() -> AnyResult<()> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_address)
.await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
+1 -1
View File
@@ -173,7 +173,7 @@ async fn pumpfun_copy_trade_with_grpc(
trade_info.is_cashback_coin,
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
+2 -2
View File
@@ -167,7 +167,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
@@ -219,7 +219,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false,
+2 -2
View File
@@ -156,7 +156,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -202,7 +202,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
+2 -2
View File
@@ -40,7 +40,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -80,7 +80,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
+16 -11
View File
@@ -1,6 +1,5 @@
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
@@ -148,8 +147,7 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?;
let params = PumpSwapParams::from_trade(
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
trade_info.quote_mint,
@@ -162,9 +160,13 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
Pubkey::default(),
trade_info.coin_creator,
false,
0,
trade_info.lp_fee_basis_points,
trade_info.protocol_fee_basis_points,
trade_info.coin_creator_fee_basis_points,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -179,8 +181,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?;
let params = PumpSwapParams::from_trade(
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
trade_info.quote_mint,
@@ -193,9 +194,13 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
Pubkey::default(),
trade_info.coin_creator,
false,
0,
trade_info.lp_fee_basis_points,
trade_info.protocol_fee_basis_points,
trade_info.coin_creator_fee_basis_points,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -234,7 +239,7 @@ async fn pumpswap_trade_with_grpc(
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_sol,
@@ -276,7 +281,7 @@ async fn pumpswap_trade_with_grpc(
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_sol,
+2 -2
View File
@@ -155,7 +155,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
@@ -198,7 +198,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
+2 -2
View File
@@ -159,7 +159,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
@@ -200,7 +200,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
+2 -2
View File
@@ -40,7 +40,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
@@ -83,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
+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(())
+2483
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+158 -42
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,
@@ -186,8 +187,8 @@ pub struct SimpleBuyParams {
/// Recent blockhash for non-nonce transactions.
///
/// The SDK intentionally does not fetch blockhash on the hot path. Use
/// [`SimpleBuyParams::with_durable_nonce`] instead when submitting multiple
/// SWQoS lanes against a pinned nonce.
/// [`SimpleBuyParams::with_durable_nonce`] only when the caller specifically
/// wants durable-nonce transactions.
pub recent_blockhash: Option<Hash>,
/// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
pub extension_params: DexParamEnum,
@@ -195,12 +196,15 @@ pub struct SimpleBuyParams {
pub gas_fee_strategy: GasFeeStrategy,
/// ATA creation/close behavior. See [`AccountPolicy`].
pub account_policy: AccountPolicy,
/// Optional Address Lookup Table to reduce transaction size.
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Optional Address Lookup Tables to reduce transaction size.
/// Pass one element for a single ALT or multiple elements for multi-ALT.
pub address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
/// Wait until the transaction is confirmed before returning.
pub wait_tx_confirmed: bool,
/// Fast-submit mode only: wait for every SWQoS route's submit response so all
/// signatures can be returned.
/// Wait for every SWQoS route's submit response so all signatures can be
/// returned. Useful when confirming through poll-any semantics or monitoring
/// route variants externally. Recent-blockhash variants are not mutually
/// exclusive; durable nonce variants are.
pub wait_for_all_submits: bool,
/// Durable nonce info. Mutually exclusive with `recent_blockhash`.
pub durable_nonce: Option<DurableNonceInfo>,
@@ -234,12 +238,15 @@ pub struct SimpleSellParams {
pub gas_fee_strategy: GasFeeStrategy,
/// ATA creation/close behavior. See [`AccountPolicy`].
pub account_policy: AccountPolicy,
/// Optional Address Lookup Table to reduce transaction size.
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Optional Address Lookup Tables to reduce transaction size.
/// Pass one element for a single ALT or multiple elements for multi-ALT.
pub address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
/// Wait until the transaction is confirmed before returning.
pub wait_tx_confirmed: bool,
/// Fast-submit mode only: wait for every SWQoS route's submit response so all
/// signatures can be returned.
/// Wait for every SWQoS route's submit response so all signatures can be
/// returned. Useful when confirming through poll-any semantics or monitoring
/// route variants externally. Recent-blockhash variants are not mutually
/// exclusive; durable nonce variants are.
pub wait_for_all_submits: bool,
/// Durable nonce info. Mutually exclusive with `recent_blockhash`.
pub durable_nonce: Option<DurableNonceInfo>,
@@ -280,7 +287,7 @@ impl SimpleBuyParams {
extension_params,
gas_fee_strategy,
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
@@ -329,9 +336,20 @@ impl SimpleBuyParams {
self
}
/// Attach an Address Lookup Table to reduce transaction size.
pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
self.address_lookup_table_account = Some(value);
/// Attach Address Lookup Tables to reduce transaction size.
/// Pass one element for a single ALT or multiple elements for multi-ALT.
pub fn address_lookup_table_accounts(mut self, values: Vec<AddressLookupTableAccount>) -> Self {
self.address_lookup_table_accounts = values;
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
}
@@ -341,7 +359,7 @@ impl SimpleBuyParams {
self
}
/// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
/// Wait for all SWQoS submit responses and return submitted signatures.
pub fn wait_for_all_submits(mut self, value: bool) -> Self {
self.wait_for_all_submits = value;
self
@@ -390,7 +408,7 @@ impl SimpleSellParams {
extension_params,
gas_fee_strategy,
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
@@ -438,9 +456,20 @@ impl SimpleSellParams {
self
}
/// Attach an Address Lookup Table to reduce transaction size.
pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
self.address_lookup_table_account = Some(value);
/// Attach Address Lookup Tables to reduce transaction size.
/// Pass one element for a single ALT or multiple elements for multi-ALT.
pub fn address_lookup_table_accounts(mut self, values: Vec<AddressLookupTableAccount>) -> Self {
self.address_lookup_table_accounts = values;
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
}
@@ -450,7 +479,7 @@ impl SimpleSellParams {
self
}
/// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
/// Wait for all SWQoS submit responses and return submitted signatures.
pub fn wait_for_all_submits(mut self, value: bool) -> Self {
self.wait_for_all_submits = value;
self
@@ -761,14 +790,16 @@ pub struct TradeBuyParams {
/// Protocol-specific parameters (PumpFun, Raydium, etc.)
pub extension_params: DexParamEnum,
// Extended configuration
/// Optional address lookup table for transaction size optimization
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Optional address lookup tables for transaction size optimization.
/// Pass one element for a single ALT or multiple elements for multi-ALT.
pub address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
/// Whether to wait for transaction confirmation before returning
pub wait_tx_confirmed: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Set to true when confirming externally against a pinned
/// durable nonce; defaults to false. See `SwapParams.wait_for_all_submits`.
/// When true, wait for every SWQOS route's HTTP submit response so all
/// submitted signatures are returned. This applies whether SDK confirmation
/// is enabled or the caller monitors externally. Recent-blockhash route
/// variants are not mutually exclusive; durable nonce variants are.
/// Defaults to false. See `SwapParams.wait_for_all_submits`.
pub wait_for_all_submits: bool,
/// Whether to create input token associated token account
pub create_input_token_ata: bool,
@@ -818,14 +849,16 @@ pub struct TradeSellParams {
/// Protocol-specific parameters (PumpFun, Raydium, etc.)
pub extension_params: DexParamEnum,
// Extended configuration
/// Optional address lookup table for transaction size optimization
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Optional address lookup tables for transaction size optimization.
/// Pass one element for a single ALT or multiple elements for multi-ALT.
pub address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
/// Whether to wait for transaction confirmation before returning
pub wait_tx_confirmed: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Set to true when confirming externally against a pinned
/// durable nonce; defaults to false. See `SwapParams.wait_for_all_submits`.
/// When true, wait for every SWQOS route's HTTP submit response so all
/// submitted signatures are returned. This applies whether SDK confirmation
/// is enabled or the caller monitors externally. Recent-blockhash route
/// variants are not mutually exclusive; durable nonce variants are.
/// Defaults to false. See `SwapParams.wait_for_all_submits`.
pub wait_for_all_submits: bool,
/// Whether to create output token associated token account
pub create_output_token_ata: bool,
@@ -885,7 +918,7 @@ impl From<SimpleBuyParams> for TradeBuyParams {
slippage_basis_points: params.slippage_basis_points,
recent_blockhash: params.recent_blockhash,
extension_params: params.extension_params,
address_lookup_table_account: params.address_lookup_table_account,
address_lookup_table_accounts: params.address_lookup_table_accounts,
wait_tx_confirmed: params.wait_tx_confirmed,
wait_for_all_submits: params.wait_for_all_submits,
create_input_token_ata,
@@ -921,7 +954,7 @@ impl From<SimpleSellParams> for TradeSellParams {
recent_blockhash: params.recent_blockhash,
with_tip: params.with_tip,
extension_params: params.extension_params,
address_lookup_table_account: params.address_lookup_table_account,
address_lookup_table_accounts: params.address_lookup_table_accounts,
wait_tx_confirmed: params.wait_tx_confirmed,
wait_for_all_submits: params.wait_for_all_submits,
create_output_token_ata,
@@ -1360,7 +1393,7 @@ impl TradingClient {
output_token_program: None,
input_amount: Some(params.input_token_amount),
slippage_basis_points: params.slippage_basis_points,
address_lookup_table_account: params.address_lookup_table_account,
address_lookup_table_accounts: params.address_lookup_table_accounts,
recent_blockhash: params.recent_blockhash,
wait_tx_confirmed: params.wait_tx_confirmed,
protocol_params,
@@ -1488,7 +1521,7 @@ impl TradingClient {
output_token_program: None,
input_amount: Some(params.input_token_amount),
slippage_basis_points: params.slippage_basis_points,
address_lookup_table_account: params.address_lookup_table_account,
address_lookup_table_accounts: params.address_lookup_table_accounts,
recent_blockhash: params.recent_blockhash,
wait_tx_confirmed: params.wait_tx_confirmed,
protocol_params,
@@ -1839,7 +1872,7 @@ mod tests {
extension_params: dummy_pumpfun_params(),
gas_fee_strategy: GasFeeStrategy::new(),
account_policy: AccountPolicy::HotPathMinimal,
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
@@ -1870,7 +1903,7 @@ mod tests {
extension_params: dummy_pumpfun_params(),
gas_fee_strategy: GasFeeStrategy::new(),
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
@@ -1910,6 +1943,62 @@ mod tests {
assert!(!low.wait_tx_confirmed);
}
#[test]
fn simple_buy_builder_maps_multiple_lookup_tables() {
let alt1 = AddressLookupTableAccount {
key: Pubkey::new_unique(),
addresses: vec![Pubkey::new_unique()],
};
let alt2 = AddressLookupTableAccount {
key: Pubkey::new_unique(),
addresses: vec![Pubkey::new_unique()],
};
let simple = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
Pubkey::new_unique(),
BuyAmount::ExactInput(10_000),
dummy_pumpfun_params(),
Hash::new_unique(),
GasFeeStrategy::new(),
)
.address_lookup_table_accounts(vec![alt1.clone(), alt2.clone()]);
let low: TradeBuyParams = simple.into();
assert_eq!(low.address_lookup_table_accounts.len(), 2);
assert_eq!(low.address_lookup_table_accounts[0].key, alt1.key);
assert_eq!(low.address_lookup_table_accounts[1].key, alt2.key);
}
#[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 {
@@ -1922,7 +2011,7 @@ mod tests {
extension_params: dummy_pumpfun_params(),
gas_fee_strategy: GasFeeStrategy::new(),
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
@@ -1939,4 +2028,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]
+1 -1
View File
@@ -365,7 +365,7 @@ mod tests {
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::Bonk(bonk_params()),
+1 -1
View File
@@ -347,7 +347,7 @@ mod tests {
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::MeteoraDammV2(protocol_params),
+140 -27
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"));
@@ -908,7 +944,7 @@ mod tests {
output_token_program: None,
input_amount: Some(10_000_000),
slippage_basis_points: Some(300),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::PumpFun(params),
@@ -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,13 +1119,48 @@ 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,
&[],
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,
150_000,
500_000,
&business_instructions,
None,
&[],
Some(solana_hash::Hash::new_unique()),
None,
"PumpFun",
@@ -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();
+40 -24
View File
@@ -20,7 +20,9 @@ use crate::{
params::{PumpSwapParams, SwapParams},
traits::InstructionBuilder,
},
utils::calc::pumpswap::{buy_quote_input_internal, sell_base_input_internal},
utils::calc::pumpswap::{
buy_quote_input_internal_with_fees, sell_base_input_internal_with_fees,
},
};
use anyhow::{anyhow, Result};
use solana_sdk::{
@@ -85,34 +87,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let output_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
let output_trade_token_program =
if quote_is_wsol_or_usdc { base_token_program } else { quote_token_program };
let mut creator = Pubkey::default();
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
creator = params_coin_creator_vault_authority;
}
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
let fee_basis_points = protocol_params.fee_basis_points;
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
(output_amount, params.input_amount.unwrap_or(0))
} else if quote_is_wsol_or_usdc {
let result = buy_quote_input_internal(
let result = buy_quote_input_internal_with_fees(
params.input_amount.unwrap_or(0),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
&fee_basis_points,
)
.unwrap();
// base_amount_out, max_quote_amount_in
(result.base, result.max_quote)
} else {
let result = sell_base_input_internal(
let result = sell_base_input_internal_with_fees(
params.input_amount.unwrap_or(0),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
&fee_basis_points,
)
.unwrap();
// min_quote_amount_out, base_amount_in
@@ -321,34 +317,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let output_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
let output_stable_token_program =
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
let mut creator = Pubkey::default();
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
creator = params_coin_creator_vault_authority;
}
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
let fee_basis_points = protocol_params.fee_basis_points;
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
(params.input_amount.unwrap(), output_amount)
} else if quote_is_wsol_or_usdc {
let result = sell_base_input_internal(
let result = sell_base_input_internal_with_fees(
params.input_amount.unwrap(),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
&fee_basis_points,
)
.unwrap();
// base_amount_in, min_quote_amount_out
(params.input_amount.unwrap(), result.min_quote)
} else {
let result = buy_quote_input_internal(
let result = buy_quote_input_internal_with_fees(
params.input_amount.unwrap(),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
pool_base_token_reserves,
pool_quote_token_reserves,
&creator,
cashback_fee_bps,
&fee_basis_points,
)
.unwrap();
// max_quote_amount_in, base_amount_out
@@ -572,7 +562,7 @@ mod tests {
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::PumpSwap(pumpswap_params()),
@@ -657,4 +647,30 @@ mod tests {
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
}
#[tokio::test]
async fn pumpswap_buy_uses_fee_basis_points_from_params_without_rpc() {
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 instructions =
PumpSwapInstructionBuilder.build_buy_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
let base_amount_out = u64::from_le_bytes(ix.data[8..16].try_into().unwrap());
let expected = crate::utils::calc::pumpswap::buy_quote_input_internal_with_fees(
1_000_000,
100,
1_000_000_000,
2_000_000_000,
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
)
.unwrap();
assert_eq!(base_amount_out, expected.base);
}
}
+1 -1
View File
@@ -365,7 +365,7 @@ mod tests {
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::RaydiumAmmV4(protocol_params),
+1 -1
View File
@@ -377,7 +377,7 @@ mod tests {
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
address_lookup_table_accounts: Vec::new(),
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::RaydiumCpmm(cpmm_params()),
+311 -2
View File
@@ -179,21 +179,76 @@ pub mod accounts {
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PumpSwapFeeBasisPoints {
pub lp_fee_basis_points: u64,
pub protocol_fee_basis_points: u64,
pub coin_creator_fee_basis_points: u64,
}
impl PumpSwapFeeBasisPoints {
#[inline]
pub const fn new(
lp_fee_basis_points: u64,
protocol_fee_basis_points: u64,
coin_creator_fee_basis_points: u64,
) -> Self {
Self { lp_fee_basis_points, protocol_fee_basis_points, coin_creator_fee_basis_points }
}
#[inline]
pub const fn legacy_default() -> Self {
Self::new(
accounts::LP_FEE_BASIS_POINTS,
accounts::PROTOCOL_FEE_BASIS_POINTS,
accounts::COIN_CREATOR_FEE_BASIS_POINTS,
)
}
}
impl Default for PumpSwapFeeBasisPoints {
#[inline]
fn default() -> Self {
Self::legacy_default()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PumpSwapFeeTier {
pub market_cap_lamports_threshold: u128,
pub fees: PumpSwapFeeBasisPoints,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PumpSwapFeeConfig {
pub flat_fees: PumpSwapFeeBasisPoints,
pub fee_tiers: Vec<PumpSwapFeeTier>,
pub stable_fee_tiers: Vec<PumpSwapFeeTier>,
}
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
const PUMPSWAP_GLOBAL_CONFIG_TTL: Duration = Duration::from_secs(90);
const PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT: Duration = Duration::from_millis(180);
const PUMPSWAP_FEE_CONFIG_TTL: Duration = Duration::from_secs(300);
const PUMPSWAP_FEE_CONFIG_RPC_TIMEOUT: Duration = Duration::from_millis(180);
const PUBKEY_LEN: usize = 32;
const U64_LEN: usize = 8;
const U8_LEN: usize = 1;
const BOOL_LEN: usize = 1;
const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8;
const FEE_CONFIG_DISCRIMINATOR_LEN: usize = 8;
const FEE_CONFIG_BUMP_LEN: usize = 1;
const FEE_TIER_LEN: usize = 16 + U64_LEN * 3;
#[derive(Clone, Debug)]
pub struct GlobalConfig {
pub lp_fee_basis_points: u64,
pub protocol_fee_basis_points: u64,
pub coin_creator_fee_basis_points: u64,
pub protocol_fee_recipients: [Pubkey; 8],
pub reserved_fee_recipient: Pubkey,
pub reserved_fee_recipients: [Pubkey; 7],
@@ -206,9 +261,16 @@ struct CachedGlobalConfig {
config: GlobalConfig,
}
#[derive(Clone)]
struct CachedFeeConfig {
fetched_at: Instant,
config: PumpSwapFeeConfig,
}
static GLOBAL_CONFIG_CACHE: Lazy<RwLock<Option<CachedGlobalConfig>>> =
Lazy::new(|| RwLock::new(None));
static GLOBAL_CONFIG_REFRESH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
static FEE_CONFIG_CACHE: Lazy<RwLock<Option<CachedFeeConfig>>> = Lazy::new(|| RwLock::new(None));
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
@@ -223,15 +285,34 @@ fn read_pubkey_array<const N: usize>(data: &[u8], offset: usize) -> Option<[Pubk
Some(keys)
}
fn read_u64(data: &[u8], offset: usize) -> Option<u64> {
let bytes = data.get(offset..offset + U64_LEN)?;
Some(u64::from_le_bytes(bytes.try_into().ok()?))
}
fn read_u128(data: &[u8], offset: usize) -> Option<u128> {
let bytes = data.get(offset..offset + 16)?;
Some(u128::from_le_bytes(bytes.try_into().ok()?))
}
fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
let bytes = data.get(offset..offset + 4)?;
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN;
offset += PUBKEY_LEN; // admin
offset += U64_LEN * 2; // lp_fee_basis_points + protocol_fee_basis_points
let lp_fee_basis_points = read_u64(data, offset)?;
offset += U64_LEN;
let protocol_fee_basis_points = read_u64(data, offset)?;
offset += U64_LEN;
offset += U8_LEN; // disable_flags
let protocol_fee_recipients = read_pubkey_array::<8>(data, offset)?;
offset += PUBKEY_LEN * 8;
offset += U64_LEN; // coin_creator_fee_basis_points
let coin_creator_fee_basis_points = read_u64(data, offset)?;
offset += U64_LEN;
offset += PUBKEY_LEN; // admin_set_coin_creator_authority
offset += PUBKEY_LEN; // whitelist_pda
@@ -246,6 +327,9 @@ fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
let buyback_fee_recipients = read_pubkey_array::<8>(data, offset)?;
Some(GlobalConfig {
lp_fee_basis_points,
protocol_fee_basis_points,
coin_creator_fee_basis_points,
protocol_fee_recipients,
reserved_fee_recipient,
reserved_fee_recipients,
@@ -253,6 +337,46 @@ fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
})
}
fn decode_fees(data: &[u8], offset: usize) -> Option<PumpSwapFeeBasisPoints> {
Some(PumpSwapFeeBasisPoints::new(
read_u64(data, offset)?,
read_u64(data, offset + U64_LEN)?,
read_u64(data, offset + U64_LEN * 2)?,
))
}
fn decode_fee_tiers(data: &[u8], offset: &mut usize) -> Option<Vec<PumpSwapFeeTier>> {
let len = read_u32(data, *offset)? as usize;
*offset += 4;
let byte_len = len.checked_mul(FEE_TIER_LEN)?;
let end = (*offset).checked_add(byte_len)?;
data.get(*offset..end)?;
let mut tiers = Vec::with_capacity(len);
for _ in 0..len {
let market_cap_lamports_threshold = read_u128(data, *offset)?;
*offset += 16;
let fees = decode_fees(data, *offset)?;
*offset += U64_LEN * 3;
tiers.push(PumpSwapFeeTier { market_cap_lamports_threshold, fees });
}
Some(tiers)
}
pub fn decode_fee_config(data: &[u8]) -> Option<PumpSwapFeeConfig> {
let mut offset = FEE_CONFIG_DISCRIMINATOR_LEN;
offset += FEE_CONFIG_BUMP_LEN;
offset += PUBKEY_LEN; // admin
let flat_fees = decode_fees(data, offset)?;
offset += U64_LEN * 3;
let fee_tiers = decode_fee_tiers(data, &mut offset)?;
let stable_fee_tiers = decode_fee_tiers(data, &mut offset)?;
Some(PumpSwapFeeConfig { flat_fees, fee_tiers, stable_fee_tiers })
}
async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfig> {
let account = match tokio::time::timeout(
PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT,
@@ -289,6 +413,42 @@ async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfi
Some(config)
}
async fn refresh_fee_config_once(rpc: &SolanaRpcClient) -> Option<PumpSwapFeeConfig> {
let account = match tokio::time::timeout(
PUMPSWAP_FEE_CONFIG_RPC_TIMEOUT,
rpc.get_account(&accounts::FEE_CONFIG),
)
.await
{
Ok(Ok(account)) => account,
Ok(Err(e)) => {
warn!(target: "pumpswap_fee_config", "PumpSwap FeeConfig 读取失败: {}", e);
return None;
}
Err(_) => {
warn!(
target: "pumpswap_fee_config",
timeout_ms = PUMPSWAP_FEE_CONFIG_RPC_TIMEOUT.as_millis(),
"PumpSwap FeeConfig 读取超时"
);
return None;
}
};
let Some(config) = decode_fee_config(&account.data) else {
warn!(
target: "pumpswap_fee_config",
data_len = account.data.len(),
"PumpSwap FeeConfig 解析失败"
);
return None;
};
*FEE_CONFIG_CACHE.write() =
Some(CachedFeeConfig { fetched_at: Instant::now(), config: config.clone() });
Some(config)
}
pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
let Some(rpc) = rpc else {
return;
@@ -302,6 +462,7 @@ pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
let rpc = Arc::clone(rpc);
tokio::spawn(async move {
let _ = refresh_global_config_once(rpc.as_ref()).await;
let _ = refresh_fee_config_once(rpc.as_ref()).await;
GLOBAL_CONFIG_REFRESH_IN_FLIGHT.store(false, Ordering::Release);
});
}
@@ -313,6 +474,93 @@ fn cached_global_config() -> Option<GlobalConfig> {
(cached.fetched_at.elapsed() <= PUMPSWAP_GLOBAL_CONFIG_TTL).then(|| cached.config.clone())
}
fn cached_fee_config() -> Option<PumpSwapFeeConfig> {
let guard = FEE_CONFIG_CACHE.read();
let cached = guard.as_ref()?;
(cached.fetched_at.elapsed() <= PUMPSWAP_FEE_CONFIG_TTL).then(|| cached.config.clone())
}
pub async fn fetch_fee_config(rpc: &SolanaRpcClient) -> Option<PumpSwapFeeConfig> {
if let Some(config) = cached_fee_config() {
return Some(config);
}
refresh_fee_config_once(rpc).await
}
#[inline]
pub fn global_fee_basis_points() -> PumpSwapFeeBasisPoints {
cached_global_config()
.map(|config| {
PumpSwapFeeBasisPoints::new(
config.lp_fee_basis_points,
config.protocol_fee_basis_points,
config.coin_creator_fee_basis_points,
)
})
.unwrap_or_default()
}
#[inline]
pub fn is_canonical_pump_pool(base_mint: &Pubkey, pool_creator: &Pubkey) -> bool {
get_pump_pool_authority_pda(base_mint) == *pool_creator
}
#[inline]
pub fn pool_market_cap_lamports(
base_mint_supply: u64,
base_reserve: u64,
quote_reserve: u64,
) -> Option<u128> {
if base_reserve == 0 {
return None;
}
Some((quote_reserve as u128) * (base_mint_supply as u128) / (base_reserve as u128))
}
pub fn calculate_fee_tier(
fee_tiers: &[PumpSwapFeeTier],
market_cap_lamports: u128,
) -> Option<PumpSwapFeeBasisPoints> {
let first = fee_tiers.first()?;
if market_cap_lamports < first.market_cap_lamports_threshold {
return Some(first.fees);
}
fee_tiers
.iter()
.rev()
.find(|tier| market_cap_lamports >= tier.market_cap_lamports_threshold)
.map(|tier| tier.fees)
.or(Some(first.fees))
}
pub fn compute_fee_basis_points(
fee_config: Option<&PumpSwapFeeConfig>,
pool_creator: Pubkey,
base_mint: Pubkey,
base_mint_supply: Option<u64>,
base_reserve: u64,
quote_reserve: u64,
) -> PumpSwapFeeBasisPoints {
let Some(fee_config) = fee_config else {
return global_fee_basis_points();
};
if !is_canonical_pump_pool(&base_mint, &pool_creator) {
return fee_config.flat_fees;
}
let Some(base_mint_supply) = base_mint_supply else {
return global_fee_basis_points();
};
let Some(market_cap_lamports) =
pool_market_cap_lamports(base_mint_supply, base_reserve, quote_reserve)
else {
return global_fee_basis_points();
};
calculate_fee_tier(&fee_config.fee_tiers, market_cap_lamports).unwrap_or(fee_config.flat_fees)
}
fn choose_nonzero(keys: &[Pubkey]) -> Option<Pubkey> {
let mut valid = [Pubkey::default(); 8];
let mut len = 0;
@@ -656,6 +904,31 @@ mod tests {
use super::*;
use solana_sdk::pubkey::Pubkey;
fn fee_config_fixture() -> PumpSwapFeeConfig {
PumpSwapFeeConfig {
flat_fees: PumpSwapFeeBasisPoints::new(25, 5, 0),
fee_tiers: vec![
PumpSwapFeeTier {
market_cap_lamports_threshold: 0,
fees: PumpSwapFeeBasisPoints::new(2, 93, 30),
},
PumpSwapFeeTier {
market_cap_lamports_threshold: 420_000_000_000,
fees: PumpSwapFeeBasisPoints::new(20, 5, 95),
},
PumpSwapFeeTier {
market_cap_lamports_threshold: 4_420_000_000_000,
fees: PumpSwapFeeBasisPoints::new(20, 5, 75),
},
PumpSwapFeeTier {
market_cap_lamports_threshold: 9_820_000_000_000,
fees: PumpSwapFeeBasisPoints::new(20, 5, 70),
},
],
stable_fee_tiers: Vec::new(),
}
}
#[test]
fn pumpswap_user_volume_accumulator_pda_deterministic() {
let user = Pubkey::new_unique();
@@ -677,4 +950,40 @@ mod tests {
let b = get_pool_v2_pda(&base_mint).unwrap();
assert_eq!(a, b);
}
#[test]
fn pumpswap_fee_tier_selects_issue_106_fee_bucket() {
let selected = calculate_fee_tier(&fee_config_fixture().fee_tiers, 4_500_000_000_000);
assert_eq!(selected, Some(PumpSwapFeeBasisPoints::new(20, 5, 75)));
}
#[test]
fn pumpswap_compute_fees_uses_flat_fee_for_non_canonical_pool() {
let base_mint = Pubkey::new_unique();
let non_canonical_creator = Pubkey::new_unique();
let fees = compute_fee_basis_points(
Some(&fee_config_fixture()),
non_canonical_creator,
base_mint,
Some(1_000_000_000_000_000),
1_000_000_000_000_000,
4_500_000_000_000,
);
assert_eq!(fees, PumpSwapFeeBasisPoints::new(25, 5, 0));
}
#[test]
fn pumpswap_compute_fees_uses_tier_for_canonical_pool() {
let base_mint = Pubkey::new_unique();
let canonical_creator = get_pump_pool_authority_pda(&base_mint);
let fees = compute_fee_basis_points(
Some(&fee_config_fixture()),
canonical_creator,
base_mint,
Some(1_000_000_000_000_000),
1_000_000_000_000_000,
4_500_000_000_000,
);
assert_eq!(fees, PumpSwapFeeBasisPoints::new(20, 5, 75));
}
}
+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
}
}
+63 -37
View File
@@ -36,7 +36,7 @@ pub fn build_transaction(
unit_limit: u32,
unit_price: u64,
business_instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
address_lookup_table_accounts: &[AddressLookupTableAccount],
recent_blockhash: Option<Hash>,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
@@ -46,12 +46,12 @@ 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,
business_instructions,
address_lookup_table_account,
address_lookup_table_accounts,
recent_blockhash,
middleware_manager,
protocol_name,
@@ -63,49 +63,37 @@ 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_accounts.len()
);
}
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,
business_instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
address_lookup_table_accounts: &[AddressLookupTableAccount],
recent_blockhash: Option<Hash>,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
@@ -139,7 +127,7 @@ fn build_transaction_with_compute_budget(
build_versioned_transaction(
payer,
instructions,
address_lookup_table_account,
address_lookup_table_accounts,
blockhash,
middleware_manager,
protocol_name,
@@ -150,7 +138,7 @@ fn build_transaction_with_compute_budget(
fn build_versioned_transaction(
payer: &Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<&AddressLookupTableAccount>,
address_lookup_table_accounts: &[AddressLookupTableAccount],
blockhash: Hash,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
@@ -168,7 +156,7 @@ fn build_versioned_transaction(
let build_result = builder.build_zero_alloc(
&payer.pubkey(),
&full_instructions,
address_lookup_table_account,
address_lookup_table_accounts,
blockhash,
);
release_builder(builder);
@@ -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,
&[],
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}");
}
}
+27 -26
View File
@@ -47,12 +47,13 @@ const SWQOS_POOL_WORKERS: usize = 18;
const SWQOS_QUEUE_CAP: usize = 128;
const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
const FAST_SUBMIT_RESULT_TIMEOUT: Duration = Duration::from_secs(5);
const FAST_SUBMIT_DRAIN_GRACE: Duration = Duration::from_millis(20);
/// Shared across all jobs in one batch; built once, cloned as single Arc per job (minimal hot-path clone).
struct SwqosSharedContext {
payer: Arc<Keypair>,
instructions: Arc<Vec<Instruction>>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
address_lookup_table_accounts: Arc<Vec<AddressLookupTableAccount>>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
middleware_manager: Option<Arc<MiddlewareManager>>,
@@ -92,7 +93,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
job.unit_limit,
job.unit_price,
s.instructions.as_ref(),
s.address_lookup_table_account.as_ref(),
s.address_lookup_table_accounts.as_slice(),
s.recent_blockhash,
s.middleware_manager.as_ref(),
s.protocol_name,
@@ -508,10 +509,11 @@ impl ResultCollector {
}
}
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有已返回的签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
/// Re-enabled via `SwapParams.wait_for_all_submits` for callers that confirm
/// externally against a pinned durable nonce and need every submitted sig.
/// Re-enabled via `SwapParams.wait_for_all_submits` for callers that need
/// every submitted signature, either for external monitoring or for
/// executor-level poll-any confirmation after parallel submit.
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
@@ -525,21 +527,10 @@ impl ResultCollector {
}
tokio::time::sleep(poll_interval).await;
}
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
if all_submitted {
tokio::task::yield_now().await;
} else {
tokio::time::sleep(Duration::from_millis(600)).await;
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
if start.elapsed() > primary + Duration::from_secs(6) {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
tokio::time::sleep(Duration::from_millis(120)).await;
}
// Bound the opt-in "all submits" path tightly. A slow relay must not
// delay poll-any confirmation by multiple seconds after the submit
// window; give only a short grace for a just-finished worker to publish.
tokio::time::sleep(FAST_SUBMIT_DRAIN_GRACE).await;
self.get_first()
}
}
@@ -608,7 +599,7 @@ pub async fn execute_parallel(
swqos_clients: &[Arc<SwqosClient>],
payer: Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
middleware_manager: Option<Arc<MiddlewareManager>>,
@@ -660,17 +651,13 @@ pub async fn execute_parallel(
return Err(anyhow!("No available gas fee strategy configs"));
}
if is_buy && selected_task_configs.len() > 1 && durable_nonce.is_none() {
return Err(anyhow!("Multiple swqos transactions require durable_nonce to be set.",));
}
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
let channel_count = selected_task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
address_lookup_table_account,
address_lookup_table_accounts: Arc::new(address_lookup_table_accounts),
recent_blockhash,
durable_nonce,
middleware_manager,
@@ -848,4 +835,18 @@ mod tests {
assert_eq!(selected[0].gas_fee_config.2.cu_price, 700_000);
assert_eq!(selected[0].gas_fee_config.2.tip, 0.0);
}
#[tokio::test]
async fn wait_for_all_submitted_timeout_is_bounded() {
let collector = ResultCollector::new(1);
let start = Instant::now();
let result = collector.wait_for_all_submitted(0).await;
assert!(result.is_none());
assert!(
start.elapsed() < Duration::from_millis(150),
"wait_for_all_submitted should not add multi-second grace after timeout"
);
}
}
+9 -8
View File
@@ -96,6 +96,7 @@ impl TradeExecutor for GenericTradeExecutor {
total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
let before_submit_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
.then(crate::common::clock::now_micros);
let address_lookup_table_accounts = params.address_lookup_table_accounts.clone();
if params.simulate {
let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
@@ -103,7 +104,7 @@ impl TradeExecutor for GenericTradeExecutor {
params.rpc,
params.payer,
final_instructions,
params.address_lookup_table_account,
address_lookup_table_accounts,
params.recent_blockhash,
params.durable_nonce,
params.middleware_manager,
@@ -157,16 +158,16 @@ impl TradeExecutor for GenericTradeExecutor {
}
let need_confirm = params.wait_tx_confirmed;
// When the caller confirms externally (need_confirm = false) and opts in
// via SwapParams.wait_for_all_submits, return every route's signature so
// pinned-nonce confirmation can poll all of them.
let wait_for_all_submits = !need_confirm && params.wait_for_all_submits;
// Each SWQOS lane may submit a distinct transaction because relay tips
// can use different accounts, so confirmation must be able to poll every
// returned signature when the caller opts in.
let wait_for_all_submits = params.wait_for_all_submits;
let sender_config = params.sender_concurrency_config();
let result = execute_parallel(
params.swqos_clients.as_slice(),
params.payer,
final_instructions,
params.address_lookup_table_account,
address_lookup_table_accounts,
params.recent_blockhash,
params.durable_nonce,
params.middleware_manager,
@@ -253,7 +254,7 @@ async fn simulate_transaction(
rpc: Option<Arc<SolanaRpcClient>>,
payer: Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
middleware_manager: Option<Arc<MiddlewareManager>>,
@@ -288,7 +289,7 @@ async fn simulate_transaction(
unit_limit,
unit_price,
&instructions,
address_lookup_table_account.as_ref(),
address_lookup_table_accounts.as_slice(),
recent_blockhash,
middleware_manager.as_ref(),
protocol_name,
+10 -8
View File
@@ -61,7 +61,7 @@ pub struct SwapParams {
pub output_token_program: Option<Pubkey>,
pub input_amount: Option<u64>,
pub slippage_basis_points: Option<u64>,
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
pub address_lookup_table_accounts: Vec<AddressLookupTableAccount>,
pub recent_blockhash: Option<Hash>,
pub wait_tx_confirmed: bool,
pub protocol_params: DexParamEnum,
@@ -82,13 +82,15 @@ pub struct SwapParams {
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Defaults to false (post-4.0.11 behaviour: return after the
/// first route accepts). Set to true when the caller does its own on-chain
/// confirmation against a pinned durable nonce — only one route's tx can
/// land, but the caller cannot know which in advance, so it needs every
/// signature to poll via `getSignatureStatuses`.
/// When true, wait for every SWQOS route's HTTP submit response before
/// returning so the result includes all submitted signatures.
///
/// This is useful when confirmation or external monitoring polls all
/// signatures: each route can submit a distinct transaction because relay
/// tips may use different accounts. With a durable nonce, at most one route
/// can consume the nonce; with a recent blockhash, multiple route variants
/// may be valid, so callers must choose strategy and account state
/// accordingly. Defaults to false for lower submit latency.
pub wait_for_all_submits: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
+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
);
}
}
+138 -1
View File
@@ -1,8 +1,13 @@
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::SolanaRpcClient;
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
use crate::instruction::utils::pumpswap::{
accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP, PumpSwapFeeBasisPoints,
};
use solana_sdk::pubkey::Pubkey;
const SPL_MINT_SUPPLY_OFFSET: usize = 36;
const SPL_MINT_SUPPLY_LEN: usize = 8;
/// PumpSwap Protocol Specific Parameters
///
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
@@ -39,6 +44,9 @@ pub struct PumpSwapParams {
pub quote_token_program: Pubkey,
/// Whether the pool is in mayhem mode
pub is_mayhem_mode: bool,
/// Pool creator. Canonical PumpSwap pools use the Pump program pool-authority PDA here;
/// fee tiers are selected from this value without doing RPC in the instruction builder.
pub pool_creator: Pubkey,
/// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap
/// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()`
/// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053).
@@ -50,6 +58,12 @@ pub struct PumpSwapParams {
/// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket.
/// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps).
pub cashback_fee_basis_points: u64,
/// Base mint supply used by PumpSwap fee-tier market-cap selection. Filled by RPC
/// constructors and optional for parser/event fast paths.
pub base_mint_supply: Option<u64>,
/// Effective PumpSwap fee bps for this pool snapshot. Instruction building reads this
/// only from params, so hot-path trading never adds an RPC call for fee discovery.
pub fee_basis_points: PumpSwapFeeBasisPoints,
}
impl PumpSwapParams {
@@ -71,6 +85,12 @@ impl PumpSwapParams {
cashback_fee_basis_points: u64,
) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
let creator_fee_basis_points = if coin_creator == Pubkey::default() {
0
} else {
crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS
}
.saturating_add(cashback_fee_basis_points);
Self {
pool,
base_mint,
@@ -84,12 +104,46 @@ impl PumpSwapParams {
base_token_program,
quote_token_program,
is_mayhem_mode,
pool_creator: Pubkey::default(),
coin_creator,
is_cashback_coin,
cashback_fee_basis_points,
base_mint_supply: None,
fee_basis_points: PumpSwapFeeBasisPoints::new(
crate::instruction::utils::pumpswap::accounts::LP_FEE_BASIS_POINTS,
crate::instruction::utils::pumpswap::accounts::PROTOCOL_FEE_BASIS_POINTS,
creator_fee_basis_points,
),
}
}
pub fn with_pool_creator(mut self, pool_creator: Pubkey) -> Self {
self.pool_creator = pool_creator;
self
}
pub fn with_base_mint_supply(mut self, base_mint_supply: u64) -> Self {
self.base_mint_supply = Some(base_mint_supply);
self
}
pub fn with_fee_basis_points(
mut self,
lp_fee_basis_points: u64,
protocol_fee_basis_points: u64,
coin_creator_fee_basis_points: u64,
) -> Self {
let creator_fee_basis_points =
if self.coin_creator == Pubkey::default() { 0 } else { coin_creator_fee_basis_points }
.saturating_add(self.cashback_fee_basis_points);
self.fee_basis_points = PumpSwapFeeBasisPoints::new(
lp_fee_basis_points,
protocol_fee_basis_points,
creator_fee_basis_points,
);
self
}
/// Fast-path constructor for building PumpSwap parameters directly from decoded
/// trade/event data and the accompanying instruction accounts, avoiding RPC
/// lookups and associated latency. Token program IDs should be sourced from
@@ -135,6 +189,57 @@ impl PumpSwapParams {
)
}
/// Fast-path constructor for parser/event feeds that already include fee bps.
///
/// This avoids any fee-discovery RPC and is the preferred path when sol-parser-sdk or
/// another stream parser provides `lp_fee_basis_points`, `protocol_fee_basis_points`, and
/// `coin_creator_fee_basis_points` from PumpSwap events.
pub fn from_trade_with_fee_basis_points(
pool: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
pool_creator: Pubkey,
coin_creator: Pubkey,
is_cashback_coin: bool,
cashback_fee_basis_points: u64,
lp_fee_basis_points: u64,
protocol_fee_basis_points: u64,
coin_creator_fee_basis_points: u64,
) -> Self {
Self::new(
pool,
base_mint,
quote_mint,
pool_base_token_account,
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
quote_token_program,
fee_recipient,
coin_creator,
is_cashback_coin,
cashback_fee_basis_points,
)
.with_pool_creator(pool_creator)
.with_fee_basis_points(
lp_fee_basis_points,
protocol_fee_basis_points,
coin_creator_fee_basis_points,
)
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,
@@ -172,6 +277,21 @@ 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 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(
fee_config.as_ref(),
pool_data.creator,
pool_data.base_mint,
base_mint_supply,
pool_base_token_reserves,
pool_quote_token_reserves,
);
let creator_fee_basis_points = if pool_data.coin_creator == Pubkey::default() {
0
} else {
raw_fee_basis_points.coin_creator_fee_basis_points
};
let creator = pool_data.coin_creator;
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
creator,
@@ -213,8 +333,25 @@ impl PumpSwapParams {
crate::constants::TOKEN_PROGRAM_2022
},
is_mayhem_mode: pool_data.is_mayhem_mode,
pool_creator: pool_data.creator,
coin_creator: pool_data.coin_creator,
cashback_fee_basis_points: 0,
base_mint_supply,
fee_basis_points: PumpSwapFeeBasisPoints::new(
raw_fee_basis_points.lp_fee_basis_points,
raw_fee_basis_points.protocol_fee_basis_points,
creator_fee_basis_points,
),
})
}
}
fn decode_mint_supply(data: &[u8]) -> Option<u64> {
let bytes = data.get(SPL_MINT_SUPPLY_OFFSET..SPL_MINT_SUPPLY_OFFSET + SPL_MINT_SUPPLY_LEN)?;
Some(u64::from_le_bytes(bytes.try_into().ok()?))
}
async fn fetch_mint_supply(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<u64, anyhow::Error> {
let account = rpc.get_account(mint).await?;
decode_mint_supply(&account.data).ok_or_else(|| anyhow::anyhow!("Failed to decode mint supply"))
}
+6 -6
View File
@@ -55,7 +55,7 @@ impl PreallocatedTxBuilder {
///
/// # 交易版本自动选择
///
/// - **有地址查找表** (`lookup_table = Some`): 使用 `VersionedMessage::V0`
/// - **有地址查找表** (`lookup_tables` 非空): 使用 `VersionedMessage::V0`
/// - 支持地址查找表压缩
/// - 减少交易大小
/// - 需要 RPC 支持 V0
@@ -69,11 +69,11 @@ impl PreallocatedTxBuilder {
///
/// ```rust,ignore
/// // 无查找表 -> Legacy 消息
/// let msg = builder.build_zero_alloc(&payer, &ixs, None, blockhash);
/// let msg = builder.build_zero_alloc(&payer, &ixs, &[], blockhash);
/// assert!(matches!(msg, VersionedMessage::Legacy(_)));
///
/// // 有查找表 -> V0 消息
/// let msg = builder.build_zero_alloc(&payer, &ixs, Some(table_key), blockhash);
/// let msg = builder.build_zero_alloc(&payer, &ixs, &[lookup_table], blockhash);
/// assert!(matches!(msg, VersionedMessage::V0(_)));
/// ```
#[inline(always)]
@@ -81,17 +81,17 @@ impl PreallocatedTxBuilder {
&mut self,
payer: &Pubkey,
instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
address_lookup_table_accounts: &[AddressLookupTableAccount],
recent_blockhash: Hash,
) -> Result<VersionedMessage> {
self.reset();
self.instructions.extend_from_slice(instructions);
if let Some(alt) = address_lookup_table_account {
if !address_lookup_table_accounts.is_empty() {
let message = v0::Message::try_compile(
payer,
&self.instructions,
std::slice::from_ref(alt),
address_lookup_table_accounts,
recent_blockhash,
)?;
Ok(VersionedMessage::V0(message))
+117 -19
View File
@@ -4,6 +4,7 @@ use super::common::{
use crate::instruction::utils::pumpswap::accounts::{
COIN_CREATOR_FEE_BASIS_POINTS, LP_FEE_BASIS_POINTS, PROTOCOL_FEE_BASIS_POINTS,
};
use crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints;
use solana_sdk::pubkey::Pubkey;
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
@@ -81,6 +82,26 @@ pub fn buy_base_input_internal(
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<BuyBaseInputResult, String> {
buy_base_input_internal_with_fees(
base,
slippage_basis_points,
base_reserve,
quote_reserve,
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
),
)
}
pub fn buy_base_input_internal_with_fees(
base: u64,
slippage_basis_points: u64,
base_reserve: u64,
quote_reserve: u64,
fee_basis_points: &PumpSwapFeeBasisPoints,
) -> Result<BuyBaseInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -100,12 +121,15 @@ pub fn buy_base_input_internal(
let quote_amount_in = ceil_div(numerator, denominator as u128) as u64;
// Calculate fees
let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64;
let lp_fee =
compute_fee(quote_amount_in as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
let protocol_fee =
compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
let creator_bps =
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64;
compute_fee(quote_amount_in as u128, fee_basis_points.protocol_fee_basis_points as u128)
as u64;
let coin_creator_fee = compute_fee(
quote_amount_in as u128,
fee_basis_points.coin_creator_fee_basis_points as u128,
) as u64;
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
// Calculate max quote with slippage
@@ -137,23 +161,54 @@ pub fn buy_quote_input_internal(
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<BuyQuoteInputResult, String> {
buy_quote_input_internal_with_fees(
quote,
slippage_basis_points,
base_reserve,
quote_reserve,
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
),
)
}
pub fn buy_quote_input_internal_with_fees(
quote: u64,
slippage_basis_points: u64,
base_reserve: u64,
quote_reserve: u64,
fee_basis_points: &PumpSwapFeeBasisPoints,
) -> Result<BuyQuoteInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
}
// Calculate total fee basis points
let total_fee_bps = LP_FEE_BASIS_POINTS
+ PROTOCOL_FEE_BASIS_POINTS
+ creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points);
let total_fee_bps = fee_basis_points
.lp_fee_basis_points
.saturating_add(fee_basis_points.protocol_fee_basis_points)
.saturating_add(fee_basis_points.coin_creator_fee_basis_points);
let denominator = 10_000 + total_fee_bps;
// Calculate effective quote amount after fees
let effective_quote = (quote as u128 * 10_000) / denominator as u128;
let mut effective_quote = (quote as u128 * 10_000) / denominator as u128;
let lp_fee = compute_fee(effective_quote, fee_basis_points.lp_fee_basis_points as u128);
let protocol_fee =
compute_fee(effective_quote, fee_basis_points.protocol_fee_basis_points as u128);
let coin_creator_fee =
compute_fee(effective_quote, fee_basis_points.coin_creator_fee_basis_points as u128);
let total_with_fees = effective_quote + lp_fee + protocol_fee + coin_creator_fee;
if total_with_fees > quote as u128 {
effective_quote = effective_quote.saturating_sub(total_with_fees - quote as u128);
}
let input_amount = effective_quote.saturating_sub(1);
// Calculate base amount out using constant product formula
let numerator = (base_reserve as u128) * effective_quote;
let denominator_effective = (quote_reserve as u128) + effective_quote;
let numerator = (base_reserve as u128) * input_amount;
let denominator_effective = (quote_reserve as u128) + input_amount;
if denominator_effective == 0 {
return Err("Pool would be depleted; denominator is zero.".to_string());
@@ -190,6 +245,26 @@ pub fn sell_base_input_internal(
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<SellBaseInputResult, String> {
sell_base_input_internal_with_fees(
base,
slippage_basis_points,
base_reserve,
quote_reserve,
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
),
)
}
pub fn sell_base_input_internal_with_fees(
base: u64,
slippage_basis_points: u64,
base_reserve: u64,
quote_reserve: u64,
fee_basis_points: &PumpSwapFeeBasisPoints,
) -> Result<SellBaseInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -200,12 +275,15 @@ pub fn sell_base_input_internal(
/ ((base_reserve as u128) + (base as u128))) as u64;
// Calculate fees
let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64;
let lp_fee =
compute_fee(quote_amount_out as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
let protocol_fee =
compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
let creator_bps =
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64;
compute_fee(quote_amount_out as u128, fee_basis_points.protocol_fee_basis_points as u128)
as u64;
let coin_creator_fee = compute_fee(
quote_amount_out as u128,
fee_basis_points.coin_creator_fee_basis_points as u128,
) as u64;
// Calculate final quote after fees
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
@@ -259,6 +337,26 @@ pub fn sell_quote_input_internal(
quote_reserve: u64,
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> Result<SellQuoteInputResult, String> {
sell_quote_input_internal_with_fees(
quote,
slippage_basis_points,
base_reserve,
quote_reserve,
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
),
)
}
pub fn sell_quote_input_internal_with_fees(
quote: u64,
slippage_basis_points: u64,
base_reserve: u64,
quote_reserve: u64,
fee_basis_points: &PumpSwapFeeBasisPoints,
) -> Result<SellQuoteInputResult, String> {
if base_reserve == 0 || quote_reserve == 0 {
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
@@ -270,9 +368,9 @@ pub fn sell_quote_input_internal(
// Calculate raw quote amount including fees
let raw_quote = calculate_quote_amount_out(
quote,
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
fee_basis_points.lp_fee_basis_points,
fee_basis_points.protocol_fee_basis_points,
fee_basis_points.coin_creator_fee_basis_points,
);
// Calculate base amount needed using inverse constant product formula