Compare commits

..

16 Commits

Author SHA1 Message Date
0xfnzero e459f175f5 Release sol-trade-sdk v4.0.17 2026-06-07 01:42:08 +08:00
0xfnzero e5e58bef89 Release sol-trade-sdk v4.0.16 2026-06-06 12:31:08 +08:00
0xfnzero 9874cd5358 Release sol-trade-sdk v4.0.15 2026-06-01 14:57:49 +08:00
0xfnzero 1d3c0ff5da fix: force Token-2022 for pump suffix mints 2026-05-28 05:21:24 +08:00
Wood 34f400e89d Merge pull request #103 from HelvetiCrypt/wait-for-all-submits
Add SwapParams.wait_for_all_submits to restore all-routes signature collection
2026-05-28 02:46:39 +08:00
HelvetiCrypt bd1772bed2 Add SwapParams.wait_for_all_submits to restore all-routes signature collection
v4.0.11 switched the fast-submit (`wait_tx_confirmed = false`) result loop
from `wait_for_all_submitted` to `wait_for_first_submitted`. The new
function returns as soon as one SWQOS route's HTTP submit completes and
drains whatever signatures are in `ResultCollector.results` at that
moment — slower routes' signatures arrive after the caller has returned.

This is correct for fire-and-forget low-latency submits, but breaks
callers that do their own on-chain confirmation against a pinned durable
nonce. Only one of the submitted txs can land (nonce is consumed by
whichever lands first), but the caller has no way of knowing in advance
which route it will be. Their post-submit logic feeds every returned
signature to `getSignatureStatuses` to find the landed one — with only
the fastest HTTP responder returned (often not the one that lands), the
landed tx isn't in the polled set.

Adds an opt-in `SwapParams.wait_for_all_submits: bool` (default false,
plumbed through `TradeBuyParams` / `TradeSellParams`). When true and
`wait_tx_confirmed = false`, `execute_parallel` uses
`wait_for_all_submitted` (still in the codebase, was
`#[allow(dead_code)]`) so every submitted signature is returned. Zero
behaviour change for current users.
2026-05-27 08:02:49 +00:00
0xfnzero 5449d51591 Release sol-trade-sdk 4.0.14 2026-05-25 22:01:15 +08:00
0xfnzero b973061f92 Update DEX protocol support for 4.0.13 2026-05-25 01:47:48 +08:00
0xfnzero 57e4c5d221 Fix PumpFun V2 buyback fee mutability 2026-05-22 08:56:20 +08:00
0xfnzero 8ed2aef931 Fix trade execution correctness and release 4.0.11 2026-05-21 17:50:06 +08:00
0xfnzero 0cf0064e80 docs: clarify PumpFun V2 configuration 2026-05-21 14:48:43 +08:00
0xfnzero 4968c48a2e Release sol-trade-sdk v4.0.10
Sync Pump program IDLs with the latest definitions.
2026-05-20 19:41:16 +08:00
0xfnzero 5908c84798 Merge remote-tracking branch 'origin/main' into codex/swqos-dual-fee-mode 2026-05-19 06:21:44 +08:00
0xfnzero 6dab47f8f9 Optimize SWQOS dual fee lane submission 2026-05-19 06:21:37 +08:00
0xfnzero a076e0f8cd chore: flatten synced IDL files
Remove the redundant idl/bitquery source snapshot tree and keep synced Bitquery IDLs only under the SDK's canonical local IDL filenames.
2026-05-16 15:03:10 +08:00
0xfnzero 05ac079d6d release: update DEX protocol support for v4.0.9
Sync supported DEX IDLs from bitquery/solana-idl-lib, upgrade protocol builders, and keep trade submission hot paths free of RPC query calls.

Include exact-output coverage, token-account setup fixes for WSOL and stable pairs, and low-latency transaction build improvements.
2026-05-16 06:42:50 +08:00
69 changed files with 19346 additions and 1050 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.8"
version = "4.0.17"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+44 -27
View File
@@ -79,19 +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 |
## 🔖 Current Release
**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.
## ✨ Features
1. **PumpFun Trading**: Support for `buy`, `sell`, `buy_exact_sol_in`, and the new unified `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` instructions (SOL + USDC)
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)
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
5. **Raydium AMM V4 Trading**: Support for Raydium AMM V4 (Automated Market Maker) trading operations
6. **Meteora DAMM V2 Trading**: Support for Meteora DAMM V2 (Dynamic AMM) trading operations
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane and other services
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
8. **Concurrent Trading**: Submit through every configured SWQoS provider plus the default RPC lane; the first accepted result can return early while slower routes continue submitting
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage
12. **Hot-Path RPC Boundary**: Trade execution uses caller-supplied blockhash or durable nonce and never queries RPC for blockhash, account, or balance data
## 📦 Installation
@@ -108,14 +115,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.8"
sol-trade-sdk = "4.0.17"
```
## 🛠️ Usage Examples
@@ -152,7 +159,6 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // default: false - filter SWQOS below min tip
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
// .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
.build();
// Create TradingClient
@@ -335,7 +341,7 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi
- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step.
- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK:
- **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvents `is_cashback_enabled` or BondingCurves `is_cashback_coin`).
- **PumpFun**: `PumpFunParams::from_trade(..., mint, quote_mint, creator, ..., is_cashback_coin, mayhem_mode)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvents `is_cashback_enabled` or BondingCurves `is_cashback_coin`).
- **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data.
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
@@ -355,7 +361,7 @@ Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vau
The SDK does not fetch creator_vault from RPC on every sell (to avoid latency); pass the up-to-date vault from gRPC/events when available.
#### PumpFun V1 vs V2 Instructions
#### PumpFun Unified Buy/Sell With V1/V2 Instructions
PumpFun has two instruction sets for bonding-curve trading:
@@ -366,38 +372,49 @@ PumpFun has two instruction sets for bonding-curve trading:
| 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) |
**Default: V1** (`use_pumpfun_v2 = false`). 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.
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.
**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.
**Key changes in v2 instructions:**
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
- 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
- USDC-paired coins must be bought with USDC and sell back to USDC. The SDK rejects SOL input for USDC quote pools before transaction submission.
**How to enable V2:**
**Pass `quote_mint` into `PumpFunParams::from_trade`**:
**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet):
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.
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.use_pumpfun_v2(true) // Switch all PumpFun trades to V2 instructions (27 accounts)
.build();
// 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
let quote_mint = e.quote_mint;
let params = PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_quote_reserves,
e.real_token_reserves,
e.real_quote_reserves,
close_token_account_when_sell,
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
);
```
**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
// SOL-paired coin with v2 layout
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
// USDC-paired coin (requires v2)
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(USDC_TOKEN_ACCOUNT);
```
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.
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.
+46 -22
View File
@@ -79,19 +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 支持 |
## 🔖 当前版本
**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、账户或余额数据。
## ✨ 项目特性
1. **PumpFun 交易**: 支持 `buy``sell``buy_exact_sol_in` 以及全新的统一 `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` 指令(SOL + USDC
1. **PumpFun 交易**: SDK 侧统一 `buy``sell``buy_exact_quote_in` 流程,内部按需选择旧版或 V2 链上指令(SOL/WSOL + USDC
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
3. **Bonk 交易**: 支持 Bonk 的交易操作
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
8. **并发交易**: 所有已配置的 SWQoS 通道和默认 RPC 通道都会发出提交;首个成功只影响返回,较慢通道会继续提交
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
12. **热路径 RPC 边界**: 交易执行使用调用方传入的 blockhash 或 durable nonce,不在热路径查询 blockhash、账户或余额
## 📦 安装
@@ -108,14 +115,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.8"
sol-trade-sdk = "4.0.17"
```
## 🛠️ 使用示例
@@ -333,7 +340,7 @@ PumpFun 与 PumpSwap 支持**返现(Cashback**:部分手续费可返还
- **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc``PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。
- **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK:
- **PumpFun**`PumpFunParams::from_trade(..., is_cashback_coin)``PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
- **PumpFun**`PumpFunParams::from_trade(..., mint, quote_mint, creator, ..., is_cashback_coin, mayhem_mode)``PumpFunParams::from_dev_trade(..., is_cashback_coin)` 都需要传入 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
- **PumpSwap**`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`
- **领取返现**:使用 `client.claim_cashback_pumpfun()``client.claim_cashback_pumpswap(...)` 领取累计的返现。
@@ -360,10 +367,12 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
- **sol-parser-sdk**:指令解析从账户 17、18 写入;若事件来自日志,账户填充器也会从指令补全。用 `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` 即可。
- **solana-streamer**:指令解析从 `accounts.get(17)``accounts.get(18)` 写入。同样用事件的 `coin_creator_vault_ata``coin_creator_vault_authority` 调用 `from_trade`
### Pump.fun Bonding Curve v2buy_v2 / sell_v2 / buy_exact_quote_in_v2
### Pump.fun Bonding Curve 统一买卖入口与 v2 指令
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 自动选择正确的链上 discriminator 和账户布局。
**v2 指令关键变化:**
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
@@ -372,30 +381,46 @@ Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过
**使用方式:**
设置 `PumpFunParams``quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade``quote_mint` 不是 PDA,它就是 quote SPL mint`Pubkey::default()` 和 Solscan SOL`So11111111111111111111111111111111111111111`)表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2USDC 表示 USDC V2
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
// legacy SOL 池:log 事件里可能是 Pubkey::default()parser 数据里是 Solscan SOL sentinel
// SOL V2 池:WSOL_TOKEN_ACCOUNT
// USDC 池:就是 USDC mint
let quote_mint = e.quote_mint;
// SOL 配对币 — 传包装 SOL mint
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
// USDC 配对币(即将开放 — 必须使用 v2)
let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(USDC_TOKEN_ACCOUNT);
let params = PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_quote_reserves,
e.real_token_reserves,
e.real_quote_reserves,
close_token_account_when_sell,
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
);
// 之后正常交易
client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|-----------|-------------|---------|------|
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
| `WSOL_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
| `USDC_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2 |
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。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 配对,统一布局 |
| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2 |
## 🛡️ MEV 保护服务
@@ -446,4 +471,3 @@ MIT 许可证
3. 注意滑点设置避免交易失败
4. 监控余额和交易费用
5. 遵循相关法律法规
+2 -2
View File
@@ -35,7 +35,7 @@ The `TradeBuyParams` struct contains all parameters required for executing buy o
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
| `create_mint_ata` | `bool` | ✅ | Whether to create token mint ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
@@ -67,7 +67,7 @@ The `TradeSellParams` struct contains all parameters required for executing sell
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
+2 -2
View File
@@ -35,7 +35,7 @@
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
| `create_mint_ata` | `bool` | ✅ | 是否创建代币 mint ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出Meteora DAMM V2 必需) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算Meteora DAMM V2 必需) |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
@@ -67,7 +67,7 @@
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出Meteora DAMM V2 必需) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算Meteora DAMM V2 必需) |
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = "0.4.14"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+15 -4
View File
@@ -130,6 +130,16 @@ async fn pumpfun_copy_trade_with_grpc(
let client = create_solana_trade_client().await?;
let mint_pubkey = trade_info.mint;
let virtual_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.virtual_quote_reserves
} else {
trade_info.virtual_sol_reserves
};
let real_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.real_quote_reserves
} else {
trade_info.real_sol_reserves
};
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
@@ -154,12 +164,13 @@ async fn pumpfun_copy_trade_with_grpc(
trade_info.bonding_curve,
trade_info.associated_bonding_curve,
trade_info.mint,
trade_info.quote_mint,
trade_info.creator,
trade_info.creator_vault,
trade_info.virtual_token_reserves,
trade_info.virtual_sol_reserves,
virtual_quote_reserves,
trade_info.real_token_reserves,
trade_info.real_sol_reserves,
real_quote_reserves,
None,
trade_info.fee_recipient,
trade_info.token_program,
@@ -167,7 +178,8 @@ async fn pumpfun_copy_trade_with_grpc(
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -176,7 +188,6 @@ async fn pumpfun_copy_trade_with_grpc(
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
+4 -3
View File
@@ -170,7 +170,8 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: true,
@@ -179,7 +180,6 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -222,7 +222,8 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
with_tip: false,
durable_nonce: None,
create_output_token_ata: false,
+4 -3
View File
@@ -138,7 +138,8 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -147,7 +148,6 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -183,7 +183,8 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+30 -25
View File
@@ -627,7 +627,8 @@ async fn handle_buy_pumpfun(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -636,11 +637,10 @@ async fn handle_buy_pumpfun(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from PumpFun!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -684,7 +684,8 @@ async fn handle_buy_pumpswap(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -693,11 +694,10 @@ async fn handle_buy_pumpswap(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -741,7 +741,8 @@ async fn handle_buy_bonk(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -750,11 +751,10 @@ async fn handle_buy_bonk(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Bonk!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -802,7 +802,8 @@ async fn handle_buy_raydium_v4(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -811,11 +812,10 @@ async fn handle_buy_raydium_v4(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Raydium V4!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -864,7 +864,8 @@ async fn handle_buy_raydium_cpmm(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -873,11 +874,10 @@ async fn handle_buy_raydium_cpmm(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Raydium CPMM!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1035,7 +1035,8 @@ async fn handle_sell_pumpfun(
with_tip: false,
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1047,7 +1048,7 @@ async fn handle_sell_pumpfun(
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from PumpFun!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1094,7 +1095,8 @@ async fn handle_sell_pumpswap(
with_tip: false,
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1105,7 +1107,7 @@ async fn handle_sell_pumpswap(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1153,7 +1155,8 @@ async fn handle_sell_bonk(
with_tip: false,
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1164,7 +1167,7 @@ async fn handle_sell_bonk(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Bonk!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1215,7 +1218,8 @@ async fn handle_sell_raydium_v4(
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1226,7 +1230,7 @@ async fn handle_sell_raydium_v4(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Raydium V4!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1278,7 +1282,8 @@ async fn handle_sell_raydium_cpmm(
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1289,7 +1294,7 @@ async fn handle_sell_raydium_cpmm(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Raydium CPMM!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -42,7 +42,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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.
close_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
create_mint_ata: true,
@@ -51,7 +52,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -84,7 +84,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
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.
close_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_mint_token_ata: false,
+2 -2
View File
@@ -103,7 +103,8 @@ async fn test_middleware() -> AnyResult<()> {
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -112,7 +113,6 @@ async fn test_middleware() -> AnyResult<()> {
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = "0.4.14"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
spl-associated-token-account = "7.0.0"
+15 -4
View File
@@ -129,6 +129,16 @@ async fn pumpfun_copy_trade_with_grpc(
let client = create_solana_trade_client().await?;
let mint_pubkey = trade_info.mint;
let virtual_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.virtual_quote_reserves
} else {
trade_info.virtual_sol_reserves
};
let real_quote_reserves = if trade_info.virtual_quote_reserves != 0 {
trade_info.real_quote_reserves
} else {
trade_info.real_sol_reserves
};
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
@@ -150,12 +160,13 @@ async fn pumpfun_copy_trade_with_grpc(
trade_info.bonding_curve,
trade_info.associated_bonding_curve,
trade_info.mint,
trade_info.quote_mint,
trade_info.creator,
trade_info.creator_vault,
trade_info.virtual_token_reserves,
trade_info.virtual_sol_reserves,
virtual_quote_reserves,
trade_info.real_token_reserves,
trade_info.real_sol_reserves,
real_quote_reserves,
None,
trade_info.fee_recipient,
trade_info.token_program,
@@ -163,7 +174,8 @@ async fn pumpfun_copy_trade_with_grpc(
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -172,7 +184,6 @@ async fn pumpfun_copy_trade_with_grpc(
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = "0.4.14"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+17 -7
View File
@@ -128,6 +128,13 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let mint_pubkey = e.mint;
let virtual_quote_reserves = if e.virtual_quote_reserves != 0 {
e.virtual_quote_reserves
} else {
e.virtual_sol_reserves
};
let real_quote_reserves =
if e.virtual_quote_reserves != 0 { e.real_quote_reserves } else { e.real_sol_reserves };
let slippage_basis_points = Some(100u64);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
@@ -147,12 +154,13 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
virtual_quote_reserves,
e.real_token_reserves,
e.real_sol_reserves,
real_quote_reserves,
None,
e.fee_recipient,
e.token_program,
@@ -160,7 +168,8 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -169,7 +178,6 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -198,12 +206,13 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
virtual_quote_reserves,
e.real_token_reserves,
e.real_sol_reserves,
real_quote_reserves,
Some(true),
e.fee_recipient,
e.token_program,
@@ -211,7 +220,8 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false,
close_output_token_ata: false,
close_mint_token_ata: false,
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.2.2"
sol-parser-sdk = "0.4.14"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+14 -5
View File
@@ -118,6 +118,13 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let mint_pubkey = e.mint;
let virtual_quote_reserves = if e.virtual_quote_reserves != 0 {
e.virtual_quote_reserves
} else {
e.virtual_sol_reserves
};
let real_quote_reserves =
if e.virtual_quote_reserves != 0 { e.real_quote_reserves } else { e.real_sol_reserves };
let slippage_basis_points = Some(300u64);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
@@ -150,7 +157,8 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -159,7 +167,6 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -182,12 +189,13 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.quote_mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
virtual_quote_reserves,
e.real_token_reserves,
e.real_sol_reserves,
real_quote_reserves,
Some(true),
e.fee_recipient,
e.token_program,
@@ -195,7 +203,8 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+4 -3
View File
@@ -41,7 +41,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -50,7 +51,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -81,7 +81,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+6 -5
View File
@@ -164,7 +164,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
trade_info.cashback_fee_basis_points,
0,
);
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
@@ -195,7 +195,7 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
trade_info.cashback_fee_basis_points,
0,
);
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
@@ -235,7 +235,8 @@ async fn pumpswap_trade_with_grpc(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_sol,
close_input_token_ata: is_sol,
create_mint_ata: true,
@@ -244,7 +245,6 @@ async fn pumpswap_trade_with_grpc(
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -277,7 +277,8 @@ async fn pumpswap_trade_with_grpc(
with_tip: false,
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_sol,
close_output_token_ata: is_sol,
close_mint_token_ata: false,
+14 -29
View File
@@ -8,10 +8,7 @@ use sol_trade_sdk::{
},
SolanaTrade,
};
use sol_trade_sdk::{
common::TradeConfig, instruction::utils::raydium_amm_v4::fetch_amm_info,
trading::common::get_multi_token_balances, TradeTokenType,
};
use sol_trade_sdk::{common::TradeConfig, TradeTokenType};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
@@ -131,29 +128,16 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let amm_info = fetch_amm_info(&client.infrastructure.rpc, trade_info.amm).await?;
let (coin_reserve, pc_reserve) = get_multi_token_balances(
&client.infrastructure.rpc,
&amm_info.token_coin,
&amm_info.token_pc,
)
.await?;
let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
let params =
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm)
.await?;
let mint_pubkey = if params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
{
amm_info.coin_mint
params.coin_mint
} else {
amm_info.pc_mint
params.pc_mint
};
let params = RaydiumAmmV4Params::new(
trade_info.amm,
amm_info.coin_mint,
amm_info.pc_mint,
amm_info.token_coin,
amm_info.token_pc,
coin_reserve,
pc_reserve,
);
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
@@ -161,8 +145,8 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
// Buy tokens
println!("Buying tokens from Raydium_amm_v4...");
let input_token_amount = 100_000;
let is_wsol = amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_wsol = params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::RaydiumAmmV4,
input_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
@@ -172,7 +156,8 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -181,7 +166,6 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -215,7 +199,8 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
+4 -3
View File
@@ -160,7 +160,8 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -169,7 +170,6 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -201,7 +201,8 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
+4 -3
View File
@@ -41,7 +41,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -50,7 +51,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -84,7 +84,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+1 -1
View File
@@ -47,7 +47,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
SwqosConfig::Temporal("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::FlashBlock("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::Astralane(
"your_api_token".to_string(),
SwqosRegion::Frankfurt,
+16
View File
@@ -0,0 +1,16 @@
# IDL files
Supported DEX IDLs are stored directly in this directory. The files below were
synced from `bitquery/solana-idl-lib` commit `f804b17` and written to the SDK's
canonical local filenames.
| SDK protocol | Local IDL | Source path |
| --- | --- | --- |
| Bonk / Raydium Launchpad | `bonk.json` | `raydium/launchpad.json` |
| Raydium CPMM | `raydium_cpmm.json` | `raydium/raydium_cp.json` |
| Raydium AMM v4 | `raydium_amm_v4.json` | `raydium/raydium_amm.json` |
| Meteora DAMM v2 | `meteora_damm_v2.json` | `meteora/cp_amm_016.json` |
| PumpFun | `pump.json` | `pumpfun/pump.json` |
| PumpSwap | `pump_amm.json` | `pumpswap/amm.json` |
No separate source snapshot tree is kept in the repository.
+5783
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+288
View File
@@ -1943,6 +1943,117 @@
],
"args": []
},
{
"name": "collect_creator_fee_v2",
"docs": [
"Collects creator_fee from creator_vault to the coin creator account"
],
"discriminator": [207, 17, 138, 242, 4, 34, 19, 56],
"accounts": [
{
"name": "creator",
"writable": true
},
{
"name": "creator_token_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "creator"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "creator_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116
]
},
{
"kind": "account",
"path": "creator"
}
]
}
},
{
"name": "creator_vault_token_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "creator_vault"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "quote_mint"
},
{
"name": "quote_token_program"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111,
114, 105, 116, 121
]
}
]
}
},
{
"name": "program"
}
],
"args": []
},
{
"name": "create",
"docs": ["Creates a new coin and bonding curve."],
@@ -2446,6 +2557,153 @@
}
}
},
{
"name": "distribute_creator_fees_v2",
"discriminator": [255, 203, 19, 79, 244, 68, 8, 159],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "mint",
"relations": ["sharing_config"]
},
{
"name": "bonding_curve",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101
]
},
{
"kind": "account",
"path": "mint"
}
]
}
},
{
"name": "sharing_config",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105,
103
]
},
{
"kind": "account",
"path": "mint"
}
],
"program": {
"kind": "const",
"value": [
12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21,
39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20,
194, 124, 112
]
}
}
},
{
"name": "creator_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116
]
},
{
"kind": "account",
"path": "bonding_curve.creator",
"account": "BondingCurve"
}
]
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111,
114, 105, 116, 121
]
}
]
}
},
{
"name": "program",
"address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
},
{
"name": "creator_vault_quote_token_account",
"docs": [
"Deserialized manually in the handler for non-legacy quote mints."
],
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "creator_vault"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "quote_mint"
},
{
"name": "quote_token_program"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
}
],
"args": [
{
"name": "initialize_ata",
"type": "bool"
}
],
"returns": {
"defined": {
"name": "DistributeCreatorFeesEvent"
}
}
},
{
"name": "extend_account",
"docs": ["Extends the size of program-owned accounts"],
@@ -5256,6 +5514,16 @@
"code": 6069,
"name": "QuoteMintNotEligibleForWhitelist",
"msg": "Quote mint cannot be added or removed via whitelist (default or native SOL mint)"
},
{
"code": 6070,
"name": "UnableToDistributeCreatorFeesToUninitializedAccount",
"msg": "Unable to distribute creator fees to uninitialized account"
},
{
"code": 6071,
"name": "MayhemModeQuoteMintNotAllowed",
"msg": "Mayhem mode quote mint not allowed"
}
],
"types": [
@@ -5495,6 +5763,10 @@
{
"name": "creator_fee",
"type": "u64"
},
{
"name": "quote_mint",
"type": "pubkey"
}
]
}
@@ -5563,6 +5835,10 @@
{
"name": "pool",
"type": "pubkey"
},
{
"name": "quote_mint",
"type": "pubkey"
}
]
}
@@ -5695,6 +5971,10 @@
{
"name": "distributed",
"type": "u64"
},
{
"name": "quote_mint",
"type": "pubkey"
}
]
}
@@ -6517,6 +6797,14 @@
{
"name": "total_cashback_claimed",
"type": "u64"
},
{
"name": "stable_cashback_earned",
"type": "u64"
},
{
"name": "total_stable_cashback_claimed",
"type": "u64"
}
]
}
+381 -1
View File
@@ -3965,6 +3965,254 @@
],
"args": []
},
{
"name": "transfer_creator_fees_to_pump_v2",
"discriminator": [
1,
33,
78,
185,
33,
67,
44,
92
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "quote_mint"
},
{
"name": "token_program"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
},
{
"name": "coin_creator"
},
{
"name": "coin_creator_vault_authority",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99,
114,
101,
97,
116,
111,
114,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "coin_creator"
}
]
}
},
{
"name": "coin_creator_vault_ata",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "coin_creator_vault_authority"
},
{
"kind": "account",
"path": "token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "const",
"value": [
140,
151,
37,
143,
78,
36,
137,
241,
187,
61,
16,
41,
20,
142,
13,
131,
11,
90,
19,
153,
218,
255,
16,
132,
4,
142,
123,
216,
219,
233,
248,
89
]
}
}
},
{
"name": "pump_creator_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99,
114,
101,
97,
116,
111,
114,
45,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "coin_creator"
}
],
"program": {
"kind": "const",
"value": [
1,
86,
224,
246,
147,
102,
90,
207,
68,
219,
21,
104,
191,
23,
91,
170,
81,
137,
203,
151,
245,
210,
255,
59,
101,
93,
43,
182,
253,
109,
24,
176
]
}
}
},
{
"name": "pump_creator_vault_ata",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "pump_creator_vault"
},
{
"kind": "account",
"path": "token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": []
},
{
"name": "update_admin",
"discriminator": [
@@ -4027,6 +4275,72 @@
],
"args": []
},
{
"name": "update_buyback_config",
"discriminator": [
251,
224,
171,
146,
160,
26,
113,
233
],
"accounts": [
{
"name": "admin",
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "global_config",
"writable": true
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": [
{
"name": "buyback_basis_points",
"type": {
"option": "u64"
}
}
]
},
{
"name": "update_fee_config",
"discriminator": [
@@ -4835,7 +5149,34 @@
},
{
"code": 6052,
"name": "CashbackEarnedDoesNotMatchTokenInVault"
"name": "TokensInVaultLessThanCashbackEarned"
},
{
"code": 6053,
"name": "BuybackFeeRecipientNotAuthorized",
"msg": "Buyback fee recipient not authorized"
},
{
"code": 6054,
"name": "AllBuybackFeeRecipientsShouldBeNonZero"
},
{
"code": 6055,
"name": "NotUniqueBuybackFeeRecipients"
},
{
"code": 6056,
"name": "BuybackBasisPointsOutOfRange",
"msg": "buyback_basis_points must be <= 10_000"
},
{
"code": 6057,
"name": "WrongBuybackFeeRecipientsCount",
"msg": "buyback fee recipients require exactly 8 remaining accounts (or none)"
},
{
"code": 6058,
"name": "BuybackFeeRecipientMissing"
}
],
"types": [
@@ -5086,6 +5427,14 @@
{
"name": "cashback",
"type": "u64"
},
{
"name": "buyback_fee_basis_points",
"type": "u64"
},
{
"name": "buyback_fee",
"type": "u64"
}
]
}
@@ -5523,6 +5872,16 @@
}
}
}
},
{
"name": "stable_fee_tiers",
"type": {
"vec": {
"defined": {
"name": "FeeTier"
}
}
}
}
]
}
@@ -5646,6 +6005,19 @@
{
"name": "is_cashback_enabled",
"type": "bool"
},
{
"name": "buyback_fee_recipients",
"type": {
"array": [
"pubkey",
8
]
}
},
{
"name": "buyback_basis_points",
"type": "u64"
}
]
}
@@ -5941,6 +6313,14 @@
{
"name": "cashback",
"type": "u64"
},
{
"name": "buyback_fee_basis_points",
"type": "u64"
},
{
"name": "buyback_fee",
"type": "u64"
}
]
}
+3146 -4
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
+84 -18
View File
@@ -14,6 +14,7 @@ use crate::constants::WSOL_TOKEN_ACCOUNT;
use crate::swqos::common::TradeError;
use crate::swqos::SwqosClient;
use crate::swqos::SwqosConfig;
use crate::swqos::SwqosType;
use crate::swqos::TradeType;
use crate::trading::core::params::BonkParams;
use crate::trading::core::params::DexParamEnum;
@@ -49,6 +50,15 @@ fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool {
}
}
#[inline]
fn normalize_swqos_configs(rpc_url: &str, configs: &[SwqosConfig]) -> Vec<SwqosConfig> {
let mut out = configs.to_vec();
if !out.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)) {
out.push(SwqosConfig::Default(rpc_url.to_string()));
}
out
}
/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。
///
/// * `dex_type`PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。
@@ -83,7 +93,7 @@ pub struct TradingInfrastructure {
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
/// Configuration used to create this infrastructure
pub config: InfrastructureConfig,
/// Precomputed at init: min(swqos_clients.len(), 2/3 * num_cores). Not computed on trade hot path.
/// Precomputed at init: min(max SWQOS submit lanes, 2/3 * num_cores). Not computed on trade hot path.
pub max_sender_concurrency: usize,
/// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path.
pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
@@ -133,8 +143,9 @@ impl TradingInfrastructure {
// Create SWQOS clients with blacklist checkingQUIC 握手可能较慢,单节点超时 15s)
const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
let swqos_configs = normalize_swqos_configs(&config.rpc_url, &config.swqos_configs);
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
for swqos in &config.swqos_configs {
for swqos in &swqos_configs {
if swqos.is_blacklisted() {
if sdk_log::sdk_log_enabled() {
warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
@@ -225,11 +236,21 @@ impl TradingInfrastructure {
eprintln!("️ SWQOS 通道已就绪: {} 条 → [{}]", swqos_clients.len(), labels.join(", "));
}
let swqos_count = swqos_clients.len();
let max_submit_lanes = swqos_clients
.iter()
.map(|client| {
if matches!(client.get_swqos_type(), SwqosType::Default) {
1usize
} else {
2usize
}
})
.sum::<usize>()
.max(1);
let (max_sender_concurrency, effective_core_ids) = {
let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0);
let max_by_cores = (num_cores * 2 / 3).max(1);
let cap = swqos_count.min(max_by_cores).max(1);
let cap = max_submit_lanes.min(max_by_cores).max(1);
let ids = core_affinity::get_core_ids()
.map(|all| {
let v: Vec<_> = all.into_iter().collect();
@@ -301,8 +322,6 @@ pub struct TradingClient {
pub log_enabled: bool,
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency.
pub check_min_tip: bool,
/// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27-account metas). Default false.
pub use_pumpfun_v2: bool,
}
static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
@@ -323,7 +342,6 @@ impl Clone for TradingClient {
effective_core_ids: self.effective_core_ids.clone(),
log_enabled: self.log_enabled,
check_min_tip: self.check_min_tip,
use_pumpfun_v2: self.use_pumpfun_v2,
}
}
}
@@ -354,6 +372,11 @@ pub struct TradeBuyParams {
pub address_lookup_table_account: Option<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`.
pub wait_for_all_submits: bool,
/// Whether to create input token associated token account
pub create_input_token_ata: bool,
/// Whether to close input token associated token account after trade
@@ -362,13 +385,14 @@ pub struct TradeBuyParams {
pub create_mint_ata: bool,
/// Durable nonce information
pub durable_nonce: Option<DurableNonceInfo>,
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
/// Optional fixed output token amount. On exact-out capable DEXes this uses
/// the exact-out instruction and treats `input_token_amount` as max input.
pub fixed_output_token_amount: Option<u64>,
/// Gas fee strategy
pub gas_fee_strategy: GasFeeStrategy,
/// Whether to simulate the transaction instead of executing it
pub simulate: bool,
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
/// Use exact quote-input buy instructions (legacy PumpFun uses SOL quote; V2/PumpSwap use generic quote).
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
@@ -405,6 +429,11 @@ pub struct TradeSellParams {
pub address_lookup_table_account: Option<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`.
pub wait_for_all_submits: bool,
/// Whether to create output token associated token account
pub create_output_token_ata: bool,
/// Whether to close output token associated token account after trade
@@ -413,7 +442,8 @@ pub struct TradeSellParams {
pub close_mint_token_ata: bool,
/// Durable nonce information
pub durable_nonce: Option<DurableNonceInfo>,
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
/// Optional fixed output token amount. On exact-out capable DEXes this uses
/// the exact-out instruction and treats `input_token_amount` as max input.
pub fixed_output_token_amount: Option<u64>,
/// Gas fee strategy
pub gas_fee_strategy: GasFeeStrategy,
@@ -458,7 +488,6 @@ impl TradingClient {
effective_core_ids,
log_enabled: true,
check_min_tip: false,
use_pumpfun_v2: false,
}
}
@@ -505,7 +534,6 @@ impl TradingClient {
effective_core_ids,
log_enabled: true,
check_min_tip: false,
use_pumpfun_v2: false,
}
}
@@ -685,7 +713,6 @@ impl TradingClient {
effective_core_ids: infrastructure.effective_core_ids.clone(),
log_enabled: trade_config.log_enabled,
check_min_tip: trade_config.check_min_tip,
use_pumpfun_v2: trade_config.use_pumpfun_v2,
};
let mut current = INSTANCE.lock();
@@ -711,7 +738,7 @@ impl TradingClient {
/// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores).
/// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs.
/// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores).
/// Concurrency and core count are capped internally (≤ max submit lanes, ≤ 2/3 of CPU cores).
/// - `None`: keep default (shared tokio pool).
/// - `Some(vec![])`: dedicated threads with default count, no core pinning.
/// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap).
@@ -735,6 +762,12 @@ impl TradingClient {
Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v }));
}
}
if self.use_dedicated_sender_threads {
crate::trading::core::async_executor::warm_dedicated_sender_pool(
self.sender_thread_cores.as_ref().map(|v| v.as_slice()),
self.max_sender_concurrency,
);
}
self
}
@@ -861,6 +894,7 @@ impl TradingClient {
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
log_enabled: self.log_enabled,
wait_for_all_submits: params.wait_for_all_submits,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
@@ -868,12 +902,15 @@ impl TradingClient {
check_min_tip: self.check_min_tip,
grpc_recv_us: params.grpc_recv_us,
use_exact_sol_amount: params.use_exact_sol_amount,
use_pumpfun_v2: self.use_pumpfun_v2,
};
let swap_result = executor.swap(buy_params).await;
let result = swap_result.map(|(success, sigs, err, timings)| {
(success, sigs, err.map(TradeError::from), timings)
let legacy_timings = timings
.into_iter()
.map(|timing| (timing.swqos_type, timing.submit_done_us))
.collect();
(success, sigs, err.map(TradeError::from), legacy_timings)
});
result
}
@@ -973,6 +1010,7 @@ impl TradingClient {
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
log_enabled: self.log_enabled,
wait_for_all_submits: params.wait_for_all_submits,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
@@ -980,12 +1018,15 @@ impl TradingClient {
check_min_tip: self.check_min_tip,
grpc_recv_us: params.grpc_recv_us,
use_exact_sol_amount: None,
use_pumpfun_v2: self.use_pumpfun_v2,
};
let swap_result = executor.swap(sell_params).await;
let result = swap_result.map(|(success, sigs, err, timings)| {
(success, sigs, err.map(TradeError::from), timings)
let legacy_timings = timings
.into_iter()
.map(|timing| (timing.swqos_type, timing.submit_done_us))
.collect();
(success, sigs, err.map(TradeError::from), legacy_timings)
});
result
}
@@ -1238,3 +1279,28 @@ impl TradingClient {
Ok(signature.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::swqos::SwqosRegion;
#[test]
fn normalize_swqos_configs_adds_default_rpc_route() {
let configs = vec![SwqosConfig::Jito("uuid".to_string(), SwqosRegion::Frankfurt, None)];
let normalized = normalize_swqos_configs("https://rpc.example", &configs);
assert_eq!(normalized.len(), 2);
assert!(normalized.iter().any(|c| matches!(c.swqos_type(), SwqosType::Jito)));
assert!(normalized.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)));
}
#[test]
fn normalize_swqos_configs_does_not_duplicate_default_rpc_route() {
let configs = vec![SwqosConfig::Default("https://rpc.example".to_string())];
let normalized = normalize_swqos_configs("https://rpc.example", &configs);
assert_eq!(normalized.len(), 1);
assert!(matches!(normalized[0].swqos_type(), SwqosType::Default));
}
}
+108 -5
View File
@@ -31,7 +31,7 @@ use solana_sdk::pubkey::Pubkey;
use crate::instruction::utils::pumpfun::global_constants::{
INITIAL_REAL_TOKEN_RESERVES, INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES,
TOKEN_TOTAL_SUPPLY,
INITIAL_VIRTUAL_USDC_RESERVES, TOKEN_TOTAL_SUPPLY,
};
use crate::instruction::utils::pumpfun::{get_bonding_curve_pda, get_creator_vault_pda};
@@ -62,9 +62,57 @@ pub struct BondingCurveAccount {
pub is_mayhem_mode: bool,
/// Whether this coin has cashback enabled (creator fee redirected to users)
pub is_cashback_coin: bool,
/// Quote mint for V2 curves. Defaults to WSOL for legacy/event payloads that do not expose it.
pub quote_mint: Pubkey,
}
impl BondingCurveAccount {
#[inline]
pub fn normalize_quote_mint(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
crate::constants::WSOL_TOKEN_ACCOUNT
} else {
quote_mint
}
}
#[inline]
pub fn effective_quote_mint(&self) -> Pubkey {
Self::normalize_quote_mint(self.quote_mint)
}
#[inline]
pub fn initial_virtual_quote_reserves_for_quote_mint(quote_mint: &Pubkey) -> u64 {
if *quote_mint == crate::constants::USDC_TOKEN_ACCOUNT {
INITIAL_VIRTUAL_USDC_RESERVES
} else {
INITIAL_VIRTUAL_SOL_RESERVES
}
}
#[inline]
pub fn virtual_quote_reserves(&self) -> u64 {
self.virtual_sol_reserves
}
#[inline]
pub fn real_quote_reserves(&self) -> u64 {
self.real_sol_reserves
}
#[inline]
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
let quote_mint = Self::normalize_quote_mint(quote_mint);
let old_initial =
Self::initial_virtual_quote_reserves_for_quote_mint(&self.effective_quote_mint());
let new_initial = Self::initial_virtual_quote_reserves_for_quote_mint(&quote_mint);
if self.virtual_sol_reserves == old_initial.saturating_add(self.real_sol_reserves) {
self.virtual_sol_reserves = new_initial.saturating_add(self.real_sol_reserves);
}
self.quote_mint = quote_mint;
self
}
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
pub fn from_dev_trade(
@@ -75,24 +123,50 @@ impl BondingCurveAccount {
creator: Pubkey,
is_mayhem_mode: bool,
is_cashback_coin: bool,
) -> Self {
Self::from_dev_trade_with_quote_mint(
bonding_curve,
mint,
dev_token_amount,
dev_sol_amount,
creator,
is_mayhem_mode,
is_cashback_coin,
crate::constants::WSOL_TOKEN_ACCOUNT,
)
}
/// Same as [`Self::from_dev_trade`], but quote-aware for V2 pools such as USDC.
pub fn from_dev_trade_with_quote_mint(
bonding_curve: Pubkey,
mint: &Pubkey,
dev_token_amount: u64,
dev_quote_amount: u64,
creator: Pubkey,
is_mayhem_mode: bool,
is_cashback_coin: bool,
quote_mint: Pubkey,
) -> Self {
let account = if bonding_curve != Pubkey::default() {
bonding_curve
} else {
get_bonding_curve_pda(&mint).unwrap()
};
let quote_mint = Self::normalize_quote_mint(quote_mint);
Self {
discriminator: 0,
account: account,
virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_token_amount,
virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_sol_amount,
virtual_sol_reserves: Self::initial_virtual_quote_reserves_for_quote_mint(&quote_mint)
+ dev_quote_amount,
real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_token_amount,
real_sol_reserves: dev_sol_amount,
real_sol_reserves: dev_quote_amount,
token_total_supply: TOKEN_TOTAL_SUPPLY,
complete: false,
creator: creator,
is_mayhem_mode: is_mayhem_mode,
is_cashback_coin,
quote_mint,
}
}
@@ -108,24 +182,53 @@ impl BondingCurveAccount {
real_sol_reserves: u64,
is_mayhem_mode: bool,
is_cashback_coin: bool,
) -> Self {
Self::from_trade_with_quote_mint(
bonding_curve,
mint,
creator,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
real_sol_reserves,
is_mayhem_mode,
is_cashback_coin,
crate::constants::WSOL_TOKEN_ACCOUNT,
)
}
/// Same as [`Self::from_trade`], but carries the V2 quote mint alongside quote reserves.
pub fn from_trade_with_quote_mint(
bonding_curve: Pubkey,
mint: Pubkey,
creator: Pubkey,
virtual_token_reserves: u64,
virtual_quote_reserves: u64,
real_token_reserves: u64,
real_quote_reserves: u64,
is_mayhem_mode: bool,
is_cashback_coin: bool,
quote_mint: Pubkey,
) -> Self {
let account = if bonding_curve != Pubkey::default() {
bonding_curve
} else {
get_bonding_curve_pda(&mint).unwrap()
};
let quote_mint = Self::normalize_quote_mint(quote_mint);
Self {
discriminator: 0,
account: account,
virtual_token_reserves: virtual_token_reserves,
virtual_sol_reserves: virtual_sol_reserves,
virtual_sol_reserves: virtual_quote_reserves,
real_token_reserves: real_token_reserves,
real_sol_reserves: real_sol_reserves,
real_sol_reserves: real_quote_reserves,
token_total_supply: TOKEN_TOTAL_SUPPLY,
complete: false,
creator: creator,
is_mayhem_mode: is_mayhem_mode,
is_cashback_coin,
quote_mint,
}
}
+64 -25
View File
@@ -4,7 +4,7 @@ use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
use crate::common::{
spl_associated_token_account::get_associated_token_address_with_program_id,
@@ -21,6 +21,22 @@ const MAX_PDA_CACHE_SIZE: usize = 100_000;
const MAX_ATA_CACHE_SIZE: usize = 100_000;
const MAX_INSTRUCTION_CACHE_SIZE: usize = 100_000;
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
// --------------------- Instruction Cache ---------------------
/// Instruction cache key for uniquely identifying instruction types and parameters
@@ -66,7 +82,10 @@ where
};
// Lock-free cache lookup with entry API
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
let instructions =
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone();
prune_cache(&INSTRUCTION_CACHE, MAX_INSTRUCTION_CACHE_SIZE);
instructions
}
// --------------------- Associated Token Account ---------------------
@@ -106,8 +125,26 @@ pub fn _create_associated_token_account_idempotent_fast(
use_seed,
};
// Only use seed if the mint address is not wSOL or SOL
// 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全)
let build_standard_create = || {
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
};
// Only use seed if the mint address is not wSOL or SOL.
// Seed accounts are deterministic token accounts, not ATAs; callers must ensure they are not
// recreating an existing seeded account. Fall back to idempotent ATA if seed assembly fails.
let arc_instructions = if use_seed
&& !mint.eq(&crate::constants::WSOL_TOKEN_ACCOUNT)
&& !mint.eq(&crate::constants::SOL_TOKEN_ACCOUNT)
@@ -117,29 +154,18 @@ pub fn _create_associated_token_account_idempotent_fast(
// Use cache to get instruction
get_cached_instructions(cache_key, || {
super::seed::create_associated_token_account_use_seed(payer, owner, mint, token_program)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account setup failed for mint {}: {}; fallback to idempotent ATA",
mint,
err
);
build_standard_create()
})
})
} else {
// Use cache to get instruction
get_cached_instructions(cache_key, || {
// Get Associated Token Address using cache
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
// Create Associated Token Account instruction
// Reference implementation of spl_associated_token_account::instruction::create_associated_token_account
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
})
get_cached_instructions(cache_key, build_standard_create)
};
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
@@ -183,6 +209,7 @@ where
if let Some(pda) = pda_result {
PDA_CACHE.insert(cache_key, pda);
prune_cache(&PDA_CACHE, MAX_PDA_CACHE_SIZE);
}
pda_result
@@ -265,7 +292,18 @@ fn _get_associated_token_address_with_program_id_fast(
token_mint_address,
token_program_id,
)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account address failed for mint {}: {}; fallback to ATA",
token_mint_address,
err
);
get_associated_token_address_with_program_id(
wallet_address,
token_mint_address,
token_program_id,
)
})
} else {
get_associated_token_address_with_program_id(
wallet_address,
@@ -276,6 +314,7 @@ fn _get_associated_token_address_with_program_id_fast(
// Store computation result in cache (lock-free)
ATA_CACHE.insert(cache_key, ata);
prune_cache(&ATA_CACHE, MAX_ATA_CACHE_SIZE);
ata
}
+12 -7
View File
@@ -141,8 +141,8 @@ mod tests {
std::thread::sleep(Duration::from_millis(10));
let elapsed = fast_elapsed_nanos(start);
// 应该大约是 10ms = 10,000,000 纳秒
assert!(elapsed >= 9_000_000 && elapsed <= 12_000_000);
// Scheduler jitter can exceed the sleep duration; the fast timer must not under-report.
assert!(elapsed >= 9_000_000);
}
#[test]
@@ -151,13 +151,18 @@ mod tests {
std::thread::sleep(Duration::from_millis(10));
let elapsed_ms = sw.elapsed_millis();
assert!(elapsed_ms >= 9 && elapsed_ms <= 12);
assert!(elapsed_ms >= 9);
}
#[test]
fn test_fast_now_overhead() {
// 测试调用开销
let iterations = 10_000;
// This is a coarse regression guard, not a benchmark. On non-Linux targets
// the fast timer uses `Instant::elapsed()`, and CI/desktop scheduler jitter can
// move a single run above 100ns even when the code path is still sub-microsecond.
let iterations = 100_000;
for _ in 0..1_000 {
let _ = fast_now_nanos();
}
let start = Instant::now();
for _ in 0..iterations {
@@ -171,8 +176,8 @@ mod tests {
println!("Average fast_now_nanos() call: {}ns", avg_per_call);
}
// 快速时间戳应该非常快(< 100ns per call
assert!(avg_per_call < 100);
// Keep the hot-path timer safely sub-microsecond without making the test flaky.
assert!(avg_per_call < 500);
}
#[test]
+179 -10
View File
@@ -1,6 +1,6 @@
use crate::swqos::{SwqosType, TradeType};
use arc_swap::ArcSwap;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -14,6 +14,15 @@ impl GasFeeStrategyType {
pub fn values() -> Vec<Self> {
vec![Self::Normal, Self::LowTipHighCuPrice, Self::HighTipLowCuPrice]
}
#[inline]
pub fn as_str(&self) -> &'static str {
match self {
Self::Normal => "Normal",
Self::LowTipHighCuPrice => "LowTipHighCuPrice",
Self::HighTipLowCuPrice => "HighTipLowCuPrice",
}
}
}
#[derive(Debug, Clone, Copy)]
@@ -84,6 +93,33 @@ impl GasFeeStrategy {
);
}
/// 设置 Default/RPC 的优先费-only 策略。Default 没有 relay tip account
/// 但仍应携带 ComputeBudget 优先费。
pub fn set_default_rpc_fee_strategy(
&self,
buy_cu_limit: u32,
sell_cu_limit: u32,
buy_cu_price: u64,
sell_cu_price: u64,
) {
self.set(
SwqosType::Default,
TradeType::Buy,
GasFeeStrategyType::Normal,
buy_cu_limit,
buy_cu_price,
0.0,
);
self.set(
SwqosType::Default,
TradeType::Sell,
GasFeeStrategyType::Normal,
sell_cu_limit,
sell_cu_price,
0.0,
);
}
/// 为多个服务类型添加高低费率策略,会移除(SwqosType,TradeType)的默认策略。
/// Add high-low fee strategies for multiple service types, Will remove the default strategy of (SwqosType,TradeType)
pub fn set_high_low_fee_strategies(
@@ -271,7 +307,7 @@ impl GasFeeStrategy {
) -> Vec<(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)> {
let strategies = self.strategies.load();
let mut result = Vec::new();
let mut swqos_types = std::collections::HashSet::new();
let mut swqos_types = HashSet::new();
for (swqos_type, t_type, _) in strategies.keys() {
if *t_type == trade_type {
swqos_types.insert(*swqos_type);
@@ -298,10 +334,17 @@ impl GasFeeStrategy {
/// 动态更新买入小费(保持其他参数不变)
/// Dynamically update buy tip (keep other parameters unchanged)
pub fn update_buy_tip(&self, buy_tip: f64) {
self.update_buy_tip_for_strategy(GasFeeStrategyType::Normal, buy_tip);
}
/// 动态更新指定买入策略的小费(保持其他参数不变)。
/// Dynamic updates should generally target Normal only; updating all strategies would
/// collapse the low-tip/high-tip dual-lane spread into the same tip.
pub fn update_buy_tip_for_strategy(&self, strategy_type: GasFeeStrategyType, buy_tip: f64) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy && *s_type == strategy_type {
value.tip = buy_tip;
}
}
@@ -312,10 +355,15 @@ impl GasFeeStrategy {
/// 动态更新卖出小费(保持其他参数不变)
/// Dynamically update sell tip (keep other parameters unchanged)
pub fn update_sell_tip(&self, sell_tip: f64) {
self.update_sell_tip_for_strategy(GasFeeStrategyType::Normal, sell_tip);
}
/// 动态更新指定卖出策略的小费(保持其他参数不变)。
pub fn update_sell_tip_for_strategy(&self, strategy_type: GasFeeStrategyType, sell_tip: f64) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell && *s_type == strategy_type {
value.tip = sell_tip;
}
}
@@ -326,10 +374,19 @@ impl GasFeeStrategy {
/// 动态更新买入优先费(保持其他参数不变)
/// Dynamically update buy compute unit price (keep other parameters unchanged)
pub fn update_buy_cu_price(&self, buy_cu_price: u64) {
self.update_buy_cu_price_for_strategy(GasFeeStrategyType::Normal, buy_cu_price);
}
/// 动态更新指定买入策略的优先费(保持其他参数不变)。
pub fn update_buy_cu_price_for_strategy(
&self,
strategy_type: GasFeeStrategyType,
buy_cu_price: u64,
) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy && *s_type == strategy_type {
value.cu_price = buy_cu_price;
}
}
@@ -340,10 +397,19 @@ impl GasFeeStrategy {
/// 动态更新卖出优先费(保持其他参数不变)
/// Dynamically update sell compute unit price (keep other parameters unchanged)
pub fn update_sell_cu_price(&self, sell_cu_price: u64) {
self.update_sell_cu_price_for_strategy(GasFeeStrategyType::Normal, sell_cu_price);
}
/// 动态更新指定卖出策略的优先费(保持其他参数不变)。
pub fn update_sell_cu_price_for_strategy(
&self,
strategy_type: GasFeeStrategyType,
sell_cu_price: u64,
) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell && *s_type == strategy_type {
value.cu_price = sell_cu_price;
}
}
@@ -365,3 +431,106 @@ impl GasFeeStrategy {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn find_strategy(
strategies: &[(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)],
swqos_type: SwqosType,
strategy_type: GasFeeStrategyType,
) -> GasFeeStrategyValue {
strategies
.iter()
.find(|(s, t, _)| *s == swqos_type && *t == strategy_type)
.map(|(_, _, v)| *v)
.expect("strategy exists")
}
#[test]
fn high_low_fee_strategy_expands_two_lanes_per_swqos() {
let strategy = GasFeeStrategy::new();
strategy.set_high_low_fee_strategies(
&[SwqosType::Jito, SwqosType::Helius],
TradeType::Buy,
100_000,
180_000,
400_000,
0.002,
0.005,
);
let strategies = strategy.get_strategies(TradeType::Buy);
assert_eq!(strategies.len(), 4);
for swqos_type in [SwqosType::Jito, SwqosType::Helius] {
let low_tip_high_cu =
find_strategy(&strategies, swqos_type, GasFeeStrategyType::LowTipHighCuPrice);
assert_eq!(low_tip_high_cu.cu_limit, 100_000);
assert_eq!(low_tip_high_cu.cu_price, 400_000);
assert_eq!(low_tip_high_cu.tip, 0.002);
let high_tip_low_cu =
find_strategy(&strategies, swqos_type, GasFeeStrategyType::HighTipLowCuPrice);
assert_eq!(high_tip_low_cu.cu_limit, 100_000);
assert_eq!(high_tip_low_cu.cu_price, 180_000);
assert_eq!(high_tip_low_cu.tip, 0.005);
}
}
#[test]
fn dynamic_updates_do_not_collapse_dual_lane_fees() {
let strategy = GasFeeStrategy::new();
strategy.set_high_low_fee_strategy(
SwqosType::Jito,
TradeType::Buy,
100_000,
180_000,
400_000,
0.002,
0.005,
);
strategy.update_buy_tip(0.009);
strategy.update_buy_cu_price(999_999);
let strategies = strategy.get_strategies(TradeType::Buy);
let low_tip_high_cu =
find_strategy(&strategies, SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice);
let high_tip_low_cu =
find_strategy(&strategies, SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice);
assert_eq!(low_tip_high_cu.cu_price, 400_000);
assert_eq!(low_tip_high_cu.tip, 0.002);
assert_eq!(high_tip_low_cu.cu_price, 180_000);
assert_eq!(high_tip_low_cu.tip, 0.005);
}
#[test]
fn default_rpc_strategy_uses_priority_fee_without_tip() {
let strategy = GasFeeStrategy::new();
strategy.set_default_rpc_fee_strategy(100_000, 90_000, 700_000, 800_000);
let buy = find_strategy(
&strategy.get_strategies(TradeType::Buy),
SwqosType::Default,
GasFeeStrategyType::Normal,
);
assert_eq!(buy.cu_limit, 100_000);
assert_eq!(buy.cu_price, 700_000);
assert_eq!(buy.tip, 0.0);
let sell = find_strategy(
&strategy.get_strategies(TradeType::Sell),
SwqosType::Default,
GasFeeStrategyType::Normal,
);
assert_eq!(sell.cu_limit, 90_000);
assert_eq!(sell.cu_price, 800_000);
assert_eq!(sell.tip, 0.0);
}
}
+12 -10
View File
@@ -84,7 +84,7 @@ pub fn print_sdk_timing_block(
start_us: Option<i64>,
build_end_us: Option<i64>,
before_submit_us: Option<i64>,
submit_timings: &[(crate::swqos::SwqosType, i64)],
submit_timings: &[crate::common::SwqosSubmitTiming],
confirm_us: Option<i64>,
) {
println!();
@@ -112,13 +112,14 @@ pub fn print_sdk_timing_block(
}
if let Some(confirm_done_us) = confirm_us {
let total_ms = (confirm_done_us - start_us) as f64 / 1000.0;
for (swqos_type, submit_done_us) in submit_timings {
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
let confirmed_ms = (confirm_done_us - *submit_done_us).max(0) as f64 / 1000.0;
for timing in submit_timings {
let submit_ms = (timing.submit_done_us - start_us).max(0) as f64 / 1000.0;
let confirmed_ms = (confirm_done_us - timing.submit_done_us).max(0) as f64 / 1000.0;
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
swqos_type.as_str(),
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
timing.swqos_type.as_str(),
dir,
timing.strategy_type.as_str(),
submit_ms,
confirmed_ms,
total_ms,
@@ -126,12 +127,13 @@ pub fn print_sdk_timing_block(
);
}
} else {
for (swqos_type, submit_done_us) in submit_timings {
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
for timing in submit_timings {
let submit_ms = (timing.submit_done_us - start_us).max(0) as f64 / 1000.0;
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
swqos_type.as_str(),
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
timing.swqos_type.as_str(),
dir,
timing.strategy_type.as_str(),
submit_ms,
submit_ms,
width = SWQOS_LABEL_WIDTH
+31 -34
View File
@@ -1,5 +1,4 @@
use crate::common::SolanaRpcClient;
use anyhow::anyhow;
use fnv::FnvHasher;
use once_cell::sync::Lazy;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
@@ -50,6 +49,26 @@ async fn fetch_rent_for_token_account(
Ok(client.get_minimum_balance_for_rent_exemption(165).await?)
}
#[inline]
fn derive_seed_from_mint(mint: &Pubkey) -> String {
// Keep the legacy 8-hex seed stable. Changing this derivation changes the token account
// address and can strand balances created by earlier buys.
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
let mut seed = String::with_capacity(8);
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
let byte = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
seed.push(byte as char);
}
seed
}
pub fn create_associated_token_account_use_seed(
payer: &Pubkey,
owner: &Pubkey,
@@ -63,33 +82,23 @@ pub fn create_associated_token_account_use_seed(
let rent = if is_2022_token {
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
} else {
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
};
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(mint);
// 🔧 修复:使用传入的 token_program 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program,地址自然一致
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
let ata_like = Pubkey::create_with_seed(payer, &seed, token_program)?;
let len = 165;
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
@@ -98,7 +107,7 @@ pub fn create_associated_token_account_use_seed(
payer,
&ata_like,
payer,
seed,
&seed,
rent,
len,
token_program,
@@ -118,21 +127,9 @@ pub fn get_associated_token_address_with_program_id_use_seed(
token_mint_address: &Pubkey,
token_program_id: &Pubkey,
) -> Result<Pubkey, anyhow::Error> {
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(token_mint_address.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(token_mint_address);
// 🔧 修复:使用传入的 token_program_id 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program_id,地址自然一致
let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program_id)?;
let ata_like = Pubkey::create_with_seed(wallet_address, &seed, token_program_id)?;
Ok(ata_like)
}
+10 -16
View File
@@ -1,4 +1,5 @@
use crate::swqos::SwqosConfig;
use crate::common::GasFeeStrategyType;
use crate::swqos::{SwqosConfig, SwqosType};
use solana_commitment_config::CommitmentConfig;
use std::hash::{Hash, Hasher};
@@ -75,6 +76,13 @@ impl PartialEq for InfrastructureConfig {
impl Eq for InfrastructureConfig {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SwqosSubmitTiming {
pub swqos_type: SwqosType,
pub strategy_type: GasFeeStrategyType,
pub submit_done_us: i64,
}
#[derive(Debug, Clone)]
pub struct TradeConfig {
pub rpc_url: String,
@@ -95,9 +103,6 @@ pub struct TradeConfig {
/// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
pub mev_protection: bool,
/// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27/26-account metas, quote_mint support).
/// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface.
pub use_pumpfun_v2: bool,
}
impl TradeConfig {
@@ -112,7 +117,7 @@ impl TradeConfig {
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
///
/// # Example
/// ```rust
/// ```rust,ignore
/// let config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
/// .mev_protection(true)
/// .check_min_tip(true)
@@ -152,7 +157,6 @@ pub struct TradeConfigBuilder {
check_min_tip: bool,
swqos_cores_from_end: bool,
mev_protection: bool,
use_pumpfun_v2: bool,
}
impl TradeConfigBuilder {
@@ -167,7 +171,6 @@ impl TradeConfigBuilder {
check_min_tip: false,
swqos_cores_from_end: false,
mev_protection: false,
use_pumpfun_v2: false,
}
}
@@ -213,14 +216,6 @@ impl TradeConfigBuilder {
self
}
/// Use PumpFun V2 instructions (`buy_v2` / `sell_v2`, 27-account metas, `quote_mint` support).
/// Default: `false` (V1 — 18-account metas, legacy SOL-paired, smaller transaction).
/// Set to `true` when PumpFun officially deploys V2 on mainnet.
pub fn use_pumpfun_v2(mut self, v: bool) -> Self {
self.use_pumpfun_v2 = v;
self
}
/// Consume the builder and produce a [`TradeConfig`].
pub fn build(self) -> TradeConfig {
TradeConfig {
@@ -233,7 +228,6 @@ impl TradeConfigBuilder {
check_min_tip: self.check_min_tip,
swqos_cores_from_end: self.swqos_cores_from_end,
mev_protection: self.mev_protection,
use_pumpfun_v2: self.use_pumpfun_v2,
}
}
}
+171 -45
View File
@@ -1,8 +1,14 @@
use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::bonk::{
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
SELL_EXECT_IN_DISCRIMINATOR,
instruction::{
token_account_setup::{
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
push_create_user_token_account,
},
utils::bonk::{
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
BUY_EXECT_OUT_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR, SELL_EXECT_OUT_DISCRIMINATOR,
},
},
trading::core::{
params::{BonkParams, SwapParams},
@@ -55,6 +61,11 @@ impl InstructionBuilder for BonkInstructionBuilder {
accounts::GLOBAL_CONFIG_META
};
let quote_mint = if usd1_pool {
crate::constants::USD1_TOKEN_ACCOUNT
} else {
crate::constants::WSOL_TOKEN_ACCOUNT
};
let quote_token_mint = if usd1_pool {
crate::constants::USD1_TOKEN_ACCOUNT_META
} else {
@@ -88,11 +99,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
let user_quote_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if usd1_pool {
&crate::constants::USD1_TOKEN_ACCOUNT
} else {
&crate::constants::WSOL_TOKEN_ACCOUNT
},
&quote_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -103,11 +110,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
protocol_params.base_vault
};
let quote_vault_account = if protocol_params.quote_vault == Pubkey::default() {
if usd1_pool {
get_vault_pda(&pool_state, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
} else {
get_vault_pda(&pool_state, &crate::constants::WSOL_TOKEN_ACCOUNT).unwrap()
}
get_vault_pda(&pool_state, &quote_mint).unwrap()
} else {
protocol_params.quote_vault
};
@@ -117,9 +120,15 @@ impl InstructionBuilder for BonkInstructionBuilder {
// ========================================
let mut instructions = Vec::with_capacity(6);
if params.create_input_mint_ata && !usd1_pool {
instructions
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), amount_in));
if params.create_input_mint_ata {
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&quote_mint,
&crate::constants::TOKEN_PROGRAM,
amount_in,
params.open_seed_optimize,
);
}
if params.create_output_mint_ata {
@@ -135,12 +144,18 @@ impl InstructionBuilder for BonkInstructionBuilder {
}
let mut data = [0u8; 32];
data[..8].copy_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
if let Some(amount_out) = params.fixed_output_amount {
data[..8].copy_from_slice(&BUY_EXECT_OUT_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_out.to_le_bytes());
data[16..24].copy_from_slice(&amount_in.to_le_bytes());
} else {
data[..8].copy_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
}
data[24..32].copy_from_slice(&share_fee_rate.to_le_bytes());
let accounts: [AccountMeta; 18] = [
let accounts: [AccountMeta; 15] = [
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
accounts::AUTHORITY_META, // Authority (readonly)
global_config, // Global Config (readonly)
@@ -156,15 +171,12 @@ impl InstructionBuilder for BonkInstructionBuilder {
crate::constants::TOKEN_PROGRAM_META, // Quote Token Program (readonly)
accounts::EVENT_AUTHORITY_META, // Event Authority (readonly)
accounts::BONK_META, // Program (readonly)
crate::constants::SYSTEM_PROGRAM_META, // System Program (readonly)
AccountMeta::new(protocol_params.platform_associated_account, false), // Platform Associated Account
AccountMeta::new(protocol_params.creator_associated_account, false), // Creator Associated Account
];
instructions.push(Instruction::new_with_bytes(accounts::BONK, &data, accounts.to_vec()));
if params.close_input_mint_ata {
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &quote_mint);
}
Ok(instructions)
@@ -203,6 +215,11 @@ impl InstructionBuilder for BonkInstructionBuilder {
accounts::GLOBAL_CONFIG_META
};
let quote_mint = if usd1_pool {
crate::constants::USD1_TOKEN_ACCOUNT
} else {
crate::constants::WSOL_TOKEN_ACCOUNT
};
let quote_token_mint = if usd1_pool {
crate::constants::USD1_TOKEN_ACCOUNT_META
} else {
@@ -235,11 +252,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
let user_quote_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if usd1_pool {
&crate::constants::USD1_TOKEN_ACCOUNT
} else {
&crate::constants::WSOL_TOKEN_ACCOUNT
},
&quote_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -250,11 +263,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
protocol_params.base_vault
};
let quote_vault_account = if protocol_params.quote_vault == Pubkey::default() {
if usd1_pool {
get_vault_pda(&pool_state, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
} else {
get_vault_pda(&pool_state, &crate::constants::WSOL_TOKEN_ACCOUNT).unwrap()
}
get_vault_pda(&pool_state, &quote_mint).unwrap()
} else {
protocol_params.quote_vault
};
@@ -262,19 +271,31 @@ impl InstructionBuilder for BonkInstructionBuilder {
// ========================================
// Build instructions
// ========================================
let mut instructions = Vec::with_capacity(3);
let mut instructions = Vec::with_capacity(4);
if params.close_output_mint_ata && !usd1_pool {
instructions.extend(crate::trading::common::create_wsol_ata(&params.payer.pubkey()));
if params.create_output_mint_ata {
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&quote_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
}
let mut data = [0u8; 32];
data[..8].copy_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
if let Some(amount_out) = params.fixed_output_amount {
data[..8].copy_from_slice(&SELL_EXECT_OUT_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_out.to_le_bytes());
data[16..24].copy_from_slice(&amount.to_le_bytes());
} else {
data[..8].copy_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
}
data[24..32].copy_from_slice(&share_fee_rate.to_le_bytes());
let accounts: [AccountMeta; 18] = [
let accounts: [AccountMeta; 15] = [
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
accounts::AUTHORITY_META, // Authority (readonly)
global_config, // Global Config (readonly)
@@ -290,15 +311,12 @@ impl InstructionBuilder for BonkInstructionBuilder {
crate::constants::TOKEN_PROGRAM_META, // Quote Token Program (readonly)
accounts::EVENT_AUTHORITY_META, // Event Authority (readonly)
accounts::BONK_META, // Program (readonly)
crate::constants::SYSTEM_PROGRAM_META, // System Program (readonly)
AccountMeta::new(protocol_params.platform_associated_account, false), // Platform Associated Account
AccountMeta::new(protocol_params.creator_associated_account, false), // Creator Associated Account
];
instructions.push(Instruction::new_with_bytes(accounts::BONK, &data, accounts.to_vec()));
if params.close_output_mint_ata {
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &quote_mint);
}
if params.close_input_mint_ata {
instructions.push(crate::common::spl_token::close_account(
@@ -313,3 +331,111 @@ impl InstructionBuilder for BonkInstructionBuilder {
Ok(instructions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{common::GasFeeStrategy, swqos::TradeType, trading::core::params::DexParamEnum};
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
fn pk(seed: u8) -> Pubkey {
Pubkey::new_from_array([seed; 32])
}
fn bonk_params() -> BonkParams {
BonkParams {
mint_token_program: crate::constants::TOKEN_PROGRAM,
platform_config: pk(8),
platform_associated_account: pk(9),
creator_associated_account: pk(10),
global_config: accounts::GLOBAL_CONFIG,
..Default::default()
}
}
fn swap_params(trade_type: TradeType) -> SwapParams {
SwapParams {
rpc: None,
payer: Arc::new(Keypair::new()),
trade_type,
input_mint: pk(3),
input_token_program: None,
output_mint: pk(3),
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::Bonk(bonk_params()),
open_seed_optimize: true,
swqos_clients: Arc::new(Vec::new()),
middleware_manager: None,
durable_nonce: None,
with_tip: false,
create_input_mint_ata: false,
close_input_mint_ata: false,
create_output_mint_ata: false,
close_output_mint_ata: false,
fixed_output_amount: Some(42),
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
effective_core_ids: Arc::new(Vec::new()),
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
}
}
#[tokio::test]
async fn bonk_buy_uses_exact_out_when_fixed_output_is_set() {
let instructions = BonkInstructionBuilder
.build_buy_instructions(&swap_params(TradeType::Buy))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts.len(), 15);
assert_eq!(ix.accounts[14].pubkey, accounts::BONK);
assert_eq!(&ix.data[..8], BUY_EXECT_OUT_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
}
#[tokio::test]
async fn bonk_sell_uses_exact_out_when_fixed_output_is_set() {
let instructions = BonkInstructionBuilder
.build_sell_instructions(&swap_params(TradeType::Sell))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts.len(), 15);
assert_eq!(ix.accounts[14].pubkey, accounts::BONK);
assert_eq!(&ix.data[..8], SELL_EXECT_OUT_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
}
#[tokio::test]
async fn bonk_usd1_buy_create_input_builds_usd1_ata_not_wsol_wrap() {
let mut params = swap_params(TradeType::Buy);
if let DexParamEnum::Bonk(protocol_params) = &mut params.protocol_params {
protocol_params.global_config = accounts::USD1_GLOBAL_CONFIG;
}
params.create_input_mint_ata = true;
params.open_seed_optimize = false;
let instructions = BonkInstructionBuilder.build_buy_instructions(&params).await.unwrap();
let create_ix = instructions.first().unwrap();
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USD1_TOKEN_ACCOUNT);
}
}
+287 -71
View File
@@ -1,5 +1,14 @@
use crate::{
instruction::utils::meteora_damm_v2::{accounts, get_event_authority_pda, SWAP_DISCRIMINATOR},
instruction::{
token_account_setup::{
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
push_create_user_token_account,
},
utils::meteora_damm_v2::{
accounts, get_event_authority_pda, SWAP2_DISCRIMINATOR, SWAP_MODE_EXACT_IN,
SWAP_MODE_EXACT_OUT, SWAP_MODE_PARTIAL_FILL,
},
},
trading::core::{
params::{MeteoraDammV2Params, SwapParams},
traits::InstructionBuilder,
@@ -42,32 +51,43 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
// ========================================
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let input_mint =
if is_a_in { protocol_params.token_a_mint } else { protocol_params.token_b_mint };
let input_token_program =
if is_a_in { protocol_params.token_a_program } else { protocol_params.token_b_program };
let output_mint =
if is_a_in { protocol_params.token_b_mint } else { protocol_params.token_a_mint };
let output_token_program =
if is_a_in { protocol_params.token_b_program } else { protocol_params.token_a_program };
let amount_in: u64 = params.input_amount.unwrap_or(0);
let minimum_amount_out: u64 = match params.fixed_output_amount {
Some(fixed) => fixed,
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
let (amount_0, amount_1) = match protocol_params.swap_mode {
SWAP_MODE_EXACT_OUT => {
let amount_out = params.fixed_output_amount.ok_or_else(|| {
anyhow!("fixed_output_amount must be set for MeteoraDammV2 exact-out swap2")
})?;
(amount_out, amount_in)
}
SWAP_MODE_EXACT_IN | SWAP_MODE_PARTIAL_FILL => {
let minimum_amount_out = params.fixed_output_amount.ok_or_else(|| {
anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap2 min output")
})?;
(amount_in, minimum_amount_out)
}
mode => return Err(anyhow!("Unsupported MeteoraDammV2 swap_mode {}", mode)),
};
let input_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.input_mint,
if is_a_in {
&protocol_params.token_a_program
} else {
&protocol_params.token_b_program
},
&input_mint,
&input_token_program,
params.open_seed_optimize,
);
let output_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.output_mint,
if is_a_in {
&protocol_params.token_b_program
} else {
&protocol_params.token_a_program
},
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
@@ -77,28 +97,32 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
let mut instructions = Vec::with_capacity(6);
if params.create_input_mint_ata {
instructions
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), amount_in));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&input_mint,
&input_token_program,
amount_in,
params.open_seed_optimize,
);
}
if params.create_output_mint_ata {
instructions.extend(
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.output_mint,
if is_a_in {
&protocol_params.token_b_program
} else {
&protocol_params.token_a_program
},
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
}
// Create buy instruction
let accounts: [AccountMeta; 14] = [
let mut account_metas = Vec::with_capacity(
13 + usize::from(protocol_params.referral_token_account.is_some())
+ usize::from(protocol_params.include_rate_limiter_sysvar),
);
account_metas.extend([
accounts::AUTHORITY_META, // Pool Authority (readonly)
AccountMeta::new(protocol_params.pool, false), // Pool
AccountMeta::new(input_token_account, false), // Input Token Account
@@ -110,25 +134,32 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
AccountMeta::new(params.payer.pubkey(), true), // User Transfer Authority
AccountMeta::new_readonly(protocol_params.token_a_program, false), // Token Program (readonly)
AccountMeta::new_readonly(protocol_params.token_b_program, false), // Token Program (readonly)
accounts::METEORA_DAMM_V2_META, // Referral Token Account (readonly)
]);
if let Some(referral_token_account) = protocol_params.referral_token_account {
account_metas.push(AccountMeta::new(referral_token_account, false));
}
account_metas.extend([
AccountMeta::new_readonly(get_event_authority_pda(), false), // Event Authority (readonly)
accounts::METEORA_DAMM_V2_META, // Program (readonly)
];
]);
if protocol_params.include_rate_limiter_sysvar {
account_metas.push(accounts::SYSVAR_INSTRUCTIONS_META);
}
// Create instruction data
let mut data = [0u8; 24];
data[..8].copy_from_slice(&SWAP_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
let mut data = [0u8; 25];
data[..8].copy_from_slice(&SWAP2_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_0.to_le_bytes());
data[16..24].copy_from_slice(&amount_1.to_le_bytes());
data[24] = protocol_params.swap_mode;
instructions.push(Instruction::new_with_bytes(
accounts::METEORA_DAMM_V2,
&data,
accounts.to_vec(),
account_metas,
));
if params.close_input_mint_ata {
// Close wSOL ATA account, reclaim rent
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &input_mint);
}
Ok(instructions)
@@ -161,45 +192,67 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
// ========================================
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let minimum_amount_out: u64 = match params.fixed_output_amount {
Some(fixed) => fixed,
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
let input_mint =
if is_a_in { protocol_params.token_a_mint } else { protocol_params.token_b_mint };
let input_token_program =
if is_a_in { protocol_params.token_a_program } else { protocol_params.token_b_program };
let output_mint =
if is_a_in { protocol_params.token_b_mint } else { protocol_params.token_a_mint };
let output_token_program =
if is_a_in { protocol_params.token_b_program } else { protocol_params.token_a_program };
let amount_in = params.input_amount.unwrap_or(0);
let (amount_0, amount_1) = match protocol_params.swap_mode {
SWAP_MODE_EXACT_OUT => {
let amount_out = params.fixed_output_amount.ok_or_else(|| {
anyhow!("fixed_output_amount must be set for MeteoraDammV2 exact-out swap2")
})?;
(amount_out, amount_in)
}
SWAP_MODE_EXACT_IN | SWAP_MODE_PARTIAL_FILL => {
let minimum_amount_out = params.fixed_output_amount.ok_or_else(|| {
anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap2 min output")
})?;
(amount_in, minimum_amount_out)
}
mode => return Err(anyhow!("Unsupported MeteoraDammV2 swap_mode {}", mode)),
};
let input_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.input_mint,
if is_a_in {
&protocol_params.token_a_program
} else {
&protocol_params.token_b_program
},
&input_mint,
&input_token_program,
params.open_seed_optimize,
);
let output_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.output_mint,
if is_a_in {
&protocol_params.token_b_program
} else {
&protocol_params.token_a_program
},
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
// ========================================
// Build instructions
// ========================================
let mut instructions = Vec::with_capacity(3);
let mut instructions = Vec::with_capacity(4);
if params.create_output_mint_ata {
instructions.extend(crate::trading::common::create_wsol_ata(&params.payer.pubkey()));
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
}
// Create buy instruction
let accounts: [AccountMeta; 14] = [
let mut account_metas = Vec::with_capacity(
13 + usize::from(protocol_params.referral_token_account.is_some())
+ usize::from(protocol_params.include_rate_limiter_sysvar),
);
account_metas.extend([
accounts::AUTHORITY_META, // Pool Authority (readonly)
AccountMeta::new(protocol_params.pool, false), // Pool
AccountMeta::new(input_token_account, false), // Input Token Account
@@ -211,32 +264,36 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
AccountMeta::new(params.payer.pubkey(), true), // User Transfer Authority
AccountMeta::new_readonly(protocol_params.token_a_program, false), // Token Program (readonly)
AccountMeta::new_readonly(protocol_params.token_b_program, false), // Token Program (readonly)
accounts::METEORA_DAMM_V2_META, // Referral Token Account (readonly)
]);
if let Some(referral_token_account) = protocol_params.referral_token_account {
account_metas.push(AccountMeta::new(referral_token_account, false));
}
account_metas.extend([
AccountMeta::new_readonly(get_event_authority_pda(), false), // Event Authority (readonly)
accounts::METEORA_DAMM_V2_META, // Program (readonly)
];
]);
if protocol_params.include_rate_limiter_sysvar {
account_metas.push(accounts::SYSVAR_INSTRUCTIONS_META);
}
// Create instruction data
let mut data = [0u8; 24];
data[..8].copy_from_slice(&SWAP_DISCRIMINATOR);
data[8..16].copy_from_slice(&params.input_amount.unwrap_or_default().to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
let mut data = [0u8; 25];
data[..8].copy_from_slice(&SWAP2_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_0.to_le_bytes());
data[16..24].copy_from_slice(&amount_1.to_le_bytes());
data[24] = protocol_params.swap_mode;
instructions.push(Instruction::new_with_bytes(
accounts::METEORA_DAMM_V2,
&data,
accounts.to_vec(),
account_metas,
));
if params.close_output_mint_ata {
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &output_mint);
}
if params.close_input_mint_ata {
instructions.push(crate::common::spl_token::close_account(
if is_a_in {
&protocol_params.token_a_program
} else {
&protocol_params.token_b_program
},
&input_token_program,
&input_token_account,
&params.payer.pubkey(),
&params.payer.pubkey(),
@@ -247,3 +304,162 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
Ok(instructions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
common::GasFeeStrategy,
swqos::TradeType,
trading::core::params::{DexParamEnum, SwapParams},
};
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
fn pk(seed: u8) -> Pubkey {
Pubkey::new_from_array([seed; 32])
}
fn meteora_params(referral: Option<Pubkey>) -> MeteoraDammV2Params {
let params = MeteoraDammV2Params::new(
pk(1),
pk(2),
pk(3),
crate::constants::WSOL_TOKEN_ACCOUNT,
pk(4),
crate::constants::TOKEN_PROGRAM,
crate::constants::TOKEN_PROGRAM,
);
match referral {
Some(account) => params.with_referral_token_account(account),
None => params,
}
}
fn swap_params(protocol_params: MeteoraDammV2Params) -> SwapParams {
SwapParams {
rpc: None,
payer: Arc::new(Keypair::new()),
trade_type: TradeType::Buy,
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
input_token_program: None,
output_mint: pk(4),
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::MeteoraDammV2(protocol_params),
open_seed_optimize: true,
swqos_clients: Arc::new(Vec::new()),
middleware_manager: None,
durable_nonce: None,
with_tip: false,
create_input_mint_ata: false,
close_input_mint_ata: false,
create_output_mint_ata: false,
close_output_mint_ata: false,
fixed_output_amount: Some(1),
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
effective_core_ids: Arc::new(Vec::new()),
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
}
}
#[tokio::test]
async fn meteora_omits_optional_referral_account() {
let instructions = MeteoraDammV2InstructionBuilder
.build_buy_instructions(&swap_params(meteora_params(None)))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts.len(), 13);
assert_eq!(ix.accounts[11].pubkey, get_event_authority_pda());
assert_eq!(ix.accounts[12].pubkey, accounts::METEORA_DAMM_V2);
assert_eq!(&ix.data[..8], SWAP2_DISCRIMINATOR);
assert_eq!(ix.data[24], SWAP_MODE_PARTIAL_FILL);
}
#[tokio::test]
async fn meteora_includes_writable_referral_account_when_set() {
let referral = pk(9);
let instructions = MeteoraDammV2InstructionBuilder
.build_buy_instructions(&swap_params(meteora_params(Some(referral))))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts.len(), 14);
assert_eq!(ix.accounts[11].pubkey, referral);
assert!(ix.accounts[11].is_writable);
assert_eq!(ix.accounts[12].pubkey, get_event_authority_pda());
assert_eq!(ix.accounts[13].pubkey, accounts::METEORA_DAMM_V2);
}
#[tokio::test]
async fn meteora_includes_sysvar_only_when_rate_limiter_is_set() {
let protocol_params = meteora_params(None).with_rate_limiter_sysvar(true);
let instructions = MeteoraDammV2InstructionBuilder
.build_buy_instructions(&swap_params(protocol_params))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts.len(), 14);
assert_eq!(ix.accounts[11].pubkey, get_event_authority_pda());
assert_eq!(ix.accounts[12].pubkey, accounts::METEORA_DAMM_V2);
assert_eq!(ix.accounts[13].pubkey, accounts::SYSVAR_INSTRUCTIONS);
}
#[tokio::test]
async fn meteora_swap2_exact_out_uses_amount_out_then_max_input() {
let protocol_params = meteora_params(None).with_swap_mode(SWAP_MODE_EXACT_OUT);
let instructions = MeteoraDammV2InstructionBuilder
.build_buy_instructions(&swap_params(protocol_params))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], SWAP2_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 1);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
assert_eq!(ix.data[24], SWAP_MODE_EXACT_OUT);
}
#[tokio::test]
async fn meteora_sol_buy_uses_pool_wsol_mint_for_user_input_account() {
let mut params = swap_params(meteora_params(None));
params.input_mint = crate::constants::SOL_TOKEN_ACCOUNT;
let instructions =
MeteoraDammV2InstructionBuilder.build_buy_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
let expected_wsol_ata =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&crate::constants::WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
let wrong_sol_ata =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&crate::constants::SOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
assert_eq!(ix.accounts[2].pubkey, expected_wsol_ata);
assert_ne!(ix.accounts[2].pubkey, wrong_sol_ata);
}
}
+1
View File
@@ -6,4 +6,5 @@ pub mod pumpswap;
pub(crate) mod pumpswap_ix_data;
pub mod raydium_amm_v4;
pub mod raydium_cpmm;
pub(crate) mod token_account_setup;
pub mod utils;
+663 -129
View File
File diff suppressed because it is too large Load Diff
+78 -64
View File
@@ -1,8 +1,9 @@
//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` 与 **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`**
//! instruction data 栈上编码(热路径零堆分配)。
//! Pump.fun bonding-curve `buy` / `buy_exact_quote_in` / `sell`
//! instruction data stack encoding. The public helper names are version-neutral;
//! [`PumpFunIxVersion`] selects the legacy or V2 on-chain discriminator.
//!
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 在 ix 参数中为
//! **1 字节 option tag + 1 字节值**Anchor `Option<bool>` = 2 字节),共 26 字节 ix data。
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 是单字段
//! structTypeScript 传 `[true]`),在 ix 参数中为 1 字节 bool,共 25 字节 ix data。
//! `*_v2` 指令无 `track_volume` 字节(见 [pump-public-docs](https://github.com/pump-fun/pump-public-docs))。
use crate::instruction::utils::pumpfun::{
@@ -10,77 +11,90 @@ use crate::instruction::utils::pumpfun::{
BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR,
};
/// 与官方 `getBuyInstructionInternal` 对齐:`OptionBool` 在 ix 中为 1 字节 option tag + 1 字节值。
/// `track_volume` 在调用侧组合为 `(1u8, cashback as u8)`Option tag + value),共 26 字节 ix data。
pub const TRACK_VOLUME_OPTION_TAG: u8 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PumpFunIxVersion {
Legacy { track_volume: u8 },
V2,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PumpFunIxData {
Bytes25([u8; 25]),
Bytes24([u8; 24]),
}
impl PumpFunIxData {
#[inline(always)]
pub(crate) fn as_slice(&self) -> &[u8] {
match self {
Self::Bytes25(data) => data,
Self::Bytes24(data) => data,
}
}
}
#[inline(always)]
pub fn encode_pumpfun_buy_ix_data(
pub(crate) fn encode_pumpfun_buy_ix_data(
token_amount: u64,
max_sol_cost: u64,
track_volume_val: u8,
) -> [u8; 26] {
let mut d = [0u8; 26];
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
d[24] = TRACK_VOLUME_OPTION_TAG;
d[25] = track_volume_val;
d
max_quote_cost: u64,
version: PumpFunIxVersion,
) -> PumpFunIxData {
match version {
PumpFunIxVersion::Legacy { track_volume } => {
let mut d = [0u8; 25];
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
d[16..24].copy_from_slice(&max_quote_cost.to_le_bytes());
d[24] = track_volume;
PumpFunIxData::Bytes25(d)
}
PumpFunIxVersion::V2 => {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&BUY_V2_DISCRIMINATOR);
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
d[16..24].copy_from_slice(&max_quote_cost.to_le_bytes());
PumpFunIxData::Bytes24(d)
}
}
}
#[inline(always)]
pub fn encode_pumpfun_buy_exact_sol_in_ix_data(
spendable_sol_in: u64,
min_tokens_out: u64,
track_volume_val: u8,
) -> [u8; 26] {
let mut d = [0u8; 26];
d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes());
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
d[24] = TRACK_VOLUME_OPTION_TAG;
d[25] = track_volume_val;
d
}
#[inline(always)]
pub fn encode_pumpfun_sell_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
d
}
// --- v2 instruction data encoders (no track_volume arg — 2 args each, 24 bytes total) ---
#[inline(always)]
pub fn encode_pumpfun_buy_v2_ix_data(amount: u64, max_sol_cost: u64) -> [u8; 24] {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&BUY_V2_DISCRIMINATOR);
d[8..16].copy_from_slice(&amount.to_le_bytes());
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
d
}
#[inline(always)]
pub fn encode_pumpfun_buy_exact_quote_in_v2_ix_data(
pub(crate) fn encode_pumpfun_buy_exact_quote_in_ix_data(
spendable_quote_in: u64,
min_tokens_out: u64,
) -> [u8; 24] {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR);
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
d
version: PumpFunIxVersion,
) -> PumpFunIxData {
match version {
PumpFunIxVersion::Legacy { track_volume } => {
let mut d = [0u8; 25];
d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
d[24] = track_volume;
PumpFunIxData::Bytes25(d)
}
PumpFunIxVersion::V2 => {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR);
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
PumpFunIxData::Bytes24(d)
}
}
}
#[inline(always)]
pub fn encode_pumpfun_sell_v2_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
pub(crate) fn encode_pumpfun_sell_ix_data(
token_amount: u64,
min_quote_output: u64,
version: PumpFunIxVersion,
) -> PumpFunIxData {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&SELL_V2_DISCRIMINATOR);
d[..8].copy_from_slice(match version {
PumpFunIxVersion::Legacy { .. } => &SELL_DISCRIMINATOR,
PumpFunIxVersion::V2 => &SELL_V2_DISCRIMINATOR,
});
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
d
d[16..24].copy_from_slice(&min_quote_output.to_le_bytes());
PumpFunIxData::Bytes24(d)
}
+206 -43
View File
@@ -4,18 +4,21 @@ use crate::{
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
},
instruction::utils::pumpswap::{
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
get_user_volume_accumulator_wsol_ata, warm_pumpswap_global_config,
},
trading::{
common::wsol_manager,
core::{
params::{PumpSwapParams, SwapParams},
traits::InstructionBuilder,
instruction::{
token_account_setup::{
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
push_create_user_token_account,
},
utils::pumpswap::{
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
get_user_volume_accumulator_wsol_ata,
},
},
trading::core::{
params::{PumpSwapParams, SwapParams},
traits::InstructionBuilder,
},
utils::calc::pumpswap::{buy_quote_input_internal, sell_base_input_internal},
};
@@ -32,7 +35,6 @@ pub struct PumpSwapInstructionBuilder;
#[async_trait::async_trait]
impl InstructionBuilder for PumpSwapInstructionBuilder {
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
warm_pumpswap_global_config(params.rpc.as_ref()).await;
// ========================================
// Parameter validation and basic data preparation
// ========================================
@@ -53,7 +55,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
let create_wsol_ata = params.create_input_mint_ata;
let create_input_ata = params.create_input_mint_ata;
let close_wsol_ata = params.close_input_mint_ata;
let base_token_program = protocol_params.base_token_program;
let quote_token_program = protocol_params.quote_token_program;
@@ -77,13 +79,21 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
// ========================================
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let input_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
let input_stable_token_program =
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
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 (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
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(
params.input_amount.unwrap_or(0),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
@@ -109,10 +119,6 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
(result.min_quote, params.input_amount.unwrap_or(0))
};
if params.fixed_output_amount.is_some() {
token_amount = params.fixed_output_amount.unwrap();
}
let user_base_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
@@ -143,7 +149,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
// ========================================
let mut instructions = Vec::with_capacity(6);
if create_wsol_ata {
if create_input_ata {
// Determine wrap amount based on instruction type:
// - buy_exact_quote_in: program spends exactly input_amount, wrap input_amount
// - buy: program may spend up to max_quote, wrap max_quote
@@ -153,19 +159,23 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
} else {
sol_amount
};
instructions
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), wrap_amount));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&input_stable_mint,
&input_stable_token_program,
wrap_amount,
params.open_seed_optimize,
);
}
if params.create_output_mint_ata {
instructions.extend(
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&params.payer.pubkey(),
&params.payer.pubkey(),
if quote_is_wsol_or_usdc { &base_mint } else { &quote_mint },
if quote_is_wsol_or_usdc { &base_token_program } else { &quote_token_program },
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_trade_mint,
&output_trade_token_program,
params.open_seed_optimize,
);
}
@@ -225,7 +235,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
if quote_is_wsol_or_usdc {
let ix_data = if params.use_exact_sol_amount.unwrap_or(true) {
let ix_data = if params.fixed_output_amount.is_some() {
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
} else if params.use_exact_sol_amount.unwrap_or(true) {
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
token_amount,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
@@ -252,14 +264,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
));
}
if close_wsol_ata {
// Close wSOL ATA account, reclaim rent
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(
&mut instructions,
&params.payer.pubkey(),
&input_stable_mint,
);
}
Ok(instructions)
}
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
warm_pumpswap_global_config(params.rpc.as_ref()).await;
// ========================================
// Parameter validation and basic data preparation
// ========================================
@@ -278,7 +292,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let pool_quote_token_account = protocol_params.pool_quote_token_account;
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
let create_wsol_ata = params.create_output_mint_ata;
let create_output_ata = params.create_output_mint_ata;
let close_wsol_ata = params.close_output_mint_ata;
let base_token_program = protocol_params.base_token_program;
let quote_token_program = protocol_params.quote_token_program;
@@ -304,13 +318,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
// ========================================
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
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 (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
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(
params.input_amount.unwrap(),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
@@ -336,10 +355,6 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
(result.max_quote, result.base)
};
if params.fixed_output_amount.is_some() {
sol_amount = params.fixed_output_amount.unwrap();
}
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
let is_mayhem_mode = protocol_params.is_mayhem_mode;
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
@@ -368,10 +383,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
// ========================================
// Build instructions
// ========================================
let mut instructions = Vec::with_capacity(3);
let mut instructions = Vec::with_capacity(4);
if create_wsol_ata {
instructions.extend(wsol_manager::create_wsol_ata(&params.payer.pubkey()));
if create_output_ata {
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_stable_mint,
&output_stable_token_program,
params.open_seed_optimize,
);
}
// Create sell instruction
@@ -442,7 +463,11 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
if close_wsol_ata {
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(
&mut instructions,
&params.payer.pubkey(),
&output_stable_mint,
);
}
if params.close_input_mint_ata {
instructions.push(crate::common::spl_token::close_account(
@@ -495,3 +520,141 @@ pub fn claim_cashback_pumpswap_instruction(
accounts,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
common::GasFeeStrategy,
swqos::TradeType,
trading::core::params::{DexParamEnum, PumpSwapParams, SwapParams},
};
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
fn pk(seed: u8) -> Pubkey {
Pubkey::new_from_array([seed; 32])
}
fn pumpswap_params() -> PumpSwapParams {
PumpSwapParams::new(
pk(1),
pk(2),
crate::constants::WSOL_TOKEN_ACCOUNT,
pk(3),
pk(4),
1_000_000_000,
2_000_000_000,
pk(5),
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
crate::constants::TOKEN_PROGRAM,
crate::constants::TOKEN_PROGRAM,
accounts::PROTOCOL_FEE_RECIPIENT,
Pubkey::default(),
false,
0,
)
}
fn swap_params(trade_type: TradeType, fixed_output_amount: Option<u64>) -> SwapParams {
let (input_mint, output_mint) = if trade_type == TradeType::Sell {
(pk(2), crate::constants::WSOL_TOKEN_ACCOUNT)
} else {
(crate::constants::WSOL_TOKEN_ACCOUNT, pk(2))
};
SwapParams {
rpc: None,
payer: Arc::new(Keypair::new()),
trade_type,
input_mint,
input_token_program: None,
output_mint,
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::PumpSwap(pumpswap_params()),
open_seed_optimize: true,
swqos_clients: Arc::new(Vec::new()),
middleware_manager: None,
durable_nonce: None,
with_tip: false,
create_input_mint_ata: false,
close_input_mint_ata: false,
create_output_mint_ata: false,
close_output_mint_ata: false,
fixed_output_amount,
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
effective_core_ids: Arc::new(Vec::new()),
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: Some(true),
}
}
#[tokio::test]
async fn pumpswap_fixed_output_uses_buy_with_max_input_budget() {
let instructions = PumpSwapInstructionBuilder
.build_buy_instructions(&swap_params(TradeType::Buy, Some(42)))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
}
#[tokio::test]
async fn pumpswap_sell_fixed_output_uses_min_quote_directly() {
let instructions = PumpSwapInstructionBuilder
.build_sell_instructions(&swap_params(TradeType::Sell, Some(42)))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
}
#[tokio::test]
async fn pumpswap_usdc_buy_create_input_builds_usdc_ata() {
let mut params = swap_params(TradeType::Buy, Some(42));
params.protocol_params = DexParamEnum::PumpSwap(PumpSwapParams::new(
pk(1),
pk(2),
crate::constants::USDC_TOKEN_ACCOUNT,
pk(3),
pk(4),
1_000_000_000,
2_000_000_000,
pk(5),
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
crate::constants::TOKEN_PROGRAM,
crate::constants::TOKEN_PROGRAM,
accounts::PROTOCOL_FEE_RECIPIENT,
Pubkey::default(),
false,
0,
));
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
params.create_input_mint_ata = true;
params.open_seed_optimize = false;
let instructions =
PumpSwapInstructionBuilder.build_buy_instructions(&params).await.unwrap();
let create_ix = instructions.first().unwrap();
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
}
}
+287 -76
View File
@@ -1,6 +1,14 @@
use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::raydium_amm_v4::{accounts, SWAP_BASE_IN_DISCRIMINATOR},
instruction::{
token_account_setup::{
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
push_create_user_token_account,
},
utils::raydium_amm_v4::{
accounts, SWAP_BASE_IN_DISCRIMINATOR, SWAP_BASE_OUT_DISCRIMINATOR,
},
},
trading::core::{
params::{RaydiumAmmV4Params, SwapParams},
traits::InstructionBuilder,
@@ -10,12 +18,38 @@ use crate::{
use anyhow::{anyhow, Result};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signer::Signer,
};
/// Instruction builder for RaydiumCpmm protocol
pub struct RaydiumAmmV4InstructionBuilder;
fn ensure_market_accounts(params: &RaydiumAmmV4Params) -> Result<()> {
let required = [
("amm_open_orders", params.amm_open_orders),
("amm_target_orders", params.amm_target_orders),
("serum_program", params.serum_program),
("serum_market", params.serum_market),
("serum_bids", params.serum_bids),
("serum_asks", params.serum_asks),
("serum_event_queue", params.serum_event_queue),
("serum_coin_vault_account", params.serum_coin_vault_account),
("serum_pc_vault_account", params.serum_pc_vault_account),
("serum_vault_signer", params.serum_vault_signer),
];
for (name, account) in required {
if account == Pubkey::default() {
return Err(anyhow!(
"Raydium AMM v4 requires {}; use RaydiumAmmV4Params::from_amm_address_by_rpc or with_market_accounts",
name
));
}
}
Ok(())
}
#[async_trait::async_trait]
impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
@@ -30,6 +64,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
.as_any()
.downcast_ref::<RaydiumAmmV4Params>()
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
ensure_market_accounts(protocol_params)?;
let is_wsol = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
@@ -47,33 +82,22 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.coin_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let amount_in: u64 = params.input_amount.unwrap_or(0);
let swap_result = compute_swap_amount(
protocol_params.coin_reserve,
protocol_params.pc_reserve,
is_base_in,
amount_in,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let minimum_amount_out = match params.fixed_output_amount {
Some(fixed) => fixed,
None => swap_result.min_amount_out,
};
let input_mint =
if is_base_in { protocol_params.coin_mint } else { protocol_params.pc_mint };
let output_mint =
if is_base_in { protocol_params.pc_mint } else { protocol_params.coin_mint };
let user_source_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&input_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
let user_destination_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.output_mint,
&output_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -84,47 +108,66 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
let mut instructions = Vec::with_capacity(6);
if params.create_input_mint_ata {
instructions
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), amount_in));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&input_mint,
&crate::constants::TOKEN_PROGRAM,
amount_in,
params.open_seed_optimize,
);
}
if params.create_output_mint_ata {
instructions.extend(
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.output_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
}
// Create buy instruction
let accounts: [AccountMeta; 17] = [
let accounts: [AccountMeta; 18] = [
crate::constants::TOKEN_PROGRAM_META, // Token Program (readonly)
AccountMeta::new(protocol_params.amm, false), // Amm
accounts::AUTHORITY_META, // Authority (readonly)
AccountMeta::new(protocol_params.amm, false), // Amm Open Orders
AccountMeta::new(protocol_params.amm_open_orders, false), // Amm Open Orders
AccountMeta::new(protocol_params.amm_target_orders, false), // Amm Target Orders
AccountMeta::new(protocol_params.token_coin, false), // Pool Coin Token Account
AccountMeta::new(protocol_params.token_pc, false), // Pool Pc Token Account
AccountMeta::new(protocol_params.amm, false), // Serum Program
AccountMeta::new(protocol_params.amm, false), // Serum Market
AccountMeta::new(protocol_params.amm, false), // Serum Bids
AccountMeta::new(protocol_params.amm, false), // Serum Asks
AccountMeta::new(protocol_params.amm, false), // Serum Event Queue
AccountMeta::new(protocol_params.amm, false), // Serum Coin Vault Account
AccountMeta::new(protocol_params.amm, false), // Serum Pc Vault Account
AccountMeta::new(protocol_params.amm, false), // Serum Vault Signer
AccountMeta::new_readonly(protocol_params.serum_program, false), // Serum Program
AccountMeta::new(protocol_params.serum_market, false), // Serum Market
AccountMeta::new(protocol_params.serum_bids, false), // Serum Bids
AccountMeta::new(protocol_params.serum_asks, false), // Serum Asks
AccountMeta::new(protocol_params.serum_event_queue, false), // Serum Event Queue
AccountMeta::new(protocol_params.serum_coin_vault_account, false), // Serum Coin Vault Account
AccountMeta::new(protocol_params.serum_pc_vault_account, false), // Serum Pc Vault Account
AccountMeta::new_readonly(protocol_params.serum_vault_signer, false), // Serum Vault Signer
AccountMeta::new(user_source_token_account, false), // User Source Token Account
AccountMeta::new(user_destination_token_account, false), // User Destination Token Account
AccountMeta::new(params.payer.pubkey(), true), // User Source Owner
];
// Create instruction data
let mut data = [0u8; 17];
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
if let Some(amount_out) = params.fixed_output_amount {
data[..1].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
data[9..17].copy_from_slice(&amount_out.to_le_bytes());
} else {
let minimum_amount_out = compute_swap_amount(
protocol_params.coin_reserve,
protocol_params.pc_reserve,
is_base_in,
amount_in,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
)
.min_amount_out;
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
}
instructions.push(Instruction::new_with_bytes(
accounts::RAYDIUM_AMM_V4,
@@ -133,8 +176,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
));
if params.close_input_mint_ata {
// Close wSOL ATA account, reclaim rent
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &input_mint);
}
Ok(instructions)
@@ -149,6 +191,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
.as_any()
.downcast_ref::<RaydiumAmmV4Params>()
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
ensure_market_accounts(protocol_params)?;
if params.input_amount.is_none() || params.input_amount.unwrap_or(0) == 0 {
return Err(anyhow!("Token amount is not set"));
@@ -169,33 +212,22 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
// ========================================
let is_base_in = protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.pc_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let swap_result = compute_swap_amount(
protocol_params.coin_reserve,
protocol_params.pc_reserve,
is_base_in,
params.input_amount.unwrap_or(0),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let minimum_amount_out = match params.fixed_output_amount {
Some(fixed) => fixed,
None => swap_result.min_amount_out,
};
let input_mint =
if is_base_in { protocol_params.coin_mint } else { protocol_params.pc_mint };
let output_mint =
if is_base_in { protocol_params.pc_mint } else { protocol_params.coin_mint };
let user_source_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.input_mint,
&input_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
let user_destination_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&output_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -203,37 +235,59 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
// ========================================
// Build instructions
// ========================================
let mut instructions = Vec::with_capacity(3);
let mut instructions = Vec::with_capacity(4);
if params.create_output_mint_ata {
instructions.extend(crate::trading::common::create_wsol_ata(&params.payer.pubkey()));
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
}
// Create buy instruction
let accounts: [AccountMeta; 17] = [
let accounts: [AccountMeta; 18] = [
crate::constants::TOKEN_PROGRAM_META, // Token Program (readonly)
AccountMeta::new(protocol_params.amm, false), // Amm
accounts::AUTHORITY_META, // Authority (readonly)
AccountMeta::new(protocol_params.amm, false), // Amm Open Orders
AccountMeta::new(protocol_params.amm_open_orders, false), // Amm Open Orders
AccountMeta::new(protocol_params.amm_target_orders, false), // Amm Target Orders
AccountMeta::new(protocol_params.token_coin, false), // Pool Coin Token Account
AccountMeta::new(protocol_params.token_pc, false), // Pool Pc Token Account
AccountMeta::new(protocol_params.amm, false), // Serum Program
AccountMeta::new(protocol_params.amm, false), // Serum Market
AccountMeta::new(protocol_params.amm, false), // Serum Bids
AccountMeta::new(protocol_params.amm, false), // Serum Asks
AccountMeta::new(protocol_params.amm, false), // Serum Event Queue
AccountMeta::new(protocol_params.amm, false), // Serum Coin Vault Account
AccountMeta::new(protocol_params.amm, false), // Serum Pc Vault Account
AccountMeta::new(protocol_params.amm, false), // Serum Vault Signer
AccountMeta::new_readonly(protocol_params.serum_program, false), // Serum Program
AccountMeta::new(protocol_params.serum_market, false), // Serum Market
AccountMeta::new(protocol_params.serum_bids, false), // Serum Bids
AccountMeta::new(protocol_params.serum_asks, false), // Serum Asks
AccountMeta::new(protocol_params.serum_event_queue, false), // Serum Event Queue
AccountMeta::new(protocol_params.serum_coin_vault_account, false), // Serum Coin Vault Account
AccountMeta::new(protocol_params.serum_pc_vault_account, false), // Serum Pc Vault Account
AccountMeta::new_readonly(protocol_params.serum_vault_signer, false), // Serum Vault Signer
AccountMeta::new(user_source_token_account, false), // User Source Token Account
AccountMeta::new(user_destination_token_account, false), // User Destination Token Account
AccountMeta::new(params.payer.pubkey(), true), // User Source Owner
];
// Create instruction data
let mut data = [0u8; 17];
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[1..9].copy_from_slice(&params.input_amount.unwrap_or(0).to_le_bytes());
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
let amount_in = params.input_amount.unwrap_or(0);
if let Some(amount_out) = params.fixed_output_amount {
data[..1].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
data[9..17].copy_from_slice(&amount_out.to_le_bytes());
} else {
let minimum_amount_out = compute_swap_amount(
protocol_params.coin_reserve,
protocol_params.pc_reserve,
is_base_in,
amount_in,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
)
.min_amount_out;
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
}
instructions.push(Instruction::new_with_bytes(
accounts::RAYDIUM_AMM_V4,
@@ -242,7 +296,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
));
if params.close_output_mint_ata {
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &output_mint);
}
if params.close_input_mint_ata {
instructions.push(crate::common::spl_token::close_account(
@@ -257,3 +311,160 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
Ok(instructions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
common::GasFeeStrategy,
swqos::TradeType,
trading::core::params::{DexParamEnum, SwapParams},
};
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
fn pk(seed: u8) -> Pubkey {
Pubkey::new_from_array([seed; 32])
}
fn market_params() -> RaydiumAmmV4Params {
RaydiumAmmV4Params::new(
pk(1),
crate::constants::WSOL_TOKEN_ACCOUNT,
pk(2),
pk(3),
pk(4),
1_000_000_000,
2_000_000_000,
)
.with_market_accounts(
pk(5),
pk(6),
pk(7),
pk(8),
pk(9),
pk(10),
pk(11),
pk(12),
pk(13),
pk(14),
)
}
fn swap_params(
protocol_params: RaydiumAmmV4Params,
fixed_output_amount: Option<u64>,
) -> SwapParams {
SwapParams {
rpc: None,
payer: Arc::new(Keypair::new()),
trade_type: TradeType::Buy,
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
input_token_program: None,
output_mint: pk(2),
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::RaydiumAmmV4(protocol_params),
open_seed_optimize: true,
swqos_clients: Arc::new(Vec::new()),
middleware_manager: None,
durable_nonce: None,
with_tip: false,
create_input_mint_ata: false,
close_input_mint_ata: false,
create_output_mint_ata: false,
close_output_mint_ata: false,
fixed_output_amount,
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
effective_core_ids: Arc::new(Vec::new()),
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
}
}
#[tokio::test]
async fn raydium_amm_v4_uses_idl_market_account_order() {
let instructions = RaydiumAmmV4InstructionBuilder
.build_buy_instructions(&swap_params(market_params(), None))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts.len(), 18);
assert_eq!(&ix.data[..1], SWAP_BASE_IN_DISCRIMINATOR);
assert_eq!(ix.accounts[3].pubkey, pk(5));
assert_eq!(ix.accounts[4].pubkey, pk(6));
assert_eq!(ix.accounts[7].pubkey, pk(7));
assert_eq!(ix.accounts[8].pubkey, pk(8));
assert_eq!(ix.accounts[9].pubkey, pk(9));
assert_eq!(ix.accounts[10].pubkey, pk(10));
assert_eq!(ix.accounts[11].pubkey, pk(11));
assert_eq!(ix.accounts[12].pubkey, pk(12));
assert_eq!(ix.accounts[13].pubkey, pk(13));
assert_eq!(ix.accounts[14].pubkey, pk(14));
assert!(!ix.accounts[7].is_writable);
assert!(!ix.accounts[14].is_writable);
}
#[tokio::test]
async fn raydium_amm_v4_uses_base_out_when_fixed_output_is_set() {
let instructions = RaydiumAmmV4InstructionBuilder
.build_buy_instructions(&swap_params(market_params(), Some(42)))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..1], SWAP_BASE_OUT_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[1..9].try_into().unwrap()), 100_000);
assert_eq!(u64::from_le_bytes(ix.data[9..17].try_into().unwrap()), 42);
}
#[tokio::test]
async fn raydium_amm_v4_rejects_placeholder_market_accounts() {
let err = RaydiumAmmV4InstructionBuilder
.build_buy_instructions(&swap_params(
RaydiumAmmV4Params::new(
pk(1),
crate::constants::WSOL_TOKEN_ACCOUNT,
pk(2),
pk(3),
pk(4),
1_000_000_000,
2_000_000_000,
),
None,
))
.await
.unwrap_err();
assert!(err.to_string().contains("amm_open_orders"));
}
#[tokio::test]
async fn raydium_amm_v4_usdc_buy_create_input_builds_usdc_ata() {
let mut protocol_params = market_params();
protocol_params.coin_mint = crate::constants::USDC_TOKEN_ACCOUNT;
let mut params = swap_params(protocol_params, Some(42));
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
params.create_input_mint_ata = true;
params.open_seed_optimize = false;
let instructions =
RaydiumAmmV4InstructionBuilder.build_buy_instructions(&params).await.unwrap();
let create_ix = instructions.first().unwrap();
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
}
}
+229 -110
View File
@@ -1,9 +1,15 @@
use crate::{
common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed,
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::raydium_cpmm::{
accounts, get_observation_state_pda, get_pool_pda, get_vault_account,
SWAP_BASE_IN_DISCRIMINATOR,
instruction::{
token_account_setup::{
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
push_create_user_token_account,
},
utils::raydium_cpmm::{
accounts, get_observation_state_pda, get_pool_pda, get_vault_account,
SWAP_BASE_IN_DISCRIMINATOR, SWAP_BASE_OUT_DISCRIMINATOR,
},
},
trading::core::{
params::{RaydiumCpmmParams, SwapParams},
@@ -63,53 +69,38 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
// ========================================
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let mint_token_program = if is_base_in {
let input_mint =
if is_base_in { protocol_params.base_mint } else { protocol_params.quote_mint };
let input_token_program = if is_base_in {
protocol_params.base_token_program
} else {
protocol_params.quote_token_program
};
let output_mint =
if is_base_in { protocol_params.quote_mint } else { protocol_params.base_mint };
let output_token_program = if is_base_in {
protocol_params.quote_token_program
} else {
protocol_params.base_token_program
};
let amount_in: u64 = params.input_amount.unwrap_or(0);
let result = compute_swap_amount(
protocol_params.base_reserve,
protocol_params.quote_reserve,
is_base_in,
amount_in,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let minimum_amount_out = match params.fixed_output_amount {
Some(fixed) => fixed,
None => result.min_amount_out,
};
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&crate::constants::TOKEN_PROGRAM,
&input_mint,
&input_token_program,
params.open_seed_optimize,
);
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.output_mint,
&mint_token_program,
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
let input_vault_account = get_vault_account(
&pool_state,
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
protocol_params,
);
let output_vault_account =
get_vault_account(&pool_state, &params.output_mint, protocol_params);
let input_vault_account = get_vault_account(&pool_state, &input_mint, protocol_params);
let output_vault_account = get_vault_account(&pool_state, &output_mint, protocol_params);
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
get_observation_state_pda(&pool_state).unwrap()
@@ -123,19 +114,23 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let mut instructions = Vec::with_capacity(6);
if params.create_input_mint_ata {
instructions
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), amount_in));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&input_mint,
&input_token_program,
amount_in,
params.open_seed_optimize,
);
}
if params.create_output_mint_ata {
instructions.extend(
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.output_mint,
&mint_token_program,
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
}
@@ -143,27 +138,37 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let accounts: [AccountMeta; 13] = [
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
accounts::AUTHORITY_META, // Authority (readonly)
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
AccountMeta::new_readonly(protocol_params.amm_config, false), // Amm Config (readonly)
AccountMeta::new(pool_state, false), // Pool State
AccountMeta::new(input_token_account, false), // Input Token Account
AccountMeta::new(output_token_account, false), // Output Token Account
AccountMeta::new(input_vault_account, false), // Input Vault Account
AccountMeta::new(output_vault_account, false), // Output Vault Account
crate::constants::TOKEN_PROGRAM_META, // Input Token Program (readonly)
AccountMeta::new_readonly(mint_token_program, false), // Output Token Program (readonly)
if is_wsol {
crate::constants::WSOL_TOKEN_ACCOUNT_META
} else {
crate::constants::USDC_TOKEN_ACCOUNT_META
}, // Input token mint (readonly)
AccountMeta::new_readonly(params.output_mint, false), // Output token mint (readonly)
AccountMeta::new(observation_state_account, false), // Observation State Account
AccountMeta::new_readonly(input_token_program, false), // Input Token Program (readonly)
AccountMeta::new_readonly(output_token_program, false), // Output Token Program (readonly)
AccountMeta::new_readonly(input_mint, false), // Input token mint (readonly)
AccountMeta::new_readonly(output_mint, false), // Output token mint (readonly)
AccountMeta::new(observation_state_account, false), // Observation State Account
];
// Create instruction data
let mut data = [0u8; 24];
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
if let Some(amount_out) = params.fixed_output_amount {
data[..8].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&amount_out.to_le_bytes());
} else {
let minimum_amount_out = compute_swap_amount(
protocol_params.base_reserve,
protocol_params.quote_reserve,
is_base_in,
amount_in,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
)
.min_amount_out;
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
}
instructions.push(Instruction::new_with_bytes(
accounts::RAYDIUM_CPMM,
@@ -172,8 +177,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
));
if params.close_input_mint_ata {
// Close wSOL ATA account, reclaim rent
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &input_mint);
}
Ok(instructions)
@@ -219,54 +223,36 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
// ========================================
let is_quote_out = protocol_params.quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let mint_token_program = if is_quote_out {
let input_mint =
if is_quote_out { protocol_params.base_mint } else { protocol_params.quote_mint };
let input_token_program = if is_quote_out {
protocol_params.base_token_program
} else {
protocol_params.quote_token_program
};
let minimum_amount_out: u64 = match params.fixed_output_amount {
Some(fixed) => fixed,
None => {
compute_swap_amount(
protocol_params.base_reserve,
protocol_params.quote_reserve,
is_quote_out,
params.input_amount.unwrap_or(0),
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
)
.min_amount_out
}
let output_mint =
if is_quote_out { protocol_params.quote_mint } else { protocol_params.base_mint };
let output_token_program = if is_quote_out {
protocol_params.quote_token_program
} else {
protocol_params.base_token_program
};
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&crate::constants::TOKEN_PROGRAM,
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
&params.input_mint,
&mint_token_program,
&input_mint,
&input_token_program,
params.open_seed_optimize,
);
let output_vault_account = get_vault_account(
&pool_state,
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
protocol_params,
);
let input_vault_account =
get_vault_account(&pool_state, &params.input_mint, protocol_params);
let output_vault_account = get_vault_account(&pool_state, &output_mint, protocol_params);
let input_vault_account = get_vault_account(&pool_state, &input_mint, protocol_params);
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
get_observation_state_pda(&pool_state).unwrap()
@@ -277,37 +263,54 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
// ========================================
// Build instructions
// ========================================
let mut instructions = Vec::with_capacity(3);
let mut instructions = Vec::with_capacity(4);
if params.create_output_mint_ata {
instructions.extend(crate::trading::common::create_wsol_ata(&params.payer.pubkey()));
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&output_mint,
&output_token_program,
params.open_seed_optimize,
);
}
// Create sell instruction
let accounts: [AccountMeta; 13] = [
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
accounts::AUTHORITY_META, // Authority (readonly)
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
AccountMeta::new_readonly(protocol_params.amm_config, false), // Amm Config (readonly)
AccountMeta::new(pool_state, false), // Pool State
AccountMeta::new(input_token_account, false), // Input Token Account
AccountMeta::new(output_token_account, false), // Output Token Account
AccountMeta::new(input_vault_account, false), // Input Vault Account
AccountMeta::new(output_vault_account, false), // Output Vault Account
AccountMeta::new_readonly(mint_token_program, false), // Input Token Program (readonly)
crate::constants::TOKEN_PROGRAM_META, // Output Token Program (readonly)
AccountMeta::new_readonly(params.input_mint, false), // Input token mint (readonly)
if is_wsol {
crate::constants::WSOL_TOKEN_ACCOUNT_META
} else {
crate::constants::USDC_TOKEN_ACCOUNT_META
}, // Output token mint (readonly)
AccountMeta::new(observation_state_account, false), // Observation State Account
AccountMeta::new_readonly(input_token_program, false), // Input Token Program (readonly)
AccountMeta::new_readonly(output_token_program, false), // Output Token Program (readonly)
AccountMeta::new_readonly(input_mint, false), // Input token mint (readonly)
AccountMeta::new_readonly(output_mint, false), // Output token mint (readonly)
AccountMeta::new(observation_state_account, false), // Observation State Account
];
// Create instruction data
let mut data = [0u8; 24];
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&params.input_amount.unwrap_or(0).to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
let amount_in = params.input_amount.unwrap_or(0);
if let Some(amount_out) = params.fixed_output_amount {
data[..8].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&amount_out.to_le_bytes());
} else {
let minimum_amount_out = compute_swap_amount(
protocol_params.base_reserve,
protocol_params.quote_reserve,
is_quote_out,
amount_in,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
)
.min_amount_out;
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
}
instructions.push(Instruction::new_with_bytes(
accounts::RAYDIUM_CPMM,
@@ -316,12 +319,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
));
if params.close_output_mint_ata {
// Close wSOL ATA account, reclaim rent
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &output_mint);
}
if params.close_input_mint_ata {
instructions.push(crate::common::spl_token::close_account(
&mint_token_program,
&input_token_program,
&input_token_account,
&params.payer.pubkey(),
&params.payer.pubkey(),
@@ -332,3 +334,120 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
Ok(instructions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
common::GasFeeStrategy,
swqos::TradeType,
trading::core::params::{DexParamEnum, SwapParams},
};
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
fn pk(seed: u8) -> Pubkey {
Pubkey::new_from_array([seed; 32])
}
fn cpmm_params() -> RaydiumCpmmParams {
RaydiumCpmmParams {
pool_state: pk(1),
amm_config: pk(2),
base_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
quote_mint: pk(3),
base_reserve: 1_000_000_000,
quote_reserve: 2_000_000_000,
base_vault: pk(4),
quote_vault: pk(5),
base_token_program: crate::constants::TOKEN_PROGRAM,
quote_token_program: crate::constants::TOKEN_PROGRAM,
observation_state: pk(6),
}
}
fn swap_params(fixed_output_amount: Option<u64>) -> SwapParams {
SwapParams {
rpc: None,
payer: Arc::new(Keypair::new()),
trade_type: TradeType::Buy,
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
input_token_program: None,
output_mint: pk(3),
output_token_program: None,
input_amount: Some(100_000),
slippage_basis_points: Some(100),
address_lookup_table_account: None,
recent_blockhash: None,
wait_tx_confirmed: false,
protocol_params: DexParamEnum::RaydiumCpmm(cpmm_params()),
open_seed_optimize: true,
swqos_clients: Arc::new(Vec::new()),
middleware_manager: None,
durable_nonce: None,
with_tip: false,
create_input_mint_ata: false,
close_input_mint_ata: false,
create_output_mint_ata: false,
close_output_mint_ata: false,
fixed_output_amount,
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
effective_core_ids: Arc::new(Vec::new()),
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
}
}
#[tokio::test]
async fn raydium_cpmm_uses_base_in_and_readonly_amm_config_by_default() {
let instructions =
RaydiumCpmmInstructionBuilder.build_buy_instructions(&swap_params(None)).await.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], SWAP_BASE_IN_DISCRIMINATOR);
assert_eq!(ix.accounts[2].pubkey, pk(2));
assert!(!ix.accounts[2].is_writable);
}
#[tokio::test]
async fn raydium_cpmm_uses_base_output_when_fixed_output_is_set() {
let instructions = RaydiumCpmmInstructionBuilder
.build_buy_instructions(&swap_params(Some(42)))
.await
.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], SWAP_BASE_OUT_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
}
#[tokio::test]
async fn raydium_cpmm_usdc_buy_create_input_uses_usdc_accounts() {
let mut protocol_params = cpmm_params();
protocol_params.base_mint = crate::constants::USDC_TOKEN_ACCOUNT;
protocol_params.quote_mint = pk(3);
let mut params = swap_params(Some(42));
params.protocol_params = DexParamEnum::RaydiumCpmm(protocol_params);
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
params.create_input_mint_ata = true;
params.open_seed_optimize = false;
let instructions =
RaydiumCpmmInstructionBuilder.build_buy_instructions(&params).await.unwrap();
let create_ix = instructions.first().unwrap();
let swap_ix = instructions.last().unwrap();
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
assert_eq!(swap_ix.accounts[10].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
}
}
+47
View File
@@ -0,0 +1,47 @@
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
#[inline]
pub(crate) fn push_create_user_token_account(
instructions: &mut Vec<Instruction>,
payer: &Pubkey,
mint: &Pubkey,
token_program: &Pubkey,
use_seed: bool,
) {
instructions.extend(
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
payer,
payer,
mint,
token_program,
use_seed,
),
);
}
#[inline]
pub(crate) fn push_create_or_wrap_user_token_account(
instructions: &mut Vec<Instruction>,
payer: &Pubkey,
mint: &Pubkey,
token_program: &Pubkey,
amount: u64,
use_seed: bool,
) {
if *mint == crate::constants::WSOL_TOKEN_ACCOUNT {
instructions.extend(crate::trading::common::handle_wsol(payer, amount));
} else {
push_create_user_token_account(instructions, payer, mint, token_program, use_seed);
}
}
#[inline]
pub(crate) fn push_close_wsol_if_needed(
instructions: &mut Vec<Instruction>,
payer: &Pubkey,
mint: &Pubkey,
) {
if *mint == crate::constants::WSOL_TOKEN_ACCOUNT {
instructions.extend(crate::trading::common::close_wsol(payer));
}
}
+2
View File
@@ -57,7 +57,9 @@ pub mod accounts {
}
pub const BUY_EXECT_IN_DISCRIMINATOR: [u8; 8] = [250, 234, 13, 123, 213, 156, 19, 236];
pub const BUY_EXECT_OUT_DISCRIMINATOR: [u8; 8] = [24, 211, 116, 40, 105, 3, 153, 56];
pub const SELL_EXECT_IN_DISCRIMINATOR: [u8; 8] = [149, 39, 222, 155, 211, 124, 152, 26];
pub const SELL_EXECT_OUT_DISCRIMINATOR: [u8; 8] = [95, 200, 71, 34, 8, 9, 11, 166];
pub async fn fetch_pool_state(
rpc: &SolanaRpcClient,
+12
View File
@@ -16,6 +16,7 @@ pub mod accounts {
pub const AUTHORITY: Pubkey = pubkey!("HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC");
pub const METEORA_DAMM_V2: Pubkey = pubkey!("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG");
pub const SYSVAR_INSTRUCTIONS: Pubkey = pubkey!("Sysvar1nstructions1111111111111111111111111");
// META
@@ -32,9 +33,20 @@ pub mod accounts {
is_signer: false,
is_writable: false,
};
pub const SYSVAR_INSTRUCTIONS_META: solana_sdk::instruction::AccountMeta =
solana_sdk::instruction::AccountMeta {
pubkey: SYSVAR_INSTRUCTIONS,
is_signer: false,
is_writable: false,
};
}
pub const SWAP_DISCRIMINATOR: &[u8] = &[248, 198, 158, 145, 225, 117, 135, 200];
pub const SWAP2_DISCRIMINATOR: &[u8] = &[65, 75, 63, 76, 235, 91, 91, 136];
pub const SWAP_MODE_EXACT_IN: u8 = 0;
pub const SWAP_MODE_PARTIAL_FILL: u8 = 1;
pub const SWAP_MODE_EXACT_OUT: u8 = 2;
pub async fn fetch_pool(
rpc: &SolanaRpcClient,
+30 -19
View File
@@ -39,6 +39,7 @@ pub mod global_constants {
pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000;
pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000;
pub const INITIAL_VIRTUAL_USDC_RESERVES: u64 = 4_292_000_000;
pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000;
pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000;
pub const FEE_BASIS_POINTS: u64 = 95;
@@ -212,7 +213,7 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
#[inline]
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
*pubkey == global_constants::FEE_RECIPIENT
}
#[inline]
@@ -242,11 +243,12 @@ pub fn reconcile_mayhem_mode_for_trade(
#[inline]
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
let is_m = is_mayhem_fee_recipient(pk);
let is_amm = is_amm_fee_recipient(pk);
let is_s = is_standard_bonding_fee_recipient(pk);
if is_mayhem_mode {
!(is_s && !is_m)
is_m || (!is_s && !is_amm && *pk != Pubkey::default())
} else {
!(is_m && !is_s)
is_s || (!is_m && !is_amm && *pk != Pubkey::default())
}
}
@@ -260,18 +262,10 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
#[inline]
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
const POOL: &[Pubkey] = &[
global_constants::FEE_RECIPIENT,
global_constants::PUMPFUN_AMM_FEE_1,
global_constants::PUMPFUN_AMM_FEE_2,
global_constants::PUMPFUN_AMM_FEE_3,
global_constants::PUMPFUN_AMM_FEE_4,
global_constants::PUMPFUN_AMM_FEE_5,
global_constants::PUMPFUN_AMM_FEE_6,
global_constants::PUMPFUN_AMM_FEE_7,
];
let recipient = *POOL.choose(&mut rand::rng()).unwrap_or(&global_constants::FEE_RECIPIENT);
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
// Historical name kept for API compatibility. Do not randomize across static AMM fee
// recipients for bonding-curve buy/sell; stale AMM protocol fee accounts can fail
// Pump.fun authorization with error 6000 when Global has rotated.
AccountMeta { pubkey: global_constants::FEE_RECIPIENT, is_signer: false, is_writable: true }
}
#[inline]
@@ -707,12 +701,12 @@ mod tests {
#[test]
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
let fee = global_constants::FEE_RECIPIENT;
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
}
#[test]
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
fn pump_fee_meta_rejects_amm_fee_when_building_mayhem_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
let m = pump_fun_fee_recipient_meta(fee, true);
assert!(
@@ -723,11 +717,28 @@ mod tests {
}
#[test]
fn pump_fee_meta_uses_observed_standard_fee_for_standard_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_7;
fn pump_fee_meta_uses_observed_non_amm_fee_for_standard_ix() {
let fee = Pubkey::new_unique();
let m = pump_fun_fee_recipient_meta(fee, false);
assert_eq!(m.pubkey, fee);
assert!(m.is_writable);
assert!(!m.is_signer);
}
#[test]
fn pump_fee_meta_rejects_amm_fee_for_standard_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_7;
let m = pump_fun_fee_recipient_meta(fee, false);
assert_eq!(m.pubkey, global_constants::FEE_RECIPIENT);
assert!(m.is_writable);
assert!(!m.is_signer);
}
#[test]
fn pump_fee_meta_default_standard_uses_main_fee_recipient() {
let m = pump_fun_fee_recipient_meta(Pubkey::default(), false);
assert_eq!(m.pubkey, global_constants::FEE_RECIPIENT);
assert!(m.is_writable);
assert!(!m.is_signer);
}
}
+11 -3
View File
@@ -11,7 +11,10 @@ use parking_lot::RwLock;
use rand::seq::IndexedRandom;
use solana_account_decoder::UiAccountEncoding;
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
use std::sync::Arc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::{Duration, Instant};
use tracing::warn;
@@ -205,6 +208,7 @@ struct CachedGlobalConfig {
static GLOBAL_CONFIG_CACHE: Lazy<RwLock<Option<CachedGlobalConfig>>> =
Lazy::new(|| RwLock::new(None));
static GLOBAL_CONFIG_REFRESH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
@@ -294,8 +298,12 @@ pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
.as_ref()
.map(|c| c.fetched_at.elapsed() > PUMPSWAP_GLOBAL_CONFIG_TTL)
.unwrap_or(true);
if stale {
let _ = refresh_global_config_once(rpc.as_ref()).await;
if stale && !GLOBAL_CONFIG_REFRESH_IN_FLIGHT.swap(true, Ordering::AcqRel) {
let rpc = Arc::clone(rpc);
tokio::spawn(async move {
let _ = refresh_global_config_once(rpc.as_ref()).await;
GLOBAL_CONFIG_REFRESH_IN_FLIGHT.store(false, Ordering::Release);
});
}
}
+25 -1
View File
@@ -1,6 +1,8 @@
use crate::{
common::SolanaRpcClient,
instruction::utils::raydium_amm_v4_types::{amm_info_decode, AmmInfo},
instruction::utils::raydium_amm_v4_types::{
amm_info_decode, market_state_decode, AmmInfo, MarketState,
},
};
use anyhow::anyhow;
use solana_sdk::pubkey::Pubkey;
@@ -40,3 +42,25 @@ pub async fn fetch_amm_info(rpc: &SolanaRpcClient, amm: Pubkey) -> Result<AmmInf
amm_info_decode(&amm_info).ok_or_else(|| anyhow!("Failed to decode amm info"))?;
Ok(amm_info)
}
pub async fn fetch_market_state(
rpc: &SolanaRpcClient,
market: Pubkey,
) -> Result<MarketState, anyhow::Error> {
let market_data = rpc.get_account_data(&market).await?;
market_state_decode(&market_data).ok_or_else(|| anyhow!("Failed to decode market state"))
}
pub fn derive_serum_vault_signer(
serum_program: &Pubkey,
serum_market: &Pubkey,
vault_signer_nonce: u64,
) -> Result<Pubkey, anyhow::Error> {
let nonce = vault_signer_nonce.to_le_bytes();
Pubkey::create_program_address(&[serum_market.as_ref(), &nonce], serum_program)
.or_else(|_| {
let legacy_nonce = [vault_signer_nonce as u8];
Pubkey::create_program_address(&[serum_market.as_ref(), &legacy_nonce], serum_program)
})
.map_err(|err| anyhow!("Failed to derive Serum vault signer: {}", err))
}
@@ -77,3 +77,38 @@ pub fn amm_info_decode(data: &[u8]) -> Option<AmmInfo> {
}
borsh::from_slice::<AmmInfo>(&data[..AMM_INFO_SIZE]).ok()
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct MarketState {
pub padding: [u8; 5],
pub account_flags: u64,
pub own_address: Pubkey,
pub vault_signer_nonce: u64,
pub coin_mint: Pubkey,
pub pc_mint: Pubkey,
pub serum_coin_vault_account: Pubkey,
pub coin_deposits_total: u64,
pub coin_fees_accrued: u64,
pub serum_pc_vault_account: Pubkey,
pub pc_deposits_total: u64,
pub pc_fees_accrued: u64,
pub pc_dust_threshold: u64,
pub request_queue: Pubkey,
pub serum_event_queue: Pubkey,
pub serum_bids: Pubkey,
pub serum_asks: Pubkey,
pub coin_lot_size: u64,
pub pc_lot_size: u64,
pub fee_rate_bps: u64,
pub referrer_rebate_accrued: u64,
pub padding2: [u8; 7],
}
pub const MARKET_STATE_SIZE: usize = 388;
pub fn market_state_decode(data: &[u8]) -> Option<MarketState> {
if data.len() < MARKET_STATE_SIZE {
return None;
}
borsh::from_slice::<MarketState>(&data[..MARKET_STATE_SIZE]).ok()
}
+4 -1
View File
@@ -288,7 +288,10 @@ impl OptimizationFlags {
"+popcnt".to_string(),
];
#[cfg(not(target_arch = "x86_64"))]
#[cfg(target_arch = "aarch64")]
let target_features = vec!["+neon".to_string()];
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
let target_features = vec![];
Self {
opt_level: OptLevel::Aggressive,
+11 -4
View File
@@ -172,10 +172,17 @@ impl SIMDMemoryOps {
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
match len {
1 => *a == *b,
2 => *(a as *const u16) == *(b as *const u16),
3 => *(a as *const u16) == *(b as *const u16) && *a.add(2) == *b.add(2),
4 => *(a as *const u32) == *(b as *const u32),
5..=8 => *(a as *const u64) == *(b as *const u64),
2 => ptr::read_unaligned(a as *const u16) == ptr::read_unaligned(b as *const u16),
3 => {
ptr::read_unaligned(a as *const u16) == ptr::read_unaligned(b as *const u16)
&& *a.add(2) == *b.add(2)
}
4 => ptr::read_unaligned(a as *const u32) == ptr::read_unaligned(b as *const u32),
5..=7 => {
ptr::read_unaligned(a as *const u32) == ptr::read_unaligned(b as *const u32)
&& (4..len).all(|i| *a.add(i) == *b.add(i))
}
8 => ptr::read_unaligned(a as *const u64) == ptr::read_unaligned(b as *const u64),
_ => unreachable!(),
}
}
+5 -2
View File
@@ -49,7 +49,7 @@ impl Default for SyscallBypassConfig {
pub struct SyscallBatchProcessor {
pending_calls: crossbeam_queue::ArrayQueue<SyscallRequest>,
_executor: tokio::runtime::Handle,
_executor: Option<tokio::runtime::Handle>,
batch_stats: CachePadded<AtomicU64>,
}
@@ -94,6 +94,7 @@ pub struct FastTimeProvider {
/// 上次更新时间
last_update: CachePadded<AtomicU64>,
/// 启用vDSO
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
vdso_enabled: bool,
}
@@ -121,6 +122,7 @@ impl FastTimeProvider {
/// 🚀 超快速获取当前时间 - 绕过系统调用
#[inline(always)]
pub fn fast_now_nanos(&self) -> u64 {
#[cfg(target_os = "linux")]
if self.vdso_enabled {
// 使用vDSO快速获取时间
return self.vdso_time_nanos();
@@ -140,6 +142,7 @@ impl FastTimeProvider {
/// vDSO时间获取
#[inline(always)]
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn vdso_time_nanos(&self) -> u64 {
#[cfg(target_os = "linux")]
{
@@ -371,7 +374,7 @@ impl SyscallBatchProcessor {
/// 创建系统调用批处理器
pub fn new(batch_size: usize) -> Result<Self> {
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
let executor = tokio::runtime::Handle::current();
let executor = tokio::runtime::Handle::try_current().ok();
tracing::info!(target: "sol_trade_sdk","🚀 Syscall batch processor created with batch size: {}", batch_size);
+21 -1
View File
@@ -3,7 +3,9 @@ use once_cell::sync::Lazy;
use smallvec::SmallVec;
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction;
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
const MAX_COMPUTE_BUDGET_CACHE_SIZE: usize = 4_096;
/// Cache key containing all parameters for compute budget instructions
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -17,6 +19,22 @@ struct ComputeBudgetCacheKey {
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, Arc<SmallVec<[Instruction; 2]>>>> =
Lazy::new(|| DashMap::new());
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
/// Extend `instructions` with compute budget instructions; on cache hit extends from cached Arc (no SmallVec clone).
#[inline(always)]
pub fn extend_compute_budget_instructions(
@@ -41,6 +59,7 @@ pub fn extend_compute_budget_instructions(
let arc = Arc::new(insts);
instructions.extend(arc.iter().cloned());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
}
/// Returns compute budget instructions (allocates on cache hit; prefer `extend_compute_budget_instructions` on hot path).
@@ -59,5 +78,6 @@ pub fn compute_budget_instructions(unit_price: u64, unit_limit: u32) -> SmallVec
}
let arc = Arc::new(insts.clone());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
insts
}
+11 -12
View File
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{
@@ -9,7 +10,7 @@ use std::sync::Arc;
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
use crate::{
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
common::nonce_cache::DurableNonceInfo,
trading::{
core::transaction_pool::{acquire_builder, release_builder},
MiddlewareManager,
@@ -26,11 +27,10 @@ fn sol_f64_to_lamports(sol: f64) -> u64 {
(lamports.min(u64::MAX as f64)).round() as u64
}
/// Build standard RPC transaction (worker hot path).
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/rpc/middleware.
pub async fn build_transaction(
/// Build signed transaction (worker hot path, no RPC).
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/middleware.
pub fn build_transaction(
payer: &Arc<Keypair>,
_rpc: Option<&Arc<SolanaRpcClient>>,
unit_limit: u32,
unit_price: u64,
business_instructions: &[Instruction],
@@ -74,10 +74,9 @@ pub async fn build_transaction(
protocol_name,
is_buy,
)
.await
}
async fn build_versioned_transaction(
fn build_versioned_transaction(
payer: &Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<&AddressLookupTableAccount>,
@@ -95,19 +94,19 @@ async fn build_versioned_transaction(
// 使用预分配的交易构建器以降低延迟
let mut builder = acquire_builder();
let versioned_msg = builder.build_zero_alloc(
let build_result = builder.build_zero_alloc(
&payer.pubkey(),
&full_instructions,
address_lookup_table_account,
blockhash,
);
release_builder(builder);
let versioned_msg = build_result?;
let msg_bytes = versioned_msg.serialize();
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
let signature =
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
// 归还构建器到池
release_builder(builder);
Ok(tx)
}
+340 -89
View File
@@ -35,8 +35,8 @@ use fnv::FnvHasher;
type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
use crate::{
common::nonce_cache::DurableNonceInfo,
common::{GasFeeStrategy, SolanaRpcClient},
common::gas_fee_strategy::{GasFeeStrategyType, GasFeeStrategyValue},
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SwqosSubmitTiming},
swqos::{SwqosClient, SwqosType, TradeType},
trading::core::params::SenderConcurrencyConfig,
trading::{common::build_transaction, MiddlewareManager},
@@ -46,12 +46,12 @@ use crate::{
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);
/// 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>>,
rpc: Option<Arc<SolanaRpcClient>>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
durable_nonce: Option<DurableNonceInfo>,
@@ -72,6 +72,7 @@ struct SwqosJob {
tip_account: Arc<Pubkey>,
swqos_client: Arc<SwqosClient>,
swqos_type: SwqosType,
strategy_type: GasFeeStrategyType,
core_id: Option<core_affinity::CoreId>,
use_affinity: bool,
}
@@ -88,7 +89,6 @@ async fn run_one_swqos_job(job: SwqosJob) {
let transaction = match build_transaction(
&s.payer,
s.rpc.as_ref(),
job.unit_limit,
job.unit_price,
s.instructions.as_ref(),
@@ -101,9 +101,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
&job.tip_account,
tip_amount,
s.durable_nonce.as_ref(),
)
.await
{
) {
Ok(tx) => tx,
Err(e) => {
s.collector.submit(TaskResult {
@@ -111,6 +109,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
signature: Signature::default(),
error: Some(e),
swqos_type: job.swqos_type,
strategy_type: job.strategy_type,
landed_on_chain: false,
submit_done_us: crate::common::clock::now_micros(),
});
@@ -140,6 +139,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
signature: sig,
error: err,
swqos_type: job.swqos_type,
strategy_type: job.strategy_type,
landed_on_chain,
submit_done_us: crate::common::clock::now_micros(),
});
@@ -147,43 +147,46 @@ async fn run_one_swqos_job(job: SwqosJob) {
async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>, notify: Arc<Notify>) {
loop {
let notified = notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if let Some(job) = queue.pop() {
drop(notified);
run_one_swqos_job(job).await;
} else {
notify.notified().await;
notified.await;
}
}
}
static SWQOS_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
static SWQOS_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
static SWQOS_WORKERS_STARTED: AtomicBool = AtomicBool::new(false);
static SWQOS_WORKER_COUNT: AtomicUsize = AtomicUsize::new(0);
/// Dedicated OS-thread sender pool. Queue and notify are in OnceCell so hot path never takes a lock after init.
static DEDICATED_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
static DEDICATED_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
static DEDICATED_WORKER_COUNT: AtomicUsize = AtomicUsize::new(0);
/// JoinHandles kept so dedicated threads are not detached; only touched during init under lock.
static DEDICATED_INIT: Mutex<Option<Vec<std::thread::JoinHandle<()>>>> = Mutex::new(None);
fn ensure_dedicated_pool(
fn desired_dedicated_workers(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
return (q.clone(), n.clone());
}
let mut guard = DEDICATED_INIT.lock();
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
return (q.clone(), n.clone());
}
let n = sender_thread_cores
) -> usize {
sender_thread_cores
.map(|v| v.len().min(max_sender_concurrency))
.unwrap_or_else(|| SWQOS_DEDICATED_DEFAULT_THREADS.min(max_sender_concurrency))
.min(32)
.max(1);
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
let notify = Arc::new(Notify::new());
let core_ids: Vec<core_affinity::CoreId> = core_affinity::get_core_ids()
.max(1)
}
fn dedicated_core_ids(
sender_thread_cores: Option<&[usize]>,
n: usize,
) -> Vec<core_affinity::CoreId> {
core_affinity::get_core_ids()
.map(|all_ids| {
sender_thread_cores
.map(|indices| {
@@ -191,9 +194,92 @@ fn ensure_dedicated_pool(
})
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
})
.unwrap_or_default();
let mut handles = Vec::with_capacity(n);
for i in 0..n {
.unwrap_or_default()
}
fn ensure_dedicated_pool(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
let target_workers = desired_dedicated_workers(sender_thread_cores, max_sender_concurrency);
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) >= target_workers {
return (q.clone(), n.clone());
}
ensure_dedicated_worker_count(q.clone(), n.clone(), sender_thread_cores, target_workers);
return (q.clone(), n.clone());
}
let mut guard = DEDICATED_INIT.lock();
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) < target_workers {
ensure_dedicated_worker_count_locked(
q.clone(),
n.clone(),
sender_thread_cores,
target_workers,
&mut guard,
);
}
return (q.clone(), n.clone());
}
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
let notify = Arc::new(Notify::new());
let _ = DEDICATED_QUEUE.set(queue.clone());
let _ = DEDICATED_NOTIFY.set(notify.clone());
*guard = Some(Vec::with_capacity(target_workers));
ensure_dedicated_worker_count_locked(
queue.clone(),
notify.clone(),
sender_thread_cores,
target_workers,
&mut guard,
);
(queue, notify)
}
/// Pre-spawn dedicated sender threads during SDK initialization, avoiding first-submit thread
/// creation cost on the trading hot path.
pub fn warm_dedicated_sender_pool(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) {
let _ = ensure_dedicated_pool(sender_thread_cores, max_sender_concurrency);
}
fn ensure_dedicated_worker_count(
queue: Arc<ArrayQueue<SwqosJob>>,
notify: Arc<Notify>,
sender_thread_cores: Option<&[usize]>,
target_workers: usize,
) {
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) >= target_workers {
return;
}
let mut guard = DEDICATED_INIT.lock();
ensure_dedicated_worker_count_locked(
queue,
notify,
sender_thread_cores,
target_workers,
&mut guard,
);
}
fn ensure_dedicated_worker_count_locked(
queue: Arc<ArrayQueue<SwqosJob>>,
notify: Arc<Notify>,
sender_thread_cores: Option<&[usize]>,
target_workers: usize,
guard: &mut Option<Vec<std::thread::JoinHandle<()>>>,
) {
let current = DEDICATED_WORKER_COUNT.load(Ordering::Acquire);
if current >= target_workers {
return;
}
let core_ids = dedicated_core_ids(sender_thread_cores, target_workers);
let handles = guard.get_or_insert_with(Vec::new);
handles.reserve(target_workers.saturating_sub(current));
for i in current..target_workers {
let queue = queue.clone();
let notify = notify.clone();
let core_id = core_ids.get(i).cloned();
@@ -209,19 +295,23 @@ fn ensure_dedicated_pool(
});
handles.push(handle);
}
let _ = DEDICATED_QUEUE.set(queue.clone());
let _ = DEDICATED_NOTIFY.set(notify.clone());
*guard = Some(handles);
(queue, notify)
DEDICATED_WORKER_COUNT.store(target_workers, Ordering::Release);
}
fn ensure_swqos_pool(queue: Arc<ArrayQueue<SwqosJob>>, max_sender_concurrency: usize) {
if SWQOS_WORKERS_STARTED.swap(true, Ordering::AcqRel) {
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
let mut current = SWQOS_WORKER_COUNT.load(Ordering::Acquire);
while current < n {
match SWQOS_WORKER_COUNT.compare_exchange(current, n, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
if current >= n {
return;
}
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
let notify = SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone();
for _ in 0..n {
for _ in current..n {
tokio::spawn(swqos_worker_loop(queue.clone(), notify.clone()));
}
}
@@ -232,6 +322,7 @@ struct TaskResult {
signature: Signature,
error: Option<anyhow::Error>,
swqos_type: SwqosType,
strategy_type: GasFeeStrategyType,
landed_on_chain: bool,
/// Microsecond timestamp when this task finished (SWQOS returned); for per-SWQOS event→submit timing.
submit_done_us: i64,
@@ -299,7 +390,7 @@ impl ResultCollector {
async fn wait_for_success(
&self,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let timeout = std::time::Duration::from_secs(5);
let poll_interval = std::time::Duration::from_millis(1000);
@@ -311,7 +402,7 @@ impl ResultCollector {
let mut submit_timings = Vec::new();
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
if result.success {
has_success = true;
}
@@ -329,7 +420,7 @@ impl ResultCollector {
let mut submit_timings = Vec::new();
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
// Prefer the error from the tx that actually landed
if result.landed_on_chain && result.error.is_some() {
landed_error = result.error;
@@ -348,7 +439,7 @@ impl ResultCollector {
let mut submit_timings = Vec::new();
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
if result.success {
any_success = true;
}
@@ -371,7 +462,7 @@ impl ResultCollector {
fn get_first(
&self,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let mut signatures = Vec::new();
let mut has_success = false;
let mut last_error = None;
@@ -379,7 +470,7 @@ impl ResultCollector {
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
if result.success {
has_success = true;
}
@@ -395,12 +486,36 @@ impl ResultCollector {
}
}
/// Fast submit mode for callers that do not wait for on-chain confirmation.
/// Return as soon as one route accepts, a landed failure consumes the nonce, all routes finish,
/// or the submit result window expires. Slow HTTP routes continue in worker tasks but no longer
/// block post-buy monitoring / sell scheduling.
async fn wait_for_first_submitted(
&self,
timeout: Duration,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let poll_interval = Duration::from_millis(1);
loop {
if self.success_flag.load(Ordering::Acquire)
|| self.landed_failed_flag.load(Ordering::Acquire)
|| self.completed_count.load(Ordering::Acquire) >= self.total_tasks
|| start.elapsed() >= timeout
{
return self.get_first();
}
tokio::time::sleep(poll_interval).await;
}
}
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 轮询间隔 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.
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let primary = Duration::from_secs(timeout_secs);
let poll_interval = Duration::from_millis(2);
@@ -414,8 +529,7 @@ impl ResultCollector {
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
if all_submitted {
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
tokio::time::sleep(Duration::from_millis(35)).await;
tokio::task::yield_now().await;
} else {
tokio::time::sleep(Duration::from_millis(600)).await;
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
@@ -430,13 +544,69 @@ impl ResultCollector {
}
}
type GasFeeConfig = (SwqosType, GasFeeStrategyType, GasFeeStrategyValue);
#[derive(Debug, Clone, Copy)]
struct SwqosTaskConfig {
task_ordinal: usize,
swqos_index: usize,
gas_fee_config: GasFeeConfig,
}
impl TaskResult {
#[inline]
fn submit_timing(&self) -> SwqosSubmitTiming {
SwqosSubmitTiming {
swqos_type: self.swqos_type,
strategy_type: self.strategy_type,
submit_done_us: self.submit_done_us,
}
}
}
fn select_swqos_task_configs(
swqos_types: &[SwqosType],
gas_fee_configs: &[GasFeeConfig],
with_tip: bool,
check_min_tip: bool,
min_tip_by_swqos: impl Fn(SwqosType) -> f64,
) -> Vec<SwqosTaskConfig> {
let mut task_configs = Vec::with_capacity(swqos_types.len() * 3);
for (i, swqos_type) in swqos_types.iter().copied().enumerate() {
if !with_tip && !matches!(swqos_type, SwqosType::Default) {
continue;
}
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
let min_tip = if check_tip { min_tip_by_swqos(swqos_type) } else { 0.0 };
for config in gas_fee_configs {
if config.0 != swqos_type {
continue;
}
if check_tip && config.2.tip < min_tip {
if crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
config.0, config.2.tip, min_tip
);
}
continue;
}
task_configs.push(SwqosTaskConfig {
task_ordinal: task_configs.len(),
swqos_index: i,
gas_fee_config: *config,
});
}
}
task_configs
}
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
///
/// `sender_config` merges sender_thread_cores, effective_core_ids, max_sender_concurrency (precomputed at SDK init; no get_core_ids on hot path).
pub async fn execute_parallel(
swqos_clients: &[Arc<SwqosClient>],
payer: Arc<Keypair>,
rpc: Option<&Arc<SolanaRpcClient>>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
@@ -445,12 +615,13 @@ pub async fn execute_parallel(
protocol_name: &'static str,
is_buy: bool,
wait_transaction_confirmed: bool,
wait_for_all_submits: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
use_dedicated_sender_threads: bool,
sender_config: SenderConcurrencyConfig,
check_min_tip: bool,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
if swqos_clients.is_empty() {
return Err(anyhow!("swqos_clients is empty"));
}
@@ -469,51 +640,36 @@ pub async fn execute_parallel(
// One get_strategies call per batch (avoid N calls in loop).
let gas_fee_configs =
gas_fee_strategy.get_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
for (i, swqos_client) in swqos_clients.iter().enumerate() {
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
continue;
}
let swqos_type = swqos_client.get_swqos_type();
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
let min_tip = if check_tip { swqos_client.min_tip_sol() } else { 0.0 };
for config in &gas_fee_configs {
if config.0 != swqos_type {
continue;
}
if check_tip {
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
config.0, config.2.tip, min_tip
);
}
if config.2.tip < min_tip {
continue;
}
}
task_configs.push((i, swqos_client.clone(), *config));
}
}
let swqos_types: Vec<SwqosType> =
swqos_clients.iter().map(|swqos| swqos.get_swqos_type()).collect();
let selected_task_configs = select_swqos_task_configs(
&swqos_types,
&gas_fee_configs,
with_tip,
check_min_tip,
|swqos_type| {
swqos_clients
.iter()
.find(|swqos| swqos.get_swqos_type() == swqos_type)
.map(|swqos| swqos.min_tip_sol())
.unwrap_or(0.0)
},
);
if task_configs.is_empty() {
if selected_task_configs.is_empty() {
return Err(anyhow!("No available gas fee strategy configs"));
}
if is_buy && task_configs.len() > 1 && durable_nonce.is_none() {
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 = task_configs.len().max(1);
let channel_count = selected_task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
let submit_timeout_secs: u64 =
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
rpc: rpc.cloned(),
address_lookup_table_account,
recent_blockhash,
durable_nonce,
@@ -540,10 +696,15 @@ pub async fn execute_parallel(
let effective_core_ids = sender_config.effective_core_ids.as_slice();
let core_len = effective_core_ids.len().max(1);
let mut tip_cache: FnvHashMap<*const (), Arc<Pubkey>> =
FnvHashMap::with_capacity_and_hasher(task_configs.len(), BuildHasherDefault::default());
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
let core_id = effective_core_ids.get(i % core_len).copied();
FnvHashMap::with_capacity_and_hasher(
selected_task_configs.len(),
BuildHasherDefault::default(),
);
for task_config in selected_task_configs {
let swqos_client = swqos_clients[task_config.swqos_index].clone();
let core_id = effective_core_ids.get(task_config.task_ordinal % core_len).copied();
let swqos_type = swqos_client.get_swqos_type();
let gas_fee_strategy_config = task_config.gas_fee_config;
let key = Arc::as_ptr(&swqos_client) as *const ();
let tip_account = match tip_cache.get(&key) {
Some(t) => t.clone(),
@@ -567,10 +728,21 @@ pub async fn execute_parallel(
tip_account,
swqos_client,
swqos_type,
strategy_type: gas_fee_strategy_config.1,
core_id,
use_affinity: !effective_core_ids.is_empty(),
};
let _ = queue.push(job);
if let Err(job) = queue.push(job) {
shared.collector.submit(TaskResult {
success: false,
signature: Signature::default(),
error: Some(anyhow!("SWQOS sender queue is full")),
swqos_type: job.swqos_type,
strategy_type: job.strategy_type,
landed_on_chain: false,
submit_done_us: crate::common::clock::now_micros(),
});
}
}
}
@@ -579,14 +751,23 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
vec![],
));
let ret = if wait_for_all_submits {
collector.wait_for_all_submitted(FAST_SUBMIT_RESULT_TIMEOUT.as_secs()).await.unwrap_or(
(
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
),
)
} else {
collector.wait_for_first_submitted(FAST_SUBMIT_RESULT_TIMEOUT).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
))
};
let (success, signatures, last_error, submit_timings) = ret;
return Ok((success, signatures, last_error, submit_timings));
}
@@ -598,3 +779,73 @@ pub async fn execute_parallel(
Err(anyhow!("All transactions failed"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn value(cu_price: u64, tip: f64) -> GasFeeStrategyValue {
GasFeeStrategyValue { cu_limit: 100_000, cu_price, tip }
}
#[test]
fn select_task_configs_keeps_two_fee_lanes_per_swqos() {
let swqos_types = [SwqosType::Jito, SwqosType::Helius];
let configs = [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
(SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
(SwqosType::Helius, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
];
let selected = select_swqos_task_configs(&swqos_types, &configs, true, false, |_| 0.0);
assert_eq!(selected.len(), 4);
assert_eq!(
selected.iter().filter(|task| task.gas_fee_config.0 == SwqosType::Jito).count(),
2
);
assert_eq!(
selected.iter().filter(|task| task.gas_fee_config.0 == SwqosType::Helius).count(),
2
);
assert_eq!(
selected.iter().map(|task| task.task_ordinal).collect::<Vec<_>>(),
vec![0, 1, 2, 3]
);
assert_eq!(
selected.iter().map(|task| task.swqos_index).collect::<Vec<_>>(),
vec![0, 0, 1, 1]
);
}
#[test]
fn select_task_configs_applies_min_tip_per_lane() {
let swqos_types = [SwqosType::Jito];
let configs = [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.0001)),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
];
let selected = select_swqos_task_configs(&swqos_types, &configs, true, true, |_| 0.001);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].gas_fee_config.1, GasFeeStrategyType::HighTipLowCuPrice);
}
#[test]
fn select_task_configs_without_tip_keeps_default_priority_fee_only() {
let swqos_types = [SwqosType::Jito, SwqosType::Default];
let configs = [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
(SwqosType::Default, GasFeeStrategyType::Normal, value(700_000, 0.0)),
];
let selected = select_swqos_task_configs(&swqos_types, &configs, false, false, |_| 0.0);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].gas_fee_config.0, SwqosType::Default);
assert_eq!(selected[0].gas_fee_config.2.cu_price, 700_000);
assert_eq!(selected[0].gas_fee_config.2.tip, 0.0);
}
}
+25 -18
View File
@@ -12,10 +12,9 @@ use std::{
use tracing::{info, trace, warn};
use super::{params::SwapParams, traits::InstructionBuilder};
use crate::swqos::SwqosType;
use crate::swqos::TradeType;
use crate::{
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient, SwqosSubmitTiming},
perf::syscall_bypass::SystemCallBypassManager,
swqos::common::poll_any_transaction_confirmation,
trading::core::{
@@ -56,7 +55,7 @@ impl TradeExecutor for GenericTradeExecutor {
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
let timing_start_us: Option<i64> = if params.log_enabled {
@@ -158,11 +157,14 @@ 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;
let sender_config = params.sender_concurrency_config();
let result = execute_parallel(
params.swqos_clients.as_slice(),
params.payer,
params.rpc.as_ref(),
final_instructions,
params.address_lookup_table_account,
params.recent_blockhash,
@@ -171,6 +173,7 @@ impl TradeExecutor for GenericTradeExecutor {
self.protocol_name,
is_buy,
false, // submit only here; confirmation and log timing handled below
wait_for_all_submits,
if is_buy { true } else { params.with_tip },
params.gas_fee_strategy,
params.use_dedicated_sender_threads,
@@ -188,7 +191,7 @@ impl TradeExecutor for GenericTradeExecutor {
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e)), vec![]),
};
// submit_timings 为完成先后顺序(先完成的先 push),打印不排序、不增加延迟
let submit_timings_ref: &[(crate::swqos::SwqosType, i64)] = submit_timings.as_slice();
let submit_timings_ref: &[SwqosSubmitTiming] = submit_timings.as_slice();
let result = if need_confirm {
let confirm_result = if let Some(rpc) = params.rpc.as_ref() {
@@ -258,7 +261,7 @@ async fn simulate_transaction(
is_buy: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
use crate::trading::common::build_transaction;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_commitment_config::CommitmentLevel;
@@ -282,7 +285,6 @@ async fn simulate_transaction(
let transaction = build_transaction(
&payer,
Some(&rpc),
unit_limit,
unit_price,
&instructions,
@@ -295,8 +297,7 @@ async fn simulate_transaction(
&Pubkey::default(),
tip,
durable_nonce.as_ref(),
)
.await?;
)?;
// Simulate the transaction
use solana_commitment_config::CommitmentConfig;
@@ -354,6 +355,7 @@ async fn simulate_transaction(
#[cfg(test)]
mod tests {
use crate::common::GasFeeStrategyType;
use crate::swqos::SwqosType;
/// 运行 `cargo test -p sol-trade-sdk log_timing_preview -- --nocapture` 查看日志打印效果
@@ -380,15 +382,17 @@ mod tests {
);
println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
(SwqosType::Jito, 45.12, 83.38, 128.50),
(SwqosType::Helius, 52.30, 76.20, 128.50),
(SwqosType::ZeroSlot, 48.90, 79.60, 128.50),
for (swqos_type, strategy_type, submit_ms, confirmed_ms, total_ms) in [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 45.12, 83.38, 128.50),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 46.08, 82.42, 128.50),
(SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, 52.30, 76.20, 128.50),
(SwqosType::ZeroSlot, GasFeeStrategyType::Normal, 48.90, 79.60, 128.50),
] {
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
swqos_type.as_str(),
dir,
strategy_type.as_str(),
submit_ms,
confirmed_ms,
total_ms,
@@ -399,13 +403,16 @@ mod tests {
println!(
"\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n"
);
for (swqos_type, submit_ms, total_ms) in
[(SwqosType::Jito, 44.20, 44.20), (SwqosType::Helius, 51.80, 51.80)]
{
for (swqos_type, strategy_type, submit_ms, total_ms) in [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 44.20, 44.20),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 45.10, 45.10),
(SwqosType::Helius, GasFeeStrategyType::Normal, 51.80, 51.80),
] {
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
swqos_type.as_str(),
dir,
strategy_type.as_str(),
submit_ms,
total_ms,
width = w
+11 -4
View File
@@ -75,11 +75,21 @@ pub struct SwapParams {
pub close_input_mint_ata: bool,
pub create_output_mint_ata: bool,
pub close_output_mint_ata: bool,
/// Fixed output amount. For protocols with exact-out instructions this selects exact-out
/// semantics and treats `input_amount` as the maximum input budget.
pub fixed_output_amount: Option<u64>,
pub gas_fee_strategy: GasFeeStrategy,
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`.
pub wait_for_all_submits: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
@@ -92,14 +102,11 @@ pub struct SwapParams {
pub check_min_tip: bool,
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
pub grpc_recv_us: Option<i64>,
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
/// Use exact quote-input buy instructions (legacy PumpFun uses SOL quote; V2/PumpSwap use generic quote).
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
pub use_exact_sol_amount: Option<bool>,
/// Use PumpFun V2 instructions (buy_v2 / sell_v2 / buy_exact_quote_in_v2, 27/26-account metas, quote_mint support).
/// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface.
pub use_pumpfun_v2: bool,
}
impl SwapParams {
@@ -12,6 +12,11 @@ pub struct MeteoraDammV2Params {
pub token_b_mint: Pubkey,
pub token_a_program: Pubkey,
pub token_b_program: Pubkey,
pub referral_token_account: Option<Pubkey>,
/// `swap2` mode: 0 exact-in, 1 partial-fill (recommended default), 2 exact-out.
pub swap_mode: u8,
/// Include the instructions sysvar remaining account when the pool's rate limiter applies.
pub include_rate_limiter_sysvar: bool,
}
impl MeteoraDammV2Params {
@@ -32,9 +37,27 @@ impl MeteoraDammV2Params {
token_b_mint,
token_a_program,
token_b_program,
referral_token_account: None,
swap_mode: crate::instruction::utils::meteora_damm_v2::SWAP_MODE_PARTIAL_FILL,
include_rate_limiter_sysvar: false,
}
}
pub fn with_referral_token_account(mut self, referral_token_account: Pubkey) -> Self {
self.referral_token_account = Some(referral_token_account);
self
}
pub fn with_swap_mode(mut self, swap_mode: u8) -> Self {
self.swap_mode = swap_mode;
self
}
pub fn with_rate_limiter_sysvar(mut self, include: bool) -> Self {
self.include_rate_limiter_sysvar = include;
self
}
pub async fn from_pool_address_by_rpc(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
@@ -61,6 +84,9 @@ impl MeteoraDammV2Params {
token_b_mint: pool_data.token_b_mint,
token_a_program,
token_b_program,
referral_token_account: None,
swap_mode: crate::instruction::utils::meteora_damm_v2::SWAP_MODE_PARTIAL_FILL,
include_rate_limiter_sysvar: false,
})
}
}
+161 -33
View File
@@ -14,9 +14,9 @@ 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**: Set `use_v2_ix = true` to use `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2`
/// with unified 27/26-account layout. Required for USDC-paired coins (`quote_mint != WSOL`).
/// For SOL-paired coins, legacy instructions still work and are the default.
/// **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.
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
@@ -32,22 +32,40 @@ pub struct PumpFunParams {
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
/// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache).
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致)。
/// `*.pump` mint 在 Pump.fun 指令构造层会强制使用 Token-2022,避免陈旧 parser/cache
/// 传入 legacy Token Program 后创建出 owner 不匹配的临时 token account。
pub token_program: Pubkey,
/// Whether to close token account when selling, only effective during sell operations
pub close_token_account_when_sell: Option<bool>,
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值
pub fee_recipient: Pubkey,
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
/// Quote mint layout selector. Default and `SOL_TOKEN_ACCOUNT` use legacy SOL layout.
/// `WSOL_TOKEN_ACCOUNT` selects SOL v2; USDC selects USDC v2.
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
pub quote_mint: Pubkey,
/// Whether to use v2 instructions (`buy_v2`/`sell_v2`/`buy_exact_quote_in_v2`).
/// Default `false` for backward compatibility. Must be `true` for USDC-paired coins.
pub use_v2_ix: bool,
}
impl PumpFunParams {
#[inline]
fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
Pubkey::default()
} else {
BondingCurveAccount::normalize_quote_mint(quote_mint)
}
}
#[inline]
fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
crate::constants::SOL_TOKEN_ACCOUNT
} else {
BondingCurveAccount::normalize_quote_mint(quote_mint)
}
}
pub fn immediate_sell(
creator_vault: Pubkey,
token_program: Pubkey,
@@ -63,12 +81,12 @@ impl PumpFunParams {
close_token_account_when_sell: Some(close_token_account_when_sell),
fee_recipient: Pubkey::default(),
quote_mint: Pubkey::default(),
use_v2_ix: false,
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
/// Build PumpFun params from a SOL-paired dev trade event. For USDC-paired dev trades,
/// use [`Self::from_dev_trade_with_quote_mint`] so reserve reconstruction uses the right quote mint.
/// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts.
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
pub fn from_dev_trade(
@@ -85,15 +103,51 @@ impl PumpFunParams {
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
bonding_curve,
&mint,
Self::from_dev_trade_with_quote_mint(
mint,
token_amount,
max_sol_cost,
creator,
bonding_curve,
associated_bonding_curve,
creator_vault,
close_token_account_when_sell,
fee_recipient,
token_program,
is_cashback_coin,
mayhem_mode,
Pubkey::default(),
)
}
/// Quote-aware constructor for PumpFun V2 pools. Use this for USDC-paired coins so
/// dev-trade reserve reconstruction uses the quote mint's initial virtual reserves.
pub fn from_dev_trade_with_quote_mint(
mint: Pubkey,
token_amount: u64,
max_quote_cost: u64,
creator: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
creator_vault: Pubkey,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
quote_mint: Pubkey,
) -> Self {
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint);
let bonding_curve_account = BondingCurveAccount::from_dev_trade_with_quote_mint(
bonding_curve,
&mint,
token_amount,
max_quote_cost,
creator,
is_mayhem_mode,
is_cashback_coin,
effective_quote_mint,
);
let creator_vault_resolved =
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
@@ -117,13 +171,14 @@ impl PumpFunParams {
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
quote_mint: Pubkey::default(),
use_v2_ix: false,
quote_mint: Self::quote_mint_for_layout(quote_mint),
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
/// 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.
/// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts.
///
/// `mayhem_mode`:
/// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。
@@ -132,12 +187,13 @@ impl PumpFunParams {
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
mint: Pubkey,
quote_mint: Pubkey,
creator: Pubkey,
creator_vault: Pubkey,
virtual_token_reserves: u64,
virtual_sol_reserves: u64,
virtual_quote_reserves: u64,
real_token_reserves: u64,
real_sol_reserves: u64,
real_quote_reserves: u64,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
@@ -145,16 +201,18 @@ impl PumpFunParams {
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
let bonding_curve = BondingCurveAccount::from_trade(
let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint);
let bonding_curve = BondingCurveAccount::from_trade_with_quote_mint(
bonding_curve,
mint,
creator,
virtual_token_reserves,
virtual_sol_reserves,
virtual_quote_reserves,
real_token_reserves,
real_sol_reserves,
real_quote_reserves,
is_mayhem_mode,
is_cashback_coin,
effective_quote_mint,
);
let creator_vault_resolved =
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
@@ -176,11 +234,48 @@ impl PumpFunParams {
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
quote_mint: Pubkey::default(),
use_v2_ix: false,
quote_mint: Self::quote_mint_for_layout(quote_mint),
}
}
/// Deprecated compatibility alias. Prefer [`Self::from_trade`] and pass `quote_mint` there.
#[deprecated(note = "use PumpFunParams::from_trade(..., quote_mint)")]
pub fn from_trade_with_quote_mint(
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
mint: Pubkey,
creator: Pubkey,
creator_vault: Pubkey,
virtual_token_reserves: u64,
virtual_quote_reserves: u64,
real_token_reserves: u64,
real_quote_reserves: u64,
close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
quote_mint: Pubkey,
) -> Self {
Self::from_trade(
bonding_curve,
associated_bonding_curve,
mint,
quote_mint,
creator,
creator_vault,
virtual_token_reserves,
virtual_quote_reserves,
real_token_reserves,
real_quote_reserves,
close_token_account_when_sell,
fee_recipient,
token_program,
is_cashback_coin,
mayhem_mode,
)
}
/// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
@@ -201,6 +296,7 @@ impl PumpFunParams {
creator: account.0.creator,
is_mayhem_mode: account.0.is_mayhem_mode,
is_cashback_coin: account.0.is_cashback_coin,
quote_mint: account.0.quote_mint,
};
let associated_bonding_curve = get_associated_token_address_with_program_id(
&bonding_curve.account,
@@ -223,6 +319,7 @@ impl PumpFunParams {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
})
.unwrap_or_default();
let quote_mint = bonding_curve.quote_mint;
Ok(Self {
bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve,
@@ -232,8 +329,7 @@ impl PumpFunParams {
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
quote_mint: Pubkey::default(),
use_v2_ix: false,
quote_mint: Self::quote_mint_for_rpc_return(quote_mint),
})
}
@@ -272,12 +368,15 @@ impl PumpFunParams {
Ok(self)
}
/// Sets `quote_mint` and enables v2 instructions. Required for USDC-paired coins.
/// For SOL-paired coins, pass `WSOL_TOKEN_ACCOUNT` or leave default.
/// 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.
#[inline]
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
self.quote_mint = quote_mint;
self.use_v2_ix = quote_mint != Pubkey::default();
let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint);
self.quote_mint = Self::quote_mint_for_layout(quote_mint);
let curve = Arc::make_mut(&mut self.bonding_curve);
*curve = curve.clone().with_quote_mint(effective_quote_mint);
self
}
@@ -305,3 +404,32 @@ impl PumpFunParams {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rpc_return_quote_mint_normalizes_legacy_sol_to_sol_sentinel() {
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(Pubkey::default()),
crate::constants::SOL_TOKEN_ACCOUNT
);
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(crate::constants::SOL_TOKEN_ACCOUNT),
crate::constants::SOL_TOKEN_ACCOUNT
);
}
#[test]
fn rpc_return_quote_mint_keeps_real_quote_mints() {
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(crate::constants::USDC_TOKEN_ACCOUNT),
crate::constants::USDC_TOKEN_ACCOUNT
);
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT),
crate::constants::WSOL_TOKEN_ACCOUNT
);
}
}
+86 -1
View File
@@ -16,6 +16,26 @@ pub struct RaydiumAmmV4Params {
pub token_coin: Pubkey,
/// Pool's pc token account address
pub token_pc: Pubkey,
/// AMM open orders account
pub amm_open_orders: Pubkey,
/// AMM target orders account
pub amm_target_orders: Pubkey,
/// Serum/OpenBook program used by the AMM market
pub serum_program: Pubkey,
/// Serum/OpenBook market account
pub serum_market: Pubkey,
/// Serum/OpenBook bids account
pub serum_bids: Pubkey,
/// Serum/OpenBook asks account
pub serum_asks: Pubkey,
/// Serum/OpenBook event queue account
pub serum_event_queue: Pubkey,
/// Serum/OpenBook coin vault account
pub serum_coin_vault_account: Pubkey,
/// Serum/OpenBook pc vault account
pub serum_pc_vault_account: Pubkey,
/// Serum/OpenBook vault signer PDA
pub serum_vault_signer: Pubkey,
/// Current coin reserve amount in the pool
pub coin_reserve: u64,
/// Current pc reserve amount in the pool
@@ -32,13 +52,68 @@ impl RaydiumAmmV4Params {
coin_reserve: u64,
pc_reserve: u64,
) -> Self {
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
Self {
amm,
coin_mint,
pc_mint,
token_coin,
token_pc,
amm_open_orders: Pubkey::default(),
amm_target_orders: Pubkey::default(),
serum_program: Pubkey::default(),
serum_market: Pubkey::default(),
serum_bids: Pubkey::default(),
serum_asks: Pubkey::default(),
serum_event_queue: Pubkey::default(),
serum_coin_vault_account: Pubkey::default(),
serum_pc_vault_account: Pubkey::default(),
serum_vault_signer: Pubkey::default(),
coin_reserve,
pc_reserve,
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_market_accounts(
mut self,
amm_open_orders: Pubkey,
amm_target_orders: Pubkey,
serum_program: Pubkey,
serum_market: Pubkey,
serum_bids: Pubkey,
serum_asks: Pubkey,
serum_event_queue: Pubkey,
serum_coin_vault_account: Pubkey,
serum_pc_vault_account: Pubkey,
serum_vault_signer: Pubkey,
) -> Self {
self.amm_open_orders = amm_open_orders;
self.amm_target_orders = amm_target_orders;
self.serum_program = serum_program;
self.serum_market = serum_market;
self.serum_bids = serum_bids;
self.serum_asks = serum_asks;
self.serum_event_queue = serum_event_queue;
self.serum_coin_vault_account = serum_coin_vault_account;
self.serum_pc_vault_account = serum_pc_vault_account;
self.serum_vault_signer = serum_vault_signer;
self
}
pub async fn from_amm_address_by_rpc(
rpc: &SolanaRpcClient,
amm: Pubkey,
) -> Result<Self, anyhow::Error> {
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
let market_state =
crate::instruction::utils::raydium_amm_v4::fetch_market_state(rpc, amm_info.market)
.await?;
let serum_vault_signer =
crate::instruction::utils::raydium_amm_v4::derive_serum_vault_signer(
&amm_info.serum_dex,
&amm_info.market,
market_state.vault_signer_nonce,
)?;
let (coin_reserve, pc_reserve) =
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
Ok(Self {
@@ -47,6 +122,16 @@ impl RaydiumAmmV4Params {
pc_mint: amm_info.pc_mint,
token_coin: amm_info.token_coin,
token_pc: amm_info.token_pc,
amm_open_orders: amm_info.open_orders,
amm_target_orders: amm_info.target_orders,
serum_program: amm_info.serum_dex,
serum_market: amm_info.market,
serum_bids: market_state.serum_bids,
serum_asks: market_state.serum_asks,
serum_event_queue: market_state.serum_event_queue,
serum_coin_vault_account: market_state.serum_coin_vault_account,
serum_pc_vault_account: market_state.serum_pc_vault_account,
serum_vault_signer,
coin_reserve,
pc_reserve,
})
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::swqos::SwqosType;
use crate::common::SwqosSubmitTiming;
use crate::trading::SwapParams;
use anyhow::Result;
use solana_sdk::{instruction::Instruction, signature::Signature};
@@ -12,7 +12,7 @@ pub trait TradeExecutor: Send + Sync {
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)>;
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)>;
/// 获取协议名称
fn protocol_name(&self) -> &'static str;
}
+5 -5
View File
@@ -17,6 +17,7 @@ const PARALLEL_SENDER_COUNT: usize = 18;
/// 启动时预填充数量,必须 >= PARALLEL_SENDER_COUNT,否则 18 路并发 build 会触发分配或争抢
const TX_BUILDER_POOL_PREFILL: usize = 64;
use anyhow::Result;
use crossbeam_queue::ArrayQueue;
use once_cell::sync::Lazy;
use solana_message::AddressLookupTableAccount;
@@ -82,7 +83,7 @@ impl PreallocatedTxBuilder {
instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Hash,
) -> VersionedMessage {
) -> Result<VersionedMessage> {
self.reset();
self.instructions.extend_from_slice(instructions);
@@ -92,14 +93,13 @@ impl PreallocatedTxBuilder {
&self.instructions,
std::slice::from_ref(alt),
recent_blockhash,
)
.expect("v0 message compile failed");
VersionedMessage::V0(message)
)?;
Ok(VersionedMessage::V0(message))
} else {
// ✅ 没有查找表,使用 Legacy 消息(兼容所有 RPC
let message =
Message::new_with_blockhash(&self.instructions, Some(payer), &recent_blockhash);
VersionedMessage::Legacy(message)
Ok(VersionedMessage::Legacy(message))
}
}
}
+12 -29
View File
@@ -1,22 +1,10 @@
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
use solana_sdk::pubkey::Pubkey;
fn sol_to_lamports(sol: f64) -> u64 {
(sol * 1_000_000_000.0) as u64
}
use crate::{
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
utils::calc::common::compute_fee,
};
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
#[inline]
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
}
/// Calculates the amount of tokens that can be purchased with a given SOL amount
/// using the bonding curve formula.
///
@@ -54,26 +42,21 @@ pub fn get_buy_token_amount_from_sol_amount(
let input_amount = amount_128
.checked_mul(10_000)
.unwrap()
.checked_div(total_fee_basis_points_128 + 10_000)
.unwrap();
.and_then(|v| v.checked_div(total_fee_basis_points_128 + 10_000))
.unwrap_or(0);
let denominator = virtual_sol_reserves + input_amount;
let mut tokens_received =
input_amount.checked_mul(virtual_token_reserves).unwrap().checked_div(denominator).unwrap();
tokens_received = tokens_received.min(real_token_reserves);
if tokens_received <= 100 * 1_000_000_u128 {
tokens_received = if amount > sol_str_to_lamports("0.01").unwrap_or(0) {
25547619 * 1_000_000_u128
} else {
255476 * 1_000_000_u128
};
let Some(denominator) = virtual_sol_reserves.checked_add(input_amount) else { return 0 };
if denominator == 0 {
return 0;
}
tokens_received as u64
let tokens_received = input_amount
.checked_mul(virtual_token_reserves)
.and_then(|v| v.checked_div(denominator))
.unwrap_or(0)
.min(real_token_reserves);
tokens_received.min(u64::MAX as u128) as u64
}
/// Calculates the amount of SOL that will be received when selling a given token amount