Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7471bcba09 | |||
| b8325b2b52 | |||
| e459f175f5 | |||
| e5e58bef89 | |||
| 9874cd5358 | |||
| 1d3c0ff5da | |||
| 34f400e89d | |||
| bd1772bed2 | |||
| 5449d51591 | |||
| b973061f92 | |||
| 57e4c5d221 | |||
| 8ed2aef931 | |||
| 0cf0064e80 |
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.10"
|
||||
version = "4.0.19"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -34,6 +34,7 @@ members = [
|
||||
"examples/cli_trading",
|
||||
"examples/gas_fee_strategy",
|
||||
"examples/meteora_damm_v2_direct_trading",
|
||||
"examples/simple_trading",
|
||||
]
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -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.9" }
|
||||
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.9"
|
||||
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
|
||||
@@ -190,40 +196,74 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001,
|
||||
For detailed information about all trading parameters, see the [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
|
||||
|
||||
```rust
|
||||
// Import DexParamEnum for protocol-specific parameters
|
||||
use sol_trade_sdk::trading::core::params::DexParamEnum;
|
||||
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::PumpSwap,
|
||||
input_token_type: TradeTokenType::WSOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: buy_sol_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
// Use DexParamEnum for type-safe protocol parameters (zero-overhead abstraction)
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None, // Optional: specify exact output amount
|
||||
gas_fee_strategy: gas_fee_strategy.clone(), // Gas fee strategy configuration
|
||||
simulate: false, // Set to true for simulation only
|
||||
use_exact_sol_amount: None, // Use exact SOL input for PumpFun/PumpSwap (defaults to true)
|
||||
use sol_trade_sdk::{
|
||||
AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType,
|
||||
trading::core::params::DexParamEnum,
|
||||
};
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
// Token used to pay. For PumpFun V2 SOL/WSOL quote pools, keep this as SOL
|
||||
// when you want to spend native SOL; the SDK will still use V2 accounts.
|
||||
TradeTokenType::SOL,
|
||||
// Mint of the meme/token you want to buy.
|
||||
mint_pubkey,
|
||||
// Regular PumpFun/PumpSwap buy. The SDK estimates token output and applies
|
||||
// slippage to the maximum quote cost.
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
// Protocol state from parser/RPC cache, for example PumpFunParams::from_trade(...).
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
// Pass a cached recent blockhash; the SDK does not fetch it on the hot path.
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
// 300 = 3%.
|
||||
.slippage_basis_points(300)
|
||||
// For bots/sniping: assume ATAs are already prepared and keep the tx small.
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
```
|
||||
|
||||
#### 4. Execute Trading
|
||||
|
||||
```rust
|
||||
client.buy(buy_params).await?;
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
### ⚡ Trading Parameters
|
||||
|
||||
For comprehensive information about all trading parameters including `TradeBuyParams` and `TradeSellParams`, see the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
|
||||
Use `SimpleBuyParams` / `SimpleSellParams` for new integrations. They describe trading intent and hide low-level ATA flags. Most users only choose:
|
||||
|
||||
- `pay_with` / `receive_as`: quote token direction. Use `SOL` when the wallet spends or receives native SOL. For PumpFun V2 SOL-paired pools whose quote mint is WSOL, still use `SOL` if you want native SOL settlement.
|
||||
- `amount`: trade sizing intent. Pick one enum variant instead of combining `input_token_amount`, `fixed_output_token_amount`, and `use_exact_sol_amount`.
|
||||
- `account_policy`: account creation behavior. Bots usually use `HotPathMinimal`; normal apps can keep the default `Auto`.
|
||||
|
||||
| Parameter | Meaning | Recommendation |
|
||||
|---|---|---|
|
||||
| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum output. | Normal swaps |
|
||||
| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy with slippage applied to max quote cost. | Sniping/arbitrage |
|
||||
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount with a max quote budget. | Exact-output workflows |
|
||||
| `SellAmount::ExactInput(amount)` | Sell exactly this token amount. | Normal sells |
|
||||
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where the DEX supports it. | Exact-output sells |
|
||||
| `AccountPolicy::Auto` | SDK creates practical ATAs when needed. | General usage |
|
||||
| `AccountPolicy::HotPathMinimal` | Avoid ATA create/close instructions in the trade tx. | Bots, sniping, latency-sensitive flows |
|
||||
| `AccountPolicy::CreateMissing` | Include ATA creation instructions where possible. | Convenience over transaction size |
|
||||
| `AccountPolicy::AssumePrepared` | Caller prepared every required ATA. | Deterministic advanced flows |
|
||||
|
||||
Optional builder methods:
|
||||
|
||||
| Method | Meaning |
|
||||
|---|---|
|
||||
| `.slippage_basis_points(300)` | Set slippage. `300` means 3%. |
|
||||
| `.address_lookup_table_account(alt)` | Attach an ALT to reduce transaction size. Useful for large PumpFun V2 transactions. |
|
||||
| `.wait_tx_confirmed(true)` | Return only after confirmation. Usually disabled for fastest submit paths. |
|
||||
| `.wait_for_all_submits(true)` | In fast-submit mode, wait for all SWQoS lane responses and return all signatures. |
|
||||
| `.simulate(true)` | Build and simulate the transaction instead of sending it. |
|
||||
| `.grpc_recv_us(ts)` | Attach upstream receive timestamp for latency tracing. |
|
||||
| `.durable_nonce(nonce_info)` | Use durable nonce and clear `recent_blockhash`. Recommended when you start from `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)`. |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Construct params directly with durable nonce instead of `recent_blockhash`. |
|
||||
| `SimpleSellParams::with_tip(false)` | Disable relay tips for sells. Buys use the gas fee strategy/tip settings. |
|
||||
|
||||
`TradeBuyParams` and `TradeSellParams` remain available as advanced low-level APIs. See the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
|
||||
|
||||
#### About ShredStream
|
||||
|
||||
@@ -234,6 +274,7 @@ Please ensure that the parameters your trading logic depends on are available in
|
||||
|
||||
| Description | Run Command | Source Code |
|
||||
|-------------|-------------|-------------|
|
||||
| Simple buy/sell parameter API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
|
||||
| Create and configure TradingClient instance | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
||||
| Share infrastructure across multiple wallets | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
|
||||
| PumpFun token sniping trading | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
||||
@@ -283,7 +324,29 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- If no custom URL is provided (`None`), the system will use the default endpoint for the specified `SwqosRegion`
|
||||
- This allows for maximum flexibility while maintaining backward compatibility
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. Fetch the latest nonce value and attach it to the high-level buy/sell params:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
recent_blockhash, // will be cleared by `.durable_nonce(...)`
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(nonce_info)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||
|
||||
@@ -335,7 +398,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. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `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. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `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 +418,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 +429,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.
|
||||
|
||||
|
||||
+128
-46
@@ -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.9" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "4.0.9"
|
||||
sol-trade-sdk = "4.0.17"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -188,40 +195,74 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001,
|
||||
有关所有交易参数的详细信息,请参阅 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
|
||||
|
||||
```rust
|
||||
// 导入 DexParamEnum 用于协议特定参数
|
||||
use sol_trade_sdk::trading::core::params::DexParamEnum;
|
||||
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::PumpSwap,
|
||||
input_token_type: TradeTokenType::WSOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: buy_sol_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
// 使用 DexParamEnum 实现类型安全的协议参数(零开销抽象)
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None, // 可选:指定精确输出数量
|
||||
gas_fee_strategy: gas_fee_strategy.clone(), // Gas 费用策略配置
|
||||
simulate: false, // 设为 true 仅进行模拟
|
||||
use_exact_sol_amount: None, // 对 PumpFun/PumpSwap 使用精确 SOL 输入(默认为 true)
|
||||
use sol_trade_sdk::{
|
||||
AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType,
|
||||
trading::core::params::DexParamEnum,
|
||||
};
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
// 支付币种。PumpFun V2 的 SOL/WSOL quote 池,如果你想花原生 SOL,
|
||||
// 这里仍然传 SOL;SDK 内部会按 V2 账户布局处理。
|
||||
TradeTokenType::SOL,
|
||||
// 要买入的 meme/token mint。
|
||||
mint_pubkey,
|
||||
// 常规 PumpFun/PumpSwap buy。SDK 先估算能买到多少 token,
|
||||
// 再把滑点应用到最大 quote 成本上。
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
// 协议状态参数,通常来自 parser/RPC 缓存,例如 PumpFunParams::from_trade(...)。
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
// 传入外部缓存的 recent_blockhash;SDK 不在热路径里临时获取。
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
// 300 = 3%。
|
||||
.slippage_basis_points(300)
|
||||
// Bot/狙击推荐:假设 ATA 已提前准备好,交易内不创建/关闭 ATA,体积更小。
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
```
|
||||
|
||||
#### 4. 执行交易
|
||||
|
||||
```rust
|
||||
client.buy(buy_params).await?;
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
### ⚡ 交易参数
|
||||
|
||||
有关所有交易参数(包括 `TradeBuyParams` 和 `TradeSellParams`)的详细信息,请参阅专门的 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
|
||||
新接入建议优先使用 `SimpleBuyParams` / `SimpleSellParams`。它们描述交易意图,SDK 内部处理底层 ATA 参数。多数用户只需要选择:
|
||||
|
||||
- `pay_with` / `receive_as`:买入时用什么 quote 支付,卖出时收什么 quote。钱包实际花/收原生 SOL 就传 `SOL`。PumpFun V2 的 SOL 配对池虽然 `quote_mint` 是 WSOL,但你想用原生 SOL 结算时这里仍传 `SOL`。
|
||||
- `amount`:交易数量语义。用一个枚举表达意图,不再同时理解 `input_token_amount`、`fixed_output_token_amount`、`use_exact_sol_amount`。
|
||||
- `account_policy`:账户创建策略。Bot 通常用 `HotPathMinimal`;普通应用可以保留默认 `Auto`。
|
||||
|
||||
| 参数 | 含义 | 推荐场景 |
|
||||
|---|---|---|
|
||||
| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量;滑点保护最小买到数量。 | 普通买入 |
|
||||
| `BuyAmount::WithMaxInput { quote_amount }` | PumpFun/PumpSwap 常规 buy,滑点作用在最大 quote 成本上。 | 狙击、套利 |
|
||||
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最大 quote 成本。 | 精确输出 |
|
||||
| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量。 | 普通卖出 |
|
||||
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | 精确输出卖出 |
|
||||
| `AccountPolicy::Auto` | SDK 按交易路径创建必要 ATA。 | 普通用户 |
|
||||
| `AccountPolicy::HotPathMinimal` | 交易内避免创建/关闭 ATA。 | Bot、狙击、低延迟 |
|
||||
| `AccountPolicy::CreateMissing` | 尽量在交易内创建缺失 ATA。 | 优先方便,不追求最小交易体积 |
|
||||
| `AccountPolicy::AssumePrepared` | 调用方保证所有 ATA 已准备好。 | 高级确定性流程 |
|
||||
|
||||
可选 builder 方法:
|
||||
|
||||
| 方法 | 含义 |
|
||||
|---|---|
|
||||
| `.slippage_basis_points(300)` | 设置滑点。`300` 表示 3%。 |
|
||||
| `.address_lookup_table_account(alt)` | 传入 ALT 以减少交易体积。PumpFun V2 交易较大时很有用。 |
|
||||
| `.wait_tx_confirmed(true)` | 等链上确认后再返回。追求最快提交时通常关闭。 |
|
||||
| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 |
|
||||
| `.simulate(true)` | 只构建并模拟交易,不真正发送。 |
|
||||
| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
| `.durable_nonce(nonce_info)` | 使用 durable nonce,并清空 `recent_blockhash`。如果你从 `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)` 开始构造,推荐用这个。 |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 直接用 durable nonce 构造参数,不使用 `recent_blockhash`。 |
|
||||
| `SimpleSellParams::with_tip(false)` | 关闭卖出交易 relay tip。买入的 tip 使用 gas fee strategy 控制。 |
|
||||
|
||||
`TradeBuyParams` 和 `TradeSellParams` 仍保留为高级低层接口。详细说明见 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
|
||||
|
||||
#### 关于shredstream
|
||||
|
||||
@@ -232,6 +273,7 @@ client.buy(buy_params).await?;
|
||||
|
||||
| 描述 | 运行命令 | 源码路径 |
|
||||
|------|---------|----------|
|
||||
| 简化买卖参数 API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
|
||||
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
||||
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
|
||||
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
||||
@@ -281,7 +323,29 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- 如果没有提供自定义 URL(`None`),系统将使用指定 `SwqosRegion` 的默认端点
|
||||
- 这提供了最大的灵活性,同时保持向后兼容性
|
||||
|
||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||
当使用多个 MEV 服务时,需要使用 `Durable Nonce`。先获取最新 nonce,再挂到新的 buy/sell 参数上:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
recent_blockhash, // 会被 `.durable_nonce(...)` 清空
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(nonce_info)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
#### Astralane(Binary / Plain / QUIC)
|
||||
|
||||
@@ -333,7 +397,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 +424,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 v2(buy_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 +438,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 V2,USDC 表示 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 买入、卖出也结算为 USDC;SOL/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 保护服务
|
||||
|
||||
|
||||
@@ -4,14 +4,104 @@ This document provides a comprehensive reference for all trading parameters used
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams)
|
||||
- [TradeBuyParams](#tradebuyparams)
|
||||
- [TradeSellParams](#tradesellparams)
|
||||
- [Parameter Categories](#parameter-categories)
|
||||
- [Important Notes](#important-notes)
|
||||
|
||||
## SimpleBuyParams / SimpleSellParams
|
||||
|
||||
Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the public API focused on trading intent and map to the lower-level `TradeBuyParams` / `TradeSellParams` internally.
|
||||
|
||||
### SimpleBuyParams
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. |
|
||||
| `pay_with` | `TradeTokenType` | ✅ | Quote token used to pay for the buy. Use `SOL` when the wallet spends native SOL. For PumpFun V2 SOL/WSOL quote pools, still use `SOL` if you want native SOL settlement. |
|
||||
| `mint` | `Pubkey` | ✅ | Mint of the token being bought. |
|
||||
| `amount` | `BuyAmount` | ✅ | Buy sizing intent. Choose one enum variant instead of combining low-level amount flags. |
|
||||
| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache, such as `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`. |
|
||||
| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. The SDK does not fetch this on the hot path. |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
|
||||
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
|
||||
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
|
||||
|
||||
### SimpleSellParams
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. |
|
||||
| `receive_as` | `TradeTokenType` | ✅ | Quote token to receive from the sell. Use `SOL` when you want native SOL output. |
|
||||
| `mint` | `Pubkey` | ✅ | Mint of the token being sold. |
|
||||
| `amount` | `SellAmount` | ✅ | Sell sizing intent. |
|
||||
| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache. |
|
||||
| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
|
||||
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
|
||||
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
|
||||
| `with_tip` | `bool` | ❌ | Whether sells include relay tips. Default is `true`; set with `.with_tip(false)`. |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
|
||||
|
||||
### Amount Selection
|
||||
|
||||
| Variant | Meaning | Low-level mapping |
|
||||
|---------|---------|-------------------|
|
||||
| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum token output. | `input_token_amount = amount`, `use_exact_sol_amount = Some(true)` |
|
||||
| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy. The SDK estimates output and applies slippage to max quote cost. | `input_token_amount = quote_amount`, `use_exact_sol_amount = Some(false)` |
|
||||
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount while limiting max quote input. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` |
|
||||
| `SellAmount::ExactInput(amount)` | Sell exactly this token amount; slippage protects minimum quote output. | `input_token_amount = amount` |
|
||||
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where supported. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` |
|
||||
|
||||
### AccountPolicy
|
||||
|
||||
| Variant | Behavior | Use when |
|
||||
|---------|----------|----------|
|
||||
| `Auto` | SDK creates practical ATAs when needed. Buy creates the target mint ATA; sell creates the output ATA for non-SOL outputs. | Normal apps and manual trading tools. |
|
||||
| `HotPathMinimal` | No ATA create/close instructions in the trade transaction. | Bots, sniping, arbitrage, and any path sensitive to transaction size. |
|
||||
| `CreateMissing` | Include ATA creation where possible. | Convenience matters more than smallest transaction size. |
|
||||
| `AssumePrepared` | Do not create or close token accounts; caller prepared everything. | Advanced deterministic flows. |
|
||||
|
||||
### Durable Nonce With Simple Params
|
||||
|
||||
`fetch_nonce_info` and `DurableNonceInfo` are re-exported from the crate root:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
recent_blockhash,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(nonce_info);
|
||||
```
|
||||
|
||||
Calling `.durable_nonce(...)` clears `recent_blockhash`; nonce transactions use the nonce value as the transaction blockhash.
|
||||
|
||||
## TradeBuyParams
|
||||
|
||||
The `TradeBuyParams` struct contains all parameters required for executing buy orders across different DEX protocols.
|
||||
`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags.
|
||||
|
||||
### Basic Trading Parameters
|
||||
|
||||
|
||||
@@ -4,14 +4,104 @@
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams)
|
||||
- [TradeBuyParams](#tradebuyparams)
|
||||
- [TradeSellParams](#tradesellparams)
|
||||
- [参数分类](#参数分类)
|
||||
- [重要说明](#重要说明)
|
||||
|
||||
## SimpleBuyParams / SimpleSellParams
|
||||
|
||||
新接入优先使用 `SimpleBuyParams` 和 `SimpleSellParams`。这两个结构体描述交易意图,SDK 内部会转换成低层 `TradeBuyParams` / `TradeSellParams`。
|
||||
|
||||
### SimpleBuyParams
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 |
|
||||
| `pay_with` | `TradeTokenType` | ✅ | 买入时用什么 quote 支付。钱包实际花原生 SOL 就传 `SOL`。PumpFun V2 的 SOL/WSOL quote 池,如果你想用原生 SOL 结算,也仍然传 `SOL`。 |
|
||||
| `mint` | `Pubkey` | ✅ | 要买入的 token mint。 |
|
||||
| `amount` | `BuyAmount` | ✅ | 买入数量语义。选择一个枚举,不再组合多个低层数量字段。 |
|
||||
| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存,例如 `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`。 |
|
||||
| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。SDK 不会在热路径临时获取。 |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
|
||||
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
|
||||
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)` 或 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
|
||||
### SimpleSellParams
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 |
|
||||
| `receive_as` | `TradeTokenType` | ✅ | 卖出后接收什么 quote。想收原生 SOL 就传 `SOL`。 |
|
||||
| `mint` | `Pubkey` | ✅ | 要卖出的 token mint。 |
|
||||
| `amount` | `SellAmount` | ✅ | 卖出数量语义。 |
|
||||
| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存。 |
|
||||
| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。 |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
|
||||
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
|
||||
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)` 或 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
|
||||
| `with_tip` | `bool` | ❌ | 卖出交易是否带 relay tip。默认 `true`,可通过 `.with_tip(false)` 关闭。 |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
|
||||
### 数量如何选择
|
||||
|
||||
| 枚举 | 含义 | 底层映射 |
|
||||
|------|------|----------|
|
||||
| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量,滑点保护最小买到 token 数量。 | `input_token_amount = amount`,`use_exact_sol_amount = Some(true)` |
|
||||
| `BuyAmount::WithMaxInput { quote_amount }` | 常规 PumpFun/PumpSwap buy。SDK 估算输出,并把滑点作用在最大 quote 成本上。 | `input_token_amount = quote_amount`,`use_exact_sol_amount = Some(false)` |
|
||||
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最多花多少 quote。 | `fixed_output_token_amount = Some(output_amount)`,`input_token_amount = max_input_amount` |
|
||||
| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量,滑点保护最少收到 quote 数量。 | `input_token_amount = amount` |
|
||||
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | `fixed_output_token_amount = Some(output_amount)`,`input_token_amount = max_input_amount` |
|
||||
|
||||
### AccountPolicy
|
||||
|
||||
| 枚举 | 行为 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `Auto` | SDK 按实际路径创建必要 ATA。买入会创建目标 token ATA;卖出接收非 SOL 时会创建输出 ATA。 | 普通应用、手动交易工具。 |
|
||||
| `HotPathMinimal` | 交易内不创建/关闭 ATA。 | Bot、狙击、套利、对交易体积敏感的路径。 |
|
||||
| `CreateMissing` | 尽量在交易内创建缺失 ATA。 | 更重视方便,不追求最小交易体积。 |
|
||||
| `AssumePrepared` | 不创建也不关闭 token account,调用方保证都已准备好。 | 高级确定性流程。 |
|
||||
|
||||
### Simple 参数使用 Durable Nonce
|
||||
|
||||
`fetch_nonce_info` 和 `DurableNonceInfo` 已从 crate root 重新导出:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
recent_blockhash,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(nonce_info);
|
||||
```
|
||||
|
||||
调用 `.durable_nonce(...)` 会清空 `recent_blockhash`;nonce 交易会使用 nonce value 作为 transaction blockhash。
|
||||
|
||||
## TradeBuyParams
|
||||
|
||||
`TradeBuyParams` 结构体包含在不同 DEX 协议上执行买入订单所需的所有参数。
|
||||
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它。
|
||||
|
||||
### 基础交易参数
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
@@ -168,6 +179,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
)),
|
||||
address_lookup_table_account,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
|
||||
@@ -171,6 +171,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
@@ -222,6 +223,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
with_tip: false,
|
||||
durable_nonce: None,
|
||||
create_output_token_ata: false,
|
||||
|
||||
@@ -139,6 +139,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -183,6 +184,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -628,6 +628,7 @@ async fn handle_buy_pumpfun(
|
||||
extension_params: DexParamEnum::PumpFun(param),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -684,6 +685,7 @@ async fn handle_buy_pumpswap(
|
||||
extension_params: DexParamEnum::PumpSwap(param),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -740,6 +742,7 @@ async fn handle_buy_bonk(
|
||||
extension_params: DexParamEnum::Bonk(param),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -800,6 +803,7 @@ async fn handle_buy_raydium_v4(
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(param),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -861,6 +865,7 @@ async fn handle_buy_raydium_cpmm(
|
||||
extension_params: DexParamEnum::RaydiumCpmm(param),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -1031,6 +1036,7 @@ async fn handle_sell_pumpfun(
|
||||
extension_params: DexParamEnum::PumpFun(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1090,6 +1096,7 @@ async fn handle_sell_pumpswap(
|
||||
extension_params: DexParamEnum::PumpSwap(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1149,6 +1156,7 @@ async fn handle_sell_bonk(
|
||||
extension_params: DexParamEnum::Bonk(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1211,6 +1219,7 @@ async fn handle_sell_raydium_v4(
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1274,6 +1283,7 @@ async fn handle_sell_raydium_cpmm(
|
||||
extension_params: DexParamEnum::RaydiumCpmm(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -43,6 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -84,6 +85,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
|
||||
@@ -104,6 +104,7 @@ async fn test_middleware() -> AnyResult<()> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
@@ -164,6 +175,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
@@ -161,6 +169,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
@@ -197,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,6 +221,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: false,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -151,6 +158,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -181,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,6 +204,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -81,6 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -236,6 +236,7 @@ async fn pumpswap_trade_with_grpc(
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -277,6 +278,7 @@ async fn pumpswap_trade_with_grpc(
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
|
||||
@@ -157,6 +157,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(params),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -199,6 +200,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(params),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
|
||||
@@ -161,6 +161,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
@@ -201,6 +202,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
|
||||
address_lookup_table_account: None,
|
||||
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,
|
||||
|
||||
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -84,6 +85,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "simple_trading"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
solana-sdk = "3.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Simple high-level trading API example.
|
||||
//!
|
||||
//! This example focuses on parameter construction. Replace the placeholder
|
||||
//! PumpFun params with real params from your parser/RPC cache before sending.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use sol_trade_sdk::{
|
||||
common::{GasFeeStrategy, TradeConfig},
|
||||
constants::{TOKEN_PROGRAM_2022, WSOL_TOKEN_ACCOUNT},
|
||||
instruction::utils::pumpfun::global_constants,
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
AccountPolicy, BuyAmount, DurableNonceInfo, SellAmount, SimpleBuyParams, SimpleSellParams,
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// Default is true. WSOL ATA is prepared on startup, not in the hot trade tx.
|
||||
.create_wsol_ata_on_startup(true)
|
||||
.build();
|
||||
let client = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
let mint = Pubkey::new_unique();
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let gas_fee_strategy = GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(150_000, 150_000, 500_000, 500_000, 0.001, 0.001);
|
||||
|
||||
// In production, fill these fields from your parser/RPC cache. They are
|
||||
// protocol state, not user preferences.
|
||||
let pumpfun_params = DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||
Pubkey::new_unique(), // bonding_curve
|
||||
Pubkey::new_unique(), // associated_bonding_curve
|
||||
mint,
|
||||
// WSOL quote_mint selects PumpFun V2 SOL layout. Users still pay with
|
||||
// native SOL below by setting `TradeTokenType::SOL`.
|
||||
WSOL_TOKEN_ACCOUNT,
|
||||
Pubkey::new_unique(), // creator from event/cache
|
||||
Pubkey::default(), // creator_vault; SDK derives it if unavailable
|
||||
1_073_000_000_000_000, // virtual_token_reserves
|
||||
30_000_000_000, // virtual_quote_reserves
|
||||
793_100_000_000_000, // real_token_reserves
|
||||
0, // real_quote_reserves
|
||||
None, // close_token_account_when_sell
|
||||
global_constants::FEE_RECIPIENT,
|
||||
// If parser/cache does not know the mint owner, PumpFun now defaults to
|
||||
// Token-2022. Passing it explicitly makes the example easier to read.
|
||||
TOKEN_PROGRAM_2022,
|
||||
false, // is_cashback_coin
|
||||
Some(false), // mayhem_mode
|
||||
));
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
// For PumpFun V2 SOL-paired coins, keep this as SOL even though V2
|
||||
// account metas use the WSOL mint as quote_mint.
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
// Regular PumpFun/PumpSwap buy. The SDK estimates expected output and
|
||||
// applies slippage to the maximum quote cost.
|
||||
BuyAmount::WithMaxInput { quote_amount: 100_000 },
|
||||
pumpfun_params.clone(),
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.slippage_basis_points(300)
|
||||
// Best for bots: do not create/close ATAs in the hot transaction.
|
||||
// Use AccountPolicy::Auto for normal integrations.
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
// client.buy_simple(buy_params).await?;
|
||||
let _ = buy_params;
|
||||
|
||||
// Durable nonce is supported by the same high-level params. In production,
|
||||
// fetch it with `sol_trade_sdk::fetch_nonce_info(...)` immediately before
|
||||
// building the transaction.
|
||||
let nonce_buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
BuyAmount::WithMaxInput { quote_amount: 100_000 },
|
||||
pumpfun_params.clone(),
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(DurableNonceInfo {
|
||||
nonce_account: Some(Pubkey::new_unique()),
|
||||
current_nonce: Some(Hash::new_unique()),
|
||||
})
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
let _ = nonce_buy_params;
|
||||
|
||||
let sell_params = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
SellAmount::ExactInput(1_000_000),
|
||||
pumpfun_params.clone(),
|
||||
client.infrastructure.rpc.get_latest_blockhash().await?,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.slippage_basis_points(300)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
// client.sell_simple(sell_params).await?;
|
||||
let _ = sell_params;
|
||||
|
||||
let nonce_sell_params = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
SellAmount::ExactInput(1_000_000),
|
||||
pumpfun_params,
|
||||
client.infrastructure.rpc.get_latest_blockhash().await?,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(DurableNonceInfo {
|
||||
nonce_account: Some(Pubkey::new_unique()),
|
||||
current_nonce: Some(Hash::new_unique()),
|
||||
})
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
let _ = nonce_sell_params;
|
||||
|
||||
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
|
||||
Ok(())
|
||||
}
|
||||
+765
-10
@@ -50,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,其他类型返回未实现错误。
|
||||
@@ -73,6 +82,419 @@ pub enum TradeTokenType {
|
||||
USDC,
|
||||
}
|
||||
|
||||
/// Account lifecycle policy for high-level trade requests.
|
||||
///
|
||||
/// This replaces low-level flags such as `create_input_token_ata`,
|
||||
/// `close_input_token_ata`, `create_mint_ata`, and `create_output_token_ata`.
|
||||
/// Use this when calling [`TradingClient::buy_simple`] or [`TradingClient::sell_simple`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccountPolicy {
|
||||
/// SDK chooses a practical default for the protocol and token route.
|
||||
///
|
||||
/// Buy: create the target token ATA because the wallet usually needs a place
|
||||
/// to receive the bought token.
|
||||
///
|
||||
/// Sell: create the output ATA only when receiving an SPL token such as USDC
|
||||
/// or WSOL. Receiving native SOL does not need an ATA.
|
||||
Auto,
|
||||
/// Keep the transaction small for sniping/arbitrage; assume hot accounts are prepared.
|
||||
///
|
||||
/// This avoids ATA create/close instructions in the trade transaction. It is
|
||||
/// the recommended policy for latency-sensitive bots that pre-create WSOL
|
||||
/// and token ATAs outside the hot path.
|
||||
HotPathMinimal,
|
||||
/// Include ATA create instructions when the route needs them.
|
||||
///
|
||||
/// This is more convenient for normal trading but can make PumpFun V2 and
|
||||
/// other large transactions exceed Solana packet size limits unless an ALT
|
||||
/// is supplied or the transaction builder can compact optional instructions.
|
||||
CreateMissing,
|
||||
/// Do not create or close token accounts in the trade transaction.
|
||||
///
|
||||
/// Use this when the caller has already prepared all required ATAs and wants
|
||||
/// deterministic instruction layout. Unlike `HotPathMinimal`, this name is
|
||||
/// intended for correctness/readability rather than bot latency.
|
||||
AssumePrepared,
|
||||
}
|
||||
|
||||
/// High-level buy sizing intent.
|
||||
///
|
||||
/// This replaces `input_token_amount`, `fixed_output_token_amount`, and
|
||||
/// `use_exact_sol_amount` in [`TradeBuyParams`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BuyAmount {
|
||||
/// Spend this exact quote amount and apply slippage to minimum output.
|
||||
///
|
||||
/// Example: spend exactly `0.1 SOL` or exactly `100 USDC` worth of quote
|
||||
/// token; if the received token amount is below the slippage-adjusted
|
||||
/// minimum, the transaction fails.
|
||||
ExactInput(u64),
|
||||
/// Buy an exact token amount, using `max_input_amount` as the quote budget.
|
||||
///
|
||||
/// Example: buy exactly `1_000_000` token base units, but fail if the quote
|
||||
/// cost would exceed `max_input_amount`.
|
||||
ExactOutput { output_amount: u64, max_input_amount: u64 },
|
||||
/// Regular PumpFun/PumpSwap buy: estimate output from `quote_amount` and apply slippage to max quote cost.
|
||||
///
|
||||
/// This maps to `use_exact_sol_amount = Some(false)` in the old API. It is
|
||||
/// useful for bots that want to know the intended token output from the SDK
|
||||
/// calculation while letting the chain fail if the max quote budget is
|
||||
/// exceeded.
|
||||
WithMaxInput { quote_amount: u64 },
|
||||
}
|
||||
|
||||
/// High-level sell sizing intent.
|
||||
///
|
||||
/// This replaces `input_token_amount` and `fixed_output_token_amount` in
|
||||
/// [`TradeSellParams`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SellAmount {
|
||||
/// Sell this exact token amount and apply slippage to minimum output.
|
||||
///
|
||||
/// Example: sell exactly `1_000_000` token base units and fail if the
|
||||
/// received quote amount is below the slippage-adjusted minimum.
|
||||
ExactInput(u64),
|
||||
/// Exact-output sell where supported; `max_input_amount` is the token budget.
|
||||
///
|
||||
/// Example: receive exactly `0.1 SOL` or `100 USDC`, but spend no more than
|
||||
/// `max_input_amount` token base units.
|
||||
ExactOutput { output_amount: u64, max_input_amount: u64 },
|
||||
}
|
||||
|
||||
/// Simpler buy request that describes trade intent instead of low-level ATA flags.
|
||||
///
|
||||
/// Prefer constructing this with [`SimpleBuyParams::new`] or
|
||||
/// [`SimpleBuyParams::with_durable_nonce`], then optionally set slippage,
|
||||
/// account policy, ALT, simulation, or confirmation flags through builder-style
|
||||
/// methods.
|
||||
#[derive(Clone)]
|
||||
pub struct SimpleBuyParams {
|
||||
/// DEX/protocol to route through, such as `DexType::PumpFun`.
|
||||
pub dex_type: DexType,
|
||||
/// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`.
|
||||
///
|
||||
/// For PumpFun V2 SOL-paired coins, use `TradeTokenType::SOL` even when the
|
||||
/// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote
|
||||
/// account but keeps settlement in native SOL.
|
||||
pub pay_with: TradeTokenType,
|
||||
/// Mint address of the token being bought.
|
||||
pub mint: Pubkey,
|
||||
/// Buy sizing intent. See [`BuyAmount`].
|
||||
pub amount: BuyAmount,
|
||||
/// Optional slippage in basis points. `100` means 1%.
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
/// Recent blockhash for non-nonce transactions.
|
||||
///
|
||||
/// The SDK intentionally does not fetch blockhash on the hot path. Use
|
||||
/// [`SimpleBuyParams::with_durable_nonce`] instead when submitting multiple
|
||||
/// SWQoS lanes against a pinned nonce.
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
/// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
|
||||
pub extension_params: DexParamEnum,
|
||||
/// Compute unit price/limit and relay tip configuration.
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
/// ATA creation/close behavior. See [`AccountPolicy`].
|
||||
pub account_policy: AccountPolicy,
|
||||
/// Optional Address Lookup Table to reduce transaction size.
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
/// Wait until the transaction is confirmed before returning.
|
||||
pub wait_tx_confirmed: bool,
|
||||
/// Fast-submit mode only: wait for every SWQoS route's submit response so all
|
||||
/// signatures can be returned.
|
||||
pub wait_for_all_submits: bool,
|
||||
/// Durable nonce info. Mutually exclusive with `recent_blockhash`.
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Build and simulate the transaction instead of submitting it.
|
||||
pub simulate: bool,
|
||||
/// Optional upstream receive timestamp in microseconds for latency tracing.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
}
|
||||
|
||||
/// Simpler sell request that describes trade intent instead of low-level ATA flags.
|
||||
///
|
||||
/// Prefer constructing this with [`SimpleSellParams::new`] or
|
||||
/// [`SimpleSellParams::with_durable_nonce`].
|
||||
#[derive(Clone)]
|
||||
pub struct SimpleSellParams {
|
||||
/// DEX/protocol to route through, such as `DexType::PumpFun`.
|
||||
pub dex_type: DexType,
|
||||
/// Quote token to receive from the sell: `SOL`, `WSOL`, `USDC`, or `USD1`.
|
||||
pub receive_as: TradeTokenType,
|
||||
/// Mint address of the token being sold.
|
||||
pub mint: Pubkey,
|
||||
/// Sell sizing intent. See [`SellAmount`].
|
||||
pub amount: SellAmount,
|
||||
/// Optional slippage in basis points. `100` means 1%.
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
/// Recent blockhash for non-nonce transactions.
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
/// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
|
||||
pub extension_params: DexParamEnum,
|
||||
/// Compute unit price/limit and relay tip configuration.
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
/// ATA creation/close behavior. See [`AccountPolicy`].
|
||||
pub account_policy: AccountPolicy,
|
||||
/// Optional Address Lookup Table to reduce transaction size.
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
/// Wait until the transaction is confirmed before returning.
|
||||
pub wait_tx_confirmed: bool,
|
||||
/// Fast-submit mode only: wait for every SWQoS route's submit response so all
|
||||
/// signatures can be returned.
|
||||
pub wait_for_all_submits: bool,
|
||||
/// Durable nonce info. Mutually exclusive with `recent_blockhash`.
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Build and simulate the transaction instead of submitting it.
|
||||
pub simulate: bool,
|
||||
/// Whether to include relay tips for sell transactions.
|
||||
pub with_tip: bool,
|
||||
/// Optional upstream receive timestamp in microseconds for latency tracing.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
}
|
||||
|
||||
impl SimpleBuyParams {
|
||||
/// Create a simple buy request using a recent blockhash.
|
||||
///
|
||||
/// Defaults:
|
||||
/// - `account_policy = AccountPolicy::Auto`
|
||||
/// - `wait_tx_confirmed = false`
|
||||
/// - `wait_for_all_submits = false`
|
||||
/// - `simulate = false`
|
||||
/// - no ALT
|
||||
/// - no slippage override
|
||||
pub fn new(
|
||||
dex_type: DexType,
|
||||
pay_with: TradeTokenType,
|
||||
mint: Pubkey,
|
||||
amount: BuyAmount,
|
||||
extension_params: DexParamEnum,
|
||||
recent_blockhash: Hash,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Self {
|
||||
Self {
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
slippage_basis_points: None,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params,
|
||||
gas_fee_strategy,
|
||||
account_policy: AccountPolicy::Auto,
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: false,
|
||||
wait_for_all_submits: false,
|
||||
durable_nonce: None,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple buy request using a durable nonce instead of a recent blockhash.
|
||||
///
|
||||
/// Use this when sending through multiple SWQoS lanes with the same nonce.
|
||||
pub fn with_durable_nonce(
|
||||
dex_type: DexType,
|
||||
pay_with: TradeTokenType,
|
||||
mint: Pubkey,
|
||||
amount: BuyAmount,
|
||||
extension_params: DexParamEnum,
|
||||
durable_nonce: DurableNonceInfo,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Self {
|
||||
Self {
|
||||
durable_nonce: Some(durable_nonce),
|
||||
recent_blockhash: None,
|
||||
..Self::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
Hash::default(),
|
||||
gas_fee_strategy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set slippage in basis points. `100` means 1%.
|
||||
pub fn slippage_basis_points(mut self, value: u64) -> Self {
|
||||
self.slippage_basis_points = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set account lifecycle behavior. Bots usually want `HotPathMinimal`;
|
||||
/// normal integrations can keep the default `Auto`.
|
||||
pub fn account_policy(mut self, value: AccountPolicy) -> Self {
|
||||
self.account_policy = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach an Address Lookup Table to reduce transaction size.
|
||||
pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
|
||||
self.address_lookup_table_account = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
|
||||
///
|
||||
/// This clears `recent_blockhash` because nonce transactions must use the
|
||||
/// nonce value as their transaction blockhash.
|
||||
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
|
||||
self.durable_nonce = Some(value);
|
||||
self.recent_blockhash = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait for confirmation before returning.
|
||||
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
|
||||
self.wait_tx_confirmed = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
|
||||
pub fn wait_for_all_submits(mut self, value: bool) -> Self {
|
||||
self.wait_for_all_submits = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Simulate instead of submitting.
|
||||
pub fn simulate(mut self, value: bool) -> Self {
|
||||
self.simulate = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach upstream receive timestamp for latency tracing.
|
||||
pub fn grpc_recv_us(mut self, value: i64) -> Self {
|
||||
self.grpc_recv_us = Some(value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSellParams {
|
||||
/// Create a simple sell request using a recent blockhash.
|
||||
///
|
||||
/// Defaults:
|
||||
/// - `account_policy = AccountPolicy::Auto`
|
||||
/// - `with_tip = true`
|
||||
/// - `wait_tx_confirmed = false`
|
||||
/// - `wait_for_all_submits = false`
|
||||
/// - `simulate = false`
|
||||
/// - no ALT
|
||||
/// - no slippage override
|
||||
pub fn new(
|
||||
dex_type: DexType,
|
||||
receive_as: TradeTokenType,
|
||||
mint: Pubkey,
|
||||
amount: SellAmount,
|
||||
extension_params: DexParamEnum,
|
||||
recent_blockhash: Hash,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Self {
|
||||
Self {
|
||||
dex_type,
|
||||
receive_as,
|
||||
mint,
|
||||
amount,
|
||||
slippage_basis_points: None,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params,
|
||||
gas_fee_strategy,
|
||||
account_policy: AccountPolicy::Auto,
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: false,
|
||||
wait_for_all_submits: false,
|
||||
durable_nonce: None,
|
||||
simulate: false,
|
||||
with_tip: true,
|
||||
grpc_recv_us: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple sell request using a durable nonce instead of a recent blockhash.
|
||||
pub fn with_durable_nonce(
|
||||
dex_type: DexType,
|
||||
receive_as: TradeTokenType,
|
||||
mint: Pubkey,
|
||||
amount: SellAmount,
|
||||
extension_params: DexParamEnum,
|
||||
durable_nonce: DurableNonceInfo,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Self {
|
||||
Self {
|
||||
durable_nonce: Some(durable_nonce),
|
||||
recent_blockhash: None,
|
||||
..Self::new(
|
||||
dex_type,
|
||||
receive_as,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
Hash::default(),
|
||||
gas_fee_strategy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set slippage in basis points. `100` means 1%.
|
||||
pub fn slippage_basis_points(mut self, value: u64) -> Self {
|
||||
self.slippage_basis_points = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set account lifecycle behavior. Bots usually want `HotPathMinimal`;
|
||||
/// normal integrations can keep the default `Auto`.
|
||||
pub fn account_policy(mut self, value: AccountPolicy) -> Self {
|
||||
self.account_policy = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach an Address Lookup Table to reduce transaction size.
|
||||
pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
|
||||
self.address_lookup_table_account = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
|
||||
///
|
||||
/// This clears `recent_blockhash` because nonce transactions must use the
|
||||
/// nonce value as their transaction blockhash.
|
||||
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
|
||||
self.durable_nonce = Some(value);
|
||||
self.recent_blockhash = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait for confirmation before returning.
|
||||
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
|
||||
self.wait_tx_confirmed = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
|
||||
pub fn wait_for_all_submits(mut self, value: bool) -> Self {
|
||||
self.wait_for_all_submits = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Simulate instead of submitting.
|
||||
pub fn simulate(mut self, value: bool) -> Self {
|
||||
self.simulate = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable or disable relay tips for sell transactions.
|
||||
pub fn with_tip(mut self, value: bool) -> Self {
|
||||
self.with_tip = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach upstream receive timestamp for latency tracing.
|
||||
pub fn grpc_recv_us(mut self, value: i64) -> Self {
|
||||
self.grpc_recv_us = Some(value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared infrastructure components that can be reused across multiple wallets
|
||||
///
|
||||
/// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients)
|
||||
@@ -134,8 +556,9 @@ impl TradingInfrastructure {
|
||||
|
||||
// Create SWQOS clients with blacklist checking(QUIC 握手可能较慢,单节点超时 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());
|
||||
@@ -312,8 +735,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);
|
||||
@@ -334,7 +755,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,6 +785,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
|
||||
@@ -380,7 +805,7 @@ pub struct TradeBuyParams {
|
||||
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.
|
||||
@@ -417,6 +842,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
|
||||
@@ -436,6 +866,96 @@ pub struct TradeSellParams {
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn buy_account_flags(policy: AccountPolicy) -> (bool, bool, bool) {
|
||||
match policy {
|
||||
AccountPolicy::Auto => (false, true, false),
|
||||
AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false),
|
||||
AccountPolicy::CreateMissing => (true, true, false),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sell_account_flags(policy: AccountPolicy, receive_as: &TradeTokenType) -> (bool, bool, bool) {
|
||||
match policy {
|
||||
AccountPolicy::Auto => (*receive_as != TradeTokenType::SOL, false, false),
|
||||
AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false),
|
||||
AccountPolicy::CreateMissing => (true, false, false),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimpleBuyParams> for TradeBuyParams {
|
||||
fn from(params: SimpleBuyParams) -> Self {
|
||||
let (input_token_amount, fixed_output_token_amount, use_exact_sol_amount) =
|
||||
match params.amount {
|
||||
BuyAmount::ExactInput(amount) => (amount, None, Some(true)),
|
||||
BuyAmount::ExactOutput { output_amount, max_input_amount } => {
|
||||
(max_input_amount, Some(output_amount), Some(true))
|
||||
}
|
||||
BuyAmount::WithMaxInput { quote_amount } => (quote_amount, None, Some(false)),
|
||||
};
|
||||
let (create_input_token_ata, create_mint_ata, close_input_token_ata) =
|
||||
buy_account_flags(params.account_policy);
|
||||
|
||||
TradeBuyParams {
|
||||
dex_type: params.dex_type,
|
||||
input_token_type: params.pay_with,
|
||||
mint: params.mint,
|
||||
input_token_amount,
|
||||
slippage_basis_points: params.slippage_basis_points,
|
||||
recent_blockhash: params.recent_blockhash,
|
||||
extension_params: params.extension_params,
|
||||
address_lookup_table_account: params.address_lookup_table_account,
|
||||
wait_tx_confirmed: params.wait_tx_confirmed,
|
||||
wait_for_all_submits: params.wait_for_all_submits,
|
||||
create_input_token_ata,
|
||||
close_input_token_ata,
|
||||
create_mint_ata,
|
||||
durable_nonce: params.durable_nonce,
|
||||
fixed_output_token_amount,
|
||||
gas_fee_strategy: params.gas_fee_strategy,
|
||||
simulate: params.simulate,
|
||||
use_exact_sol_amount,
|
||||
grpc_recv_us: params.grpc_recv_us,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimpleSellParams> for TradeSellParams {
|
||||
fn from(params: SimpleSellParams) -> Self {
|
||||
let (input_token_amount, fixed_output_token_amount) = match params.amount {
|
||||
SellAmount::ExactInput(amount) => (amount, None),
|
||||
SellAmount::ExactOutput { output_amount, max_input_amount } => {
|
||||
(max_input_amount, Some(output_amount))
|
||||
}
|
||||
};
|
||||
let (create_output_token_ata, close_output_token_ata, close_mint_token_ata) =
|
||||
sell_account_flags(params.account_policy, ¶ms.receive_as);
|
||||
|
||||
TradeSellParams {
|
||||
dex_type: params.dex_type,
|
||||
output_token_type: params.receive_as,
|
||||
mint: params.mint,
|
||||
input_token_amount,
|
||||
slippage_basis_points: params.slippage_basis_points,
|
||||
recent_blockhash: params.recent_blockhash,
|
||||
with_tip: params.with_tip,
|
||||
extension_params: params.extension_params,
|
||||
address_lookup_table_account: params.address_lookup_table_account,
|
||||
wait_tx_confirmed: params.wait_tx_confirmed,
|
||||
wait_for_all_submits: params.wait_for_all_submits,
|
||||
create_output_token_ata,
|
||||
close_output_token_ata,
|
||||
close_mint_token_ata,
|
||||
durable_nonce: params.durable_nonce,
|
||||
fixed_output_token_amount,
|
||||
gas_fee_strategy: params.gas_fee_strategy,
|
||||
simulate: params.simulate,
|
||||
grpc_recv_us: params.grpc_recv_us,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TradingClient {
|
||||
/// Create a TradingClient from shared infrastructure (fast path)
|
||||
///
|
||||
@@ -471,7 +991,6 @@ impl TradingClient {
|
||||
effective_core_ids,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +1037,6 @@ impl TradingClient {
|
||||
effective_core_ids,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +1216,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();
|
||||
@@ -748,6 +1265,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
|
||||
}
|
||||
|
||||
@@ -874,6 +1397,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,
|
||||
@@ -881,7 +1405,6 @@ 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;
|
||||
@@ -895,6 +1418,18 @@ impl TradingClient {
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute a high-level buy request.
|
||||
#[inline]
|
||||
pub async fn buy_simple(
|
||||
&self,
|
||||
params: SimpleBuyParams,
|
||||
) -> Result<
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
self.buy(params.into()).await
|
||||
}
|
||||
|
||||
/// Execute a sell order for a specified token
|
||||
///
|
||||
/// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
|
||||
@@ -990,6 +1525,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,
|
||||
@@ -997,7 +1533,6 @@ 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;
|
||||
@@ -1011,6 +1546,18 @@ impl TradingClient {
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute a high-level sell request.
|
||||
#[inline]
|
||||
pub async fn sell_simple(
|
||||
&self,
|
||||
params: SimpleSellParams,
|
||||
) -> Result<
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
self.sell(params.into()).await
|
||||
}
|
||||
|
||||
/// Execute a sell order for a percentage of the specified token amount
|
||||
///
|
||||
/// This is a convenience function that calculates the exact amount to sell based on
|
||||
@@ -1259,3 +1806,211 @@ impl TradingClient {
|
||||
Ok(signature.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::instruction::utils::pumpfun::global_constants;
|
||||
use crate::swqos::SwqosRegion;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn dummy_pumpfun_params() -> DexParamEnum {
|
||||
DexParamEnum::PumpFun(PumpFunParams {
|
||||
bonding_curve: Arc::new(Default::default()),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
observed_trade_creator: None,
|
||||
creator_vault: Pubkey::default(),
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
token_program: Pubkey::default(),
|
||||
close_token_account_when_sell: None,
|
||||
fee_recipient: global_constants::FEE_RECIPIENT,
|
||||
quote_mint: Pubkey::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_buy_hot_path_maps_to_low_level_params() {
|
||||
let simple = SimpleBuyParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
pay_with: TradeTokenType::SOL,
|
||||
mint: Pubkey::new_unique(),
|
||||
amount: BuyAmount::WithMaxInput { quote_amount: 10_000 },
|
||||
slippage_basis_points: Some(100),
|
||||
recent_blockhash: Some(Hash::new_unique()),
|
||||
extension_params: dummy_pumpfun_params(),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
account_policy: AccountPolicy::HotPathMinimal,
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: false,
|
||||
wait_for_all_submits: false,
|
||||
durable_nonce: None,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
|
||||
let low: TradeBuyParams = simple.into();
|
||||
|
||||
assert!(matches!(low.input_token_type, TradeTokenType::SOL));
|
||||
assert_eq!(low.input_token_amount, 10_000);
|
||||
assert_eq!(low.use_exact_sol_amount, Some(false));
|
||||
assert_eq!(low.fixed_output_token_amount, None);
|
||||
assert!(!low.create_input_token_ata);
|
||||
assert!(!low.close_input_token_ata);
|
||||
assert!(!low.create_mint_ata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_buy_auto_creates_target_mint_ata() {
|
||||
let simple = SimpleBuyParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
pay_with: TradeTokenType::SOL,
|
||||
mint: Pubkey::new_unique(),
|
||||
amount: BuyAmount::ExactOutput { output_amount: 42, max_input_amount: 10_000 },
|
||||
slippage_basis_points: None,
|
||||
recent_blockhash: Some(Hash::new_unique()),
|
||||
extension_params: dummy_pumpfun_params(),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
account_policy: AccountPolicy::Auto,
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: false,
|
||||
wait_for_all_submits: false,
|
||||
durable_nonce: None,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
|
||||
let low: TradeBuyParams = simple.into();
|
||||
|
||||
assert_eq!(low.input_token_amount, 10_000);
|
||||
assert_eq!(low.fixed_output_token_amount, Some(42));
|
||||
assert_eq!(low.use_exact_sol_amount, Some(true));
|
||||
assert!(low.create_mint_ata);
|
||||
assert!(!low.create_input_token_ata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_buy_builder_sets_defaults_and_overrides() {
|
||||
let simple = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
BuyAmount::ExactInput(10_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.slippage_basis_points(250)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
let low: TradeBuyParams = simple.into();
|
||||
|
||||
assert_eq!(low.slippage_basis_points, Some(250));
|
||||
assert_eq!(low.use_exact_sol_amount, Some(true));
|
||||
assert!(!low.create_input_token_ata);
|
||||
assert!(!low.create_mint_ata);
|
||||
assert!(!low.wait_tx_confirmed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_buy_builder_can_use_durable_nonce() {
|
||||
let nonce_account = Pubkey::new_unique();
|
||||
let nonce_hash = Hash::new_unique();
|
||||
let durable_nonce = DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(nonce_hash),
|
||||
};
|
||||
|
||||
let simple = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
BuyAmount::ExactInput(10_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.durable_nonce(durable_nonce.clone());
|
||||
|
||||
let low: TradeBuyParams = simple.into();
|
||||
|
||||
assert!(low.recent_blockhash.is_none());
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sell_auto_creates_non_sol_output_ata() {
|
||||
let simple = SimpleSellParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
receive_as: TradeTokenType::USDC,
|
||||
mint: Pubkey::new_unique(),
|
||||
amount: SellAmount::ExactInput(50_000),
|
||||
slippage_basis_points: None,
|
||||
recent_blockhash: Some(Hash::new_unique()),
|
||||
extension_params: dummy_pumpfun_params(),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
account_policy: AccountPolicy::Auto,
|
||||
address_lookup_table_account: None,
|
||||
wait_tx_confirmed: false,
|
||||
wait_for_all_submits: false,
|
||||
durable_nonce: None,
|
||||
simulate: false,
|
||||
with_tip: true,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
|
||||
let low: TradeSellParams = simple.into();
|
||||
|
||||
assert!(matches!(low.output_token_type, TradeTokenType::USDC));
|
||||
assert_eq!(low.input_token_amount, 50_000);
|
||||
assert!(low.create_output_token_ata);
|
||||
assert!(!low.close_output_token_ata);
|
||||
assert!(!low.close_mint_token_ata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sell_builder_can_use_durable_nonce() {
|
||||
let nonce_account = Pubkey::new_unique();
|
||||
let nonce_hash = Hash::new_unique();
|
||||
let durable_nonce = DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(nonce_hash),
|
||||
};
|
||||
|
||||
let simple = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
SellAmount::ExactInput(50_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.durable_nonce(durable_nonce.clone());
|
||||
|
||||
let low: TradeSellParams = simple.into();
|
||||
|
||||
assert!(low.recent_blockhash.is_none());
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
|
||||
}
|
||||
}
|
||||
|
||||
+108
-5
@@ -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("e_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("e_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
@@ -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
|
||||
}
|
||||
|
||||
@@ -156,8 +156,13 @@ mod tests {
|
||||
|
||||
#[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]
|
||||
|
||||
+31
-34
@@ -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)
|
||||
}
|
||||
|
||||
@@ -103,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 {
|
||||
@@ -160,7 +157,6 @@ pub struct TradeConfigBuilder {
|
||||
check_min_tip: bool,
|
||||
swqos_cores_from_end: bool,
|
||||
mev_protection: bool,
|
||||
use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl TradeConfigBuilder {
|
||||
@@ -175,7 +171,6 @@ impl TradeConfigBuilder {
|
||||
check_min_tip: false,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,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 {
|
||||
@@ -241,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +382,7 @@ mod tests {
|
||||
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,
|
||||
@@ -389,7 +390,6 @@ mod tests {
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -364,6 +364,7 @@ mod tests {
|
||||
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,
|
||||
@@ -371,7 +372,6 @@ mod tests {
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+536
-81
@@ -1,10 +1,12 @@
|
||||
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
|
||||
//!
|
||||
//! Runtime flag: set `SwapParams.use_pumpfun_v2 = true` to use `buy_v2` / `sell_v2` /
|
||||
//! `buy_exact_quote_in_v2` (27/26-account unified metas, quote_mint support).
|
||||
//! The SDK selects the legacy or V2 on-chain layout from `PumpFunParams.quote_mint`.
|
||||
//! `Pubkey::default()` and the Solscan SOL sentinel keep the smaller legacy SOL layout;
|
||||
//! explicit WSOL or USDC uses the V2 27/26-account unified metas.
|
||||
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
|
||||
|
||||
use crate::{
|
||||
common::bonding_curve::BondingCurveAccount,
|
||||
common::spl_token::close_account,
|
||||
constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022},
|
||||
trading::core::{
|
||||
@@ -30,56 +32,124 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
fn is_pump_suffix_mint(mint: &Pubkey) -> bool {
|
||||
mint.to_string().ends_with("pump")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn effective_pump_mint_token_program(mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
|
||||
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams, mint: &Pubkey) -> Pubkey {
|
||||
if mint.to_string().ends_with("pump") {
|
||||
return TOKEN_PROGRAM_2022;
|
||||
}
|
||||
let tp = protocol_params.token_program;
|
||||
if tp == Pubkey::default() || tp == TOKEN_PROGRAM_2022 {
|
||||
return TOKEN_PROGRAM_2022;
|
||||
if tp == Pubkey::default() {
|
||||
TOKEN_PROGRAM_2022
|
||||
} else {
|
||||
tp
|
||||
}
|
||||
if is_pump_suffix_mint(mint) {
|
||||
return TOKEN_PROGRAM_2022;
|
||||
}
|
||||
tp
|
||||
}
|
||||
|
||||
/// Resolve quote mint and its token program from PumpFunParams.
|
||||
/// `Pubkey::default()` means legacy SOL-paired → use WSOL mint for V2 instructions.
|
||||
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` means legacy SOL-paired; use WSOL mint when a
|
||||
/// downstream V2 helper needs a concrete SPL quote mint.
|
||||
#[inline]
|
||||
fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) {
|
||||
let quote_mint = if protocol_params.quote_mint == Pubkey::default() {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint();
|
||||
let quote_mint = if protocol_params.quote_mint != Pubkey::default() {
|
||||
BondingCurveAccount::normalize_quote_mint(protocol_params.quote_mint)
|
||||
} else if curve_quote_mint != Pubkey::default() {
|
||||
curve_quote_mint
|
||||
} else {
|
||||
protocol_params.quote_mint
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
};
|
||||
(quote_mint, crate::constants::TOKEN_PROGRAM)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_sol_quote_mint(quote_mint: &Pubkey) -> bool {
|
||||
*quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| *quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn validate_v2_buy_quote_mint(input_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> {
|
||||
if is_sol_quote_mint(quote_mint) {
|
||||
if *input_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
||||
|| *input_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
} else if input_mint == quote_mint {
|
||||
return Ok(());
|
||||
}
|
||||
Err(anyhow!(
|
||||
"PumpFun V2 buy input_mint {} does not match quote_mint {}; USDC quote pools must be bought with USDC, not SOL",
|
||||
input_mint,
|
||||
quote_mint
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn validate_v2_sell_quote_mint(output_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> {
|
||||
if is_sol_quote_mint(quote_mint) {
|
||||
if *output_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
||||
|| *output_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
} else if output_mint == quote_mint {
|
||||
return Ok(());
|
||||
}
|
||||
Err(anyhow!(
|
||||
"PumpFun V2 sell output_mint {} does not match quote_mint {}; USDC quote pools settle to USDC, not SOL",
|
||||
output_mint,
|
||||
quote_mint
|
||||
))
|
||||
}
|
||||
|
||||
pub struct PumpFunInstructionBuilder;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
if params.use_pumpfun_v2 {
|
||||
build_buy_v2(params)
|
||||
} else {
|
||||
build_buy_v1(params)
|
||||
}
|
||||
build_buy(params)
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
if params.use_pumpfun_v2 {
|
||||
build_sell_v2(params)
|
||||
} else {
|
||||
build_sell_v1(params)
|
||||
}
|
||||
build_sell(params)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn should_use_v2_layout(params: &SwapParams) -> Result<bool> {
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<PumpFunParams>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
||||
let (quote_mint, _) = effective_quote_mint_and_token_program(protocol_params);
|
||||
let explicit_v2_quote = protocol_params.quote_mint != Pubkey::default()
|
||||
&& protocol_params.quote_mint != crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
Ok(explicit_v2_quote || !is_sol_quote_mint("e_mint))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn build_buy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
if should_use_v2_layout(params)? {
|
||||
build_buy_unified(params)
|
||||
} else {
|
||||
build_buy_legacy(params)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn build_sell(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
if should_use_v2_layout(params)? {
|
||||
build_sell_unified(params)
|
||||
} else {
|
||||
build_sell_legacy(params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +157,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
// V1 (default) — 18 metas, no quote_mint ATA create
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
fn build_buy_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
use crate::instruction::pumpfun_ix_data::{
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data,
|
||||
encode_pumpfun_buy_exact_quote_in_ix_data, encode_pumpfun_buy_ix_data, PumpFunIxVersion,
|
||||
};
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random,
|
||||
@@ -123,7 +193,7 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = effective_pump_mint_token_program(¶ms.output_mint, protocol_params);
|
||||
let token_program = effective_pump_mint_token_program(protocol_params, ¶ms.output_mint);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -164,8 +234,9 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
|
||||
// ── Legacy buy path ──
|
||||
let track_volume_val = if bonding_curve.is_cashback_coin { 1u8 } else { 0u8 };
|
||||
let ix_version = PumpFunIxVersion::Legacy { track_volume: track_volume_val };
|
||||
let buy_data = if let Some(token_amount) = params.fixed_output_amount {
|
||||
encode_pumpfun_buy_ix_data(token_amount, lamports_in, track_volume_val)
|
||||
encode_pumpfun_buy_ix_data(token_amount, lamports_in, ix_version)
|
||||
} else {
|
||||
let buy_token_amount = get_buy_token_amount_from_sol_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
@@ -176,10 +247,10 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
);
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data(lamports_in, min_tokens_out, track_volume_val)
|
||||
encode_pumpfun_buy_exact_quote_in_ix_data(lamports_in, min_tokens_out, ix_version)
|
||||
} else {
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val)
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, ix_version)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,13 +281,13 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, metas));
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas));
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_ix_data;
|
||||
fn build_sell_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
use crate::instruction::pumpfun_ix_data::{encode_pumpfun_sell_ix_data, PumpFunIxVersion};
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random,
|
||||
is_phantom_default_creator_vault,
|
||||
@@ -277,7 +348,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = effective_pump_mint_token_program(¶ms.input_mint, protocol_params);
|
||||
let token_program = effective_pump_mint_token_program(protocol_params, ¶ms.input_mint);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -303,7 +374,11 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output);
|
||||
let sell_data = encode_pumpfun_sell_ix_data(
|
||||
token_amount,
|
||||
min_sol_output,
|
||||
PumpFunIxVersion::Legacy { track_volume: 0 },
|
||||
);
|
||||
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
@@ -335,7 +410,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, metas));
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, sell_data.as_slice(), metas));
|
||||
|
||||
if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata
|
||||
{
|
||||
@@ -352,12 +427,12 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// V2 (opt-in via `use_pumpfun_v2 = true`) — 27 metas, quote_mint ATA create
|
||||
// V2 — 27 metas, quote_mint ATA create
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
use crate::instruction::pumpfun_ix_data::{
|
||||
encode_pumpfun_buy_exact_quote_in_v2_ix_data, encode_pumpfun_buy_v2_ix_data,
|
||||
encode_pumpfun_buy_exact_quote_in_ix_data, encode_pumpfun_buy_ix_data, PumpFunIxVersion,
|
||||
};
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
get_buyback_fee_recipient_random, get_fee_sharing_config_pda,
|
||||
@@ -392,7 +467,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let base_token_program =
|
||||
effective_pump_mint_token_program(¶ms.output_mint, protocol_params);
|
||||
effective_pump_mint_token_program(protocol_params, ¶ms.output_mint);
|
||||
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -400,6 +475,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
};
|
||||
|
||||
let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params);
|
||||
validate_v2_buy_quote_mint(¶ms.input_mint, "e_mint)?;
|
||||
let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META;
|
||||
|
||||
let associated_base_bonding_curve =
|
||||
@@ -482,7 +558,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
}
|
||||
|
||||
let (buy_data, quote_amount_to_fund) = if let Some(token_amount) = params.fixed_output_amount {
|
||||
(encode_pumpfun_buy_v2_ix_data(token_amount, lamports_in), lamports_in)
|
||||
(encode_pumpfun_buy_ix_data(token_amount, lamports_in, PumpFunIxVersion::V2), lamports_in)
|
||||
} else {
|
||||
let buy_token_amount = get_buy_token_amount_from_sol_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
@@ -493,14 +569,26 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
);
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
(encode_pumpfun_buy_exact_quote_in_v2_ix_data(lamports_in, min_tokens_out), lamports_in)
|
||||
(
|
||||
encode_pumpfun_buy_exact_quote_in_ix_data(
|
||||
lamports_in,
|
||||
min_tokens_out,
|
||||
PumpFunIxVersion::V2,
|
||||
),
|
||||
lamports_in,
|
||||
)
|
||||
} else {
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
(encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost), max_sol_cost)
|
||||
(
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, PumpFunIxVersion::V2),
|
||||
max_sol_cost,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
let should_use_native_sol_for_wsol_quote = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
&& params.input_mint == crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
if params.create_input_mint_ata && !should_use_native_sol_for_wsol_quote {
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -520,7 +608,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
||||
fee_recipient_meta,
|
||||
AccountMeta::new(associated_quote_fee_recipient, false),
|
||||
AccountMeta::new_readonly(buyback_fee_recipient, false),
|
||||
AccountMeta::new(buyback_fee_recipient, false),
|
||||
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
|
||||
AccountMeta::new(bonding_curve_addr, false),
|
||||
AccountMeta::new(associated_base_bonding_curve, false),
|
||||
@@ -541,17 +629,17 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
accounts::PUMPFUN_META,
|
||||
];
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, metas));
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
if params.close_input_mint_ata && !should_use_native_sol_for_wsol_quote {
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_v2_ix_data;
|
||||
fn build_sell_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
use crate::instruction::pumpfun_ix_data::{encode_pumpfun_sell_ix_data, PumpFunIxVersion};
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
get_buyback_fee_recipient_random, get_fee_sharing_config_pda,
|
||||
is_phantom_default_creator_vault,
|
||||
@@ -612,7 +700,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let base_token_program = effective_pump_mint_token_program(¶ms.input_mint, protocol_params);
|
||||
let base_token_program = effective_pump_mint_token_program(protocol_params, ¶ms.input_mint);
|
||||
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -620,6 +708,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
};
|
||||
|
||||
let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params);
|
||||
validate_v2_sell_quote_mint(¶ms.output_mint, "e_mint)?;
|
||||
let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META;
|
||||
|
||||
let associated_base_bonding_curve =
|
||||
@@ -699,7 +788,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
);
|
||||
}
|
||||
|
||||
let sell_data = encode_pumpfun_sell_v2_ix_data(token_amount, min_sol_output);
|
||||
let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output, PumpFunIxVersion::V2);
|
||||
|
||||
let metas: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
@@ -710,7 +799,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
||||
fee_recipient_meta,
|
||||
AccountMeta::new(associated_quote_fee_recipient, false),
|
||||
AccountMeta::new_readonly(buyback_fee_recipient, false),
|
||||
AccountMeta::new(buyback_fee_recipient, false),
|
||||
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
|
||||
AccountMeta::new(bonding_curve_addr, false),
|
||||
AccountMeta::new(associated_base_bonding_curve, false),
|
||||
@@ -730,7 +819,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
accounts::PUMPFUN_META,
|
||||
];
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, metas));
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, sell_data.as_slice(), metas));
|
||||
|
||||
if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata
|
||||
{
|
||||
@@ -773,7 +862,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::{bonding_curve::BondingCurveAccount, GasFeeStrategy},
|
||||
constants::{TOKEN_PROGRAM, TOKEN_PROGRAM_2022},
|
||||
constants::TOKEN_PROGRAM,
|
||||
trading::core::params::{DexParamEnum, PumpFunParams, SwapParams},
|
||||
};
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -807,7 +896,6 @@ mod tests {
|
||||
close_token_account_when_sell: None,
|
||||
fee_recipient: global_constants::FEE_RECIPIENT,
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
};
|
||||
|
||||
SwapParams {
|
||||
@@ -837,6 +925,7 @@ mod tests {
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: false,
|
||||
log_enabled: false,
|
||||
wait_for_all_submits: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
@@ -844,7 +933,6 @@ mod tests {
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: Some(true),
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,10 +945,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_suffix_buy_forces_token_2022_even_when_params_legacy() {
|
||||
fn pump_suffix_buy_forces_token_2022_even_with_explicit_legacy_token_program() {
|
||||
crate::common::seed::set_default_rents();
|
||||
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
let instructions = build_buy_v1(¶ms).unwrap();
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
|
||||
assert_eq!(instructions.len(), 3);
|
||||
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
|
||||
@@ -869,11 +957,26 @@ mod tests {
|
||||
assert_eq!(instructions[2].data.len(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_suffix_sell_forces_token_2022_even_with_explicit_legacy_token_program() {
|
||||
crate::common::seed::set_default_rents();
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = pump_mint();
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
|
||||
let instructions = build_sell(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts[9].pubkey, TOKEN_PROGRAM_2022);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_pump_buy_respects_explicit_legacy_token_program() {
|
||||
crate::common::seed::set_default_rents();
|
||||
let params = swap_params_for_buy(Pubkey::new_unique(), TOKEN_PROGRAM);
|
||||
let instructions = build_buy_v1(¶ms).unwrap();
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
|
||||
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
|
||||
}
|
||||
@@ -885,7 +988,7 @@ mod tests {
|
||||
params.fixed_output_amount = Some(42);
|
||||
params.use_exact_sol_amount = Some(true);
|
||||
|
||||
let instructions = build_buy_v1(¶ms).unwrap();
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
|
||||
@@ -904,8 +1007,12 @@ mod tests {
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
params.use_exact_sol_amount = Some(true);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_buy_v2(¶ms).unwrap();
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
@@ -917,29 +1024,373 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_wraps_max_quote_budget() {
|
||||
fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
params.close_input_mint_ata = true;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_buy_v2(¶ms).unwrap();
|
||||
let transfer_ix = &instructions[1];
|
||||
let system_ix = bincode::deserialize::<
|
||||
solana_system_interface::instruction::SystemInstruction,
|
||||
>(&transfer_ix.data)
|
||||
.unwrap();
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
assert_eq!(instructions.len(), 1);
|
||||
let buy_ix = instructions.last().unwrap();
|
||||
let expected = crate::utils::calc::common::calculate_with_slippage_buy(
|
||||
params.input_amount.unwrap(),
|
||||
params.slippage_basis_points.unwrap(),
|
||||
);
|
||||
|
||||
match system_ix {
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports } => {
|
||||
assert_eq!(lamports, expected);
|
||||
}
|
||||
other => panic!("unexpected system instruction: {:?}", other),
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected);
|
||||
assert_eq!(buy_ix.accounts.len(), 27);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_skips_wsol_ata_create_on_hot_path() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
params.close_input_mint_ata = true;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
|
||||
assert_eq!(instructions.len(), 1);
|
||||
assert_eq!(
|
||||
&instructions.last().unwrap().data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_input_mint_ata = true;
|
||||
params.create_output_mint_ata = true;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let business_instructions = build_buy(¶ms).unwrap();
|
||||
let transaction = crate::trading::common::transaction_builder::build_transaction(
|
||||
¶ms.payer,
|
||||
150_000,
|
||||
500_000,
|
||||
&business_instructions,
|
||||
None,
|
||||
Some(solana_hash::Hash::new_unique()),
|
||||
None,
|
||||
"PumpFun",
|
||||
true,
|
||||
true,
|
||||
&Pubkey::new_unique(),
|
||||
0.001,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let serialized = bincode::serialize(&transaction).unwrap();
|
||||
|
||||
assert!(
|
||||
serialized.len() <= 1232,
|
||||
"serialized transaction too large: {} > 1232",
|
||||
serialized.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_buy_v2() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params = PumpFunParams::from_trade(
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
protocol_params.effective_creator_for_trade(),
|
||||
protocol_params.creator_vault,
|
||||
protocol_params.bonding_curve.virtual_token_reserves,
|
||||
protocol_params.bonding_curve.virtual_sol_reserves,
|
||||
protocol_params.bonding_curve.real_token_reserves,
|
||||
protocol_params.bonding_curve.real_sol_reserves,
|
||||
None,
|
||||
protocol_params.fee_recipient,
|
||||
TOKEN_PROGRAM,
|
||||
false,
|
||||
Some(false),
|
||||
);
|
||||
}
|
||||
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
let buy_ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
assert_eq!(buy_ix.accounts.len(), 27);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_wsol_input_does_not_auto_wrap_when_ata_create_disabled() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = false;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
|
||||
assert_eq!(instructions.len(), 1);
|
||||
assert_eq!(
|
||||
&instructions[0].data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(instructions[0].accounts.len(), 27);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_usdc_dev_trade_uses_usdc_initial_quote_reserves() {
|
||||
let mint = pump_mint();
|
||||
let dev_quote_amount = 707_080;
|
||||
let params = PumpFunParams::from_dev_trade_with_quote_mint(
|
||||
mint,
|
||||
55_855_975_892_641,
|
||||
dev_quote_amount,
|
||||
Pubkey::new_unique(),
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
None,
|
||||
global_constants::FEE_RECIPIENT,
|
||||
TOKEN_PROGRAM,
|
||||
false,
|
||||
Some(false),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
);
|
||||
|
||||
assert_eq!(params.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
assert_eq!(params.bonding_curve.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
assert_eq!(
|
||||
params.bonding_curve.virtual_sol_reserves,
|
||||
global_constants::INITIAL_VIRTUAL_USDC_RESERVES + dev_quote_amount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_with_quote_mint_adjusts_dev_trade_quote_reserves() {
|
||||
let mint = pump_mint();
|
||||
let dev_quote_amount = 707_080;
|
||||
let params = PumpFunParams::from_dev_trade(
|
||||
mint,
|
||||
55_855_975_892_641,
|
||||
dev_quote_amount,
|
||||
Pubkey::new_unique(),
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
None,
|
||||
global_constants::FEE_RECIPIENT,
|
||||
TOKEN_PROGRAM,
|
||||
false,
|
||||
Some(false),
|
||||
)
|
||||
.with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
|
||||
assert_eq!(
|
||||
params.bonding_curve.virtual_sol_reserves,
|
||||
global_constants::INITIAL_VIRTUAL_USDC_RESERVES + dev_quote_amount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_usdc_trade_event_preserves_virtual_quote_reserves() {
|
||||
let mint = pump_mint();
|
||||
let virtual_quote_reserves = 4_527_693_121;
|
||||
let real_quote_reserves = 235_693_121;
|
||||
let params = PumpFunParams::from_trade(
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
Pubkey::new_unique(),
|
||||
Pubkey::default(),
|
||||
944_144_024_107_359,
|
||||
virtual_quote_reserves,
|
||||
737_244_024_107_359,
|
||||
real_quote_reserves,
|
||||
None,
|
||||
global_constants::FEE_RECIPIENT,
|
||||
TOKEN_PROGRAM,
|
||||
false,
|
||||
Some(false),
|
||||
);
|
||||
|
||||
assert_eq!(params.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
assert_eq!(params.bonding_curve.virtual_quote_reserves(), virtual_quote_reserves);
|
||||
assert_eq!(params.bonding_curve.real_quote_reserves(), real_quote_reserves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_solscan_sol_quote_mint_keeps_legacy_layout() {
|
||||
let mint = pump_mint();
|
||||
let params = PumpFunParams::from_trade(
|
||||
Pubkey::default(),
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
crate::constants::SOL_TOKEN_ACCOUNT,
|
||||
Pubkey::new_unique(),
|
||||
Pubkey::default(),
|
||||
944_144_024_107_359,
|
||||
30_235_693_121,
|
||||
737_244_024_107_359,
|
||||
235_693_121,
|
||||
None,
|
||||
global_constants::FEE_RECIPIENT,
|
||||
TOKEN_PROGRAM,
|
||||
false,
|
||||
Some(false),
|
||||
);
|
||||
|
||||
assert_eq!(params.quote_mint, Pubkey::default());
|
||||
assert_eq!(params.bonding_curve.quote_mint, crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_with_solscan_sol_quote_mint_selects_v1() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::SOL_TOKEN_ACCOUNT);
|
||||
assert_eq!(protocol_params.quote_mint, Pubkey::default());
|
||||
assert_eq!(
|
||||
protocol_params.bonding_curve.quote_mint,
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
);
|
||||
}
|
||||
|
||||
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_EXACT_SOL_IN_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.accounts.len(), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_rpc_sol_sentinel_quote_mint_selects_v1() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
protocol_params.quote_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
assert_eq!(protocol_params.quote_mint, crate::constants::SOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_EXACT_SOL_IN_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.accounts.len(), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_usdc_buy_rejects_sol_input() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.input_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let err = build_buy(¶ms).unwrap_err().to_string();
|
||||
assert!(err.contains("USDC quote pools must be bought with USDC"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_usdc_quote_mint_selects_v2_without_global_flag() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.accounts.len(), 27);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_usdc_buy_uses_idl_accounts_17_18_19() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let associated_creator_vault =
|
||||
if let DexParamEnum::PumpFun(protocol_params) = ¶ms.protocol_params {
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&protocol_params.creator_vault,
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
)
|
||||
} else {
|
||||
Pubkey::default()
|
||||
};
|
||||
let ix = build_buy(¶ms).unwrap().pop().unwrap();
|
||||
assert_eq!(ix.accounts.len(), 27);
|
||||
assert_eq!(ix.accounts[17].pubkey, associated_creator_vault);
|
||||
assert_eq!(
|
||||
ix.accounts[18].pubkey,
|
||||
crate::instruction::utils::pumpfun::get_fee_sharing_config_pda(&mint).unwrap()
|
||||
);
|
||||
assert_eq!(ix.accounts[19].pubkey, accounts::GLOBAL_VOLUME_ACCUMULATOR);
|
||||
assert_eq!(ix.accounts[22].pubkey, accounts::FEE_CONFIG);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_buyback_fee_recipient_is_writable() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let buy_ix = build_buy(¶ms).unwrap().pop().unwrap();
|
||||
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&buy_ix.accounts[8].pubkey));
|
||||
assert!(buy_ix.accounts[8].is_writable);
|
||||
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = mint;
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
let sell_ix = build_sell(¶ms).unwrap().pop().unwrap();
|
||||
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
|
||||
assert!(sell_ix.accounts[8].is_writable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -952,7 +1403,7 @@ mod tests {
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
|
||||
let instructions = build_sell_v1(¶ms).unwrap();
|
||||
let instructions = build_sell(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR);
|
||||
@@ -973,8 +1424,12 @@ mod tests {
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_sell_v2(¶ms).unwrap();
|
||||
let instructions = build_sell(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! 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` 是单字段
|
||||
//! struct(TypeScript 传 `[true]`),在 ix 参数中为 1 字节 bool,共 25 字节 ix data。
|
||||
@@ -10,71 +11,90 @@ use crate::instruction::utils::pumpfun::{
|
||||
BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[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; 25] {
|
||||
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_sol_cost.to_le_bytes());
|
||||
d[24] = 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; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
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_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)
|
||||
}
|
||||
|
||||
@@ -589,6 +589,7 @@ mod tests {
|
||||
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,
|
||||
@@ -596,7 +597,6 @@ mod tests {
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: Some(true),
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -382,6 +382,7 @@ mod tests {
|
||||
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,
|
||||
@@ -389,7 +390,6 @@ mod tests {
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -394,6 +394,7 @@ mod tests {
|
||||
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,
|
||||
@@ -401,7 +402,6 @@ mod tests {
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-2
@@ -7,9 +7,11 @@ pub mod swqos;
|
||||
pub mod trading;
|
||||
pub mod utils;
|
||||
|
||||
pub use crate::common::nonce_cache::{fetch_nonce_info, DurableNonceInfo};
|
||||
// Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode)
|
||||
pub use crate::swqos::{AstralaneTransport, SwqosTransport};
|
||||
pub use client::{
|
||||
find_pool_by_mint, recommended_sender_thread_core_indices, SolanaTrade, TradeBuyParams,
|
||||
TradeSellParams, TradeTokenType, TradingClient, TradingInfrastructure,
|
||||
find_pool_by_mint, recommended_sender_thread_core_indices, AccountPolicy, BuyAmount,
|
||||
SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade, TradeBuyParams, TradeSellParams,
|
||||
TradeTokenType, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::anyhow;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
@@ -16,6 +17,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
const PACKET_DATA_SIZE: usize = 1232;
|
||||
|
||||
/// Convert SOL amount (f64) to lamports without string allocation (hot path).
|
||||
#[inline(always)]
|
||||
fn sol_f64_to_lamports(sol: f64) -> u64 {
|
||||
@@ -42,6 +45,75 @@ pub fn build_transaction(
|
||||
tip_account: &Pubkey,
|
||||
tip_amount: f64,
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let transaction = build_transaction_with_compute_budget(
|
||||
payer,
|
||||
unit_limit,
|
||||
unit_price,
|
||||
business_instructions,
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
middleware_manager,
|
||||
protocol_name,
|
||||
is_buy,
|
||||
with_tip,
|
||||
tip_account,
|
||||
tip_amount,
|
||||
durable_nonce,
|
||||
)?;
|
||||
|
||||
let serialized_len = bincode::serialized_size(&transaction)? as usize;
|
||||
if serialized_len <= PACKET_DATA_SIZE
|
||||
|| durable_nonce.is_some()
|
||||
|| !with_tip
|
||||
|| tip_amount <= 0.0
|
||||
|| (unit_limit == 0 && unit_price == 0)
|
||||
{
|
||||
return Ok(transaction);
|
||||
}
|
||||
|
||||
let compact_transaction = build_transaction_with_compute_budget(
|
||||
payer,
|
||||
0,
|
||||
0,
|
||||
business_instructions,
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
middleware_manager,
|
||||
protocol_name,
|
||||
is_buy,
|
||||
with_tip,
|
||||
tip_account,
|
||||
tip_amount,
|
||||
durable_nonce,
|
||||
)?;
|
||||
let compact_len = bincode::serialized_size(&compact_transaction)? as usize;
|
||||
if compact_len <= PACKET_DATA_SIZE {
|
||||
return Ok(compact_transaction);
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting",
|
||||
serialized_len,
|
||||
PACKET_DATA_SIZE,
|
||||
compact_len
|
||||
))
|
||||
}
|
||||
|
||||
fn build_transaction_with_compute_budget(
|
||||
payer: &Arc<Keypair>,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
business_instructions: &[Instruction],
|
||||
address_lookup_table_account: Option<&AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
middleware_manager: Option<&Arc<MiddlewareManager>>,
|
||||
protocol_name: &str,
|
||||
is_buy: bool,
|
||||
with_tip: bool,
|
||||
tip_account: &Pubkey,
|
||||
tip_amount: f64,
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let mut instructions = Vec::with_capacity(business_instructions.len() + 5);
|
||||
|
||||
@@ -93,19 +165,19 @@ 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)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ 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 {
|
||||
@@ -146,10 +147,15 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +237,15 @@ fn ensure_dedicated_pool(
|
||||
(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>,
|
||||
@@ -471,8 +486,32 @@ 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,
|
||||
@@ -576,6 +615,7 @@ 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,
|
||||
@@ -627,9 +667,6 @@ pub async fn execute_parallel(
|
||||
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
|
||||
let channel_count = selected_task_configs.len().max(1);
|
||||
let collector = Arc::new(ResultCollector::new(channel_count));
|
||||
// 上限最多 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,
|
||||
@@ -714,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));
|
||||
}
|
||||
|
||||
@@ -157,6 +157,10 @@ 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(),
|
||||
@@ -169,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,
|
||||
|
||||
@@ -82,6 +82,14 @@ pub struct SwapParams {
|
||||
pub simulate: bool,
|
||||
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
||||
pub log_enabled: bool,
|
||||
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
|
||||
/// SWQOS route's HTTP submit response so all submitted signatures are
|
||||
/// returned. Defaults to false (post-4.0.11 behaviour: return after the
|
||||
/// first route accepts). Set to true when the caller does its own on-chain
|
||||
/// confirmation against a pinned durable nonce — only one route's tx can
|
||||
/// land, but the caller cannot know which in advance, so it needs every
|
||||
/// signature to poll via `getSignatureStatuses`.
|
||||
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.
|
||||
@@ -94,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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user