Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c1a149e7 | |||
| 8657273ba6 | |||
| 99fb4ee604 | |||
| 3719d4a9f4 | |||
| d7326b55f4 | |||
| 7471bcba09 | |||
| f19f8a5d1c |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.18"
|
||||
version = "4.0.21"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
|
||||
@@ -83,11 +83,11 @@ This SDK is available in multiple languages:
|
||||
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.17"`
|
||||
|
||||
This release refreshes PumpFun V2 WSOL quote-pool handling, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
|
||||
This release refreshes PumpFun native-SOL quote handling so SOL/WSOL sentinels prefer the smaller V1 hot path, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, selecting legacy or V2 on-chain instructions as needed (SOL/WSOL + USDC)
|
||||
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, preferring V1 for native SOL and selecting V2 for USDC/non-native quote mints or explicit WSOL settlement
|
||||
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
|
||||
3. **Bonk Trading**: Support for Bonk trading operations
|
||||
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
|
||||
@@ -259,7 +259,8 @@ Optional builder methods:
|
||||
| `.wait_for_all_submits(true)` | In fast-submit mode, wait for all SWQoS lane responses and return all signatures. |
|
||||
| `.simulate(true)` | Build and simulate the transaction instead of sending it. |
|
||||
| `.grpc_recv_us(ts)` | Attach upstream receive timestamp for latency tracing. |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Use durable nonce instead of `recent_blockhash`. |
|
||||
| `.durable_nonce(nonce_info)` | Use durable nonce and clear `recent_blockhash`. Recommended when you start from `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)`. |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Construct params directly with durable nonce instead of `recent_blockhash`. |
|
||||
| `SimpleSellParams::with_tip(false)` | Disable relay tips for sells. Buys use the gas fee strategy/tip settings. |
|
||||
|
||||
`TradeBuyParams` and `TradeSellParams` remain available as advanced low-level APIs. See the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
|
||||
@@ -323,7 +324,29 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- If no custom URL is provided (`None`), the system will use the default endpoint for the specified `SwqosRegion`
|
||||
- This allows for maximum flexibility while maintaining backward compatibility
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. Fetch the latest nonce value and attach it to the high-level buy/sell params:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
recent_blockhash, // will be cleared by `.durable_nonce(...)`
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(nonce_info)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||
|
||||
@@ -403,15 +426,15 @@ PumpFun has two instruction sets for bonding-curve trading:
|
||||
|---|---|---|
|
||||
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
|
||||
| Account metas | 18 | 27 |
|
||||
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
|
||||
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
|
||||
| Quote mint | Native SOL (`default`, Solscan SOL sentinel, or WSOL sentinel) | Non-native quote mint, or explicit WSOL settlement |
|
||||
| Transaction size | Smaller (preferred hot path) | Larger (may require LUT for nonce/tip/ATA-heavy transactions) |
|
||||
|
||||
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` selects the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
|
||||
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` plus the requested settlement token (`pay_with` / `receive_as`) select the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
|
||||
|
||||
**Default: V1**. When `quote_mint` is `Pubkey::default()` or the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), the SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. Passing `WSOL_TOKEN_ACCOUNT` selects SOL V2; passing USDC selects USDC V2.
|
||||
**Default: V1**. When `quote_mint` is `Pubkey::default()`, the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), or `WSOL_TOKEN_ACCOUNT` (`So11111111111111111111111111111111111111112`), the SDK treats the curve as native SOL-paired and uses V1 instructions when `pay_with` / `receive_as` is `SOL`. This is the preferred hot path because it avoids the 27-account V2 layout. Passing USDC or another real quote mint selects V2. Passing `WSOL` as the buy input or sell output selects V2 only when you intentionally want to settle through an existing WSOL ATA.
|
||||
|
||||
**Key changes in v2 instructions:**
|
||||
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
|
||||
- `quote_mint` parameter — native SOL-paired curves may appear as default, Solscan SOL, or WSOL; USDC/non-native quote mints select V2
|
||||
- 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts**
|
||||
- `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory
|
||||
- Same pricing and cost as legacy instructions for SOL-paired coins
|
||||
@@ -419,13 +442,12 @@ The SDK-side builder is version-neutral: callers use the normal buy/sell flow, a
|
||||
|
||||
**Pass `quote_mint` into `PumpFunParams::from_trade`**:
|
||||
|
||||
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()` and Solscan SOL (`So11111111111111111111111111111111111111111`) mean the legacy SOL layout; `WSOL_TOKEN_ACCOUNT` means SOL V2; USDC means USDC V2.
|
||||
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()`, Solscan SOL (`So11111111111111111111111111111111111111111`), and `WSOL_TOKEN_ACCOUNT` all mean a native SOL-paired curve and default to V1 for normal SOL settlement. USDC means USDC V2.
|
||||
|
||||
```rust
|
||||
// quote_mint is not a PDA. It is the quote SPL mint carried by parser/gRPC events:
|
||||
// - Legacy SOL pool: Pubkey::default() or Solscan SOL from parser data
|
||||
// - SOL V2 pool: WSOL_TOKEN_ACCOUNT
|
||||
// - USDC V2 pool: USDC mint
|
||||
// - Native SOL pool: Pubkey::default(), Solscan SOL, or WSOL sentinel from parser data
|
||||
// - USDC/non-native pool: actual quote SPL mint
|
||||
let quote_mint = e.quote_mint;
|
||||
let params = PumpFunParams::from_trade(
|
||||
e.bonding_curve,
|
||||
@@ -446,11 +468,11 @@ let params = PumpFunParams::from_trade(
|
||||
);
|
||||
```
|
||||
|
||||
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves.
|
||||
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves. For SOL-paired curves, use `SOL` for the normal fast path; use `WSOL` only if you intentionally want V2 settlement through an existing WSOL ATA.
|
||||
When consuming parser events, map `quoteMint`, `virtualQuoteReserves`, and `realQuoteReserves` into `PumpFunParams::from_trade(...)`; USDC pools use `4_292_000_000` as the initial virtual quote reserve.
|
||||
For legacy SOL events where `quote_mint` is `Pubkey::default()` or Solscan SOL, use `virtual_sol_reserves` / `real_sol_reserves` when the quote-reserve fields are absent or zero.
|
||||
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce/tip may exceed `PACKET_DATA_SIZE`. The SDK reports this locally and does not remove compute-budget or tip instructions because that changes priority semantics. Use V1 when the curve is native SOL-paired, pre-create ATAs, or enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
|
||||
#### PumpSwap: coin_creator_vault from events (no RPC)
|
||||
|
||||
|
||||
+35
-13
@@ -83,11 +83,11 @@
|
||||
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.17"`
|
||||
|
||||
本版本刷新 PumpFun V2 WSOL quote 池处理逻辑,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
|
||||
本版本刷新 PumpFun native SOL quote 处理逻辑,SOL/WSOL sentinel 默认优先走更小的 V1 热路径,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,内部按需选择旧版或 V2 链上指令(SOL/WSOL + USDC)
|
||||
1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,native SOL 优先走 V1,USDC/非 SOL quote 或显式 WSOL 结算才走 V2
|
||||
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
|
||||
3. **Bonk 交易**: 支持 Bonk 的交易操作
|
||||
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
|
||||
@@ -258,7 +258,8 @@ client.buy_simple(buy_params).await?;
|
||||
| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 |
|
||||
| `.simulate(true)` | 只构建并模拟交易,不真正发送。 |
|
||||
| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 使用 durable nonce,不使用 `recent_blockhash`。 |
|
||||
| `.durable_nonce(nonce_info)` | 使用 durable nonce,并清空 `recent_blockhash`。如果你从 `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)` 开始构造,推荐用这个。 |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 直接用 durable nonce 构造参数,不使用 `recent_blockhash`。 |
|
||||
| `SimpleSellParams::with_tip(false)` | 关闭卖出交易 relay tip。买入的 tip 使用 gas fee strategy 控制。 |
|
||||
|
||||
`TradeBuyParams` 和 `TradeSellParams` 仍保留为高级低层接口。详细说明见 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
|
||||
@@ -322,7 +323,29 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- 如果没有提供自定义 URL(`None`),系统将使用指定 `SwqosRegion` 的默认端点
|
||||
- 这提供了最大的灵活性,同时保持向后兼容性
|
||||
|
||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||
当使用多个 MEV 服务时,需要使用 `Durable Nonce`。先获取最新 nonce,再挂到新的 buy/sell 参数上:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
recent_blockhash, // 会被 `.durable_nonce(...)` 清空
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(nonce_info)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
#### Astralane(Binary / Plain / QUIC)
|
||||
|
||||
@@ -405,22 +428,21 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
|
||||
|
||||
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
|
||||
|
||||
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 自动选择正确的链上 discriminator 和账户布局。
|
||||
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 和买/卖的结算 mint 自动选择正确的链上 discriminator 和账户布局。能用 V1 的 native SOL 池会优先用 V1。
|
||||
|
||||
**v2 指令关键变化:**
|
||||
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint
|
||||
- 新增 `quote_mint` 参数 — native SOL 配对可能表现为默认值、Solscan SOL sentinel(`So11111111111111111111111111111111111111111`)或 WSOL sentinel(`So11111111111111111111111111111111111111112`);USDC/其他真实 quote mint 才选择 V2
|
||||
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
|
||||
- `buyback_fee_recipient`、`sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
|
||||
- SOL 配对币的报价和成本与旧版一致,无额外开销
|
||||
|
||||
**使用方式:**
|
||||
|
||||
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint;`Pubkey::default()` 和 Solscan SOL(`So11111111111111111111111111111111111111111`)表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2,USDC 表示 USDC V2:
|
||||
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint 或 native SOL sentinel;`Pubkey::default()`、Solscan SOL(`So11111111111111111111111111111111111111111`)和 `WSOL_TOKEN_ACCOUNT` 都表示 native SOL 配对,正常用 SOL 结算时默认走旧版 V1;USDC 表示 USDC V2:
|
||||
|
||||
```rust
|
||||
// legacy SOL 池:log 事件里可能是 Pubkey::default(),parser 数据里是 Solscan SOL sentinel
|
||||
// SOL V2 池:WSOL_TOKEN_ACCOUNT
|
||||
// USDC 池:就是 USDC mint
|
||||
// native SOL 池:可能是 Pubkey::default()、Solscan SOL sentinel 或 WSOL sentinel
|
||||
// USDC / 非 SOL 池:就是实际 quote SPL mint
|
||||
let quote_mint = e.quote_mint;
|
||||
|
||||
let params = PumpFunParams::from_trade(
|
||||
@@ -446,14 +468,14 @@ client.buy(buy_params).await?;
|
||||
client.sell(sell_params).await?;
|
||||
```
|
||||
|
||||
USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。
|
||||
USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/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 保护服务
|
||||
|
||||
@@ -30,7 +30,7 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
|
||||
|
||||
@@ -50,7 +50,7 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
|
||||
| `with_tip` | `bool` | ❌ | Whether sells include relay tips. Default is `true`; set with `.with_tip(false)`. |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
|
||||
@@ -74,6 +74,31 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
|
||||
| `CreateMissing` | Include ATA creation where possible. | Convenience matters more than smallest transaction size. |
|
||||
| `AssumePrepared` | Do not create or close token accounts; caller prepared everything. | Advanced deterministic flows. |
|
||||
|
||||
### Durable Nonce With Simple Params
|
||||
|
||||
`fetch_nonce_info` and `DurableNonceInfo` are re-exported from the crate root:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
recent_blockhash,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(nonce_info);
|
||||
```
|
||||
|
||||
Calling `.durable_nonce(...)` clears `recent_blockhash`; nonce transactions use the nonce value as the transaction blockhash.
|
||||
|
||||
## TradeBuyParams
|
||||
|
||||
`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)` 或 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)` 或 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
|
||||
| `with_tip` | `bool` | ❌ | 卖出交易是否带 relay tip。默认 `true`,可通过 `.with_tip(false)` 关闭。 |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
@@ -74,6 +74,31 @@
|
||||
| `CreateMissing` | 尽量在交易内创建缺失 ATA。 | 更重视方便,不追求最小交易体积。 |
|
||||
| `AssumePrepared` | 不创建也不关闭 token account,调用方保证都已准备好。 | 高级确定性流程。 |
|
||||
|
||||
### Simple 参数使用 Durable Nonce
|
||||
|
||||
`fetch_nonce_info` 和 `DurableNonceInfo` 已从 crate root 重新导出:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
recent_blockhash,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(nonce_info);
|
||||
```
|
||||
|
||||
调用 `.durable_nonce(...)` 会清空 `recent_blockhash`;nonce 交易会使用 nonce value 作为 transaction blockhash。
|
||||
|
||||
## TradeBuyParams
|
||||
|
||||
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它。
|
||||
|
||||
@@ -14,11 +14,11 @@ use sol_trade_sdk::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
AccountPolicy, BuyAmount, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade,
|
||||
TradeTokenType,
|
||||
AccountPolicy, BuyAmount, DurableNonceInfo, SellAmount, SimpleBuyParams, SimpleSellParams,
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -83,7 +83,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// client.buy_simple(buy_params).await?;
|
||||
let _ = buy_params;
|
||||
|
||||
// Durable nonce is supported by the same high-level params. In production,
|
||||
// fetch it with `sol_trade_sdk::fetch_nonce_info(...)` immediately before
|
||||
// building the transaction.
|
||||
let nonce_buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
BuyAmount::WithMaxInput { quote_amount: 100_000 },
|
||||
pumpfun_params.clone(),
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(DurableNonceInfo {
|
||||
nonce_account: Some(Pubkey::new_unique()),
|
||||
current_nonce: Some(Hash::new_unique()),
|
||||
})
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
let _ = nonce_buy_params;
|
||||
|
||||
let sell_params = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
SellAmount::ExactInput(1_000_000),
|
||||
pumpfun_params.clone(),
|
||||
client.infrastructure.rpc.get_latest_blockhash().await?,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.slippage_basis_points(300)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
// client.sell_simple(sell_params).await?;
|
||||
let _ = sell_params;
|
||||
|
||||
let nonce_sell_params = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
@@ -92,11 +126,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.infrastructure.rpc.get_latest_blockhash().await?,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.slippage_basis_points(300)
|
||||
.durable_nonce(DurableNonceInfo {
|
||||
nonce_account: Some(Pubkey::new_unique()),
|
||||
current_nonce: Some(Hash::new_unique()),
|
||||
})
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
// client.sell_simple(sell_params).await?;
|
||||
let _ = sell_params;
|
||||
let _ = nonce_sell_params;
|
||||
|
||||
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
|
||||
Ok(())
|
||||
|
||||
+78
-3
@@ -173,9 +173,10 @@ pub struct SimpleBuyParams {
|
||||
pub dex_type: DexType,
|
||||
/// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`.
|
||||
///
|
||||
/// For PumpFun V2 SOL-paired coins, use `TradeTokenType::SOL` even when the
|
||||
/// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote
|
||||
/// account but keeps settlement in native SOL.
|
||||
/// For PumpFun SOL-paired coins, use `TradeTokenType::SOL` for the normal
|
||||
/// fast path even when parser data reports WSOL as the quote sentinel. The
|
||||
/// SDK will prefer V1; choose `WSOL` only when intentionally spending an
|
||||
/// existing WSOL ATA through V2.
|
||||
pub pay_with: TradeTokenType,
|
||||
/// Mint address of the token being bought.
|
||||
pub mint: Pubkey,
|
||||
@@ -335,6 +336,16 @@ impl SimpleBuyParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
|
||||
///
|
||||
/// This clears `recent_blockhash` because nonce transactions must use the
|
||||
/// nonce value as their transaction blockhash.
|
||||
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
|
||||
self.durable_nonce = Some(value);
|
||||
self.recent_blockhash = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait for confirmation before returning.
|
||||
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
|
||||
self.wait_tx_confirmed = value;
|
||||
@@ -444,6 +455,16 @@ impl SimpleSellParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
|
||||
///
|
||||
/// This clears `recent_blockhash` because nonce transactions must use the
|
||||
/// nonce value as their transaction blockhash.
|
||||
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
|
||||
self.durable_nonce = Some(value);
|
||||
self.recent_blockhash = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait for confirmation before returning.
|
||||
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
|
||||
self.wait_tx_confirmed = value;
|
||||
@@ -1910,6 +1931,33 @@ mod tests {
|
||||
assert!(!low.wait_tx_confirmed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_buy_builder_can_use_durable_nonce() {
|
||||
let nonce_account = Pubkey::new_unique();
|
||||
let nonce_hash = Hash::new_unique();
|
||||
let durable_nonce = DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(nonce_hash),
|
||||
};
|
||||
|
||||
let simple = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
BuyAmount::ExactInput(10_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.durable_nonce(durable_nonce.clone());
|
||||
|
||||
let low: TradeBuyParams = simple.into();
|
||||
|
||||
assert!(low.recent_blockhash.is_none());
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sell_auto_creates_non_sol_output_ata() {
|
||||
let simple = SimpleSellParams {
|
||||
@@ -1939,4 +1987,31 @@ mod tests {
|
||||
assert!(!low.close_output_token_ata);
|
||||
assert!(!low.close_mint_token_ata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sell_builder_can_use_durable_nonce() {
|
||||
let nonce_account = Pubkey::new_unique();
|
||||
let nonce_hash = Hash::new_unique();
|
||||
let durable_nonce = DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(nonce_hash),
|
||||
};
|
||||
|
||||
let simple = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
SellAmount::ExactInput(50_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.durable_nonce(durable_nonce.clone());
|
||||
|
||||
let low: TradeSellParams = simple.into();
|
||||
|
||||
assert!(low.recent_blockhash.is_none());
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,19 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
||||
];
|
||||
|
||||
pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("15qWd4huAkoxvhDsHMfpUn27TW1YBYMMJJ2jkAkbeam"),
|
||||
pubkey!("9XuGciSwr5wb7dLTQm91JhuBTvj3GG8WjuRDc3obeam"),
|
||||
pubkey!("kiQioJNyFG7pU36ELLsRKXkeT48kFbk3b6rSgrWbeam"),
|
||||
pubkey!("kjmVhW1UzJrW2sU5bY5NtZ79jpvjSStsj37Pzmabeam"),
|
||||
pubkey!("kREnjPWFpt4AHeY5pijPmyXaCrMnbatUQJo7d3Xbeam"),
|
||||
pubkey!("praRZG6N6MdbsT4EFpKgZJWReZGXQhAMFcH68oCbeam"),
|
||||
pubkey!("SqoKQKU5uwBxovq3R7yEBxFwptc4z7vwoghU3M9beam"),
|
||||
pubkey!("sV72TY66T1RfmDSeHPPbwX6wwJ3bBv5hd4ehJ8tbeam"),
|
||||
pubkey!("swf8MyEeLo7gtRUo27UuJj6naCASUrypU7dbteSbeam"),
|
||||
pubkey!("uiuaQsxA47JybQAVN4FTfYuoEDkMiXV1r591Aewbeam"),
|
||||
];
|
||||
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
@@ -434,6 +447,19 @@ pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SOLAMI: [&str; 10] = [
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
];
|
||||
|
||||
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
|
||||
pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001;
|
||||
pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001;
|
||||
@@ -450,6 +476,7 @@ pub const SWQOS_MIN_TIP_SOYAS: f64 = 0.001; // Soyas requires minimum 0.001 SOL
|
||||
pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires minimum 0.001 SOL tip
|
||||
/// Helius Sender: 0.0002 SOL when not swqos_only; use SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY when swqos_only=true.
|
||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||
pub const SWQOS_MIN_TIP_SOLAMI: f64 = 0.0001;
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
|
||||
@@ -476,6 +503,7 @@ mod tests {
|
||||
&SWQOS_ENDPOINTS_SOYAS,
|
||||
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
&SWQOS_ENDPOINTS_SOLAMI,
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
||||
+138
-25
@@ -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("e_mint))
|
||||
if !is_sol_quote_mint("e_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(¶ms.input_mint)
|
||||
}
|
||||
TradeType::Sell => is_explicit_wsol_settlement_mint(¶ms.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(¶ms.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(¶ms.output_mint) {
|
||||
return Err(anyhow!(
|
||||
"PumpFun native SOL sell expects output_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL output only when receiving into an existing WSOL ATA.",
|
||||
params.output_mint
|
||||
));
|
||||
}
|
||||
|
||||
let token_amount = if let Some(amount) = params.input_amount {
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
@@ -1002,14 +1038,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_fixed_output_uses_buy_with_max_input_budget() {
|
||||
fn pumpfun_v2_usdc_fixed_output_uses_buy_with_max_input_budget() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.fixed_output_amount = Some(42);
|
||||
params.use_exact_sol_amount = Some(true);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
@@ -1024,7 +1061,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() {
|
||||
fn pumpfun_wsol_quote_regular_buy_uses_v1_native_sol() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
@@ -1043,13 +1080,13 @@ mod tests {
|
||||
params.slippage_basis_points.unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected);
|
||||
assert_eq!(buy_ix.accounts.len(), 27);
|
||||
assert_eq!(buy_ix.accounts.len(), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_skips_wsol_ata_create_on_hot_path() {
|
||||
fn pumpfun_wsol_quote_regular_buy_skips_wsol_ata_create_on_hot_path() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
@@ -1065,13 +1102,15 @@ mod tests {
|
||||
assert_eq!(instructions.len(), 1);
|
||||
assert_eq!(
|
||||
&instructions.last().unwrap().data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR
|
||||
crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() {
|
||||
fn pumpfun_v2_explicit_wsol_input_with_output_ata_create_reports_oversized_without_dropping_priority(
|
||||
) {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.create_output_mint_ata = true;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
@@ -1080,6 +1119,41 @@ mod tests {
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let business_instructions = build_buy(¶ms).unwrap();
|
||||
let err = crate::trading::common::transaction_builder::build_transaction(
|
||||
¶ms.payer,
|
||||
150_000,
|
||||
500_000,
|
||||
&business_instructions,
|
||||
None,
|
||||
Some(solana_hash::Hash::new_unique()),
|
||||
None,
|
||||
"PumpFun",
|
||||
true,
|
||||
true,
|
||||
&Pubkey::new_unique(),
|
||||
0.001,
|
||||
None,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("transaction too large"), "{err}");
|
||||
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_explicit_wsol_input_hot_path_transaction_fits_when_output_ata_prepared() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.create_output_mint_ata = false;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let business_instructions = build_buy(¶ms).unwrap();
|
||||
let transaction = crate::trading::common::transaction_builder::build_transaction(
|
||||
¶ms.payer,
|
||||
@@ -1107,7 +1181,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_buy_v2() {
|
||||
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_v1() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
@@ -1135,8 +1209,8 @@ mod tests {
|
||||
let instructions = build_buy(¶ms).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(¶ms).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(¶ms).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(¶ms).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(¶ms).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(¶ms).unwrap();
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub fn build_transaction(
|
||||
tip_amount: f64,
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let transaction = build_transaction_with_compute_budget(
|
||||
let transaction = build_transaction_inner(
|
||||
payer,
|
||||
unit_limit,
|
||||
unit_price,
|
||||
@@ -63,44 +63,32 @@ pub fn build_transaction(
|
||||
)?;
|
||||
|
||||
let serialized_len = bincode::serialized_size(&transaction)? as usize;
|
||||
if serialized_len <= PACKET_DATA_SIZE
|
||||
|| durable_nonce.is_some()
|
||||
|| !with_tip
|
||||
|| tip_amount <= 0.0
|
||||
|| (unit_limit == 0 && unit_price == 0)
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
" [SDK][tx-size ] {} {} serialized={} bytes, business_ix={}, nonce={}, tip={}, cu_limit={}, cu_price={}, alt={}",
|
||||
protocol_name,
|
||||
if is_buy { "buy" } else { "sell" },
|
||||
serialized_len,
|
||||
business_instructions.len(),
|
||||
durable_nonce.is_some(),
|
||||
with_tip && tip_amount > 0.0,
|
||||
unit_limit,
|
||||
unit_price,
|
||||
address_lookup_table_account.is_some()
|
||||
);
|
||||
}
|
||||
if serialized_len <= PACKET_DATA_SIZE {
|
||||
return Ok(transaction);
|
||||
}
|
||||
|
||||
let compact_transaction = build_transaction_with_compute_budget(
|
||||
payer,
|
||||
0,
|
||||
0,
|
||||
business_instructions,
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
middleware_manager,
|
||||
protocol_name,
|
||||
is_buy,
|
||||
with_tip,
|
||||
tip_account,
|
||||
tip_amount,
|
||||
durable_nonce,
|
||||
)?;
|
||||
let compact_len = bincode::serialized_size(&compact_transaction)? as usize;
|
||||
if compact_len <= PACKET_DATA_SIZE {
|
||||
return Ok(compact_transaction);
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting",
|
||||
"transaction too large: {} > {}; SDK did not remove compute budget or relay tip because that changes transaction priority semantics. Use an address lookup table or pre-create token ATAs before submitting",
|
||||
serialized_len,
|
||||
PACKET_DATA_SIZE,
|
||||
compact_len
|
||||
PACKET_DATA_SIZE
|
||||
))
|
||||
}
|
||||
|
||||
fn build_transaction_with_compute_budget(
|
||||
fn build_transaction_inner(
|
||||
payer: &Arc<Keypair>,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
@@ -181,3 +169,41 @@ fn build_versioned_transaction(
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
|
||||
fn oversized_instruction(account_count: usize, data_len: usize) -> Instruction {
|
||||
let accounts =
|
||||
(0..account_count).map(|_| AccountMeta::new(Pubkey::new_unique(), false)).collect();
|
||||
Instruction { program_id: Pubkey::new_unique(), accounts, data: vec![7; data_len] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_transaction_returns_error_without_dropping_priority_semantics() {
|
||||
let payer = Arc::new(Keypair::new());
|
||||
let business_instructions = vec![oversized_instruction(36, 700)];
|
||||
let err = build_transaction(
|
||||
&payer,
|
||||
80_000,
|
||||
100_000,
|
||||
&business_instructions,
|
||||
None,
|
||||
Some(Hash::new_unique()),
|
||||
None,
|
||||
"test",
|
||||
true,
|
||||
true,
|
||||
&Pubkey::new_unique(),
|
||||
0.001,
|
||||
None,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("transaction too large"), "{err}");
|
||||
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user