Compare commits

..

3 Commits

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