Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ed2aef931 | |||
| 0cf0064e80 | |||
| 4968c48a2e | |||
| 5908c84798 | |||
| 6dab47f8f9 | |||
| a076e0f8cd | |||
| 05ac079d6d | |||
| f6db55c874 | |||
| 69b4f9bb9f | |||
| 532bc1195d | |||
| 8944c5455c | |||
| 0d846930ad | |||
| 703ae5e4db | |||
| 8e0210af97 | |||
| e605485ecf | |||
| 8dec2f8d8c | |||
| b505bebdac | |||
| 1cc051a874 | |||
| 0311a0b876 | |||
| b49c1c803e | |||
| ce8fb99626 | |||
| 49843d8a20 | |||
| 60d1db4755 | |||
| 7a8d2a9c17 | |||
| 75bfbf6d54 | |||
| 184f1cc583 |
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.5"
|
||||
version = "4.0.11"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
"wei <1415121722@qq.com>",
|
||||
]
|
||||
repository = "https://github.com/0xfnzero/sol-trade-sdk"
|
||||
description = "Rust SDK to interact with the dex trade Solana program."
|
||||
description = "A high-performance Rust SDK for Solana DEX trading."
|
||||
license = "MIT"
|
||||
keywords = ["solana", "memecoins", "pumpfun", "pumpswap", "raydium"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
||||
</p>
|
||||
|
||||
> ☕ **Support This Project**
|
||||
>
|
||||
> This SDK is completely free and open source. However, maintaining and continuously updating it requires significant AI computing resources and token consumption. If this SDK helps with your trading development, consider making a monthly SOL donation — any amount is appreciated and helps keep this project alive!
|
||||
>
|
||||
> **Donation Wallet:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [✨ Features](#-features)
|
||||
@@ -53,6 +59,7 @@
|
||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||
- [🔍 Nonce Cache](#-nonce-cache)
|
||||
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
|
||||
- [🔄 PumpFun V1 vs V2 Instructions](#-pumpfun-v1-vs-v2-instructions)
|
||||
- [🛡️ MEV Protection Services](#️-mev-protection-services)
|
||||
- [📁 Project Structure](#-project-structure)
|
||||
- [📄 License](#-license)
|
||||
@@ -74,7 +81,7 @@ This SDK is available in multiple languages:
|
||||
|
||||
## ✨ Features
|
||||
|
||||
1. **PumpFun Trading**: Support for `buy` and `sell` operations
|
||||
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)
|
||||
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
|
||||
@@ -101,14 +108,14 @@ Add the dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "4.0.3"
|
||||
sol-trade-sdk = "4.0.9"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -130,15 +137,12 @@ let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// Create TradeConfig instance
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -148,6 +152,7 @@ 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(true) // PumpFun V2 (27 accounts, quote_mint); required for USDC-paired PumpFun coins
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
@@ -266,7 +271,7 @@ let jito_config = SwqosConfig::Jito(
|
||||
);
|
||||
|
||||
// Using default regional endpoint (third parameter is None)
|
||||
let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
let temporal_config = SwqosConfig::Temporal(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::NewYork, // Will use the default endpoint for this region
|
||||
None // No custom URL, uses SwqosRegion
|
||||
@@ -335,6 +340,10 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi
|
||||
- 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.
|
||||
|
||||
#### PumpFun: troubleshooting (on-chain errors)
|
||||
|
||||
For **Anchor 2006 / `NotAuthorized` (6000) / wrong token program / BuyZeroAmount (6020) / slippage (6042)** and related issues, see **[docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)** (Chinese). An English appendix may be added later.
|
||||
|
||||
#### PumpFun: Creator Rewards Sharing (creator_vault)
|
||||
|
||||
Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vault` can differ from the default derivation. If you reuse cached params from a **buy** when **selling**, you may see program error **2006 (seeds constraint violated)**. To avoid this:
|
||||
@@ -346,25 +355,73 @@ 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 has two instruction sets for bonding-curve trading:
|
||||
|
||||
| | V1 (default) | V2 (opt-in) |
|
||||
|---|---|---|
|
||||
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
|
||||
| Account metas | 18 | 27 |
|
||||
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
|
||||
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
|
||||
|
||||
**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.
|
||||
|
||||
**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
|
||||
|
||||
**How to enable V2:**
|
||||
|
||||
**Method 1 — Global runtime flag** (recommended for `TradingClient`; required for USDC-paired coins):
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
**Method 2 — Set the quote mint on `PumpFunParams`**:
|
||||
|
||||
When using the high-level `TradingClient`, keep `.use_pumpfun_v2(true)` enabled in `TradeConfig` and set `quote_mint` on the PumpFun params to select the pair:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
`with_quote_mint(...)` also marks the params as V2-capable for lower-level instruction builders, but the high-level `TradingClient` uses the client-level `use_pumpfun_v2` runtime flag when choosing V1 vs V2.
|
||||
|
||||
> **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.
|
||||
|
||||
#### PumpSwap: coin_creator_vault from events (no RPC)
|
||||
|
||||
For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_authority` are required in buy/sell instructions. Both are available from parsed events without RPC:
|
||||
|
||||
- **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`.
|
||||
- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event’s `coin_creator_vault_ata` and `coin_creator_vault_authority`.
|
||||
- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event's `coin_creator_vault_ata` and `coin_creator_vault_authority`.
|
||||
|
||||
## 🛡️ MEV Protection Services
|
||||
|
||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||
|
||||
- **Jito**: High-performance block space
|
||||
- **ZeroSlot**: Zero-latency transactions
|
||||
- **Temporal**: Time-sensitive transactions
|
||||
- **Bloxroute**: Blockchain network acceleration
|
||||
- **FlashBlock**: High-speed transaction execution with API key authentication
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication
|
||||
- **Node1**: High-speed transaction execution with API key authentication
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC; see [Astralane](#astralane-binary--plain--quic) above)
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC)
|
||||
- **SpeedLanding**: High-speed transaction execution with API key authentication
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
|
||||
+70
-17
@@ -39,6 +39,12 @@
|
||||
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
||||
</p>
|
||||
|
||||
> ☕ **支持本项目**
|
||||
>
|
||||
> 本 SDK 完全免费且开源。但维护和持续更新需要消耗大量 AI 算力与 Token。如果这个 SDK 对您的开发有帮助,欢迎每月捐赠任意数量的 SOL,您的支持将帮助这个项目持续运行!
|
||||
>
|
||||
> **捐赠钱包:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [✨ 项目特性](#-项目特性)
|
||||
@@ -53,6 +59,7 @@
|
||||
- [🔍 地址查找表](#-地址查找表)
|
||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||
- [Pump.fun 常见链上错误与排错(文档)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)
|
||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||
- [📁 项目结构](#-项目结构)
|
||||
- [📄 许可证](#-许可证)
|
||||
@@ -74,13 +81,13 @@
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
1. **PumpFun 交易**: 支持`购买`、`卖出`功能
|
||||
1. **PumpFun 交易**: 支持 `buy`、`sell`、`buy_exact_sol_in` 以及全新的统一化 `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` 指令(SOL + 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、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane 等服务
|
||||
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
|
||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||
@@ -101,14 +108,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "4.0.3"
|
||||
sol-trade-sdk = "4.0.9"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -130,15 +137,12 @@ let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain(/iris)或 Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -148,6 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||
// .use_pumpfun_v2(true) // PumpFun V2(27 个账户,quote_mint);USDC 配对 PumpFun 币必须开启
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
@@ -265,7 +270,7 @@ let jito_config = SwqosConfig::Jito(
|
||||
);
|
||||
|
||||
// 使用默认区域端点(第三个参数为 None)
|
||||
let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
let temporal_config = SwqosConfig::Temporal(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::NewYork, // 将使用该区域的默认端点
|
||||
None // 没有自定义 URL,使用 SwqosRegion
|
||||
@@ -334,6 +339,10 @@ PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还
|
||||
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。
|
||||
- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。
|
||||
|
||||
#### PumpFun:常见错误与排错思路
|
||||
|
||||
实盘集成时若遇 **Anchor 2006、`NotAuthorized`(6000)、Token program 不匹配、6020/6042** 等,请参阅专门文档:**[Pump.fun 常见链上错误与处理思路(中文)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)**。
|
||||
|
||||
#### PumpFun:Creator Rewards Sharing(creator_vault)
|
||||
|
||||
部分 PumpFun 代币启用了 **Creator Rewards Sharing**,链上 `creator_vault` 可能与默认推导结果不同。若在**卖出**时复用**买入**时缓存的 params,可能触发程序错误 **2006(seeds constraint violated)**。建议:
|
||||
@@ -352,18 +361,63 @@ 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 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
|
||||
|
||||
**v2 指令关键变化:**
|
||||
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint
|
||||
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
|
||||
- `buyback_fee_recipient`、`sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
|
||||
- SOL 配对币的报价和成本与旧版一致,无额外开销
|
||||
|
||||
**使用方式:**
|
||||
|
||||
高层 `TradingClient` 需要先在 `TradeConfig` 开启 PumpFun V2;USDC 配对币必须开启:
|
||||
|
||||
```rust
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.use_pumpfun_v2(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
然后在 `PumpFunParams` 设置 `quote_mint` 来选择 SOL/USDC 配对:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
// 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);
|
||||
|
||||
// 之后正常交易
|
||||
client.buy(buy_params).await?;
|
||||
client.sell(sell_params).await?;
|
||||
```
|
||||
|
||||
`with_quote_mint(...)` 会把参数标记为可使用 V2,底层 instruction builder 可据此走 V2;但高层 `TradingClient` 选择 V1/V2 时使用的是 client 级别的 `use_pumpfun_v2` 运行时开关。
|
||||
|
||||
| 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) |
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
||||
|
||||
- **Jito**: 高性能区块空间
|
||||
- **ZeroSlot**: 零延迟交易
|
||||
- **Temporal**: 时间敏感交易
|
||||
- **Bloxroute**: 区块链网络加速
|
||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||
- **Node1**: 高速交易执行,支持 API 密钥认证
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC,见 [Astralane](#astralanebinary--plain--quic))
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC)
|
||||
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
@@ -403,4 +457,3 @@ MIT 许可证
|
||||
3. 注意滑点设置避免交易失败
|
||||
4. 监控余额和交易费用
|
||||
5. 遵循相关法律法规
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# Pump.fun(Bonding Curve)常见链上错误与处理思路
|
||||
|
||||
本文档面向使用 **sol-trade-sdk** 组装 Pump.fun Program(`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`)买卖交易的集成方,汇总**实战中高频**的失败形态、日志特征,以及与 SDK 参数的对应关系和**推荐处理方式**。
|
||||
|
||||
> 自定义错误码以仓库内 `idl/pump.json`、`idl/pump_fees.json` 为准;**2006** 等部分错误来自 **Anchor 框架**,不在 Pump 自定义枚举里。
|
||||
|
||||
---
|
||||
|
||||
## 1. Anchor `2006` / `ConstraintSeeds`(`creator_vault`)
|
||||
|
||||
### 现象
|
||||
|
||||
- Solana Explorer / `simulateTransaction`:**Program Error: custom program error: 2006**,或 Anchor 文案 **A seeds constraint was violated**。
|
||||
- 日志里常见于账户 **`creator_vault`**:打印 **Left**(你传入的 pubkey)与 **Right**(程序按当前 curve 推导的 PDA)。
|
||||
|
||||
### 含义
|
||||
|
||||
Pump 校验 `creator_vault` 必须满足程序的 **PDAs seeds**(与 bonding curve 上记录的 **creator / fee-sharing 布局**一致)。传入地址与程序期望不一致即失败。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. **`bonding_curve.creator` 与链上不符**:事件/缓存里用的是 create 交易的 `creator` 字段、`user`,或陈旧快照,与 curve 账户内真实 creator 不一致;据此推导或缓存的 vault 会错。
|
||||
2. **买单侧「旧 vault」被沿用到卖单**:买入时 ix 里的 `creator_vault` 在后续 trade 语境下已不再与程序约束一致(例如 creator / sharing 语义在链上演进),卖单仍填旧地址 → **Left ≠ Right**。
|
||||
3. **Creator Rewards / pump-fees 分成布局**:部分 mint 使用 `sharing-config` 相关 seeds,单靠 `PDA(["creator-vault"], bonding_curve.creator)` 不足以覆盖全部历史状态;Stale 的 offline hint 也会把 resolve 引向错误 vault。
|
||||
4. **Phantom vault**:历史上若用 `Pubkey::default()` 推导出的占位 vault(SDK 常量 `phantom_default_creator_vault`),链上必定失败。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **买入 / 卖出指令**(同一套解析) | 使用 `resolve_creator_vault_for_ix_with_fee_sharing`(`src/instruction/utils/pumpfun.rs`):有 **非 default、非 phantom** 的 ix / 解析器回填 **`creator_vault` 时原样采用**(**不会**再根据 `creator` 做 `get_creator_vault_pda` 覆盖费分成等非传统布局);**未传 ix vault** 时依次:`fee_sharing_creator_vault_if_active` hint → `PDA(effective_creator)`。`PumpFunInstructionBuilder` 买卖均走此逻辑。 |
|
||||
| **权威对齐** | 低延迟路径外,可 **`PumpFunParams::from_mint_by_rpc`** 或由解析器 **`fill_trade_accounts`**(如 sol-parser-sdk)持续刷新 `creator` / `creator_vault`;必要时对 **fee-sharing** 使用 `fetch_fee_sharing_creator_vault_if_active` / `refresh_fee_sharing_creator_vault_from_rpc`。 |
|
||||
| **bonding_curve 账户地址** | 指令构建时使用 **`get_bonding_curve_pda(mint)`** 作为 canonical bonding curve pubkey,避免缓存中的曲线地址错位导致读到错误 **creator**。 |
|
||||
|
||||
集成方若在 **bot** 侧维护持仓快照,建议在**每笔**解析到的 Pump trade 后刷新缓存/仓位中的 `creator` 与 `creator_vault`,避免「只写一次建仓快照」。
|
||||
|
||||
---
|
||||
|
||||
## 2. Pump `6000` `NotAuthorized`(常见:`feeRecipient`)
|
||||
|
||||
### 现象
|
||||
|
||||
- 日志:`AnchorError thrown in programs/pump/src/fee_recipient.rs` 或 **`Error Code: NotAuthorized`**(与 Global 授权的 fee recipient 池有关)。
|
||||
|
||||
### 含义
|
||||
|
||||
账户 **#2 fee recipient** 不是当前 Pump **Global / 协议**允许使用的收款地址之一,或与 **Mayhem / 非 Mayhem** 池不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. 使用了**过期**或**错误池**的 fee recipient(静态列表落后于主网轮换)。
|
||||
2. **`mayhem_mode` 与 `fee_recipient` 不匹配**:声明 Mayhem 却传普通池地址,或相反(见 `reconcile_mayhem_mode_for_trade`,`src/instruction/utils/pumpfun.rs`)。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **优先事件** | 使用 gRPC / 解析器里的 **`tradeEvent.feeRecipient`** 或同笔 **create_v2 + buy** 观测到的 fee recipient。 |
|
||||
| **纠偏** | `PumpFunParams::from_trade` 会对 `mayhem_mode` 与 `fee_recipient` 做池一致性纠偏;发单前若事件缺省,可走 `pump_fun_fee_recipient_meta`(按 `is_mayhem_mode` 从静态池选)。 |
|
||||
| **提交前保留观测值** | 不要无条件把已观测的 `fee_recipient` 清成 default;否则会退回 SDK 内置静态池,静态池若落后于主网 Global 授权,仍可能触发 6000。只有缺少观测值时才让 builder 兜底。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. SPL:Token **`initializeAccount3`**——`incorrect program id for instruction`
|
||||
|
||||
### 现象
|
||||
|
||||
- 内联指令里 **`Token Program: initializeAccount3`**(或 Token-2022 等价指令)报错 **`incorrect program id for instruction`**。
|
||||
|
||||
### 含义
|
||||
|
||||
为 **Mint** 创建用户 ATA 时,使用的 **token program(Legacy SPL vs Token-2022)** 与 **Mint 的实际 owner(`mint.owner`)** 不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. Pump 新发多为 **Token-2022**,但代码写死 **`Tokenkeg…`**。
|
||||
2. 少数 Legacy mint(`Tokenkeg…`),却被强制按 Token-2022 建账。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::token_program`** 必须与非 default 的 **mint owner** 一致;从 **gRPC / 解析结果** 带入,**勿**在已明确 program 时再用「仅按 `.pump` 后缀猜 Token-2022」覆盖(业务层若做后缀启发,应仅在 `token_program == default` 时生效)。
|
||||
|
||||
---
|
||||
|
||||
## 4. Pump `6020` `BuyZeroAmount`
|
||||
|
||||
### 现象
|
||||
|
||||
- `buy` / `buy_exact_sol_in` 报 **Buy zero amount**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- `min_tokens_out == 0`(或等价路径算出可买 **0 枚**),协议直接拒绝。
|
||||
- 使用 **Create / Shred** 事件构造曲线时 **virtual / real 储备全 0**,本地定价算出 **0**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 对「首买 / 无储备」场景用 **`PumpFunParams::from_dev_trade`** 或按协议初值回填虚拟储备(与 `global_constants` 一致),再算 **`min_tokens_out`**。
|
||||
- 适当 **放宽买入滑点**(`slippage_basis_points`),避免估算代币量略小于链上。
|
||||
|
||||
---
|
||||
|
||||
## 5. Pump `6042` `BuySlippageBelowMinTokensOut`
|
||||
|
||||
### 现象
|
||||
|
||||
- 文案:**Slippage: Would buy less tokens than expected min_tokens_out**。
|
||||
|
||||
### 含义
|
||||
|
||||
链上实际可成交代币数量 **小于** 指令参数 **`min_tokens_out`**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 市价波动、SOL 竞价导致曲线状态与本地快照不一致。
|
||||
- 本地 **`get_buy_token_amount_from_sol_amount`** 所用 **creator / 费率假设**与链上 **pfee CPI**(动态费率)不一致,**预估偏多**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **提高滑点容忍**(或降低 **`min_tokens_out`**)。
|
||||
- 尽量用 **较新**的 **virtual/real reserves**(来自最近一次 trade 解析或简短 RPC)。
|
||||
- 若运行在 **狙击手**等极端延迟场景,需接受:**保守的 min_out**(更大滑点)换成功率。
|
||||
|
||||
---
|
||||
|
||||
## 6. Pump `6024` `Overflow` 与其它算术类错误
|
||||
|
||||
### 现象
|
||||
|
||||
- `6024 Overflow`、`6025 Truncation`、`6026 DivisionByZero`(见 IDL)。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 指令参数 **`amount` / SOL / lamports** 与曲线状态组合不合法(例如极端大卖、或为 0 与后续计算冲突)。
|
||||
- SDK 外传入了 **不合理的储备快照**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 校验 **买入/卖出金额 > 0**、与余额一致。
|
||||
- 使用 **`from_mint_by_rpc`** 或与链一致的储备后再算 **`min_sol_output` / `min_tokens_out`**。
|
||||
|
||||
---
|
||||
|
||||
## 7. Pump `6027` `NotEnoughRemainingAccounts`(返现等)
|
||||
|
||||
### 现象
|
||||
|
||||
- 返现代币等路径要求 **remaining accounts**(例如 `UserVolumeAccumulator`),数量不足。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- **`is_cashback_coin`(或等价标志)为 true**,但组装指令时 **未追加**所需账户。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::from_trade` / `from_dev_trade`** 传入正确的 **`is_cashback_coin`**(来自事件)。
|
||||
- README 中与 **Cashback** 章节一致:**事件路径必须带标志**,不能默认 false。
|
||||
|
||||
---
|
||||
|
||||
## 8. Pump `6022` `SellZeroAmount`
|
||||
|
||||
### 含义
|
||||
|
||||
卖出代币数量为 **0**。在业务层过滤即可。
|
||||
|
||||
---
|
||||
|
||||
## 9. 与 pump-fees / Creator 迁移相关的错误(`6049`–`6053` 等)
|
||||
|
||||
IDL 中例如:
|
||||
|
||||
- **`6049` `CreatorMigratedToSharingConfig`**
|
||||
- **`6050` `UnableToDistributeCreatorVaultMigratedToSharingConfig`**
|
||||
- **`6053` `BondingCurveAndSharingConfigCreatorMismatch`**
|
||||
|
||||
### 思路
|
||||
|
||||
这些是 **creator / fee-sharing** 生命周期中的**专用分支**,与一般买卖路径不同。若仿真或清算类指令触发:
|
||||
|
||||
- 以 **Pump / pump-fees 官方文档** 为准使用 **`distribute_creator_fees`**、**reset_fee_sharing_config** 等;
|
||||
- **`creator_vault` resolve** 需结合 **`fetch_fee_sharing_creator_vault_if_active`** 与链上 **`SharingConfig` 状态**,避免离线 deduce 过时。
|
||||
|
||||
---
|
||||
|
||||
## 10. 调试清单(推荐给集成方)
|
||||
|
||||
1. **记下失败指令索引** + **Simulation / explorer 展开的账户列表**,重点核对:**mint、bonding_curve、associated_bonding_curve、creator_vault、fee_recipient、token_program**。
|
||||
2. **对比 Anchor 日志里的 Left / Right**(针对 2006)与本地 `PumpFunParams` 打印是否一致。
|
||||
3. **`mint.owner`** 与 **`PumpFunParams.token_program`** 是否一致。
|
||||
4. **`bonding_curve` 地址**是否与 **`get_bonding_curve_pda(mint)`** 一致。
|
||||
5. **`mayhem_mode` ↔ `fee_recipient`** 是否同池。
|
||||
6. 低延迟不足以覆盖 **creator 演进** 时,是否在卖前引入了 **RPC 或最新 trade** 刷新。
|
||||
|
||||
---
|
||||
|
||||
## 参考代码入口(本仓库)
|
||||
|
||||
| 主题 | 路径 |
|
||||
|------|------|
|
||||
| Buy / Sell vault resolve(显式 ix `creator_vault` → `fee_sharing` hint → `PDA(effective_creator)`) | `src/instruction/utils/pumpfun.rs` — `resolve_creator_vault_for_ix_with_fee_sharing`;`effective_creator_for_trade` 见 `src/trading/core/params/pumpfun.rs` |
|
||||
| Fee recipient / Mayhem | `src/instruction/utils/pumpfun.rs` — `pump_fun_fee_recipient_meta`, `reconcile_mayhem_mode_for_trade` |
|
||||
| 指令构建 | `src/instruction/pumpfun.rs` — `PumpFunInstructionBuilder` |
|
||||
| Params | `src/trading/core/params/pumpfun.rs` — `PumpFunParams::{from_trade, from_dev_trade, from_mint_by_rpc, refresh_fee_sharing_creator_vault_from_rpc}` |
|
||||
|
||||
---
|
||||
|
||||
如需英文版或对 PumpSwap/其它 DEX 的同类文档,可在 `docs/` 下按相同结构扩展。
|
||||
@@ -35,7 +35,7 @@ The `TradeBuyParams` struct contains all parameters required for executing buy o
|
||||
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
|
||||
| `create_mint_ata` | `bool` | ✅ | Whether to create token mint ATA |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
|
||||
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
|
||||
|
||||
@@ -67,7 +67,7 @@ The `TradeSellParams` struct contains all parameters required for executing sell
|
||||
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
|
||||
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
|
||||
| `create_mint_ata` | `bool` | ✅ | 是否创建代币 mint ATA |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出(Meteora DAMM V2 必需) |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算(Meteora DAMM V2 必需) |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
|
||||
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
|
||||
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出(Meteora DAMM V2 必需) |
|
||||
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算(Meteora DAMM V2 必需) |
|
||||
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
|
||||
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
Some(trade_info.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
|
||||
@@ -170,7 +170,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
trade_info.global_config,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
@@ -221,7 +221,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
trade_info.global_config,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
with_tip: false,
|
||||
durable_nonce: None,
|
||||
create_output_token_ata: false,
|
||||
|
||||
@@ -138,7 +138,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
trade_info.global_config,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -182,7 +182,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
trade_info.global_config,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -627,7 +627,7 @@ async fn handle_buy_pumpfun(
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpFun(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
@@ -639,7 +639,7 @@ async fn handle_buy_pumpfun(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully bought tokens from PumpFun!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -683,7 +683,7 @@ async fn handle_buy_pumpswap(
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpSwap(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
@@ -695,7 +695,7 @@ async fn handle_buy_pumpswap(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully bought tokens from PumpSwap!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -739,7 +739,7 @@ async fn handle_buy_bonk(
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::Bonk(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
@@ -751,7 +751,7 @@ async fn handle_buy_bonk(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully bought tokens from Bonk!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -799,7 +799,7 @@ async fn handle_buy_raydium_v4(
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
@@ -811,7 +811,7 @@ async fn handle_buy_raydium_v4(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully bought tokens from Raydium V4!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -860,7 +860,7 @@ async fn handle_buy_raydium_cpmm(
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::RaydiumCpmm(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
@@ -872,7 +872,7 @@ async fn handle_buy_raydium_cpmm(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully bought tokens from Raydium CPMM!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -1030,7 +1030,7 @@ async fn handle_sell_pumpfun(
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpFun(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1042,7 +1042,7 @@ async fn handle_sell_pumpfun(
|
||||
};
|
||||
|
||||
match client.sell(sell_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully sold tokens from PumpFun!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -1089,7 +1089,7 @@ async fn handle_sell_pumpswap(
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpSwap(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1100,7 +1100,7 @@ async fn handle_sell_pumpswap(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.sell(sell_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully sold tokens from PumpSwap!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -1148,7 +1148,7 @@ async fn handle_sell_bonk(
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::Bonk(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1159,7 +1159,7 @@ async fn handle_sell_bonk(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.sell(sell_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully sold tokens from Bonk!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -1210,7 +1210,7 @@ async fn handle_sell_raydium_v4(
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1221,7 +1221,7 @@ async fn handle_sell_raydium_v4(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.sell(sell_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully sold tokens from Raydium V4!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
@@ -1273,7 +1273,7 @@ async fn handle_sell_raydium_cpmm(
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::RaydiumCpmm(param),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
@@ -1284,7 +1284,7 @@ async fn handle_sell_raydium_cpmm(
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.sell(sell_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
Ok((_, signature, _, _)) => {
|
||||
println!(" ✅ Successfully sold tokens from Raydium CPMM!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
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,
|
||||
@@ -83,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
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,
|
||||
|
||||
@@ -103,7 +103,7 @@ async fn test_middleware() -> AnyResult<()> {
|
||||
.await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
|
||||
@@ -163,7 +163,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
Some(trade_info.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
|
||||
@@ -160,7 +160,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: true,
|
||||
@@ -210,7 +210,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: false,
|
||||
close_output_token_ata: false,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -150,7 +150,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -194,7 +194,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -41,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -80,7 +80,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -164,7 +164,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
0,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -195,7 +195,7 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
0,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -235,7 +235,7 @@ async fn pumpswap_trade_with_grpc(
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: is_sol,
|
||||
close_input_token_ata: is_sol,
|
||||
create_mint_ata: true,
|
||||
@@ -276,7 +276,7 @@ async fn pumpswap_trade_with_grpc(
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: is_sol,
|
||||
close_output_token_ata: is_sol,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -8,10 +8,7 @@ use sol_trade_sdk::{
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use sol_trade_sdk::{
|
||||
common::TradeConfig, instruction::utils::raydium_amm_v4::fetch_amm_info,
|
||||
trading::common::get_multi_token_balances, TradeTokenType,
|
||||
};
|
||||
use sol_trade_sdk::{common::TradeConfig, TradeTokenType};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
@@ -131,29 +128,16 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
let slippage_basis_points = Some(100);
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let amm_info = fetch_amm_info(&client.infrastructure.rpc, trade_info.amm).await?;
|
||||
let (coin_reserve, pc_reserve) = get_multi_token_balances(
|
||||
&client.infrastructure.rpc,
|
||||
&amm_info.token_coin,
|
||||
&amm_info.token_pc,
|
||||
)
|
||||
.await?;
|
||||
let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
let params =
|
||||
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm)
|
||||
.await?;
|
||||
let mint_pubkey = if params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| params.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
{
|
||||
amm_info.coin_mint
|
||||
params.coin_mint
|
||||
} else {
|
||||
amm_info.pc_mint
|
||||
params.pc_mint
|
||||
};
|
||||
let params = RaydiumAmmV4Params::new(
|
||||
trade_info.amm,
|
||||
amm_info.coin_mint,
|
||||
amm_info.pc_mint,
|
||||
amm_info.token_coin,
|
||||
amm_info.token_pc,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
);
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
@@ -161,8 +145,8 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
// Buy tokens
|
||||
println!("Buying tokens from Raydium_amm_v4...");
|
||||
let input_token_amount = 100_000;
|
||||
let is_wsol = amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| amm_info.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_wsol = params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| params.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::RaydiumAmmV4,
|
||||
input_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
|
||||
@@ -172,7 +156,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(params),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: is_wsol,
|
||||
close_input_token_ata: is_wsol,
|
||||
create_mint_ata: true,
|
||||
@@ -214,7 +198,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(params),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: is_wsol,
|
||||
close_output_token_ata: is_wsol,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -160,7 +160,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: is_wsol,
|
||||
close_input_token_ata: is_wsol,
|
||||
create_mint_ata: true,
|
||||
@@ -200,7 +200,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: is_wsol,
|
||||
close_output_token_ata: is_wsol,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -41,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: true,
|
||||
create_mint_ata: true,
|
||||
@@ -83,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
wait_tx_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
close_output_token_ata: true,
|
||||
close_mint_token_ata: false,
|
||||
|
||||
@@ -47,7 +47,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
SwqosConfig::Temporal("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
|
||||
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
|
||||
SwqosConfig::Astralane(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# IDL files
|
||||
|
||||
Supported DEX IDLs are stored directly in this directory. The files below were
|
||||
synced from `bitquery/solana-idl-lib` commit `f804b17` and written to the SDK's
|
||||
canonical local filenames.
|
||||
|
||||
| SDK protocol | Local IDL | Source path |
|
||||
| --- | --- | --- |
|
||||
| Bonk / Raydium Launchpad | `bonk.json` | `raydium/launchpad.json` |
|
||||
| Raydium CPMM | `raydium_cpmm.json` | `raydium/raydium_cp.json` |
|
||||
| Raydium AMM v4 | `raydium_amm_v4.json` | `raydium/raydium_amm.json` |
|
||||
| Meteora DAMM v2 | `meteora_damm_v2.json` | `meteora/cp_amm_016.json` |
|
||||
| PumpFun | `pump.json` | `pumpfun/pump.json` |
|
||||
| PumpSwap | `pump_amm.json` | `pumpswap/amm.json` |
|
||||
|
||||
No separate source snapshot tree is kept in the repository.
|
||||
+5783
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2611
-2896
File diff suppressed because it is too large
Load Diff
@@ -3965,6 +3965,254 @@
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "transfer_creator_fees_to_pump_v2",
|
||||
"discriminator": [
|
||||
1,
|
||||
33,
|
||||
78,
|
||||
185,
|
||||
33,
|
||||
67,
|
||||
44,
|
||||
92
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "payer",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "quote_mint"
|
||||
},
|
||||
{
|
||||
"name": "token_program"
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "associated_token_program",
|
||||
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
},
|
||||
{
|
||||
"name": "coin_creator"
|
||||
},
|
||||
{
|
||||
"name": "coin_creator_vault_authority",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
97,
|
||||
116,
|
||||
111,
|
||||
114,
|
||||
95,
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "coin_creator"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "coin_creator_vault_ata",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "coin_creator_vault_authority"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "token_program"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "quote_mint"
|
||||
}
|
||||
],
|
||||
"program": {
|
||||
"kind": "const",
|
||||
"value": [
|
||||
140,
|
||||
151,
|
||||
37,
|
||||
143,
|
||||
78,
|
||||
36,
|
||||
137,
|
||||
241,
|
||||
187,
|
||||
61,
|
||||
16,
|
||||
41,
|
||||
20,
|
||||
142,
|
||||
13,
|
||||
131,
|
||||
11,
|
||||
90,
|
||||
19,
|
||||
153,
|
||||
218,
|
||||
255,
|
||||
16,
|
||||
132,
|
||||
4,
|
||||
142,
|
||||
123,
|
||||
216,
|
||||
219,
|
||||
233,
|
||||
248,
|
||||
89
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pump_creator_vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
97,
|
||||
116,
|
||||
111,
|
||||
114,
|
||||
45,
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "coin_creator"
|
||||
}
|
||||
],
|
||||
"program": {
|
||||
"kind": "const",
|
||||
"value": [
|
||||
1,
|
||||
86,
|
||||
224,
|
||||
246,
|
||||
147,
|
||||
102,
|
||||
90,
|
||||
207,
|
||||
68,
|
||||
219,
|
||||
21,
|
||||
104,
|
||||
191,
|
||||
23,
|
||||
91,
|
||||
170,
|
||||
81,
|
||||
137,
|
||||
203,
|
||||
151,
|
||||
245,
|
||||
210,
|
||||
255,
|
||||
59,
|
||||
101,
|
||||
93,
|
||||
43,
|
||||
182,
|
||||
253,
|
||||
109,
|
||||
24,
|
||||
176
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pump_creator_vault_ata",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "pump_creator_vault"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "token_program"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "quote_mint"
|
||||
}
|
||||
],
|
||||
"program": {
|
||||
"kind": "account",
|
||||
"path": "associated_token_program"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "event_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
95,
|
||||
95,
|
||||
101,
|
||||
118,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "program"
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "update_admin",
|
||||
"discriminator": [
|
||||
@@ -4027,6 +4275,72 @@
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "update_buyback_config",
|
||||
"discriminator": [
|
||||
251,
|
||||
224,
|
||||
171,
|
||||
146,
|
||||
160,
|
||||
26,
|
||||
113,
|
||||
233
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "admin",
|
||||
"signer": true,
|
||||
"relations": [
|
||||
"global_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "global_config",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "event_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
95,
|
||||
95,
|
||||
101,
|
||||
118,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "program"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "buyback_basis_points",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update_fee_config",
|
||||
"discriminator": [
|
||||
@@ -4836,6 +5150,33 @@
|
||||
{
|
||||
"code": 6052,
|
||||
"name": "TokensInVaultLessThanCashbackEarned"
|
||||
},
|
||||
{
|
||||
"code": 6053,
|
||||
"name": "BuybackFeeRecipientNotAuthorized",
|
||||
"msg": "Buyback fee recipient not authorized"
|
||||
},
|
||||
{
|
||||
"code": 6054,
|
||||
"name": "AllBuybackFeeRecipientsShouldBeNonZero"
|
||||
},
|
||||
{
|
||||
"code": 6055,
|
||||
"name": "NotUniqueBuybackFeeRecipients"
|
||||
},
|
||||
{
|
||||
"code": 6056,
|
||||
"name": "BuybackBasisPointsOutOfRange",
|
||||
"msg": "buyback_basis_points must be <= 10_000"
|
||||
},
|
||||
{
|
||||
"code": 6057,
|
||||
"name": "WrongBuybackFeeRecipientsCount",
|
||||
"msg": "buyback fee recipients require exactly 8 remaining accounts (or none)"
|
||||
},
|
||||
{
|
||||
"code": 6058,
|
||||
"name": "BuybackFeeRecipientMissing"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
@@ -5086,6 +5427,14 @@
|
||||
{
|
||||
"name": "cashback",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "buyback_fee_basis_points",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "buyback_fee",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5523,6 +5872,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stable_fee_tiers",
|
||||
"type": {
|
||||
"vec": {
|
||||
"defined": {
|
||||
"name": "FeeTier"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5646,6 +6005,19 @@
|
||||
{
|
||||
"name": "is_cashback_enabled",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "buyback_fee_recipients",
|
||||
"type": {
|
||||
"array": [
|
||||
"pubkey",
|
||||
8
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "buyback_basis_points",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5941,6 +6313,14 @@
|
||||
{
|
||||
"name": "cashback",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "buyback_fee_basis_points",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "buyback_fee",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+4612
-249
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
+66
-27
@@ -3,6 +3,7 @@
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::sdk_log;
|
||||
use crate::common::GasFeeStrategy;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::common::{InfrastructureConfig, TradeConfig};
|
||||
#[cfg(feature = "perf-trace")]
|
||||
use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
|
||||
@@ -13,6 +14,7 @@ use crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use crate::swqos::common::TradeError;
|
||||
use crate::swqos::SwqosClient;
|
||||
use crate::swqos::SwqosConfig;
|
||||
use crate::swqos::SwqosType;
|
||||
use crate::swqos::TradeType;
|
||||
use crate::trading::core::params::BonkParams;
|
||||
use crate::trading::core::params::DexParamEnum;
|
||||
@@ -25,7 +27,6 @@ use crate::trading::factory::DexType;
|
||||
use crate::trading::MiddlewareManager;
|
||||
use crate::trading::SwapParams;
|
||||
use crate::trading::TradeFactory;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use parking_lot::Mutex;
|
||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||
use solana_sdk::hash::Hash;
|
||||
@@ -83,7 +84,7 @@ pub struct TradingInfrastructure {
|
||||
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
||||
/// Configuration used to create this infrastructure
|
||||
pub config: InfrastructureConfig,
|
||||
/// Precomputed at init: min(swqos_clients.len(), 2/3 * num_cores). Not computed on trade hot path.
|
||||
/// Precomputed at init: min(max SWQOS submit lanes, 2/3 * num_cores). Not computed on trade hot path.
|
||||
pub max_sender_concurrency: usize,
|
||||
/// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path.
|
||||
pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
|
||||
@@ -112,7 +113,9 @@ impl TradingInfrastructure {
|
||||
|
||||
// Initialize rent cache (with timeout so slow RPC doesn't block forever)
|
||||
const RENT_UPDATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
match tokio::time::timeout(RENT_UPDATE_TIMEOUT, crate::common::seed::update_rents(&rpc)).await {
|
||||
match tokio::time::timeout(RENT_UPDATE_TIMEOUT, crate::common::seed::update_rents(&rpc))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
@@ -218,22 +221,26 @@ impl TradingInfrastructure {
|
||||
}
|
||||
|
||||
if !swqos_clients.is_empty() {
|
||||
let labels: Vec<&str> = swqos_clients
|
||||
.iter()
|
||||
.map(|c| c.get_swqos_type().as_str())
|
||||
.collect();
|
||||
eprintln!(
|
||||
"ℹ️ SWQOS 通道已就绪: {} 条 → [{}]",
|
||||
swqos_clients.len(),
|
||||
labels.join(", ")
|
||||
);
|
||||
let labels: Vec<&str> =
|
||||
swqos_clients.iter().map(|c| c.get_swqos_type().as_str()).collect();
|
||||
eprintln!("ℹ️ SWQOS 通道已就绪: {} 条 → [{}]", swqos_clients.len(), labels.join(", "));
|
||||
}
|
||||
|
||||
let swqos_count = swqos_clients.len();
|
||||
let max_submit_lanes = swqos_clients
|
||||
.iter()
|
||||
.map(|client| {
|
||||
if matches!(client.get_swqos_type(), SwqosType::Default) {
|
||||
1usize
|
||||
} else {
|
||||
2usize
|
||||
}
|
||||
})
|
||||
.sum::<usize>()
|
||||
.max(1);
|
||||
let (max_sender_concurrency, effective_core_ids) = {
|
||||
let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0);
|
||||
let max_by_cores = (num_cores * 2 / 3).max(1);
|
||||
let cap = swqos_count.min(max_by_cores).max(1);
|
||||
let cap = max_submit_lanes.min(max_by_cores).max(1);
|
||||
let ids = core_affinity::get_core_ids()
|
||||
.map(|all| {
|
||||
let v: Vec<_> = all.into_iter().collect();
|
||||
@@ -248,6 +255,8 @@ impl TradingInfrastructure {
|
||||
(cap, Arc::new(ids))
|
||||
};
|
||||
|
||||
crate::instruction::utils::pumpswap::warm_pumpswap_global_config(Some(&rpc)).await;
|
||||
|
||||
Self {
|
||||
rpc,
|
||||
swqos_clients: Arc::new(swqos_clients),
|
||||
@@ -303,6 +312,8 @@ pub struct TradingClient {
|
||||
pub log_enabled: bool,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
/// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27-account metas). Default false.
|
||||
pub use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
|
||||
@@ -323,6 +334,7 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,7 +373,8 @@ pub struct TradeBuyParams {
|
||||
pub create_mint_ata: bool,
|
||||
/// Durable nonce information
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
|
||||
/// Optional fixed output token amount. On exact-out capable DEXes this uses
|
||||
/// the exact-out instruction and treats `input_token_amount` as max input.
|
||||
pub fixed_output_token_amount: Option<u64>,
|
||||
/// Gas fee strategy
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
@@ -372,7 +385,7 @@ pub struct TradeBuyParams {
|
||||
/// 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>,
|
||||
/// 可选:事件收到时间(微秒,与 sol-parser-sdk 的 metadata.grpc_recv_us / clock::now_micros 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点,打印起点→提交耗时。
|
||||
/// Optional upstream receive timestamp (e.g. gRPC recv) in microseconds for latency tracing.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -412,13 +425,14 @@ pub struct TradeSellParams {
|
||||
pub close_mint_token_ata: bool,
|
||||
/// Durable nonce information
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
|
||||
/// Optional fixed output token amount. On exact-out capable DEXes this uses
|
||||
/// the exact-out instruction and treats `input_token_amount` as max input.
|
||||
pub fixed_output_token_amount: Option<u64>,
|
||||
/// Gas fee strategy
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
/// Whether to simulate the transaction instead of executing it
|
||||
pub simulate: bool,
|
||||
/// 可选:事件收到时间(微秒,与 sol-parser-sdk clock 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点。
|
||||
/// Optional upstream receive timestamp (e.g. gRPC recv) in microseconds for latency tracing.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -457,6 +471,7 @@ impl TradingClient {
|
||||
effective_core_ids,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +518,7 @@ impl TradingClient {
|
||||
effective_core_ids,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,6 +698,7 @@ 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();
|
||||
@@ -707,7 +724,7 @@ impl TradingClient {
|
||||
|
||||
/// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores).
|
||||
/// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs.
|
||||
/// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores).
|
||||
/// Concurrency and core count are capped internally (≤ max submit lanes, ≤ 2/3 of CPU cores).
|
||||
/// - `None`: keep default (shared tokio pool).
|
||||
/// - `Some(vec![])`: dedicated threads with default count, no core pinning.
|
||||
/// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap).
|
||||
@@ -727,7 +744,8 @@ impl TradingClient {
|
||||
Some(v) => {
|
||||
self.use_dedicated_sender_threads = true;
|
||||
let cap = v.len().min(self.max_sender_concurrency);
|
||||
self.sender_thread_cores = Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v }));
|
||||
self.sender_thread_cores =
|
||||
Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v }));
|
||||
}
|
||||
}
|
||||
self
|
||||
@@ -790,7 +808,10 @@ impl TradingClient {
|
||||
pub async fn buy(
|
||||
&self,
|
||||
params: TradeBuyParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
|
||||
) -> Result<
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
if params.recent_blockhash.is_none() && params.durable_nonce.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)"
|
||||
@@ -860,11 +881,17 @@ impl TradingClient {
|
||||
check_min_tip: self.check_min_tip,
|
||||
grpc_recv_us: params.grpc_recv_us,
|
||||
use_exact_sol_amount: params.use_exact_sol_amount,
|
||||
use_pumpfun_v2: self.use_pumpfun_v2,
|
||||
};
|
||||
|
||||
let swap_result = executor.swap(buy_params).await;
|
||||
let result =
|
||||
swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings));
|
||||
let result = swap_result.map(|(success, sigs, err, timings)| {
|
||||
let legacy_timings = timings
|
||||
.into_iter()
|
||||
.map(|timing| (timing.swqos_type, timing.submit_done_us))
|
||||
.collect();
|
||||
(success, sigs, err.map(TradeError::from), legacy_timings)
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
@@ -897,7 +924,10 @@ impl TradingClient {
|
||||
pub async fn sell(
|
||||
&self,
|
||||
params: TradeSellParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
|
||||
) -> Result<
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
#[cfg(feature = "perf-trace")]
|
||||
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||
debug!(
|
||||
@@ -967,11 +997,17 @@ impl TradingClient {
|
||||
check_min_tip: self.check_min_tip,
|
||||
grpc_recv_us: params.grpc_recv_us,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: self.use_pumpfun_v2,
|
||||
};
|
||||
|
||||
let swap_result = executor.swap(sell_params).await;
|
||||
let result =
|
||||
swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings));
|
||||
let result = swap_result.map(|(success, sigs, err, timings)| {
|
||||
let legacy_timings = timings
|
||||
.into_iter()
|
||||
.map(|timing| (timing.swqos_type, timing.submit_done_us))
|
||||
.collect();
|
||||
(success, sigs, err.map(TradeError::from), legacy_timings)
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1006,7 +1042,10 @@ impl TradingClient {
|
||||
mut params: TradeSellParams,
|
||||
amount_token: u64,
|
||||
percent: u64,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
|
||||
) -> Result<
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ pub async fn fetch_address_lookup_table_account(
|
||||
lookup_table_address: &Pubkey,
|
||||
) -> Result<AddressLookupTableAccount, anyhow::Error> {
|
||||
let account = rpc.get_account(lookup_table_address).await?;
|
||||
|
||||
|
||||
// Parse address lookup table manually
|
||||
// Layout: 4 bytes (type) + 4 bytes (deactivation_slot) + 4 bytes (last_extended_slot) + 1 byte (last_extended_slot_start_index) + 1 byte (authority) + padding
|
||||
// Then addresses start at offset 56, each address is 32 bytes
|
||||
// First 4 bytes indicate if initialized (should be 1 or 2)
|
||||
|
||||
|
||||
if account.data.len() < 56 {
|
||||
return Err(anyhow::anyhow!("Address lookup table account data too short"));
|
||||
}
|
||||
|
||||
|
||||
// Read number of addresses (stored at offset 20 as u32, but we need to scan the bitmap)
|
||||
// Actually simpler: addresses start at offset 56, count from bitmap at offset 8-20
|
||||
let mut addresses = Vec::new();
|
||||
@@ -30,10 +30,8 @@ pub async fn fetch_address_lookup_table_account(
|
||||
}
|
||||
offset += 32;
|
||||
}
|
||||
|
||||
let address_lookup_table_account = AddressLookupTableAccount {
|
||||
key: *lookup_table_address,
|
||||
addresses,
|
||||
};
|
||||
|
||||
let address_lookup_table_account =
|
||||
AddressLookupTableAccount { key: *lookup_table_address, addresses };
|
||||
Ok(address_lookup_table_account)
|
||||
}
|
||||
|
||||
+66
-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则直接移出,否则克隆
|
||||
@@ -154,6 +180,8 @@ pub enum PdaCacheKey {
|
||||
PumpFunUserVolume(Pubkey),
|
||||
PumpFunBondingCurve(Pubkey),
|
||||
PumpFunBondingCurveV2(Pubkey),
|
||||
/// Pump-fees program `sharing-config` PDA for a mint (`feeSharingConfigPda`).
|
||||
PumpFunFeeSharingConfig(Pubkey),
|
||||
PumpFunCreatorVault(Pubkey),
|
||||
BonkPool(Pubkey, Pubkey),
|
||||
BonkVault(Pubkey, Pubkey),
|
||||
@@ -181,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
|
||||
@@ -263,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,
|
||||
@@ -274,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
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ mod tests {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
let elapsed = fast_elapsed_nanos(start);
|
||||
|
||||
// 应该大约是 10ms = 10,000,000 纳秒
|
||||
assert!(elapsed >= 9_000_000 && elapsed <= 12_000_000);
|
||||
// Scheduler jitter can exceed the sleep duration; the fast timer must not under-report.
|
||||
assert!(elapsed >= 9_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -151,7 +151,7 @@ mod tests {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
let elapsed_ms = sw.elapsed_millis();
|
||||
|
||||
assert!(elapsed_ms >= 9 && elapsed_ms <= 12);
|
||||
assert!(elapsed_ms >= 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+179
-10
@@ -1,6 +1,6 @@
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use arc_swap::ArcSwap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -14,6 +14,15 @@ impl GasFeeStrategyType {
|
||||
pub fn values() -> Vec<Self> {
|
||||
vec![Self::Normal, Self::LowTipHighCuPrice, Self::HighTipLowCuPrice]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => "Normal",
|
||||
Self::LowTipHighCuPrice => "LowTipHighCuPrice",
|
||||
Self::HighTipLowCuPrice => "HighTipLowCuPrice",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -84,6 +93,33 @@ impl GasFeeStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
/// 设置 Default/RPC 的优先费-only 策略。Default 没有 relay tip account,
|
||||
/// 但仍应携带 ComputeBudget 优先费。
|
||||
pub fn set_default_rpc_fee_strategy(
|
||||
&self,
|
||||
buy_cu_limit: u32,
|
||||
sell_cu_limit: u32,
|
||||
buy_cu_price: u64,
|
||||
sell_cu_price: u64,
|
||||
) {
|
||||
self.set(
|
||||
SwqosType::Default,
|
||||
TradeType::Buy,
|
||||
GasFeeStrategyType::Normal,
|
||||
buy_cu_limit,
|
||||
buy_cu_price,
|
||||
0.0,
|
||||
);
|
||||
self.set(
|
||||
SwqosType::Default,
|
||||
TradeType::Sell,
|
||||
GasFeeStrategyType::Normal,
|
||||
sell_cu_limit,
|
||||
sell_cu_price,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 为多个服务类型添加高低费率策略,会移除(SwqosType,TradeType)的默认策略。
|
||||
/// Add high-low fee strategies for multiple service types, Will remove the default strategy of (SwqosType,TradeType)
|
||||
pub fn set_high_low_fee_strategies(
|
||||
@@ -271,7 +307,7 @@ impl GasFeeStrategy {
|
||||
) -> Vec<(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)> {
|
||||
let strategies = self.strategies.load();
|
||||
let mut result = Vec::new();
|
||||
let mut swqos_types = std::collections::HashSet::new();
|
||||
let mut swqos_types = HashSet::new();
|
||||
for (swqos_type, t_type, _) in strategies.keys() {
|
||||
if *t_type == trade_type {
|
||||
swqos_types.insert(*swqos_type);
|
||||
@@ -298,10 +334,17 @@ impl GasFeeStrategy {
|
||||
/// 动态更新买入小费(保持其他参数不变)
|
||||
/// Dynamically update buy tip (keep other parameters unchanged)
|
||||
pub fn update_buy_tip(&self, buy_tip: f64) {
|
||||
self.update_buy_tip_for_strategy(GasFeeStrategyType::Normal, buy_tip);
|
||||
}
|
||||
|
||||
/// 动态更新指定买入策略的小费(保持其他参数不变)。
|
||||
/// Dynamic updates should generally target Normal only; updating all strategies would
|
||||
/// collapse the low-tip/high-tip dual-lane spread into the same tip.
|
||||
pub fn update_buy_tip_for_strategy(&self, strategy_type: GasFeeStrategyType, buy_tip: f64) {
|
||||
self.strategies.rcu(|current_map| {
|
||||
let mut new_map = (**current_map).clone();
|
||||
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Buy {
|
||||
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Buy && *s_type == strategy_type {
|
||||
value.tip = buy_tip;
|
||||
}
|
||||
}
|
||||
@@ -312,10 +355,15 @@ impl GasFeeStrategy {
|
||||
/// 动态更新卖出小费(保持其他参数不变)
|
||||
/// Dynamically update sell tip (keep other parameters unchanged)
|
||||
pub fn update_sell_tip(&self, sell_tip: f64) {
|
||||
self.update_sell_tip_for_strategy(GasFeeStrategyType::Normal, sell_tip);
|
||||
}
|
||||
|
||||
/// 动态更新指定卖出策略的小费(保持其他参数不变)。
|
||||
pub fn update_sell_tip_for_strategy(&self, strategy_type: GasFeeStrategyType, sell_tip: f64) {
|
||||
self.strategies.rcu(|current_map| {
|
||||
let mut new_map = (**current_map).clone();
|
||||
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Sell {
|
||||
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Sell && *s_type == strategy_type {
|
||||
value.tip = sell_tip;
|
||||
}
|
||||
}
|
||||
@@ -326,10 +374,19 @@ impl GasFeeStrategy {
|
||||
/// 动态更新买入优先费(保持其他参数不变)
|
||||
/// Dynamically update buy compute unit price (keep other parameters unchanged)
|
||||
pub fn update_buy_cu_price(&self, buy_cu_price: u64) {
|
||||
self.update_buy_cu_price_for_strategy(GasFeeStrategyType::Normal, buy_cu_price);
|
||||
}
|
||||
|
||||
/// 动态更新指定买入策略的优先费(保持其他参数不变)。
|
||||
pub fn update_buy_cu_price_for_strategy(
|
||||
&self,
|
||||
strategy_type: GasFeeStrategyType,
|
||||
buy_cu_price: u64,
|
||||
) {
|
||||
self.strategies.rcu(|current_map| {
|
||||
let mut new_map = (**current_map).clone();
|
||||
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Buy {
|
||||
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Buy && *s_type == strategy_type {
|
||||
value.cu_price = buy_cu_price;
|
||||
}
|
||||
}
|
||||
@@ -340,10 +397,19 @@ impl GasFeeStrategy {
|
||||
/// 动态更新卖出优先费(保持其他参数不变)
|
||||
/// Dynamically update sell compute unit price (keep other parameters unchanged)
|
||||
pub fn update_sell_cu_price(&self, sell_cu_price: u64) {
|
||||
self.update_sell_cu_price_for_strategy(GasFeeStrategyType::Normal, sell_cu_price);
|
||||
}
|
||||
|
||||
/// 动态更新指定卖出策略的优先费(保持其他参数不变)。
|
||||
pub fn update_sell_cu_price_for_strategy(
|
||||
&self,
|
||||
strategy_type: GasFeeStrategyType,
|
||||
sell_cu_price: u64,
|
||||
) {
|
||||
self.strategies.rcu(|current_map| {
|
||||
let mut new_map = (**current_map).clone();
|
||||
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Sell {
|
||||
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
|
||||
if *trade_type == TradeType::Sell && *s_type == strategy_type {
|
||||
value.cu_price = sell_cu_price;
|
||||
}
|
||||
}
|
||||
@@ -365,3 +431,106 @@ impl GasFeeStrategy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn find_strategy(
|
||||
strategies: &[(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)],
|
||||
swqos_type: SwqosType,
|
||||
strategy_type: GasFeeStrategyType,
|
||||
) -> GasFeeStrategyValue {
|
||||
strategies
|
||||
.iter()
|
||||
.find(|(s, t, _)| *s == swqos_type && *t == strategy_type)
|
||||
.map(|(_, _, v)| *v)
|
||||
.expect("strategy exists")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_low_fee_strategy_expands_two_lanes_per_swqos() {
|
||||
let strategy = GasFeeStrategy::new();
|
||||
|
||||
strategy.set_high_low_fee_strategies(
|
||||
&[SwqosType::Jito, SwqosType::Helius],
|
||||
TradeType::Buy,
|
||||
100_000,
|
||||
180_000,
|
||||
400_000,
|
||||
0.002,
|
||||
0.005,
|
||||
);
|
||||
|
||||
let strategies = strategy.get_strategies(TradeType::Buy);
|
||||
assert_eq!(strategies.len(), 4);
|
||||
|
||||
for swqos_type in [SwqosType::Jito, SwqosType::Helius] {
|
||||
let low_tip_high_cu =
|
||||
find_strategy(&strategies, swqos_type, GasFeeStrategyType::LowTipHighCuPrice);
|
||||
assert_eq!(low_tip_high_cu.cu_limit, 100_000);
|
||||
assert_eq!(low_tip_high_cu.cu_price, 400_000);
|
||||
assert_eq!(low_tip_high_cu.tip, 0.002);
|
||||
|
||||
let high_tip_low_cu =
|
||||
find_strategy(&strategies, swqos_type, GasFeeStrategyType::HighTipLowCuPrice);
|
||||
assert_eq!(high_tip_low_cu.cu_limit, 100_000);
|
||||
assert_eq!(high_tip_low_cu.cu_price, 180_000);
|
||||
assert_eq!(high_tip_low_cu.tip, 0.005);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_updates_do_not_collapse_dual_lane_fees() {
|
||||
let strategy = GasFeeStrategy::new();
|
||||
|
||||
strategy.set_high_low_fee_strategy(
|
||||
SwqosType::Jito,
|
||||
TradeType::Buy,
|
||||
100_000,
|
||||
180_000,
|
||||
400_000,
|
||||
0.002,
|
||||
0.005,
|
||||
);
|
||||
|
||||
strategy.update_buy_tip(0.009);
|
||||
strategy.update_buy_cu_price(999_999);
|
||||
|
||||
let strategies = strategy.get_strategies(TradeType::Buy);
|
||||
let low_tip_high_cu =
|
||||
find_strategy(&strategies, SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice);
|
||||
let high_tip_low_cu =
|
||||
find_strategy(&strategies, SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice);
|
||||
|
||||
assert_eq!(low_tip_high_cu.cu_price, 400_000);
|
||||
assert_eq!(low_tip_high_cu.tip, 0.002);
|
||||
assert_eq!(high_tip_low_cu.cu_price, 180_000);
|
||||
assert_eq!(high_tip_low_cu.tip, 0.005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_rpc_strategy_uses_priority_fee_without_tip() {
|
||||
let strategy = GasFeeStrategy::new();
|
||||
|
||||
strategy.set_default_rpc_fee_strategy(100_000, 90_000, 700_000, 800_000);
|
||||
|
||||
let buy = find_strategy(
|
||||
&strategy.get_strategies(TradeType::Buy),
|
||||
SwqosType::Default,
|
||||
GasFeeStrategyType::Normal,
|
||||
);
|
||||
assert_eq!(buy.cu_limit, 100_000);
|
||||
assert_eq!(buy.cu_price, 700_000);
|
||||
assert_eq!(buy.tip, 0.0);
|
||||
|
||||
let sell = find_strategy(
|
||||
&strategy.get_strategies(TradeType::Sell),
|
||||
SwqosType::Default,
|
||||
GasFeeStrategyType::Normal,
|
||||
);
|
||||
assert_eq!(sell.cu_limit, 90_000);
|
||||
assert_eq!(sell.cu_price, 800_000);
|
||||
assert_eq!(sell.tip, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-19
@@ -34,10 +34,7 @@ fn extract_swqos_error_message(s: &str) -> String {
|
||||
// Try parse as JSON only when input looks like JSON (avoid parsing long non-JSON strings)
|
||||
if s.starts_with('{') {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(s) {
|
||||
let obj = v
|
||||
.get("error")
|
||||
.and_then(|e| e.as_object())
|
||||
.or_else(|| v.as_object());
|
||||
let obj = v.get("error").and_then(|e| e.as_object()).or_else(|| v.as_object());
|
||||
if let Some(o) = obj {
|
||||
if let Some(m) = o.get("message").and_then(|x| x.as_str()) {
|
||||
return m.to_string();
|
||||
@@ -69,11 +66,7 @@ pub fn set_sdk_log_enabled(enabled: bool) {
|
||||
|
||||
/// Aligned log: ` [Soyas ] Buy submitted: 13.936 µs`. Call only when sdk_log_enabled().
|
||||
#[inline]
|
||||
pub fn log_swqos_submitted(
|
||||
provider: &str,
|
||||
trade_type: impl fmt::Display,
|
||||
elapsed: Duration,
|
||||
) {
|
||||
pub fn log_swqos_submitted(provider: &str, trade_type: impl fmt::Display, elapsed: Duration) {
|
||||
println!(
|
||||
" [{:width$}] {} submitted: {}",
|
||||
provider,
|
||||
@@ -91,7 +84,7 @@ pub fn print_sdk_timing_block(
|
||||
start_us: Option<i64>,
|
||||
build_end_us: Option<i64>,
|
||||
before_submit_us: Option<i64>,
|
||||
submit_timings: &[(crate::swqos::SwqosType, i64)],
|
||||
submit_timings: &[crate::common::SwqosSubmitTiming],
|
||||
confirm_us: Option<i64>,
|
||||
) {
|
||||
println!();
|
||||
@@ -119,13 +112,14 @@ pub fn print_sdk_timing_block(
|
||||
}
|
||||
if let Some(confirm_done_us) = confirm_us {
|
||||
let total_ms = (confirm_done_us - start_us) as f64 / 1000.0;
|
||||
for (swqos_type, submit_done_us) in submit_timings {
|
||||
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||
let confirmed_ms = (confirm_done_us - *submit_done_us).max(0) as f64 / 1000.0;
|
||||
for timing in submit_timings {
|
||||
let submit_ms = (timing.submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||
let confirmed_ms = (confirm_done_us - timing.submit_done_us).max(0) as f64 / 1000.0;
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||
timing.swqos_type.as_str(),
|
||||
dir,
|
||||
timing.strategy_type.as_str(),
|
||||
submit_ms,
|
||||
confirmed_ms,
|
||||
total_ms,
|
||||
@@ -133,12 +127,13 @@ pub fn print_sdk_timing_block(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (swqos_type, submit_done_us) in submit_timings {
|
||||
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||
for timing in submit_timings {
|
||||
let submit_ms = (timing.submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||
timing.swqos_type.as_str(),
|
||||
dir,
|
||||
timing.strategy_type.as_str(),
|
||||
submit_ms,
|
||||
submit_ms,
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
|
||||
+39
-35
@@ -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,39 +82,36 @@ 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 一致)
|
||||
// 否则创建的账户地址与 ata_like 不匹配,导致 initializeAccount3 失败
|
||||
let create_acc =
|
||||
system_instruction::create_account_with_seed(payer, &ata_like, payer, seed, rent, len, token_program);
|
||||
let create_acc = system_instruction::create_account_with_seed(
|
||||
payer,
|
||||
&ata_like,
|
||||
payer,
|
||||
&seed,
|
||||
rent,
|
||||
len,
|
||||
token_program,
|
||||
);
|
||||
|
||||
let init_acc = if is_2022_token {
|
||||
crate::common::spl_token_2022::initialize_account3(&token_program, &ata_like, mint, owner)?
|
||||
@@ -111,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)
|
||||
}
|
||||
|
||||
@@ -20,10 +20,7 @@ pub fn close_account(
|
||||
let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len());
|
||||
accounts.push(AccountMeta::new(*account_pubkey, false));
|
||||
accounts.push(AccountMeta::new(*destination_pubkey, false));
|
||||
accounts.push(AccountMeta::new_readonly(
|
||||
*owner_pubkey,
|
||||
signer_pubkeys.is_empty(),
|
||||
));
|
||||
accounts.push(AccountMeta::new_readonly(*owner_pubkey, signer_pubkeys.is_empty()));
|
||||
for signer_pubkey in signer_pubkeys.iter() {
|
||||
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
|
||||
}
|
||||
|
||||
+24
-2
@@ -1,4 +1,5 @@
|
||||
use crate::swqos::SwqosConfig;
|
||||
use crate::common::GasFeeStrategyType;
|
||||
use crate::swqos::{SwqosConfig, SwqosType};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
@@ -75,6 +76,13 @@ impl PartialEq for InfrastructureConfig {
|
||||
|
||||
impl Eq for InfrastructureConfig {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SwqosSubmitTiming {
|
||||
pub swqos_type: SwqosType,
|
||||
pub strategy_type: GasFeeStrategyType,
|
||||
pub submit_done_us: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TradeConfig {
|
||||
pub rpc_url: String,
|
||||
@@ -95,6 +103,9 @@ 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 {
|
||||
@@ -109,7 +120,7 @@ impl TradeConfig {
|
||||
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// let config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
/// .mev_protection(true)
|
||||
/// .check_min_tip(true)
|
||||
@@ -149,6 +160,7 @@ pub struct TradeConfigBuilder {
|
||||
check_min_tip: bool,
|
||||
swqos_cores_from_end: bool,
|
||||
mev_protection: bool,
|
||||
use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl TradeConfigBuilder {
|
||||
@@ -163,6 +175,7 @@ impl TradeConfigBuilder {
|
||||
check_min_tip: false,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +221,14 @@ 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 {
|
||||
@@ -220,6 +241,7 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,3 +57,9 @@ pub const RENT_META: solana_sdk::instruction::AccountMeta =
|
||||
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
+8
-10
@@ -212,7 +212,7 @@ pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 10] = [
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://ams.0slot.trade",
|
||||
"http://ams.0slot.trade", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams(相对 de2 等)
|
||||
"http://la.0slot.trade", // SLC: no UT PoP; nearest US-West published host
|
||||
"http://la.0slot.trade", // SLC: no UT PoP; nearest US-West published host
|
||||
"http://jp.0slot.trade",
|
||||
"http://jp.0slot.trade", // SG: 无本地 PoP;已公布 APAC 仅 jp,为表中离新加坡最近的大圆距离
|
||||
"http://ams.0slot.trade", // London: 无 UK 专用;已公布 EU 点中 ams 距伦敦最近之一
|
||||
@@ -252,11 +252,11 @@ pub const SWQOS_ENDPOINTS_NODE1: [&str; 10] = [
|
||||
"http://fra.node1.me",
|
||||
"http://ams.node1.me",
|
||||
"http://lon.node1.me", // Dublin: 已公布中英爱区域用 lon(地理上近爱尔兰)
|
||||
"http://ny.node1.me", // SLC: 已公布美国仅 ny;美西无 PoP,受可用区限制复用美东
|
||||
"http://ny.node1.me", // SLC: 已公布美国仅 ny;美西无 PoP,受可用区限制复用美东
|
||||
"http://tk.node1.me",
|
||||
"http://tk.node1.me", // SG: 已公布 APAC 仅 tk;地理上为表中离 SG 最近
|
||||
"http://lon.node1.me",
|
||||
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
|
||||
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
|
||||
"http://fra.node1.me", // Default: 非地理区域;与 QUIC 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
@@ -359,7 +359,7 @@ pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 10] = [
|
||||
"fr.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // Dublin: 同 HTTP
|
||||
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
|
||||
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
|
||||
"jp.gateway.astralane.io:7000",
|
||||
"sg.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // London: 同 HTTP
|
||||
@@ -503,12 +503,10 @@ mod tests {
|
||||
#[test]
|
||||
fn node1_http_host_matches_quic_without_port() {
|
||||
for i in 0..10 {
|
||||
let http_host = SWQOS_ENDPOINTS_NODE1[i]
|
||||
.strip_prefix("http://")
|
||||
.expect("NODE1 HTTP URL");
|
||||
let quic_host = SWQOS_ENDPOINTS_NODE1_QUIC[i]
|
||||
.strip_suffix(":16666")
|
||||
.expect("NODE1 QUIC endpoint");
|
||||
let http_host =
|
||||
SWQOS_ENDPOINTS_NODE1[i].strip_prefix("http://").expect("NODE1 HTTP URL");
|
||||
let quic_host =
|
||||
SWQOS_ENDPOINTS_NODE1_QUIC[i].strip_suffix(":16666").expect("NODE1 QUIC endpoint");
|
||||
assert_eq!(http_host, quic_host, "Node1 HTTP vs QUIC host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
+171
-45
@@ -1,8 +1,14 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::bonk::{
|
||||
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
|
||||
SELL_EXECT_IN_DISCRIMINATOR,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::bonk::{
|
||||
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
|
||||
BUY_EXECT_OUT_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR, SELL_EXECT_OUT_DISCRIMINATOR,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{BonkParams, SwapParams},
|
||||
@@ -55,6 +61,11 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
accounts::GLOBAL_CONFIG_META
|
||||
};
|
||||
|
||||
let quote_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
};
|
||||
let quote_token_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
@@ -88,11 +99,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
let user_quote_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -103,11 +110,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
protocol_params.base_vault
|
||||
};
|
||||
let quote_vault_account = if protocol_params.quote_vault == Pubkey::default() {
|
||||
if usd1_pool {
|
||||
get_vault_pda(&pool_state, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
|
||||
} else {
|
||||
get_vault_pda(&pool_state, &crate::constants::WSOL_TOKEN_ACCOUNT).unwrap()
|
||||
}
|
||||
get_vault_pda(&pool_state, "e_mint).unwrap()
|
||||
} else {
|
||||
protocol_params.quote_vault
|
||||
};
|
||||
@@ -117,9 +120,15 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata && !usd1_pool {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
if params.create_input_mint_ata {
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
@@ -135,12 +144,18 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
}
|
||||
|
||||
let mut data = [0u8; 32];
|
||||
data[..8].copy_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&BUY_EXECT_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_out.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_in.to_le_bytes());
|
||||
} else {
|
||||
data[..8].copy_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
data[24..32].copy_from_slice(&share_fee_rate.to_le_bytes());
|
||||
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
let accounts: [AccountMeta; 15] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
global_config, // Global Config (readonly)
|
||||
@@ -156,15 +171,12 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
crate::constants::TOKEN_PROGRAM_META, // Quote Token Program (readonly)
|
||||
accounts::EVENT_AUTHORITY_META, // Event Authority (readonly)
|
||||
accounts::BONK_META, // Program (readonly)
|
||||
crate::constants::SYSTEM_PROGRAM_META, // System Program (readonly)
|
||||
AccountMeta::new(protocol_params.platform_associated_account, false), // Platform Associated Account
|
||||
AccountMeta::new(protocol_params.creator_associated_account, false), // Creator Associated Account
|
||||
];
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::BONK, &data, accounts.to_vec()));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -203,6 +215,11 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
accounts::GLOBAL_CONFIG_META
|
||||
};
|
||||
|
||||
let quote_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
};
|
||||
let quote_token_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
@@ -235,11 +252,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
let user_quote_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -250,11 +263,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
protocol_params.base_vault
|
||||
};
|
||||
let quote_vault_account = if protocol_params.quote_vault == Pubkey::default() {
|
||||
if usd1_pool {
|
||||
get_vault_pda(&pool_state, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
|
||||
} else {
|
||||
get_vault_pda(&pool_state, &crate::constants::WSOL_TOKEN_ACCOUNT).unwrap()
|
||||
}
|
||||
get_vault_pda(&pool_state, "e_mint).unwrap()
|
||||
} else {
|
||||
protocol_params.quote_vault
|
||||
};
|
||||
@@ -262,19 +271,31 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.close_output_mint_ata && !usd1_pool {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
if params.create_output_mint_ata {
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
let mut data = [0u8; 32];
|
||||
data[..8].copy_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&SELL_EXECT_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_out.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount.to_le_bytes());
|
||||
} else {
|
||||
data[..8].copy_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
data[24..32].copy_from_slice(&share_fee_rate.to_le_bytes());
|
||||
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
let accounts: [AccountMeta; 15] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
global_config, // Global Config (readonly)
|
||||
@@ -290,15 +311,12 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
crate::constants::TOKEN_PROGRAM_META, // Quote Token Program (readonly)
|
||||
accounts::EVENT_AUTHORITY_META, // Event Authority (readonly)
|
||||
accounts::BONK_META, // Program (readonly)
|
||||
crate::constants::SYSTEM_PROGRAM_META, // System Program (readonly)
|
||||
AccountMeta::new(protocol_params.platform_associated_account, false), // Platform Associated Account
|
||||
AccountMeta::new(protocol_params.creator_associated_account, false), // Creator Associated Account
|
||||
];
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::BONK, &data, accounts.to_vec()));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
@@ -313,3 +331,111 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{common::GasFeeStrategy, swqos::TradeType, trading::core::params::DexParamEnum};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn bonk_params() -> BonkParams {
|
||||
BonkParams {
|
||||
mint_token_program: crate::constants::TOKEN_PROGRAM,
|
||||
platform_config: pk(8),
|
||||
platform_associated_account: pk(9),
|
||||
creator_associated_account: pk(10),
|
||||
global_config: accounts::GLOBAL_CONFIG,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_params(trade_type: TradeType) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type,
|
||||
input_mint: pk(3),
|
||||
input_token_program: None,
|
||||
output_mint: pk(3),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::Bonk(bonk_params()),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount: Some(42),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bonk_buy_uses_exact_out_when_fixed_output_is_set() {
|
||||
let instructions = BonkInstructionBuilder
|
||||
.build_buy_instructions(&swap_params(TradeType::Buy))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 15);
|
||||
assert_eq!(ix.accounts[14].pubkey, accounts::BONK);
|
||||
assert_eq!(&ix.data[..8], BUY_EXECT_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bonk_sell_uses_exact_out_when_fixed_output_is_set() {
|
||||
let instructions = BonkInstructionBuilder
|
||||
.build_sell_instructions(&swap_params(TradeType::Sell))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 15);
|
||||
assert_eq!(ix.accounts[14].pubkey, accounts::BONK);
|
||||
assert_eq!(&ix.data[..8], SELL_EXECT_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bonk_usd1_buy_create_input_builds_usd1_ata_not_wsol_wrap() {
|
||||
let mut params = swap_params(TradeType::Buy);
|
||||
if let DexParamEnum::Bonk(protocol_params) = &mut params.protocol_params {
|
||||
protocol_params.global_config = accounts::USD1_GLOBAL_CONFIG;
|
||||
}
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions = BonkInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USD1_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use crate::{
|
||||
instruction::utils::meteora_damm_v2::{accounts, get_event_authority_pda, SWAP_DISCRIMINATOR},
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::meteora_damm_v2::{
|
||||
accounts, get_event_authority_pda, SWAP2_DISCRIMINATOR, SWAP_MODE_EXACT_IN,
|
||||
SWAP_MODE_EXACT_OUT, SWAP_MODE_PARTIAL_FILL,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{MeteoraDammV2Params, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
@@ -42,32 +51,43 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
// ========================================
|
||||
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let input_mint =
|
||||
if is_a_in { protocol_params.token_a_mint } else { protocol_params.token_b_mint };
|
||||
let input_token_program =
|
||||
if is_a_in { protocol_params.token_a_program } else { protocol_params.token_b_program };
|
||||
let output_mint =
|
||||
if is_a_in { protocol_params.token_b_mint } else { protocol_params.token_a_mint };
|
||||
let output_token_program =
|
||||
if is_a_in { protocol_params.token_b_program } else { protocol_params.token_a_program };
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
|
||||
let (amount_0, amount_1) = match protocol_params.swap_mode {
|
||||
SWAP_MODE_EXACT_OUT => {
|
||||
let amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 exact-out swap2")
|
||||
})?;
|
||||
(amount_out, amount_in)
|
||||
}
|
||||
SWAP_MODE_EXACT_IN | SWAP_MODE_PARTIAL_FILL => {
|
||||
let minimum_amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap2 min output")
|
||||
})?;
|
||||
(amount_in, minimum_amount_out)
|
||||
}
|
||||
mode => return Err(anyhow!("Unsupported MeteoraDammV2 swap_mode {}", mode)),
|
||||
};
|
||||
|
||||
let input_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_a_program
|
||||
} else {
|
||||
&protocol_params.token_b_program
|
||||
},
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let output_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
@@ -77,28 +97,32 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 14] = [
|
||||
let mut account_metas = Vec::with_capacity(
|
||||
13 + usize::from(protocol_params.referral_token_account.is_some())
|
||||
+ usize::from(protocol_params.include_rate_limiter_sysvar),
|
||||
);
|
||||
account_metas.extend([
|
||||
accounts::AUTHORITY_META, // Pool Authority (readonly)
|
||||
AccountMeta::new(protocol_params.pool, false), // Pool
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
@@ -110,25 +134,32 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Transfer Authority
|
||||
AccountMeta::new_readonly(protocol_params.token_a_program, false), // Token Program (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.token_b_program, false), // Token Program (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Referral Token Account (readonly)
|
||||
]);
|
||||
if let Some(referral_token_account) = protocol_params.referral_token_account {
|
||||
account_metas.push(AccountMeta::new(referral_token_account, false));
|
||||
}
|
||||
account_metas.extend([
|
||||
AccountMeta::new_readonly(get_event_authority_pda(), false), // Event Authority (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Program (readonly)
|
||||
];
|
||||
]);
|
||||
if protocol_params.include_rate_limiter_sysvar {
|
||||
account_metas.push(accounts::SYSVAR_INSTRUCTIONS_META);
|
||||
}
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let mut data = [0u8; 25];
|
||||
data[..8].copy_from_slice(&SWAP2_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_0.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_1.to_le_bytes());
|
||||
data[24] = protocol_params.swap_mode;
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::METEORA_DAMM_V2,
|
||||
&data,
|
||||
accounts.to_vec(),
|
||||
account_metas,
|
||||
));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &input_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -161,45 +192,67 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
// ========================================
|
||||
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
|
||||
let input_mint =
|
||||
if is_a_in { protocol_params.token_a_mint } else { protocol_params.token_b_mint };
|
||||
let input_token_program =
|
||||
if is_a_in { protocol_params.token_a_program } else { protocol_params.token_b_program };
|
||||
let output_mint =
|
||||
if is_a_in { protocol_params.token_b_mint } else { protocol_params.token_a_mint };
|
||||
let output_token_program =
|
||||
if is_a_in { protocol_params.token_b_program } else { protocol_params.token_a_program };
|
||||
let amount_in = params.input_amount.unwrap_or(0);
|
||||
let (amount_0, amount_1) = match protocol_params.swap_mode {
|
||||
SWAP_MODE_EXACT_OUT => {
|
||||
let amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 exact-out swap2")
|
||||
})?;
|
||||
(amount_out, amount_in)
|
||||
}
|
||||
SWAP_MODE_EXACT_IN | SWAP_MODE_PARTIAL_FILL => {
|
||||
let minimum_amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap2 min output")
|
||||
})?;
|
||||
(amount_in, minimum_amount_out)
|
||||
}
|
||||
mode => return Err(anyhow!("Unsupported MeteoraDammV2 swap_mode {}", mode)),
|
||||
};
|
||||
|
||||
let input_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_a_program
|
||||
} else {
|
||||
&protocol_params.token_b_program
|
||||
},
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let output_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 14] = [
|
||||
let mut account_metas = Vec::with_capacity(
|
||||
13 + usize::from(protocol_params.referral_token_account.is_some())
|
||||
+ usize::from(protocol_params.include_rate_limiter_sysvar),
|
||||
);
|
||||
account_metas.extend([
|
||||
accounts::AUTHORITY_META, // Pool Authority (readonly)
|
||||
AccountMeta::new(protocol_params.pool, false), // Pool
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
@@ -211,32 +264,36 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Transfer Authority
|
||||
AccountMeta::new_readonly(protocol_params.token_a_program, false), // Token Program (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.token_b_program, false), // Token Program (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Referral Token Account (readonly)
|
||||
]);
|
||||
if let Some(referral_token_account) = protocol_params.referral_token_account {
|
||||
account_metas.push(AccountMeta::new(referral_token_account, false));
|
||||
}
|
||||
account_metas.extend([
|
||||
AccountMeta::new_readonly(get_event_authority_pda(), false), // Event Authority (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Program (readonly)
|
||||
];
|
||||
]);
|
||||
if protocol_params.include_rate_limiter_sysvar {
|
||||
account_metas.push(accounts::SYSVAR_INSTRUCTIONS_META);
|
||||
}
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or_default().to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let mut data = [0u8; 25];
|
||||
data[..8].copy_from_slice(&SWAP2_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_0.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_1.to_le_bytes());
|
||||
data[24] = protocol_params.swap_mode;
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::METEORA_DAMM_V2,
|
||||
&data,
|
||||
accounts.to_vec(),
|
||||
account_metas,
|
||||
));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &output_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
if is_a_in {
|
||||
&protocol_params.token_a_program
|
||||
} else {
|
||||
&protocol_params.token_b_program
|
||||
},
|
||||
&input_token_program,
|
||||
&input_token_account,
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -247,3 +304,162 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn meteora_params(referral: Option<Pubkey>) -> MeteoraDammV2Params {
|
||||
let params = MeteoraDammV2Params::new(
|
||||
pk(1),
|
||||
pk(2),
|
||||
pk(3),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(4),
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
match referral {
|
||||
Some(account) => params.with_referral_token_account(account),
|
||||
None => params,
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_params(protocol_params: MeteoraDammV2Params) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
input_token_program: None,
|
||||
output_mint: pk(4),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::MeteoraDammV2(protocol_params),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount: Some(1),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_omits_optional_referral_account() {
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(meteora_params(None)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 13);
|
||||
assert_eq!(ix.accounts[11].pubkey, get_event_authority_pda());
|
||||
assert_eq!(ix.accounts[12].pubkey, accounts::METEORA_DAMM_V2);
|
||||
assert_eq!(&ix.data[..8], SWAP2_DISCRIMINATOR);
|
||||
assert_eq!(ix.data[24], SWAP_MODE_PARTIAL_FILL);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_includes_writable_referral_account_when_set() {
|
||||
let referral = pk(9);
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(meteora_params(Some(referral))))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 14);
|
||||
assert_eq!(ix.accounts[11].pubkey, referral);
|
||||
assert!(ix.accounts[11].is_writable);
|
||||
assert_eq!(ix.accounts[12].pubkey, get_event_authority_pda());
|
||||
assert_eq!(ix.accounts[13].pubkey, accounts::METEORA_DAMM_V2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_includes_sysvar_only_when_rate_limiter_is_set() {
|
||||
let protocol_params = meteora_params(None).with_rate_limiter_sysvar(true);
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(protocol_params))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 14);
|
||||
assert_eq!(ix.accounts[11].pubkey, get_event_authority_pda());
|
||||
assert_eq!(ix.accounts[12].pubkey, accounts::METEORA_DAMM_V2);
|
||||
assert_eq!(ix.accounts[13].pubkey, accounts::SYSVAR_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_swap2_exact_out_uses_amount_out_then_max_input() {
|
||||
let protocol_params = meteora_params(None).with_swap_mode(SWAP_MODE_EXACT_OUT);
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(protocol_params))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], SWAP2_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 1);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
assert_eq!(ix.data[24], SWAP_MODE_EXACT_OUT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_sol_buy_uses_pool_wsol_mint_for_user_input_account() {
|
||||
let mut params = swap_params(meteora_params(None));
|
||||
params.input_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
MeteoraDammV2InstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_wsol_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let wrong_sol_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
&crate::constants::SOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
assert_eq!(ix.accounts[2].pubkey, expected_wsol_ata);
|
||||
assert_ne!(ix.accounts[2].pubkey, wrong_sol_ata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
pub mod bonk;
|
||||
pub mod meteora_damm_v2;
|
||||
pub mod pumpfun;
|
||||
pub(crate) mod pumpfun_ix_data;
|
||||
pub mod pumpswap;
|
||||
pub(crate) mod pumpswap_ix_data;
|
||||
pub mod raydium_amm_v4;
|
||||
pub mod raydium_cpmm;
|
||||
pub(crate) mod token_account_setup;
|
||||
pub mod utils;
|
||||
|
||||
Executable → Regular
+917
-276
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` 与 **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`**
|
||||
//! 的 instruction data 栈上编码(热路径零堆分配)。
|
||||
//!
|
||||
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 是单字段
|
||||
//! struct(TypeScript 传 `[true]`),在 ix 参数中为 1 字节 bool,共 25 字节 ix data。
|
||||
//! `*_v2` 指令无 `track_volume` 字节(见 [pump-public-docs](https://github.com/pump-fun/pump-public-docs))。
|
||||
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
||||
BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub 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
|
||||
}
|
||||
|
||||
#[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(
|
||||
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
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_sell_v2_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&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
|
||||
}
|
||||
+249
-100
@@ -1,17 +1,24 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
|
||||
get_user_volume_accumulator_quote_ata, get_user_volume_accumulator_wsol_ata,
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
instruction::pumpswap_ix_data::{
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
|
||||
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
trading::{
|
||||
common::wsol_manager,
|
||||
core::{
|
||||
params::{PumpSwapParams, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
|
||||
get_user_volume_accumulator_wsol_ata,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{PumpSwapParams, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
utils::calc::pumpswap::{buy_quote_input_internal, sell_base_input_internal},
|
||||
};
|
||||
@@ -48,7 +55,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
|
||||
let create_wsol_ata = params.create_input_mint_ata;
|
||||
let create_input_ata = params.create_input_mint_ata;
|
||||
let close_wsol_ata = params.close_input_mint_ata;
|
||||
let base_token_program = protocol_params.base_token_program;
|
||||
let quote_token_program = protocol_params.quote_token_program;
|
||||
@@ -72,13 +79,21 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let input_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
|
||||
let input_stable_token_program =
|
||||
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
|
||||
let output_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
|
||||
let output_trade_token_program =
|
||||
if quote_is_wsol_or_usdc { base_token_program } else { quote_token_program };
|
||||
let mut creator = Pubkey::default();
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
(output_amount, params.input_amount.unwrap_or(0))
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal(
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
@@ -104,10 +119,6 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
(result.min_quote, params.input_amount.unwrap_or(0))
|
||||
};
|
||||
|
||||
if params.fixed_output_amount.is_some() {
|
||||
token_amount = params.fixed_output_amount.unwrap();
|
||||
}
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -128,43 +139,43 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
(accounts::FEE_RECIPIENT, accounts::FEE_RECIPIENT_META)
|
||||
};
|
||||
let fee_recipient_ata = if is_mayhem_mode {
|
||||
fee_recipient_ata(fee_recipient, crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
fee_recipient_ata(fee_recipient, quote_mint)
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if create_wsol_ata {
|
||||
if create_input_ata {
|
||||
// Determine wrap amount based on instruction type:
|
||||
// - buy_exact_quote_in: program spends exactly input_amount, wrap input_amount
|
||||
// - buy: program may spend up to max_quote, wrap max_quote
|
||||
let wrap_amount = if quote_is_wsol_or_usdc
|
||||
&& params.use_exact_sol_amount.unwrap_or(true)
|
||||
{
|
||||
params.input_amount.unwrap_or(0)
|
||||
} else {
|
||||
sol_amount
|
||||
};
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), wrap_amount));
|
||||
let wrap_amount =
|
||||
if quote_is_wsol_or_usdc && params.use_exact_sol_amount.unwrap_or(true) {
|
||||
params.input_amount.unwrap_or(0)
|
||||
} else {
|
||||
sol_amount
|
||||
};
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_stable_mint,
|
||||
&input_stable_token_program,
|
||||
wrap_amount,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
if quote_is_wsol_or_usdc { &base_mint } else { "e_mint },
|
||||
if quote_is_wsol_or_usdc { &base_token_program } else { "e_token_program },
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_trade_mint,
|
||||
&output_trade_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,38 +232,43 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
false,
|
||||
));
|
||||
|
||||
// Create instruction data(buy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数)
|
||||
let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
|
||||
let data: Vec<u8> = if quote_is_wsol_or_usdc {
|
||||
let mut buf = [0u8; 26];
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = if params.fixed_output_amount.is_some() {
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
} else if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
buf[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
buf[24..26].copy_from_slice(&track_volume);
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
min_base_amount_out,
|
||||
track_volume,
|
||||
)
|
||||
} else {
|
||||
buf[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
buf[24..26].copy_from_slice(&track_volume);
|
||||
}
|
||||
buf.to_vec()
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
};
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else {
|
||||
let mut buf = [0u8; 24];
|
||||
buf[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
buf.to_vec()
|
||||
};
|
||||
|
||||
instructions.push(Instruction { program_id: accounts::AMM_PROGRAM, accounts, data });
|
||||
let ix_data = encode_pumpswap_sell_ix_data(sol_amount, token_amount);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
}
|
||||
if close_wsol_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_stable_mint,
|
||||
);
|
||||
}
|
||||
Ok(instructions)
|
||||
}
|
||||
@@ -276,7 +292,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let pool_quote_token_account = protocol_params.pool_quote_token_account;
|
||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
|
||||
let create_wsol_ata = params.create_output_mint_ata;
|
||||
let create_output_ata = params.create_output_mint_ata;
|
||||
let close_wsol_ata = params.close_output_mint_ata;
|
||||
let base_token_program = protocol_params.base_token_program;
|
||||
let quote_token_program = protocol_params.quote_token_program;
|
||||
@@ -302,13 +318,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let output_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
|
||||
let output_stable_token_program =
|
||||
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
|
||||
let mut creator = Pubkey::default();
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
(params.input_amount.unwrap(), output_amount)
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal(
|
||||
params.input_amount.unwrap(),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
@@ -334,22 +355,15 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
(result.max_quote, result.base)
|
||||
};
|
||||
|
||||
if params.fixed_output_amount.is_some() {
|
||||
sol_amount = params.fixed_output_amount.unwrap();
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
|
||||
let is_mayhem_mode = protocol_params.is_mayhem_mode;
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
(accounts::FEE_RECIPIENT, accounts::FEE_RECIPIENT_META)
|
||||
};
|
||||
let fee_recipient_ata = if is_mayhem_mode {
|
||||
fee_recipient_ata(fee_recipient, crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
fee_recipient_ata(fee_recipient, quote_mint)
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
@@ -369,10 +383,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if create_wsol_ata {
|
||||
instructions.extend(wsol_manager::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
if create_output_ata {
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_stable_mint,
|
||||
&output_stable_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
@@ -433,30 +453,21 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
false,
|
||||
));
|
||||
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
if quote_is_wsol_or_usdc {
|
||||
data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
// base_amount_in
|
||||
data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
// min_quote_amount_out
|
||||
data[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
|
||||
let ix_data = if quote_is_wsol_or_usdc {
|
||||
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
|
||||
} else {
|
||||
data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
// base_amount_out
|
||||
data[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// max_quote_amount_in
|
||||
data[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
}
|
||||
encode_pumpswap_buy_two_args(sol_amount, token_amount)
|
||||
};
|
||||
|
||||
instructions.push(Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts,
|
||||
data: data.to_vec(),
|
||||
});
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
|
||||
if close_wsol_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_stable_mint,
|
||||
);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
@@ -509,3 +520,141 @@ pub fn claim_cashback_pumpswap_instruction(
|
||||
accounts,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, PumpSwapParams, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn pumpswap_params() -> PumpSwapParams {
|
||||
PumpSwapParams::new(
|
||||
pk(1),
|
||||
pk(2),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
accounts::PROTOCOL_FEE_RECIPIENT,
|
||||
Pubkey::default(),
|
||||
false,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn swap_params(trade_type: TradeType, fixed_output_amount: Option<u64>) -> SwapParams {
|
||||
let (input_mint, output_mint) = if trade_type == TradeType::Sell {
|
||||
(pk(2), crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
(crate::constants::WSOL_TOKEN_ACCOUNT, pk(2))
|
||||
};
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type,
|
||||
input_mint,
|
||||
input_token_program: None,
|
||||
output_mint,
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::PumpSwap(pumpswap_params()),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount,
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: Some(true),
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_fixed_output_uses_buy_with_max_input_budget() {
|
||||
let instructions = PumpSwapInstructionBuilder
|
||||
.build_buy_instructions(&swap_params(TradeType::Buy, Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_fixed_output_uses_min_quote_directly() {
|
||||
let instructions = PumpSwapInstructionBuilder
|
||||
.build_sell_instructions(&swap_params(TradeType::Sell, Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_usdc_buy_create_input_builds_usdc_ata() {
|
||||
let mut params = swap_params(TradeType::Buy, Some(42));
|
||||
params.protocol_params = DexParamEnum::PumpSwap(PumpSwapParams::new(
|
||||
pk(1),
|
||||
pk(2),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
accounts::PROTOCOL_FEE_RECIPIENT,
|
||||
Pubkey::default(),
|
||||
false,
|
||||
0,
|
||||
));
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! PumpSwap AMM `buy` / `buy_exact_quote_in` / `sell` instruction **data**(栈数组、无 `Vec` 分配)。
|
||||
|
||||
use crate::instruction::utils::pumpswap::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_ix_data(
|
||||
base_amount_out: u64,
|
||||
max_quote_amount_in: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
spendable_quote_in: u64,
|
||||
min_base_amount_out: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_sell_ix_data(base_amount_in: u64, min_quote_amount_out: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_quote_amount_out.to_le_bytes());
|
||||
d
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::raydium_amm_v4::{accounts, SWAP_BASE_IN_DISCRIMINATOR},
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::raydium_amm_v4::{
|
||||
accounts, SWAP_BASE_IN_DISCRIMINATOR, SWAP_BASE_OUT_DISCRIMINATOR,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{RaydiumAmmV4Params, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
@@ -10,12 +18,38 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
/// Instruction builder for RaydiumCpmm protocol
|
||||
pub struct RaydiumAmmV4InstructionBuilder;
|
||||
|
||||
fn ensure_market_accounts(params: &RaydiumAmmV4Params) -> Result<()> {
|
||||
let required = [
|
||||
("amm_open_orders", params.amm_open_orders),
|
||||
("amm_target_orders", params.amm_target_orders),
|
||||
("serum_program", params.serum_program),
|
||||
("serum_market", params.serum_market),
|
||||
("serum_bids", params.serum_bids),
|
||||
("serum_asks", params.serum_asks),
|
||||
("serum_event_queue", params.serum_event_queue),
|
||||
("serum_coin_vault_account", params.serum_coin_vault_account),
|
||||
("serum_pc_vault_account", params.serum_pc_vault_account),
|
||||
("serum_vault_signer", params.serum_vault_signer),
|
||||
];
|
||||
|
||||
for (name, account) in required {
|
||||
if account == Pubkey::default() {
|
||||
return Err(anyhow!(
|
||||
"Raydium AMM v4 requires {}; use RaydiumAmmV4Params::from_amm_address_by_rpc or with_market_accounts",
|
||||
name
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
@@ -30,6 +64,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumAmmV4Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
|
||||
ensure_market_accounts(protocol_params)?;
|
||||
|
||||
let is_wsol = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
@@ -47,33 +82,22 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.coin_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let swap_result = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let minimum_amount_out = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => swap_result.min_amount_out,
|
||||
};
|
||||
let input_mint =
|
||||
if is_base_in { protocol_params.coin_mint } else { protocol_params.pc_mint };
|
||||
let output_mint =
|
||||
if is_base_in { protocol_params.pc_mint } else { protocol_params.coin_mint };
|
||||
|
||||
let user_source_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&input_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let user_destination_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -84,47 +108,66 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 17] = [
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
crate::constants::TOKEN_PROGRAM_META, // Token Program (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_open_orders, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_target_orders, false), // Amm Target Orders
|
||||
AccountMeta::new(protocol_params.token_coin, false), // Pool Coin Token Account
|
||||
AccountMeta::new(protocol_params.token_pc, false), // Pool Pc Token Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Pc Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Vault Signer
|
||||
AccountMeta::new_readonly(protocol_params.serum_program, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.serum_market, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.serum_bids, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.serum_asks, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.serum_event_queue, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.serum_coin_vault_account, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.serum_pc_vault_account, false), // Serum Pc Vault Account
|
||||
AccountMeta::new_readonly(protocol_params.serum_vault_signer, false), // Serum Vault Signer
|
||||
AccountMeta::new(user_source_token_account, false), // User Source Token Account
|
||||
AccountMeta::new(user_destination_token_account, false), // User Destination Token Account
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Source Owner
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 17];
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..1].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_AMM_V4,
|
||||
@@ -133,8 +176,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &input_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -149,6 +191,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumAmmV4Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
|
||||
ensure_market_accounts(protocol_params)?;
|
||||
|
||||
if params.input_amount.is_none() || params.input_amount.unwrap_or(0) == 0 {
|
||||
return Err(anyhow!("Token amount is not set"));
|
||||
@@ -169,33 +212,22 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
// ========================================
|
||||
let is_base_in = protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.pc_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let swap_result = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let minimum_amount_out = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => swap_result.min_amount_out,
|
||||
};
|
||||
let input_mint =
|
||||
if is_base_in { protocol_params.coin_mint } else { protocol_params.pc_mint };
|
||||
let output_mint =
|
||||
if is_base_in { protocol_params.pc_mint } else { protocol_params.coin_mint };
|
||||
|
||||
let user_source_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
&input_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let user_destination_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -203,37 +235,59 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 17] = [
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
crate::constants::TOKEN_PROGRAM_META, // Token Program (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_open_orders, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_target_orders, false), // Amm Target Orders
|
||||
AccountMeta::new(protocol_params.token_coin, false), // Pool Coin Token Account
|
||||
AccountMeta::new(protocol_params.token_pc, false), // Pool Pc Token Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Pc Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Vault Signer
|
||||
AccountMeta::new_readonly(protocol_params.serum_program, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.serum_market, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.serum_bids, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.serum_asks, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.serum_event_queue, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.serum_coin_vault_account, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.serum_pc_vault_account, false), // Serum Pc Vault Account
|
||||
AccountMeta::new_readonly(protocol_params.serum_vault_signer, false), // Serum Vault Signer
|
||||
AccountMeta::new(user_source_token_account, false), // User Source Token Account
|
||||
AccountMeta::new(user_destination_token_account, false), // User Destination Token Account
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Source Owner
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 17];
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let amount_in = params.input_amount.unwrap_or(0);
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..1].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_AMM_V4,
|
||||
@@ -242,7 +296,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &output_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
@@ -257,3 +311,160 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn market_params() -> RaydiumAmmV4Params {
|
||||
RaydiumAmmV4Params::new(
|
||||
pk(1),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(2),
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
)
|
||||
.with_market_accounts(
|
||||
pk(5),
|
||||
pk(6),
|
||||
pk(7),
|
||||
pk(8),
|
||||
pk(9),
|
||||
pk(10),
|
||||
pk(11),
|
||||
pk(12),
|
||||
pk(13),
|
||||
pk(14),
|
||||
)
|
||||
}
|
||||
|
||||
fn swap_params(
|
||||
protocol_params: RaydiumAmmV4Params,
|
||||
fixed_output_amount: Option<u64>,
|
||||
) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
input_token_program: None,
|
||||
output_mint: pk(2),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::RaydiumAmmV4(protocol_params),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount,
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_uses_idl_market_account_order() {
|
||||
let instructions = RaydiumAmmV4InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(market_params(), None))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 18);
|
||||
assert_eq!(&ix.data[..1], SWAP_BASE_IN_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts[3].pubkey, pk(5));
|
||||
assert_eq!(ix.accounts[4].pubkey, pk(6));
|
||||
assert_eq!(ix.accounts[7].pubkey, pk(7));
|
||||
assert_eq!(ix.accounts[8].pubkey, pk(8));
|
||||
assert_eq!(ix.accounts[9].pubkey, pk(9));
|
||||
assert_eq!(ix.accounts[10].pubkey, pk(10));
|
||||
assert_eq!(ix.accounts[11].pubkey, pk(11));
|
||||
assert_eq!(ix.accounts[12].pubkey, pk(12));
|
||||
assert_eq!(ix.accounts[13].pubkey, pk(13));
|
||||
assert_eq!(ix.accounts[14].pubkey, pk(14));
|
||||
assert!(!ix.accounts[7].is_writable);
|
||||
assert!(!ix.accounts[14].is_writable);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_uses_base_out_when_fixed_output_is_set() {
|
||||
let instructions = RaydiumAmmV4InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(market_params(), Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..1], SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[1..9].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[9..17].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_rejects_placeholder_market_accounts() {
|
||||
let err = RaydiumAmmV4InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(
|
||||
RaydiumAmmV4Params::new(
|
||||
pk(1),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(2),
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("amm_open_orders"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_usdc_buy_create_input_builds_usdc_ata() {
|
||||
let mut protocol_params = market_params();
|
||||
protocol_params.coin_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let mut params = swap_params(protocol_params, Some(42));
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions =
|
||||
RaydiumAmmV4InstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
+229
-110
@@ -1,9 +1,15 @@
|
||||
use crate::{
|
||||
common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed,
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::raydium_cpmm::{
|
||||
accounts, get_observation_state_pda, get_pool_pda, get_vault_account,
|
||||
SWAP_BASE_IN_DISCRIMINATOR,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::raydium_cpmm::{
|
||||
accounts, get_observation_state_pda, get_pool_pda, get_vault_account,
|
||||
SWAP_BASE_IN_DISCRIMINATOR, SWAP_BASE_OUT_DISCRIMINATOR,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{RaydiumCpmmParams, SwapParams},
|
||||
@@ -63,53 +69,38 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let mint_token_program = if is_base_in {
|
||||
let input_mint =
|
||||
if is_base_in { protocol_params.base_mint } else { protocol_params.quote_mint };
|
||||
let input_token_program = if is_base_in {
|
||||
protocol_params.base_token_program
|
||||
} else {
|
||||
protocol_params.quote_token_program
|
||||
};
|
||||
let output_mint =
|
||||
if is_base_in { protocol_params.quote_mint } else { protocol_params.base_mint };
|
||||
let output_token_program = if is_base_in {
|
||||
protocol_params.quote_token_program
|
||||
} else {
|
||||
protocol_params.base_token_program
|
||||
};
|
||||
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let result = compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let minimum_amount_out = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => result.min_amount_out,
|
||||
};
|
||||
|
||||
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&mint_token_program,
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
let input_vault_account = get_vault_account(
|
||||
&pool_state,
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
protocol_params,
|
||||
);
|
||||
let output_vault_account =
|
||||
get_vault_account(&pool_state, ¶ms.output_mint, protocol_params);
|
||||
let input_vault_account = get_vault_account(&pool_state, &input_mint, protocol_params);
|
||||
let output_vault_account = get_vault_account(&pool_state, &output_mint, protocol_params);
|
||||
|
||||
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
|
||||
get_observation_state_pda(&pool_state).unwrap()
|
||||
@@ -123,19 +114,23 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&mint_token_program,
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,27 +138,37 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
let accounts: [AccountMeta; 13] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new(pool_state, false), // Pool State
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
crate::constants::TOKEN_PROGRAM_META, // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(mint_token_program, false), // Output Token Program (readonly)
|
||||
if is_wsol {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
crate::constants::USDC_TOKEN_ACCOUNT_META
|
||||
}, // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(params.output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
AccountMeta::new_readonly(input_token_program, false), // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(output_token_program, false), // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(input_mint, false), // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_CPMM,
|
||||
@@ -172,8 +177,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &input_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -219,54 +223,36 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
let is_quote_out = protocol_params.quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let mint_token_program = if is_quote_out {
|
||||
let input_mint =
|
||||
if is_quote_out { protocol_params.base_mint } else { protocol_params.quote_mint };
|
||||
let input_token_program = if is_quote_out {
|
||||
protocol_params.base_token_program
|
||||
} else {
|
||||
protocol_params.quote_token_program
|
||||
};
|
||||
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => {
|
||||
compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_quote_out,
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out
|
||||
}
|
||||
let output_mint =
|
||||
if is_quote_out { protocol_params.quote_mint } else { protocol_params.base_mint };
|
||||
let output_token_program = if is_quote_out {
|
||||
protocol_params.quote_token_program
|
||||
} else {
|
||||
protocol_params.base_token_program
|
||||
};
|
||||
|
||||
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
&mint_token_program,
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
let output_vault_account = get_vault_account(
|
||||
&pool_state,
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
protocol_params,
|
||||
);
|
||||
let input_vault_account =
|
||||
get_vault_account(&pool_state, ¶ms.input_mint, protocol_params);
|
||||
let output_vault_account = get_vault_account(&pool_state, &output_mint, protocol_params);
|
||||
let input_vault_account = get_vault_account(&pool_state, &input_mint, protocol_params);
|
||||
|
||||
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
|
||||
get_observation_state_pda(&pool_state).unwrap()
|
||||
@@ -277,37 +263,54 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
let accounts: [AccountMeta; 13] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new(pool_state, false), // Pool State
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
AccountMeta::new_readonly(mint_token_program, false), // Input Token Program (readonly)
|
||||
crate::constants::TOKEN_PROGRAM_META, // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(params.input_mint, false), // Input token mint (readonly)
|
||||
if is_wsol {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
crate::constants::USDC_TOKEN_ACCOUNT_META
|
||||
}, // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
AccountMeta::new_readonly(input_token_program, false), // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(output_token_program, false), // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(input_mint, false), // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let amount_in = params.input_amount.unwrap_or(0);
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_quote_out,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_CPMM,
|
||||
@@ -316,12 +319,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &output_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
&mint_token_program,
|
||||
&input_token_program,
|
||||
&input_token_account,
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -332,3 +334,120 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn cpmm_params() -> RaydiumCpmmParams {
|
||||
RaydiumCpmmParams {
|
||||
pool_state: pk(1),
|
||||
amm_config: pk(2),
|
||||
base_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
quote_mint: pk(3),
|
||||
base_reserve: 1_000_000_000,
|
||||
quote_reserve: 2_000_000_000,
|
||||
base_vault: pk(4),
|
||||
quote_vault: pk(5),
|
||||
base_token_program: crate::constants::TOKEN_PROGRAM,
|
||||
quote_token_program: crate::constants::TOKEN_PROGRAM,
|
||||
observation_state: pk(6),
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_params(fixed_output_amount: Option<u64>) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
input_token_program: None,
|
||||
output_mint: pk(3),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::RaydiumCpmm(cpmm_params()),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount,
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_cpmm_uses_base_in_and_readonly_amm_config_by_default() {
|
||||
let instructions =
|
||||
RaydiumCpmmInstructionBuilder.build_buy_instructions(&swap_params(None)).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], SWAP_BASE_IN_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts[2].pubkey, pk(2));
|
||||
assert!(!ix.accounts[2].is_writable);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_cpmm_uses_base_output_when_fixed_output_is_set() {
|
||||
let instructions = RaydiumCpmmInstructionBuilder
|
||||
.build_buy_instructions(&swap_params(Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_cpmm_usdc_buy_create_input_uses_usdc_accounts() {
|
||||
let mut protocol_params = cpmm_params();
|
||||
protocol_params.base_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
protocol_params.quote_mint = pk(3);
|
||||
|
||||
let mut params = swap_params(Some(42));
|
||||
params.protocol_params = DexParamEnum::RaydiumCpmm(protocol_params);
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions =
|
||||
RaydiumCpmmInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
let swap_ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
assert_eq!(swap_ix.accounts[10].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_create_user_token_account(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
use_seed: bool,
|
||||
) {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
payer,
|
||||
payer,
|
||||
mint,
|
||||
token_program,
|
||||
use_seed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_create_or_wrap_user_token_account(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
amount: u64,
|
||||
use_seed: bool,
|
||||
) {
|
||||
if *mint == crate::constants::WSOL_TOKEN_ACCOUNT {
|
||||
instructions.extend(crate::trading::common::handle_wsol(payer, amount));
|
||||
} else {
|
||||
push_create_user_token_account(instructions, payer, mint, token_program, use_seed);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_close_wsol_if_needed(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
) {
|
||||
if *mint == crate::constants::WSOL_TOKEN_ACCOUNT {
|
||||
instructions.extend(crate::trading::common::close_wsol(payer));
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,9 @@ pub mod accounts {
|
||||
}
|
||||
|
||||
pub const BUY_EXECT_IN_DISCRIMINATOR: [u8; 8] = [250, 234, 13, 123, 213, 156, 19, 236];
|
||||
pub const BUY_EXECT_OUT_DISCRIMINATOR: [u8; 8] = [24, 211, 116, 40, 105, 3, 153, 56];
|
||||
pub const SELL_EXECT_IN_DISCRIMINATOR: [u8; 8] = [149, 39, 222, 155, 211, 124, 152, 26];
|
||||
pub const SELL_EXECT_OUT_DISCRIMINATOR: [u8; 8] = [95, 200, 71, 34, 8, 9, 11, 166];
|
||||
|
||||
pub async fn fetch_pool_state(
|
||||
rpc: &SolanaRpcClient,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod accounts {
|
||||
|
||||
pub const AUTHORITY: Pubkey = pubkey!("HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC");
|
||||
pub const METEORA_DAMM_V2: Pubkey = pubkey!("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG");
|
||||
pub const SYSVAR_INSTRUCTIONS: Pubkey = pubkey!("Sysvar1nstructions1111111111111111111111111");
|
||||
|
||||
// META
|
||||
|
||||
@@ -32,9 +33,20 @@ pub mod accounts {
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const SYSVAR_INSTRUCTIONS_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: SYSVAR_INSTRUCTIONS,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
}
|
||||
|
||||
pub const SWAP_DISCRIMINATOR: &[u8] = &[248, 198, 158, 145, 225, 117, 135, 200];
|
||||
pub const SWAP2_DISCRIMINATOR: &[u8] = &[65, 75, 63, 76, 235, 91, 91, 136];
|
||||
pub const SWAP_MODE_EXACT_IN: u8 = 0;
|
||||
pub const SWAP_MODE_PARTIAL_FILL: u8 = 1;
|
||||
pub const SWAP_MODE_EXACT_OUT: u8 = 2;
|
||||
|
||||
pub async fn fetch_pool(
|
||||
rpc: &SolanaRpcClient,
|
||||
|
||||
+379
-196
@@ -1,5 +1,10 @@
|
||||
//! Pump.fun bonding-curve utilities (flat module): PDAs, fee `#2`, pump-fees `SharingConfig`, cold RPC.
|
||||
//!
|
||||
//! Hot swap ix assembly stays sync; async helpers at file bottom. Layout matches `@pump-fun/pump-sdk`.
|
||||
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use borsh::BorshDeserialize;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
@@ -7,85 +12,43 @@ use solana_sdk::{
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
// --- Aligned with official `@pump-fun/pump-sdk` (npm) ---
|
||||
// - `src/fees.ts` `getFeeRecipient(global, mayhemMode)` — fee recipient pools
|
||||
// - `src/bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient`
|
||||
// - `src/sdk.ts` `BONDING_CURVE_NEW_SIZE` (151) + `extendAccountInstruction` — **not** called from the
|
||||
// trade hot path here (no RPC in `PumpFunInstructionBuilder`); use these helpers from a cold path if needed.
|
||||
// --- seeds -------------------------------------------------------------
|
||||
|
||||
/// Minimum bonding curve account data length after protocol upgrades (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
/// Anchor discriminator for `extend_account` (`pump.json`); same as `PumpSdk.extendAccountInstruction`.
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
/// Build `extend_account` for bonding curve (cold path / separate tx only — do not add RPC to hot-path builds).
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(bonding_curve: &Pubkey, user: &Pubkey) -> Instruction {
|
||||
Instruction {
|
||||
program_id: accounts::PUMPFUN,
|
||||
accounts: vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
data: EXTEND_ACCOUNT_DISCRIMINATOR.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
/// Seed for bonding curve PDAs
|
||||
|
||||
/// Seed for bonding curve PDAs (`["bonding-curve", mint]`).
|
||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||
/// Seed for bonding curve v2 PDA (required by program upgrade, readonly at end of account list)
|
||||
/// Seed for bonding curve v2 PDA (`["bonding-curve-v2", mint]`).
|
||||
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
|
||||
|
||||
/// Seed for creator vault PDAs
|
||||
/// Creator vault PDA seeds prefix (`["creator-vault", authority]`).
|
||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
||||
|
||||
/// Seed for metadata PDAs
|
||||
/// Metadata PDA seeds prefix.
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
|
||||
/// Seed for user volume accumulator PDAs
|
||||
/// User volume accumulator for cashback / bonding-curve UX.
|
||||
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
||||
|
||||
/// Seed for global volume accumulator PDAs
|
||||
/// Global volume accumulator.
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
||||
|
||||
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
||||
|
||||
/// `feeSharingConfig` PDA under pump-fees (`@pump-fun/pump-sdk` `feeSharingConfigPda`)
|
||||
/// `feeSharingConfig` PDA under pump-fees (`feeSharingConfigPda`).
|
||||
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
|
||||
}
|
||||
|
||||
pub mod global_constants {
|
||||
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
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_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;
|
||||
|
||||
pub const ENABLE_MIGRATE: bool = false;
|
||||
|
||||
pub const POOL_MIGRATION_FEE: u64 = 15_000_001;
|
||||
|
||||
pub const CREATOR_FEE: u64 = 30;
|
||||
pub const SCALE: u64 = 1_000_000;
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL;
|
||||
|
||||
pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals
|
||||
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports
|
||||
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL
|
||||
|
||||
/// Public key for the fee recipient
|
||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
@@ -94,7 +57,6 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Mayhem fee recipients (pump-public-docs: use any one randomly)
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
@@ -113,7 +75,6 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Public key for the global PDA
|
||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
@@ -122,23 +83,17 @@ pub mod global_constants {
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
/// Public key for the authority
|
||||
pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF");
|
||||
|
||||
/// Public key for the withdraw authority
|
||||
pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg");
|
||||
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ");
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX");
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz");
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY");
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM");
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz");
|
||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
||||
// Pump.fun AMM: Protocol Fee 7
|
||||
|
||||
/// Protocol extra fee recipients (Apr 2026 breaking upgrade). One is appended after `bonding-curve-v2`, **writable**.
|
||||
/// See: <https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md>
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
@@ -149,35 +104,36 @@ pub mod global_constants {
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
|
||||
/// Buyback fee recipients (v2 account #9 in buy_v2/sell_v2).
|
||||
/// 对应官方 FEE_RECIPIENTS.md "Buyback (Applies to All)" 池,与主 fee_recipient 池互斥。
|
||||
pub const BUYBACK_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Public key for the Pump.fun program
|
||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
/// Public key for the MPL Token Metadata program
|
||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
||||
|
||||
/// Authority for program events
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
||||
|
||||
/// Associated Token Program ID
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
|
||||
pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
||||
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey =
|
||||
pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y");
|
||||
|
||||
pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt");
|
||||
|
||||
// META
|
||||
pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: PUMPFUN,
|
||||
@@ -214,18 +170,35 @@ pub mod accounts {
|
||||
};
|
||||
}
|
||||
|
||||
/// Instruction discriminators for PumpFun program
|
||||
// --- Anchor / layout constants ---------------------------------------
|
||||
|
||||
/// Minimum bonding curve account data length (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
/// Check if a pubkey is one of the Mayhem fee recipients
|
||||
/// `buy_v2` — unified SOL/USDC quote interface ([pump-public-docs](https://github.com/pump-fun/pump-public-docs)).
|
||||
pub const BUY_V2_DISCRIMINATOR: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
|
||||
/// `sell_v2`
|
||||
pub const SELL_V2_DISCRIMINATOR: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
|
||||
/// `buy_exact_quote_in_v2` (native SOL spend for SOL-paired coins when `quote_mint` is WSOL)
|
||||
pub const BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
|
||||
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
pub const SHARING_CONFIG_ACCOUNT_DISCRIMINATOR: [u8; 8] = [216, 74, 9, 0, 56, 140, 93, 75];
|
||||
|
||||
pub(crate) const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
|
||||
|
||||
// --- Fee recipient pools -----------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.iter().any(|p| p == pubkey)
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(pubkey)
|
||||
}
|
||||
|
||||
/// Check if a pubkey is a Pump.fun AMM protocol fee recipient (PUMPFUN_AMM_FEE_1..7)
|
||||
#[inline]
|
||||
pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
pubkey == &global_constants::PUMPFUN_AMM_FEE_1
|
||||
@@ -237,8 +210,47 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
|
||||
}
|
||||
|
||||
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
|
||||
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
|
||||
#[inline]
|
||||
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
*pubkey == global_constants::FEE_RECIPIENT
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reconcile_mayhem_mode_for_trade(
|
||||
mayhem_from_event: Option<bool>,
|
||||
fee_recipient: &Pubkey,
|
||||
) -> bool {
|
||||
if *fee_recipient == Pubkey::default() {
|
||||
return mayhem_from_event.unwrap_or(false);
|
||||
}
|
||||
let fee_m = is_mayhem_fee_recipient(fee_recipient);
|
||||
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
|
||||
match mayhem_from_event {
|
||||
Some(log_m) => {
|
||||
if fee_m && !log_m {
|
||||
true
|
||||
} else if fee_s && log_m && !fee_m {
|
||||
false
|
||||
} else {
|
||||
log_m
|
||||
}
|
||||
}
|
||||
None => fee_m,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
|
||||
let is_m = is_mayhem_fee_recipient(pk);
|
||||
let is_amm = is_amm_fee_recipient(pk);
|
||||
let is_s = is_standard_bonding_fee_recipient(pk);
|
||||
if is_mayhem_mode {
|
||||
is_m || (!is_s && !is_amm && *pk != Pubkey::default())
|
||||
} else {
|
||||
is_s || (!is_m && !is_amm && *pk != Pubkey::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
@@ -247,31 +259,14 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
/// Non-mayhem: random among `Global::fee_recipient` + `Global::fee_recipients[0..7]`.
|
||||
/// Same pubkey set as `bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient` and `fees.ts` `getFeeRecipient` when `mayhemMode === false`.
|
||||
#[inline]
|
||||
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
||||
const POOL: &[Pubkey] = &[
|
||||
global_constants::FEE_RECIPIENT,
|
||||
global_constants::PUMPFUN_AMM_FEE_1,
|
||||
global_constants::PUMPFUN_AMM_FEE_2,
|
||||
global_constants::PUMPFUN_AMM_FEE_3,
|
||||
global_constants::PUMPFUN_AMM_FEE_4,
|
||||
global_constants::PUMPFUN_AMM_FEE_5,
|
||||
global_constants::PUMPFUN_AMM_FEE_6,
|
||||
global_constants::PUMPFUN_AMM_FEE_7,
|
||||
];
|
||||
let recipient = *POOL
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::FEE_RECIPIENT);
|
||||
AccountMeta {
|
||||
pubkey: recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
// Historical name kept for API compatibility. Do not randomize across static AMM fee
|
||||
// recipients for bonding-curve buy/sell; stale AMM protocol fee accounts can fail
|
||||
// Pump.fun authorization with error 6000 when Global has rotated.
|
||||
AccountMeta { pubkey: global_constants::FEE_RECIPIENT, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
/// Random entry from [`global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (must be last account after bonding-curve-v2, writable).
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
@@ -279,15 +274,23 @@ pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。
|
||||
/// Buyback fee recipient (#9 in buy_v2/sell_v2) — dedicated pool, distinct from protocol extra fee recipients.
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
|
||||
if from_stream != Pubkey::default() {
|
||||
AccountMeta {
|
||||
pubkey: from_stream,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
pub fn get_buyback_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::BUYBACK_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::BUYBACK_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(
|
||||
observed_fee_recipient: Pubkey,
|
||||
is_mayhem_mode: bool,
|
||||
) -> AccountMeta {
|
||||
let trust_observation = observed_fee_recipient != Pubkey::default()
|
||||
&& fee_recipient_ok_for_bonding_curve_mode(&observed_fee_recipient, is_mayhem_mode);
|
||||
if trust_observation {
|
||||
AccountMeta { pubkey: observed_fee_recipient, is_signer: false, is_writable: true }
|
||||
} else if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
@@ -295,12 +298,28 @@ pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) ->
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Symbol;
|
||||
// --- Extend bonding curve (cold path) --------------------------------
|
||||
|
||||
impl Symbol {
|
||||
pub const SOLANA: &'static str = "solana";
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(
|
||||
bonding_curve: &Pubkey,
|
||||
user: &Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&EXTEND_ACCOUNT_DISCRIMINATOR,
|
||||
vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// --- Cached PDAs + creator_vault resolve ------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -308,13 +327,11 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Bonding curve v2 PDA (seeds: ["bonding-curve-v2", mint]). Required at end of buy/sell/buy_exact_sol_in accounts.
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -322,8 +339,7 @@ pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_V2_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -333,7 +349,6 @@ pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
|
||||
if creator_vault_pda.eq(&Pubkey::default()) {
|
||||
Pubkey::default()
|
||||
} else {
|
||||
// Fast check against cached default creator vault
|
||||
static DEFAULT_CREATOR_VAULT: std::sync::LazyLock<Option<Pubkey>> =
|
||||
std::sync::LazyLock::new(|| get_creator_vault_pda(&Pubkey::default()));
|
||||
match DEFAULT_CREATOR_VAULT.as_ref() {
|
||||
@@ -350,24 +365,25 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::CREATOR_VAULT_SEED, creator.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// `feeSharingConfig` PDA per mint (`pump-sdk` `feeSharingConfigPda` → `pump-fees` program).
|
||||
#[inline]
|
||||
pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunFeeSharingConfig(*mint),
|
||||
|| {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
},
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
}
|
||||
|
||||
/// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when
|
||||
/// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006.
|
||||
#[inline]
|
||||
pub fn phantom_default_creator_vault() -> Pubkey {
|
||||
solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc")
|
||||
@@ -378,52 +394,47 @@ pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
|
||||
*pk == phantom_default_creator_vault()
|
||||
}
|
||||
|
||||
/// Resolve `creator_vault` for Pump buy/sell account #10.
|
||||
///
|
||||
/// - If `creator` is **missing** in the outer trade-event borsh (`Pubkey::default()`) but
|
||||
/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9),
|
||||
/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache).
|
||||
/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`).
|
||||
/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix).
|
||||
/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use
|
||||
/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right).
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Option<Pubkey> {
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
|
||||
}
|
||||
|
||||
/// Resolves Pump.fun bonding-curve buy/sell **account `#10` (`creator_vault`)** for ix assembly.
|
||||
///
|
||||
/// **Priority (highest first)**
|
||||
/// 1. **Explicit `creator_vault`** from ix / parser / cached observation when non-default and not the phantom
|
||||
/// sentinel — **always used as-is** (no remap to [`get_creator_vault_pda`] from `creator`);
|
||||
/// fee-sharing / multi-party layouts rely on upstream passing the vault the program expects (`pfee…` / `update_fee_shares`).
|
||||
/// 2. If ix vault missing: optional `fee_sharing_creator_vault_if_active` hint (non-default, non-phantom).
|
||||
/// 3. Else: [`get_creator_vault_pda`] from `creator` when `creator` is known.
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
_mint: &Pubkey,
|
||||
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
) -> Option<Pubkey> {
|
||||
let phantom = phantom_default_creator_vault();
|
||||
|
||||
if creator_vault_from_event != Pubkey::default() && creator_vault_from_event != phantom {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
|
||||
if let Some(v) = fee_sharing_creator_vault_if_active {
|
||||
if v != Pubkey::default() && v != phantom {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
if *creator == Pubkey::default() {
|
||||
if creator_vault_from_event == Pubkey::default() {
|
||||
return None;
|
||||
}
|
||||
if creator_vault_from_event == phantom {
|
||||
return None;
|
||||
}
|
||||
return Some(creator_vault_from_event);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
|
||||
if creator_vault_from_event == phantom {
|
||||
return get_creator_vault_pda(creator);
|
||||
}
|
||||
|
||||
let v_derived = get_creator_vault_pda(creator)?;
|
||||
if creator_vault_from_event == Pubkey::default() {
|
||||
return Some(v_derived);
|
||||
}
|
||||
if creator_vault_from_event == v_derived {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
|
||||
let v_sharing = get_creator_vault_pda(&sharing)?;
|
||||
if creator_vault_from_event == v_sharing {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
}
|
||||
Some(v_derived)
|
||||
get_creator_vault_pda(creator)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -431,34 +442,13 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunUserVolume(*user),
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let seed: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seed, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
let bonding_curve =
|
||||
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_buy_price(
|
||||
amount: u64,
|
||||
@@ -479,9 +469,70 @@ pub fn get_buy_price(
|
||||
s_u64.min(real_token_reserves)
|
||||
}
|
||||
|
||||
// --- RPC -------------------------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Option<Pubkey>, anyhow::Error> {
|
||||
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let acc = match rpc.get_account(&config_pda).await {
|
||||
Ok(a) => a,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
if acc.owner != accounts::FEE_PROGRAM {
|
||||
return Ok(None);
|
||||
}
|
||||
let d = acc.data.as_slice();
|
||||
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
|
||||
return Ok(None);
|
||||
}
|
||||
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
|
||||
return Ok(None);
|
||||
}
|
||||
let mint_on_chain = Pubkey::new_from_array(
|
||||
d[11..43].try_into().map_err(|_| anyhow!("SharingConfig mint slice"))?,
|
||||
);
|
||||
if mint_on_chain != *mint {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(get_creator_vault_pda(&config_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or_else(|| anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
// Use `deserialize` instead of `try_from_slice` so that extra trailing bytes
|
||||
// (from on-chain schema additions like new fields) are silently ignored.
|
||||
// `try_from_slice` requires the entire slice to be consumed, causing
|
||||
// "Not all bytes read" when the account has been extended.
|
||||
let mut bonding_curve = BondingCurveAccount::deserialize(&mut &account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to decode bonding curve account: {}", e))?;
|
||||
bonding_curve.account = bonding_curve_pda;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::{
|
||||
global_constants, phantom_default_creator_vault, pump_fun_fee_recipient_meta,
|
||||
reconcile_mayhem_mode_for_trade, resolve_creator_vault_for_ix,
|
||||
resolve_creator_vault_for_ix_with_fee_sharing, *,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[test]
|
||||
@@ -489,6 +540,9 @@ mod tests {
|
||||
assert_eq!(BUY_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_EXACT_SOL_IN_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(SELL_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_V2_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(SELL_V2_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -518,7 +572,11 @@ mod tests {
|
||||
#[test]
|
||||
fn default_creator_yields_fixed_creator_vault() {
|
||||
let v = get_creator_vault_pda(&Pubkey::default()).unwrap();
|
||||
assert_eq!(v, phantom_default_creator_vault(), "phantom vault constant must match PDA(default creator)");
|
||||
assert_eq!(
|
||||
v,
|
||||
phantom_default_creator_vault(),
|
||||
"phantom vault constant must match PDA(default creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -542,11 +600,48 @@ mod tests {
|
||||
fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() {
|
||||
let mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(&Pubkey::default(), phantom_default_creator_vault(), &mint),
|
||||
resolve_creator_vault_for_ix(
|
||||
&Pubkey::default(),
|
||||
phantom_default_creator_vault(),
|
||||
&mint,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_prefers_creator_pda_ix_vault_even_when_fee_sharing_hint_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, v_derived, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(v_derived),
|
||||
"observed ix uses PDA(creator); stale hint must not override → wrong vault / Anchor 2006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_trusts_ix_fee_sharing_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let creator_vault = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_ne!(vs, creator_vault);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(vs),
|
||||
"explicit ix creator_vault (e.g. fee-sharing #8) wins; no remap to PDA(creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_remaps_phantom_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
@@ -557,4 +652,92 @@ mod tests {
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_ix_vault_always_wins_when_non_default_even_if_creator_pda_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let ix_other = Pubkey::new_unique();
|
||||
assert_ne!(ix_other, v_derived);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, ix_other, &mint, None);
|
||||
assert_eq!(resolved, Some(ix_other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_fee_sharing_ix_vault_used_as_is_even_without_hint() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, None);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_falls_back_to_fee_sharing_hint_when_ix_vault_placeholder() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
Pubkey::default(),
|
||||
&mint,
|
||||
Some(vs),
|
||||
);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_false_but_fee_is_mayhem_pool() {
|
||||
let fee = global_constants::MAYHEM_FEE_RECIPIENTS[0];
|
||||
assert!(reconcile_mayhem_mode_for_trade(Some(false), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
|
||||
let fee = global_constants::FEE_RECIPIENT;
|
||||
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_rejects_amm_fee_when_building_mayhem_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
let m = pump_fun_fee_recipient_meta(fee, true);
|
||||
assert!(
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(&m.pubkey),
|
||||
"expected fallback to mayhem pool, got {}",
|
||||
m.pubkey
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_uses_observed_non_amm_fee_for_standard_ix() {
|
||||
let fee = Pubkey::new_unique();
|
||||
let m = pump_fun_fee_recipient_meta(fee, false);
|
||||
assert_eq!(m.pubkey, fee);
|
||||
assert!(m.is_writable);
|
||||
assert!(!m.is_signer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_rejects_amm_fee_for_standard_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_7;
|
||||
let m = pump_fun_fee_recipient_meta(fee, false);
|
||||
assert_eq!(m.pubkey, global_constants::FEE_RECIPIENT);
|
||||
assert!(m.is_writable);
|
||||
assert!(!m.is_signer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_default_standard_uses_main_fee_recipient() {
|
||||
let m = pump_fun_fee_recipient_meta(Pubkey::default(), false);
|
||||
assert_eq!(m.pubkey, global_constants::FEE_RECIPIENT);
|
||||
assert!(m.is_writable);
|
||||
assert!(!m.is_signer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,17 @@ use crate::{
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::warn;
|
||||
|
||||
// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22)
|
||||
|
||||
@@ -175,22 +183,185 @@ 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 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;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalConfig {
|
||||
pub protocol_fee_recipients: [Pubkey; 8],
|
||||
pub reserved_fee_recipient: Pubkey,
|
||||
pub reserved_fee_recipients: [Pubkey; 7],
|
||||
pub buyback_fee_recipients: [Pubkey; 8],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedGlobalConfig {
|
||||
fetched_at: Instant,
|
||||
config: GlobalConfig,
|
||||
}
|
||||
|
||||
static GLOBAL_CONFIG_CACHE: Lazy<RwLock<Option<CachedGlobalConfig>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
static GLOBAL_CONFIG_REFRESH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
|
||||
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
|
||||
Some(Pubkey::new_from_array(bytes.try_into().ok()?))
|
||||
}
|
||||
|
||||
fn read_pubkey_array<const N: usize>(data: &[u8], offset: usize) -> Option<[Pubkey; N]> {
|
||||
let mut keys = [Pubkey::default(); N];
|
||||
for (i, key) in keys.iter_mut().enumerate() {
|
||||
*key = read_pubkey(data, offset + i * PUBKEY_LEN)?;
|
||||
}
|
||||
Some(keys)
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
offset += PUBKEY_LEN; // admin_set_coin_creator_authority
|
||||
offset += PUBKEY_LEN; // whitelist_pda
|
||||
|
||||
let reserved_fee_recipient = read_pubkey(data, offset)?;
|
||||
offset += PUBKEY_LEN;
|
||||
offset += BOOL_LEN; // mayhem_mode_enabled
|
||||
|
||||
let reserved_fee_recipients = read_pubkey_array::<7>(data, offset)?;
|
||||
offset += PUBKEY_LEN * 7;
|
||||
offset += BOOL_LEN; // is_cashback_enabled
|
||||
|
||||
let buyback_fee_recipients = read_pubkey_array::<8>(data, offset)?;
|
||||
|
||||
Some(GlobalConfig {
|
||||
protocol_fee_recipients,
|
||||
reserved_fee_recipient,
|
||||
reserved_fee_recipients,
|
||||
buyback_fee_recipients,
|
||||
})
|
||||
}
|
||||
|
||||
async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfig> {
|
||||
let account = match tokio::time::timeout(
|
||||
PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT,
|
||||
rpc.get_account(&accounts::GLOBAL_ACCOUNT),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(account)) => account,
|
||||
Ok(Err(e)) => {
|
||||
warn!(target: "pumpswap_global_config", "PumpSwap GlobalConfig 读取失败: {}", e);
|
||||
return None;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
timeout_ms = PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT.as_millis(),
|
||||
"PumpSwap GlobalConfig 读取超时"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(config) = decode_global_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
data_len = account.data.len(),
|
||||
"PumpSwap GlobalConfig 解析失败"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
*GLOBAL_CONFIG_CACHE.write() =
|
||||
Some(CachedGlobalConfig { 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;
|
||||
};
|
||||
let stale = GLOBAL_CONFIG_CACHE
|
||||
.read()
|
||||
.as_ref()
|
||||
.map(|c| c.fetched_at.elapsed() > PUMPSWAP_GLOBAL_CONFIG_TTL)
|
||||
.unwrap_or(true);
|
||||
if stale && !GLOBAL_CONFIG_REFRESH_IN_FLIGHT.swap(true, Ordering::AcqRel) {
|
||||
let rpc = Arc::clone(rpc);
|
||||
tokio::spawn(async move {
|
||||
let _ = refresh_global_config_once(rpc.as_ref()).await;
|
||||
GLOBAL_CONFIG_REFRESH_IN_FLIGHT.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_global_config() -> Option<GlobalConfig> {
|
||||
let guard = GLOBAL_CONFIG_CACHE.read();
|
||||
let cached = guard.as_ref()?;
|
||||
(cached.fetched_at.elapsed() <= PUMPSWAP_GLOBAL_CONFIG_TTL).then(|| cached.config.clone())
|
||||
}
|
||||
|
||||
fn choose_nonzero(keys: &[Pubkey]) -> Option<Pubkey> {
|
||||
let mut valid = [Pubkey::default(); 8];
|
||||
let mut len = 0;
|
||||
for key in keys.iter().copied() {
|
||||
if key == Pubkey::default() || len == valid.len() {
|
||||
continue;
|
||||
}
|
||||
valid[len] = key;
|
||||
len += 1;
|
||||
}
|
||||
valid[..len].choose(&mut rand::rng()).copied()
|
||||
}
|
||||
|
||||
/// Returns a random Mayhem fee recipient and its AccountMeta (pump-public-docs: use any one randomly).
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
|
||||
let recipient = *accounts::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0]);
|
||||
let recipient = cached_global_config()
|
||||
.and_then(|config| {
|
||||
let mut pool = [Pubkey::default(); 8];
|
||||
pool[0] = config.reserved_fee_recipient;
|
||||
pool[1..].copy_from_slice(&config.reserved_fee_recipients);
|
||||
choose_nonzero(&pool)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
*accounts::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0])
|
||||
});
|
||||
let meta = AccountMeta { pubkey: recipient, is_signer: false, is_writable: false };
|
||||
(recipient, meta)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_protocol_fee_recipient_random() -> Pubkey {
|
||||
cached_global_config()
|
||||
.and_then(|config| choose_nonzero(&config.protocol_fee_recipients))
|
||||
.unwrap_or(accounts::FEE_RECIPIENT)
|
||||
}
|
||||
|
||||
/// Random entry from [`accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (readonly; paired with [`fee_recipient_ata`] as last account).
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
*accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
cached_global_config()
|
||||
.and_then(|config| choose_nonzero(&config.buyback_fee_recipients))
|
||||
.unwrap_or_else(|| {
|
||||
*accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
})
|
||||
}
|
||||
|
||||
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
|
||||
@@ -331,23 +502,21 @@ async fn get_program_accounts_both_sizes(
|
||||
memcmp_offset: usize,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Vec<(Pubkey, solana_sdk::account::Account)>, anyhow::Error> {
|
||||
let make_config = |data_size: u64| {
|
||||
solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
|
||||
),
|
||||
]),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
commitment: None,
|
||||
min_context_slot: None,
|
||||
},
|
||||
with_context: None,
|
||||
sort_results: None,
|
||||
}
|
||||
let make_config = |data_size: u64| solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
|
||||
),
|
||||
]),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
commitment: None,
|
||||
min_context_slot: None,
|
||||
},
|
||||
with_context: None,
|
||||
sort_results: None,
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
@@ -360,7 +529,9 @@ async fn get_program_accounts_both_sizes(
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
fn decode_pool_accounts(accounts: Vec<(Pubkey, solana_sdk::account::Account)>) -> Vec<(Pubkey, Pool)> {
|
||||
fn decode_pool_accounts(
|
||||
accounts: Vec<(Pubkey, solana_sdk::account::Account)>,
|
||||
) -> Vec<(Pubkey, Pool)> {
|
||||
accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
@@ -439,12 +610,16 @@ pub async fn find_by_mint(
|
||||
}
|
||||
|
||||
// 3. Fallback: getProgramAccounts by base_mint / quote_mint (with 3s timeout to avoid blocking)
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_base_mint(rpc, mint)).await {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_base_mint(rpc, mint))
|
||||
.await
|
||||
{
|
||||
Ok(Ok((address, pool))) => return Ok((address, pool)),
|
||||
Ok(Err(e)) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
|
||||
Err(_) => diag.push("getProgramAccounts(base_mint): timed out (3s)".into()),
|
||||
}
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_quote_mint(rpc, mint)).await {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_quote_mint(rpc, mint))
|
||||
.await
|
||||
{
|
||||
Ok(Ok((address, pool))) => return Ok((address, pool)),
|
||||
Ok(Err(e)) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
|
||||
Err(_) => diag.push("getProgramAccounts(quote_mint): timed out (3s)".into()),
|
||||
@@ -452,11 +627,7 @@ pub async fn find_by_mint(
|
||||
|
||||
let diag_str = diag.join("; ");
|
||||
eprintln!("[find_by_mint] {} failed: {}", mint, diag_str);
|
||||
Err(anyhow!(
|
||||
"No pool found for mint {}. diag: {}",
|
||||
mint,
|
||||
diag_str
|
||||
))
|
||||
Err(anyhow!("No pool found for mint {}. diag: {}", mint, diag_str))
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
instruction::utils::raydium_amm_v4_types::{amm_info_decode, AmmInfo},
|
||||
instruction::utils::raydium_amm_v4_types::{
|
||||
amm_info_decode, market_state_decode, AmmInfo, MarketState,
|
||||
},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
@@ -40,3 +42,25 @@ pub async fn fetch_amm_info(rpc: &SolanaRpcClient, amm: Pubkey) -> Result<AmmInf
|
||||
amm_info_decode(&amm_info).ok_or_else(|| anyhow!("Failed to decode amm info"))?;
|
||||
Ok(amm_info)
|
||||
}
|
||||
|
||||
pub async fn fetch_market_state(
|
||||
rpc: &SolanaRpcClient,
|
||||
market: Pubkey,
|
||||
) -> Result<MarketState, anyhow::Error> {
|
||||
let market_data = rpc.get_account_data(&market).await?;
|
||||
market_state_decode(&market_data).ok_or_else(|| anyhow!("Failed to decode market state"))
|
||||
}
|
||||
|
||||
pub fn derive_serum_vault_signer(
|
||||
serum_program: &Pubkey,
|
||||
serum_market: &Pubkey,
|
||||
vault_signer_nonce: u64,
|
||||
) -> Result<Pubkey, anyhow::Error> {
|
||||
let nonce = vault_signer_nonce.to_le_bytes();
|
||||
Pubkey::create_program_address(&[serum_market.as_ref(), &nonce], serum_program)
|
||||
.or_else(|_| {
|
||||
let legacy_nonce = [vault_signer_nonce as u8];
|
||||
Pubkey::create_program_address(&[serum_market.as_ref(), &legacy_nonce], serum_program)
|
||||
})
|
||||
.map_err(|err| anyhow!("Failed to derive Serum vault signer: {}", err))
|
||||
}
|
||||
|
||||
@@ -77,3 +77,38 @@ pub fn amm_info_decode(data: &[u8]) -> Option<AmmInfo> {
|
||||
}
|
||||
borsh::from_slice::<AmmInfo>(&data[..AMM_INFO_SIZE]).ok()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct MarketState {
|
||||
pub padding: [u8; 5],
|
||||
pub account_flags: u64,
|
||||
pub own_address: Pubkey,
|
||||
pub vault_signer_nonce: u64,
|
||||
pub coin_mint: Pubkey,
|
||||
pub pc_mint: Pubkey,
|
||||
pub serum_coin_vault_account: Pubkey,
|
||||
pub coin_deposits_total: u64,
|
||||
pub coin_fees_accrued: u64,
|
||||
pub serum_pc_vault_account: Pubkey,
|
||||
pub pc_deposits_total: u64,
|
||||
pub pc_fees_accrued: u64,
|
||||
pub pc_dust_threshold: u64,
|
||||
pub request_queue: Pubkey,
|
||||
pub serum_event_queue: Pubkey,
|
||||
pub serum_bids: Pubkey,
|
||||
pub serum_asks: Pubkey,
|
||||
pub coin_lot_size: u64,
|
||||
pub pc_lot_size: u64,
|
||||
pub fee_rate_bps: u64,
|
||||
pub referrer_rebate_accrued: u64,
|
||||
pub padding2: [u8; 7],
|
||||
}
|
||||
|
||||
pub const MARKET_STATE_SIZE: usize = 388;
|
||||
|
||||
pub fn market_state_decode(data: &[u8]) -> Option<MarketState> {
|
||||
if data.len() < MARKET_STATE_SIZE {
|
||||
return None;
|
||||
}
|
||||
borsh::from_slice::<MarketState>(&data[..MARKET_STATE_SIZE]).ok()
|
||||
}
|
||||
|
||||
@@ -288,7 +288,10 @@ impl OptimizationFlags {
|
||||
"+popcnt".to_string(),
|
||||
];
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
let target_features = vec!["+neon".to_string()];
|
||||
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
let target_features = vec![];
|
||||
Self {
|
||||
opt_level: OptLevel::Aggressive,
|
||||
|
||||
@@ -172,10 +172,17 @@ impl SIMDMemoryOps {
|
||||
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
match len {
|
||||
1 => *a == *b,
|
||||
2 => *(a as *const u16) == *(b as *const u16),
|
||||
3 => *(a as *const u16) == *(b as *const u16) && *a.add(2) == *b.add(2),
|
||||
4 => *(a as *const u32) == *(b as *const u32),
|
||||
5..=8 => *(a as *const u64) == *(b as *const u64),
|
||||
2 => ptr::read_unaligned(a as *const u16) == ptr::read_unaligned(b as *const u16),
|
||||
3 => {
|
||||
ptr::read_unaligned(a as *const u16) == ptr::read_unaligned(b as *const u16)
|
||||
&& *a.add(2) == *b.add(2)
|
||||
}
|
||||
4 => ptr::read_unaligned(a as *const u32) == ptr::read_unaligned(b as *const u32),
|
||||
5..=7 => {
|
||||
ptr::read_unaligned(a as *const u32) == ptr::read_unaligned(b as *const u32)
|
||||
&& (4..len).all(|i| *a.add(i) == *b.add(i))
|
||||
}
|
||||
8 => ptr::read_unaligned(a as *const u64) == ptr::read_unaligned(b as *const u64),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -447,23 +454,23 @@ impl BranchOptimizer {
|
||||
|
||||
/// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_read_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T0;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T0);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prefetch for write (T1 hint). 写预取(T1 提示)。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_write_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T1;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T1);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl Default for SyscallBypassConfig {
|
||||
|
||||
pub struct SyscallBatchProcessor {
|
||||
pending_calls: crossbeam_queue::ArrayQueue<SyscallRequest>,
|
||||
_executor: tokio::runtime::Handle,
|
||||
_executor: Option<tokio::runtime::Handle>,
|
||||
batch_stats: CachePadded<AtomicU64>,
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ pub struct FastTimeProvider {
|
||||
/// 上次更新时间
|
||||
last_update: CachePadded<AtomicU64>,
|
||||
/// 启用vDSO
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
vdso_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -121,6 +122,7 @@ impl FastTimeProvider {
|
||||
/// 🚀 超快速获取当前时间 - 绕过系统调用
|
||||
#[inline(always)]
|
||||
pub fn fast_now_nanos(&self) -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
if self.vdso_enabled {
|
||||
// 使用vDSO快速获取时间
|
||||
return self.vdso_time_nanos();
|
||||
@@ -140,6 +142,7 @@ impl FastTimeProvider {
|
||||
|
||||
/// vDSO时间获取
|
||||
#[inline(always)]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn vdso_time_nanos(&self) -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@@ -371,7 +374,7 @@ impl SyscallBatchProcessor {
|
||||
/// 创建系统调用批处理器
|
||||
pub fn new(batch_size: usize) -> Result<Self> {
|
||||
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
|
||||
let executor = tokio::runtime::Handle::current();
|
||||
let executor = tokio::runtime::Handle::try_current().ok();
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Syscall batch processor created with batch size: {}", batch_size);
|
||||
|
||||
|
||||
+36
-6
@@ -202,11 +202,20 @@ impl AstralaneClient {
|
||||
let _ = response.bytes().await;
|
||||
if status.is_success() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Astralane", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Astralane", trade_type, start_time.elapsed(), format!("status {}", status));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {}", status),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
|
||||
}
|
||||
@@ -214,12 +223,21 @@ impl AstralaneClient {
|
||||
AstralaneBackend::Quic(quic) => {
|
||||
if let Err(e) = quic.send_transaction(&body_bytes).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Astralane", trade_type, start_time.elapsed(), &e);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Astralane", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,14 +248,26 @@ impl AstralaneClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "Astralane", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Astralane", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ impl AstralaneQuicClient {
|
||||
"lit.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)]
|
||||
}
|
||||
_ => {
|
||||
Vec::new()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +130,8 @@ impl AstralaneQuicClient {
|
||||
/// Generates a self-signed TLS certificate with the API key as the Common Name (CN).
|
||||
pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let candidates = Self::resolve_server_candidates(server_addr)
|
||||
.context("Invalid server address")?;
|
||||
let candidates =
|
||||
Self::resolve_server_candidates(server_addr).context("Invalid server address")?;
|
||||
let addr = candidates[0];
|
||||
|
||||
info!("[astralane-quic] Building TLS config (CN = api_key)");
|
||||
@@ -167,7 +165,8 @@ impl AstralaneQuicClient {
|
||||
}
|
||||
}
|
||||
let connection = connection_opt.ok_or_else(|| {
|
||||
last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to connect to Astralane QUIC server"))
|
||||
last_err
|
||||
.unwrap_or_else(|| anyhow::anyhow!("Failed to connect to Astralane QUIC server"))
|
||||
})?;
|
||||
|
||||
info!("[astralane-quic] Connected at {}", selected_addr);
|
||||
@@ -203,7 +202,8 @@ impl AstralaneQuicClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to reconnect to Astralane QUIC server")))
|
||||
Err(last_err
|
||||
.unwrap_or_else(|| anyhow::anyhow!("Failed to reconnect to Astralane QUIC server")))
|
||||
}
|
||||
|
||||
/// Send a single bincode-serialized `VersionedTransaction`.
|
||||
|
||||
+93
-46
@@ -5,9 +5,9 @@ use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
@@ -18,8 +18,8 @@ use crate::{common::SolanaRpcClient, constants::swqos::BLOCKRAZOR_TIP_ACCOUNTS};
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
use tonic::transport::Channel;
|
||||
use tonic::metadata::AsciiMetadataValue;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
// Include pre-generated gRPC code
|
||||
pub mod serverpb {
|
||||
@@ -46,7 +46,9 @@ impl BlockRazorGrpcClient {
|
||||
let mut request = tonic::Request::new(serverpb::HealthRequest {});
|
||||
request.metadata_mut().insert("apikey", apikey);
|
||||
|
||||
let response = client.get_health(request).await
|
||||
let response = client
|
||||
.get_health(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("gRPC health check failed: {}", e))?;
|
||||
Ok(response.into_inner().status)
|
||||
}
|
||||
@@ -75,7 +77,9 @@ impl BlockRazorGrpcClient {
|
||||
});
|
||||
request.metadata_mut().insert("apikey", apikey);
|
||||
|
||||
let response = client.send_transaction(request).await
|
||||
let response = client
|
||||
.send_transaction(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("gRPC send transaction failed: {}", e))?;
|
||||
Ok(response.into_inner().signature)
|
||||
}
|
||||
@@ -151,7 +155,12 @@ impl BlockRazorClient {
|
||||
Ok(Self::new_http(rpc_url, endpoint, auth_token, false))
|
||||
}
|
||||
|
||||
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Result<Self> {
|
||||
pub async fn new_grpc(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
|
||||
// 配置 Channel,增加连接超时
|
||||
@@ -162,10 +171,8 @@ impl BlockRazorClient {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
|
||||
|
||||
let grpc_client = Arc::new(ArcSwap::from_pointee(BlockRazorGrpcClient::new(
|
||||
channel,
|
||||
auth_token.clone(),
|
||||
)));
|
||||
let grpc_client =
|
||||
Arc::new(ArcSwap::from_pointee(BlockRazorGrpcClient::new(channel, auth_token.clone())));
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
@@ -189,7 +196,12 @@ impl BlockRazorClient {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Self {
|
||||
pub fn new_http(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
mev_protection: bool,
|
||||
) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().user_agent("").build().unwrap();
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
@@ -279,7 +291,10 @@ impl BlockRazorClient {
|
||||
}
|
||||
Err(reconnect_err) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC reconnect failed: {}", reconnect_err);
|
||||
eprintln!(
|
||||
"BlockRazor gRPC reconnect failed: {}",
|
||||
reconnect_err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +324,8 @@ impl BlockRazorClient {
|
||||
let stop_ping = stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor HTTP ping request failed: {}", e);
|
||||
}
|
||||
@@ -320,7 +336,9 @@ impl BlockRazorClient {
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) =
|
||||
Self::send_http_ping(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor HTTP ping request failed: {}", e);
|
||||
}
|
||||
@@ -337,11 +355,7 @@ impl BlockRazorClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_http_ping(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
auth_token: &str,
|
||||
) -> Result<()> {
|
||||
async fn send_http_ping(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
|
||||
let response = http_client
|
||||
.post(&ping_url)
|
||||
@@ -380,56 +394,73 @@ impl BlockRazorClient {
|
||||
let start_time = Instant::now();
|
||||
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc {
|
||||
grpc_client,
|
||||
mev_protection,
|
||||
..
|
||||
} => {
|
||||
BlockRazorBackend::Grpc { grpc_client, mev_protection, .. } => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// 使用 load() 无锁获取客户端引用
|
||||
let client = grpc_client.load();
|
||||
let signature = client.send_transaction(
|
||||
content,
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revert_protection is unrelated to MEV; keep false.
|
||||
if *mev_protection { "sandwichMitigation".to_string() } else { "fast".to_string() },
|
||||
None,
|
||||
false,
|
||||
).await;
|
||||
let signature = client
|
||||
.send_transaction(
|
||||
content,
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revert_protection is unrelated to MEV; keep false.
|
||||
if *mev_protection {
|
||||
"sandwichMitigation".to_string()
|
||||
} else {
|
||||
"fast".to_string()
|
||||
},
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
match signature {
|
||||
Ok(sig) => {
|
||||
if !sig.is_empty() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("BlockRazor", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("BlockRazor", trade_type, start_time.elapsed(), "empty signature".to_string());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"empty signature".to_string(),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("BlockRazor gRPC returned empty signature"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor gRPC returned empty signature"
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("BlockRazor", trade_type, start_time.elapsed(), format!("gRPC error: {}", e));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("gRPC error: {}", e),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("BlockRazor gRPC sendTransaction failed: {}", e));
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor gRPC sendTransaction failed: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
BlockRazorBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
mev_protection,
|
||||
..
|
||||
endpoint, auth_token, http_client, mev_protection, ..
|
||||
} => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let mut query_params: Vec<(&str, &str)> = vec![
|
||||
let query_params: Vec<(&str, &str)> = vec![
|
||||
("auth", auth_token.as_str()),
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revertProtection is unrelated to MEV; not set.
|
||||
@@ -448,12 +479,21 @@ impl BlockRazorClient {
|
||||
if status.is_success() {
|
||||
let _ = response.bytes().await;
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("blockrazor", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("blockrazor", trade_type, start_time.elapsed(), format!("status {} body: {}", status, body));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {} body: {}", status, body),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor HTTP sendTransaction failed: status {} body: {}",
|
||||
@@ -485,7 +525,13 @@ impl BlockRazorClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "blockrazor", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -495,7 +541,8 @@ impl BlockRazorClient {
|
||||
impl Drop for BlockRazorClient {
|
||||
fn drop(&mut self) {
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. } | BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. }
|
||||
| BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
let ping_handle = ping_handle.clone();
|
||||
|
||||
+30
-5
@@ -100,13 +100,27 @@ impl BloxrouteClient {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("bloxroute", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [bloxroute] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [bloxroute] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("bloxroute", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -128,7 +142,13 @@ impl BloxrouteClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "bloxroute", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -170,7 +190,12 @@ impl BloxrouteClient {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" bloxroute {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" bloxroute {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-4
@@ -97,12 +97,26 @@ impl FlashBlockClient {
|
||||
// Parse response
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("success").is_some() || response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("FlashBlock", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [FlashBlock] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [FlashBlock] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("FlashBlock", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -122,7 +136,13 @@ impl FlashBlockClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "FlashBlock", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+28
-5
@@ -107,7 +107,10 @@ impl HeliusClient {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [helius] {} submission failed after {:?} status={} body={}",
|
||||
trade_type, start_time.elapsed(), status, response_text
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
status,
|
||||
response_text
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -124,15 +127,29 @@ impl HeliusClient {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("helius", trade_type, start_time.elapsed(), err_msg);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
err_msg,
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("Helius Sender error: {}", err_msg));
|
||||
}
|
||||
if response_json.get("result").is_some() && crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("helius", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("helius", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
@@ -152,7 +169,13 @@ impl HeliusClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "helius", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+37
-6
@@ -105,12 +105,26 @@ impl JitoClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("jito", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [jito] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [jito] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("jito", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -118,13 +132,25 @@ impl JitoClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "jito", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "jito", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -171,7 +197,12 @@ impl JitoClient {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" jito {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" jito {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" jito {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-4
@@ -103,12 +103,26 @@ impl LightspeedClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("lightspeed", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("lightspeed", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("lightspeed", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -128,7 +142,13 @@ impl LightspeedClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "lightspeed", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+30
-16
@@ -30,17 +30,16 @@ use crate::{
|
||||
constants::swqos::{
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
|
||||
SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL,
|
||||
SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR,
|
||||
SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK,
|
||||
SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED,
|
||||
SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_SOYAS,
|
||||
SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL,
|
||||
SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX,
|
||||
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO,
|
||||
SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||
SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
|
||||
SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
|
||||
SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
|
||||
SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1,
|
||||
SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM,
|
||||
SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
},
|
||||
swqos::{
|
||||
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
|
||||
@@ -411,15 +410,30 @@ impl SwqosConfig {
|
||||
SwqosConfig::BlockRazor(auth_token, region, url, transport) => {
|
||||
// BlockRazor: transport=None 或 transport=Grpc 时使用 gRPC,transport=Http 时使用 HTTP
|
||||
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport, mev_protection);
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::BlockRazor,
|
||||
region,
|
||||
url,
|
||||
transport,
|
||||
mev_protection,
|
||||
);
|
||||
if use_http {
|
||||
let blockrazor_client =
|
||||
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection);
|
||||
let blockrazor_client = BlockRazorClient::new_http(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
} else {
|
||||
// 使用 gRPC 模式(默认或用户明确指定了 gRPC)
|
||||
let blockrazor_client =
|
||||
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection).await?;
|
||||
let blockrazor_client = BlockRazorClient::new_grpc(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token,
|
||||
mev_protection,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
}
|
||||
}
|
||||
|
||||
+24
-4
@@ -99,12 +99,26 @@ impl NextBlockClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("nextblock", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nextblock", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nextblock", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -124,7 +138,13 @@ impl NextBlockClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "nextblock", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+24
-4
@@ -184,13 +184,27 @@ impl Node1Client {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("node1", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [node1] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [node1] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("node1", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -212,7 +226,13 @@ impl Node1Client {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "node1", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+38
-15
@@ -73,13 +73,14 @@ impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
}
|
||||
|
||||
/// TLS 客户端证书:ECDSA P-256 + CN=钱包公钥(与 Speedlanding / Astralane QUIC 策略一致)。
|
||||
fn generate_client_tls_credentials(keypair: &Keypair) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
|
||||
fn generate_client_tls_credentials(
|
||||
keypair: &Keypair,
|
||||
) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
|
||||
let tls_key = RcgenKeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||
let mut cert_params = CertificateParams::new(vec![])?;
|
||||
cert_params.distinguished_name.push(
|
||||
rcgen::DnType::CommonName,
|
||||
rcgen::DnValue::Utf8String(keypair.pubkey().to_string()),
|
||||
);
|
||||
cert_params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, rcgen::DnValue::Utf8String(keypair.pubkey().to_string()));
|
||||
let cert = cert_params.self_signed(&tls_key)?;
|
||||
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(tls_key.serialize_der()));
|
||||
@@ -103,11 +104,11 @@ pub struct SoyasClient {
|
||||
impl SoyasClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Soyas api_token 无法解析为 Solana keypair base58(QUIC mTLS 用): {}",
|
||||
e
|
||||
)
|
||||
let keypair_bytes = bs58::decode(api_key.trim()).into_vec().map_err(|e| {
|
||||
anyhow::anyhow!("Soyas api_token base58 解码失败(QUIC mTLS 用): {}", e)
|
||||
})?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Soyas api_token 无法解析为 Solana keypair(QUIC mTLS 用): {}", e)
|
||||
})?;
|
||||
let (cert, key) = generate_client_tls_credentials(&keypair)?;
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
@@ -176,13 +177,23 @@ impl SwqosClientTrait for SoyasClient {
|
||||
let connection = self.connection.load_full();
|
||||
if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Soyas", trade_type, start_time.elapsed(), "reconnecting");
|
||||
}
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"reconnecting",
|
||||
);
|
||||
}
|
||||
self.reconnect().await?;
|
||||
let connection = self.connection.load_full();
|
||||
if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Soyas", trade_type, start_time.elapsed(), &e);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
@@ -196,14 +207,26 @@ impl SwqosClientTrait for SoyasClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "Soyas", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Soyas", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+12
-11
@@ -48,11 +48,11 @@ impl SpeedlandingClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
// Speedlanding QUIC:与官方一致使用 `solana_tls_utils::new_dummy_x509_certificate`(Ed25519 dummy cert)+ SNI `speed-landing`。
|
||||
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Speedlanding api_token 无法解析为 Solana keypair base58(用于 mTLS);请确认粘贴的是机器人提供的密钥而非其它字符串: {}",
|
||||
e
|
||||
)
|
||||
let keypair_bytes = bs58::decode(api_key.trim()).into_vec().map_err(|e| {
|
||||
anyhow::anyhow!("Speedlanding api_token base58 解码失败(mTLS 用): {}", e)
|
||||
})?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Speedlanding api_token 无法解析为 Solana keypair(mTLS 用): {}", e)
|
||||
})?;
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
@@ -112,11 +112,8 @@ impl SpeedlandingClient {
|
||||
let _guard = self.reconnect.lock().await;
|
||||
let current = self.connection.load_full();
|
||||
if current.close_reason().is_some() {
|
||||
let connecting = self.endpoint.connect_with(
|
||||
self.client_config.clone(),
|
||||
self.addr,
|
||||
SPEED_SERVER,
|
||||
)?;
|
||||
let connecting =
|
||||
self.endpoint.connect_with(self.client_config.clone(), self.addr, SPEED_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC reconnect timeout")?
|
||||
@@ -170,7 +167,11 @@ impl SwqosClientTrait for SpeedlandingClient {
|
||||
match send_result.context("Speedlanding QUIC send timeout") {
|
||||
Ok(Ok(())) => {
|
||||
// 提交结果与「详细耗时/SDK 开关」无关,便于确认当前通道确实在执行
|
||||
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
|
||||
+24
-4
@@ -168,13 +168,27 @@ impl StelliumClient {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Stellium", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Stellium", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Stellium", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -196,7 +210,13 @@ impl StelliumClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Stellium", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+24
-4
@@ -209,12 +209,26 @@ impl TemporalClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("nozomi", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nozomi", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nozomi", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -232,7 +246,13 @@ impl TemporalClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "nozomi", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+79
-20
@@ -1,12 +1,10 @@
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation,
|
||||
};
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
|
||||
use bincode;
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant, time::Duration};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{sync::Arc, time::Duration, time::Instant};
|
||||
use tokio::task::JoinHandle;
|
||||
use bincode;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
@@ -101,7 +99,8 @@ impl ZeroSlotClient {
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("0slot ping request failed: {}", e);
|
||||
}
|
||||
@@ -176,41 +175,89 @@ impl ZeroSlotClient {
|
||||
200 => {
|
||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if json_value.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("0slot", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(error) = json_value.get("error") {
|
||||
let code = error.get("code")
|
||||
let code = error
|
||||
.get("code")
|
||||
.and_then(|c| c.as_i64())
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let message = error.get("message")
|
||||
let message = error
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("code {}: {}", code, message));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("code {}: {}", code, message),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx error: {}", message));
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("unexpected JSON: {}", response_text));
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx unexpected JSON: {}", response_text));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("unexpected JSON: {}", response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"0slot Binary-Tx unexpected JSON: {}",
|
||||
response_text
|
||||
));
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("invalid JSON: {}", response_text));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("invalid JSON: {}", response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx invalid JSON: {}", response_text));
|
||||
}
|
||||
}
|
||||
403 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text.clone());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text.clone(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot API key error: {}", response_text));
|
||||
}
|
||||
419 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text.clone());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text.clone(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot rate limit exceeded"));
|
||||
}
|
||||
500 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), "submission failed".to_string());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"submission failed".to_string(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot transaction submission failed"));
|
||||
}
|
||||
_ => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("status {} body: {}", status, response_text));
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx failed with status {}: {}", status, response_text));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {} body: {}", status, response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"0slot Binary-Tx failed with status {}: {}",
|
||||
status,
|
||||
response_text
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,13 +269,25 @@ impl ZeroSlotClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "0slot", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "0slot", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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,15 +1,16 @@
|
||||
use anyhow::anyhow;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey,
|
||||
signature::Keypair, signer::Signer, transaction::VersionedTransaction,
|
||||
};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer,
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
|
||||
use crate::{
|
||||
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
|
||||
common::nonce_cache::DurableNonceInfo,
|
||||
trading::{
|
||||
core::transaction_pool::{acquire_builder, release_builder},
|
||||
MiddlewareManager,
|
||||
@@ -26,11 +27,10 @@ fn sol_f64_to_lamports(sol: f64) -> u64 {
|
||||
(lamports.min(u64::MAX as f64)).round() as u64
|
||||
}
|
||||
|
||||
/// Build standard RPC transaction (worker hot path).
|
||||
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/rpc/middleware.
|
||||
pub async fn build_transaction(
|
||||
/// Build signed transaction (worker hot path, no RPC).
|
||||
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/middleware.
|
||||
pub fn build_transaction(
|
||||
payer: &Arc<Keypair>,
|
||||
_rpc: Option<&Arc<SolanaRpcClient>>,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
business_instructions: &[Instruction],
|
||||
@@ -74,10 +74,9 @@ pub async fn build_transaction(
|
||||
protocol_name,
|
||||
is_buy,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_versioned_transaction(
|
||||
fn build_versioned_transaction(
|
||||
payer: &Arc<Keypair>,
|
||||
instructions: Vec<Instruction>,
|
||||
address_lookup_table_account: Option<&AddressLookupTableAccount>,
|
||||
@@ -95,19 +94,19 @@ async fn build_versioned_transaction(
|
||||
// 使用预分配的交易构建器以降低延迟
|
||||
let mut builder = acquire_builder();
|
||||
|
||||
let versioned_msg = builder.build_zero_alloc(
|
||||
let build_result = builder.build_zero_alloc(
|
||||
&payer.pubkey(),
|
||||
&full_instructions,
|
||||
address_lookup_table_account,
|
||||
blockhash,
|
||||
);
|
||||
release_builder(builder);
|
||||
let versioned_msg = build_result?;
|
||||
|
||||
let msg_bytes = versioned_msg.serialize();
|
||||
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
|
||||
let signature =
|
||||
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
|
||||
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
|
||||
|
||||
// 归还构建器到池
|
||||
release_builder(builder);
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ pub async fn transfer_sol(
|
||||
return Err(anyhow!("Insufficient balance"));
|
||||
}
|
||||
|
||||
let transfer_instruction = system_instruction::transfer(&payer.pubkey(), receive_wallet, amount);
|
||||
let transfer_instruction =
|
||||
system_instruction::transfer(&payer.pubkey(), receive_wallet, amount);
|
||||
|
||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::common::{
|
||||
spl_token::close_account,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use solana_sdk::{instruction::Instruction, instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_sdk::{instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
|
||||
#[inline]
|
||||
|
||||
+314
-105
@@ -14,6 +14,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
@@ -21,7 +22,6 @@ use solana_sdk::{
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::{
|
||||
str::FromStr,
|
||||
@@ -35,8 +35,8 @@ use fnv::FnvHasher;
|
||||
type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
|
||||
|
||||
use crate::{
|
||||
common::nonce_cache::DurableNonceInfo,
|
||||
common::{GasFeeStrategy, SolanaRpcClient},
|
||||
common::gas_fee_strategy::{GasFeeStrategyType, GasFeeStrategyValue},
|
||||
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SwqosSubmitTiming},
|
||||
swqos::{SwqosClient, SwqosType, TradeType},
|
||||
trading::core::params::SenderConcurrencyConfig,
|
||||
trading::{common::build_transaction, MiddlewareManager},
|
||||
@@ -51,7 +51,6 @@ const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
|
||||
struct SwqosSharedContext {
|
||||
payer: Arc<Keypair>,
|
||||
instructions: Arc<Vec<Instruction>>,
|
||||
rpc: Option<Arc<SolanaRpcClient>>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
durable_nonce: Option<DurableNonceInfo>,
|
||||
@@ -72,6 +71,7 @@ struct SwqosJob {
|
||||
tip_account: Arc<Pubkey>,
|
||||
swqos_client: Arc<SwqosClient>,
|
||||
swqos_type: SwqosType,
|
||||
strategy_type: GasFeeStrategyType,
|
||||
core_id: Option<core_affinity::CoreId>,
|
||||
use_affinity: bool,
|
||||
}
|
||||
@@ -88,7 +88,6 @@ async fn run_one_swqos_job(job: SwqosJob) {
|
||||
|
||||
let transaction = match build_transaction(
|
||||
&s.payer,
|
||||
s.rpc.as_ref(),
|
||||
job.unit_limit,
|
||||
job.unit_price,
|
||||
s.instructions.as_ref(),
|
||||
@@ -101,9 +100,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
|
||||
&job.tip_account,
|
||||
tip_amount,
|
||||
s.durable_nonce.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
) {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
s.collector.submit(TaskResult {
|
||||
@@ -111,6 +108,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
|
||||
signature: Signature::default(),
|
||||
error: Some(e),
|
||||
swqos_type: job.swqos_type,
|
||||
strategy_type: job.strategy_type,
|
||||
landed_on_chain: false,
|
||||
submit_done_us: crate::common::clock::now_micros(),
|
||||
});
|
||||
@@ -140,6 +138,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
|
||||
signature: sig,
|
||||
error: err,
|
||||
swqos_type: job.swqos_type,
|
||||
strategy_type: job.strategy_type,
|
||||
landed_on_chain,
|
||||
submit_done_us: crate::common::clock::now_micros(),
|
||||
});
|
||||
@@ -157,47 +156,115 @@ async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>, notify: Arc<Notify>
|
||||
|
||||
static SWQOS_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
|
||||
static SWQOS_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
|
||||
static SWQOS_WORKERS_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
static SWQOS_WORKER_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Dedicated OS-thread sender pool. Queue and notify are in OnceCell so hot path never takes a lock after init.
|
||||
static DEDICATED_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
|
||||
static DEDICATED_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
|
||||
static DEDICATED_WORKER_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
/// JoinHandles kept so dedicated threads are not detached; only touched during init under lock.
|
||||
static DEDICATED_INIT: Mutex<Option<Vec<std::thread::JoinHandle<()>>>> = Mutex::new(None);
|
||||
|
||||
fn desired_dedicated_workers(
|
||||
sender_thread_cores: Option<&[usize]>,
|
||||
max_sender_concurrency: usize,
|
||||
) -> usize {
|
||||
sender_thread_cores
|
||||
.map(|v| v.len().min(max_sender_concurrency))
|
||||
.unwrap_or_else(|| SWQOS_DEDICATED_DEFAULT_THREADS.min(max_sender_concurrency))
|
||||
.min(32)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn dedicated_core_ids(
|
||||
sender_thread_cores: Option<&[usize]>,
|
||||
n: usize,
|
||||
) -> Vec<core_affinity::CoreId> {
|
||||
core_affinity::get_core_ids()
|
||||
.map(|all_ids| {
|
||||
sender_thread_cores
|
||||
.map(|indices| {
|
||||
indices.iter().take(n).filter_map(|&i| all_ids.get(i).cloned()).collect()
|
||||
})
|
||||
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn ensure_dedicated_pool(
|
||||
sender_thread_cores: Option<&[usize]>,
|
||||
max_sender_concurrency: usize,
|
||||
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
|
||||
let target_workers = desired_dedicated_workers(sender_thread_cores, max_sender_concurrency);
|
||||
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
|
||||
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) >= target_workers {
|
||||
return (q.clone(), n.clone());
|
||||
}
|
||||
ensure_dedicated_worker_count(q.clone(), n.clone(), sender_thread_cores, target_workers);
|
||||
return (q.clone(), n.clone());
|
||||
}
|
||||
let mut guard = DEDICATED_INIT.lock();
|
||||
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
|
||||
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) < target_workers {
|
||||
ensure_dedicated_worker_count_locked(
|
||||
q.clone(),
|
||||
n.clone(),
|
||||
sender_thread_cores,
|
||||
target_workers,
|
||||
&mut guard,
|
||||
);
|
||||
}
|
||||
return (q.clone(), n.clone());
|
||||
}
|
||||
let n = sender_thread_cores
|
||||
.map(|v| v.len().min(max_sender_concurrency))
|
||||
.unwrap_or_else(|| SWQOS_DEDICATED_DEFAULT_THREADS.min(max_sender_concurrency))
|
||||
.min(32)
|
||||
.max(1);
|
||||
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
|
||||
let notify = Arc::new(Notify::new());
|
||||
let core_ids: Vec<core_affinity::CoreId> = core_affinity::get_core_ids()
|
||||
.map(|all_ids| {
|
||||
sender_thread_cores
|
||||
.map(|indices| {
|
||||
indices
|
||||
.iter()
|
||||
.take(n)
|
||||
.filter_map(|&i| all_ids.get(i).cloned())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut handles = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let _ = DEDICATED_QUEUE.set(queue.clone());
|
||||
let _ = DEDICATED_NOTIFY.set(notify.clone());
|
||||
*guard = Some(Vec::with_capacity(target_workers));
|
||||
ensure_dedicated_worker_count_locked(
|
||||
queue.clone(),
|
||||
notify.clone(),
|
||||
sender_thread_cores,
|
||||
target_workers,
|
||||
&mut guard,
|
||||
);
|
||||
(queue, notify)
|
||||
}
|
||||
|
||||
fn ensure_dedicated_worker_count(
|
||||
queue: Arc<ArrayQueue<SwqosJob>>,
|
||||
notify: Arc<Notify>,
|
||||
sender_thread_cores: Option<&[usize]>,
|
||||
target_workers: usize,
|
||||
) {
|
||||
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) >= target_workers {
|
||||
return;
|
||||
}
|
||||
let mut guard = DEDICATED_INIT.lock();
|
||||
ensure_dedicated_worker_count_locked(
|
||||
queue,
|
||||
notify,
|
||||
sender_thread_cores,
|
||||
target_workers,
|
||||
&mut guard,
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_dedicated_worker_count_locked(
|
||||
queue: Arc<ArrayQueue<SwqosJob>>,
|
||||
notify: Arc<Notify>,
|
||||
sender_thread_cores: Option<&[usize]>,
|
||||
target_workers: usize,
|
||||
guard: &mut Option<Vec<std::thread::JoinHandle<()>>>,
|
||||
) {
|
||||
let current = DEDICATED_WORKER_COUNT.load(Ordering::Acquire);
|
||||
if current >= target_workers {
|
||||
return;
|
||||
}
|
||||
let core_ids = dedicated_core_ids(sender_thread_cores, target_workers);
|
||||
let handles = guard.get_or_insert_with(Vec::new);
|
||||
handles.reserve(target_workers.saturating_sub(current));
|
||||
for i in current..target_workers {
|
||||
let queue = queue.clone();
|
||||
let notify = notify.clone();
|
||||
let core_id = core_ids.get(i).cloned();
|
||||
@@ -213,19 +280,23 @@ fn ensure_dedicated_pool(
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
let _ = DEDICATED_QUEUE.set(queue.clone());
|
||||
let _ = DEDICATED_NOTIFY.set(notify.clone());
|
||||
*guard = Some(handles);
|
||||
(queue, notify)
|
||||
DEDICATED_WORKER_COUNT.store(target_workers, Ordering::Release);
|
||||
}
|
||||
|
||||
fn ensure_swqos_pool(queue: Arc<ArrayQueue<SwqosJob>>, max_sender_concurrency: usize) {
|
||||
if SWQOS_WORKERS_STARTED.swap(true, Ordering::AcqRel) {
|
||||
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
|
||||
let mut current = SWQOS_WORKER_COUNT.load(Ordering::Acquire);
|
||||
while current < n {
|
||||
match SWQOS_WORKER_COUNT.compare_exchange(current, n, Ordering::AcqRel, Ordering::Acquire) {
|
||||
Ok(_) => break,
|
||||
Err(actual) => current = actual,
|
||||
}
|
||||
}
|
||||
if current >= n {
|
||||
return;
|
||||
}
|
||||
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
|
||||
let notify = SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone();
|
||||
for _ in 0..n {
|
||||
for _ in current..n {
|
||||
tokio::spawn(swqos_worker_loop(queue.clone(), notify.clone()));
|
||||
}
|
||||
}
|
||||
@@ -236,6 +307,7 @@ struct TaskResult {
|
||||
signature: Signature,
|
||||
error: Option<anyhow::Error>,
|
||||
swqos_type: SwqosType,
|
||||
strategy_type: GasFeeStrategyType,
|
||||
landed_on_chain: bool,
|
||||
/// Microsecond timestamp when this task finished (SWQOS returned); for per-SWQOS event→submit timing.
|
||||
submit_done_us: i64,
|
||||
@@ -303,7 +375,7 @@ impl ResultCollector {
|
||||
|
||||
async fn wait_for_success(
|
||||
&self,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
let start = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(5);
|
||||
let poll_interval = std::time::Duration::from_millis(1000);
|
||||
@@ -315,7 +387,7 @@ impl ResultCollector {
|
||||
let mut submit_timings = Vec::new();
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
submit_timings.push(result.submit_timing());
|
||||
if result.success {
|
||||
has_success = true;
|
||||
}
|
||||
@@ -333,7 +405,7 @@ impl ResultCollector {
|
||||
let mut submit_timings = Vec::new();
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
submit_timings.push(result.submit_timing());
|
||||
// Prefer the error from the tx that actually landed
|
||||
if result.landed_on_chain && result.error.is_some() {
|
||||
landed_error = result.error;
|
||||
@@ -352,7 +424,7 @@ impl ResultCollector {
|
||||
let mut submit_timings = Vec::new();
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
submit_timings.push(result.submit_timing());
|
||||
if result.success {
|
||||
any_success = true;
|
||||
}
|
||||
@@ -375,7 +447,7 @@ impl ResultCollector {
|
||||
|
||||
fn get_first(
|
||||
&self,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
let mut signatures = Vec::new();
|
||||
let mut has_success = false;
|
||||
let mut last_error = None;
|
||||
@@ -383,7 +455,7 @@ impl ResultCollector {
|
||||
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
submit_timings.push(result.submit_timing());
|
||||
if result.success {
|
||||
has_success = true;
|
||||
}
|
||||
@@ -399,12 +471,35 @@ 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 a short submit 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 耗时。
|
||||
#[allow(dead_code)]
|
||||
async fn wait_for_all_submitted(
|
||||
&self,
|
||||
timeout_secs: u64,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
let start = Instant::now();
|
||||
let primary = Duration::from_secs(timeout_secs);
|
||||
let poll_interval = Duration::from_millis(2);
|
||||
@@ -416,11 +511,9 @@ impl ResultCollector {
|
||||
}
|
||||
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
|
||||
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
|
||||
let all_submitted =
|
||||
self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
|
||||
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
|
||||
if all_submitted {
|
||||
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
|
||||
tokio::time::sleep(Duration::from_millis(35)).await;
|
||||
tokio::task::yield_now().await;
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
@@ -435,13 +528,69 @@ impl ResultCollector {
|
||||
}
|
||||
}
|
||||
|
||||
type GasFeeConfig = (SwqosType, GasFeeStrategyType, GasFeeStrategyValue);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct SwqosTaskConfig {
|
||||
task_ordinal: usize,
|
||||
swqos_index: usize,
|
||||
gas_fee_config: GasFeeConfig,
|
||||
}
|
||||
|
||||
impl TaskResult {
|
||||
#[inline]
|
||||
fn submit_timing(&self) -> SwqosSubmitTiming {
|
||||
SwqosSubmitTiming {
|
||||
swqos_type: self.swqos_type,
|
||||
strategy_type: self.strategy_type,
|
||||
submit_done_us: self.submit_done_us,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_swqos_task_configs(
|
||||
swqos_types: &[SwqosType],
|
||||
gas_fee_configs: &[GasFeeConfig],
|
||||
with_tip: bool,
|
||||
check_min_tip: bool,
|
||||
min_tip_by_swqos: impl Fn(SwqosType) -> f64,
|
||||
) -> Vec<SwqosTaskConfig> {
|
||||
let mut task_configs = Vec::with_capacity(swqos_types.len() * 3);
|
||||
for (i, swqos_type) in swqos_types.iter().copied().enumerate() {
|
||||
if !with_tip && !matches!(swqos_type, SwqosType::Default) {
|
||||
continue;
|
||||
}
|
||||
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
|
||||
let min_tip = if check_tip { min_tip_by_swqos(swqos_type) } else { 0.0 };
|
||||
for config in gas_fee_configs {
|
||||
if config.0 != swqos_type {
|
||||
continue;
|
||||
}
|
||||
if check_tip && config.2.tip < min_tip {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
||||
config.0, config.2.tip, min_tip
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
task_configs.push(SwqosTaskConfig {
|
||||
task_ordinal: task_configs.len(),
|
||||
swqos_index: i,
|
||||
gas_fee_config: *config,
|
||||
});
|
||||
}
|
||||
}
|
||||
task_configs
|
||||
}
|
||||
|
||||
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
|
||||
///
|
||||
/// `sender_config` merges sender_thread_cores, effective_core_ids, max_sender_concurrency (precomputed at SDK init; no get_core_ids on hot path).
|
||||
pub async fn execute_parallel(
|
||||
swqos_clients: &[Arc<SwqosClient>],
|
||||
payer: Arc<Keypair>,
|
||||
rpc: Option<&Arc<SolanaRpcClient>>,
|
||||
instructions: Vec<Instruction>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
@@ -455,7 +604,7 @@ pub async fn execute_parallel(
|
||||
use_dedicated_sender_threads: bool,
|
||||
sender_config: SenderConcurrencyConfig,
|
||||
check_min_tip: bool,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
if swqos_clients.is_empty() {
|
||||
return Err(anyhow!("swqos_clients is empty"));
|
||||
}
|
||||
@@ -472,56 +621,38 @@ pub async fn execute_parallel(
|
||||
let instructions = Arc::new(instructions);
|
||||
|
||||
// One get_strategies call per batch (avoid N calls in loop).
|
||||
let gas_fee_configs = gas_fee_strategy.get_strategies(if is_buy {
|
||||
TradeType::Buy
|
||||
} else {
|
||||
TradeType::Sell
|
||||
});
|
||||
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
|
||||
for (i, swqos_client) in swqos_clients.iter().enumerate() {
|
||||
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
|
||||
continue;
|
||||
}
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
|
||||
let min_tip = if check_tip { swqos_client.min_tip_sol() } else { 0.0 };
|
||||
for config in &gas_fee_configs {
|
||||
if config.0 != swqos_type {
|
||||
continue;
|
||||
}
|
||||
if check_tip {
|
||||
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
||||
config.0, config.2.tip, min_tip
|
||||
);
|
||||
}
|
||||
if config.2.tip < min_tip {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
task_configs.push((i, swqos_client.clone(), *config));
|
||||
}
|
||||
}
|
||||
let gas_fee_configs =
|
||||
gas_fee_strategy.get_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
|
||||
let swqos_types: Vec<SwqosType> =
|
||||
swqos_clients.iter().map(|swqos| swqos.get_swqos_type()).collect();
|
||||
let selected_task_configs = select_swqos_task_configs(
|
||||
&swqos_types,
|
||||
&gas_fee_configs,
|
||||
with_tip,
|
||||
check_min_tip,
|
||||
|swqos_type| {
|
||||
swqos_clients
|
||||
.iter()
|
||||
.find(|swqos| swqos.get_swqos_type() == swqos_type)
|
||||
.map(|swqos| swqos.min_tip_sol())
|
||||
.unwrap_or(0.0)
|
||||
},
|
||||
);
|
||||
|
||||
if task_configs.is_empty() {
|
||||
if selected_task_configs.is_empty() {
|
||||
return Err(anyhow!("No available gas fee strategy configs"));
|
||||
}
|
||||
|
||||
if is_buy && task_configs.len() > 1 && durable_nonce.is_none() {
|
||||
if is_buy && selected_task_configs.len() > 1 && durable_nonce.is_none() {
|
||||
return Err(anyhow!("Multiple swqos transactions require durable_nonce to be set.",));
|
||||
}
|
||||
|
||||
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
|
||||
let channel_count = task_configs.len().max(1);
|
||||
let channel_count = selected_task_configs.len().max(1);
|
||||
let collector = Arc::new(ResultCollector::new(channel_count));
|
||||
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
|
||||
let submit_timeout_secs: u64 =
|
||||
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
|
||||
let shared = Arc::new(SwqosSharedContext {
|
||||
payer,
|
||||
instructions,
|
||||
rpc: rpc.cloned(),
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
durable_nonce,
|
||||
@@ -548,10 +679,15 @@ pub async fn execute_parallel(
|
||||
let effective_core_ids = sender_config.effective_core_ids.as_slice();
|
||||
let core_len = effective_core_ids.len().max(1);
|
||||
let mut tip_cache: FnvHashMap<*const (), Arc<Pubkey>> =
|
||||
FnvHashMap::with_capacity_and_hasher(task_configs.len(), BuildHasherDefault::default());
|
||||
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
|
||||
let core_id = effective_core_ids.get(i % core_len).copied();
|
||||
FnvHashMap::with_capacity_and_hasher(
|
||||
selected_task_configs.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
for task_config in selected_task_configs {
|
||||
let swqos_client = swqos_clients[task_config.swqos_index].clone();
|
||||
let core_id = effective_core_ids.get(task_config.task_ordinal % core_len).copied();
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let gas_fee_strategy_config = task_config.gas_fee_config;
|
||||
let key = Arc::as_ptr(&swqos_client) as *const ();
|
||||
let tip_account = match tip_cache.get(&key) {
|
||||
Some(t) => t.clone(),
|
||||
@@ -575,10 +711,21 @@ pub async fn execute_parallel(
|
||||
tip_account,
|
||||
swqos_client,
|
||||
swqos_type,
|
||||
strategy_type: gas_fee_strategy_config.1,
|
||||
core_id,
|
||||
use_affinity: !effective_core_ids.is_empty(),
|
||||
};
|
||||
let _ = queue.push(job);
|
||||
if let Err(job) = queue.push(job) {
|
||||
shared.collector.submit(TaskResult {
|
||||
success: false,
|
||||
signature: Signature::default(),
|
||||
error: Some(anyhow!("SWQOS sender queue is full")),
|
||||
swqos_type: job.swqos_type,
|
||||
strategy_type: job.strategy_type,
|
||||
landed_on_chain: false,
|
||||
submit_done_us: crate::common::clock::now_micros(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,20 +734,12 @@ 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 = collector.wait_for_first_submitted(Duration::from_millis(500)).await.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
Some(anyhow!("No SWQOS result within fast submit window")),
|
||||
vec![],
|
||||
));
|
||||
let (success, signatures, last_error, submit_timings) = ret;
|
||||
return Ok((success, signatures, last_error, submit_timings));
|
||||
}
|
||||
@@ -612,3 +751,73 @@ pub async fn execute_parallel(
|
||||
Err(anyhow!("All transactions failed"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn value(cu_price: u64, tip: f64) -> GasFeeStrategyValue {
|
||||
GasFeeStrategyValue { cu_limit: 100_000, cu_price, tip }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_task_configs_keeps_two_fee_lanes_per_swqos() {
|
||||
let swqos_types = [SwqosType::Jito, SwqosType::Helius];
|
||||
let configs = [
|
||||
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
|
||||
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
|
||||
(SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
|
||||
(SwqosType::Helius, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
|
||||
];
|
||||
|
||||
let selected = select_swqos_task_configs(&swqos_types, &configs, true, false, |_| 0.0);
|
||||
|
||||
assert_eq!(selected.len(), 4);
|
||||
assert_eq!(
|
||||
selected.iter().filter(|task| task.gas_fee_config.0 == SwqosType::Jito).count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
selected.iter().filter(|task| task.gas_fee_config.0 == SwqosType::Helius).count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
selected.iter().map(|task| task.task_ordinal).collect::<Vec<_>>(),
|
||||
vec![0, 1, 2, 3]
|
||||
);
|
||||
assert_eq!(
|
||||
selected.iter().map(|task| task.swqos_index).collect::<Vec<_>>(),
|
||||
vec![0, 0, 1, 1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_task_configs_applies_min_tip_per_lane() {
|
||||
let swqos_types = [SwqosType::Jito];
|
||||
let configs = [
|
||||
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.0001)),
|
||||
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
|
||||
];
|
||||
|
||||
let selected = select_swqos_task_configs(&swqos_types, &configs, true, true, |_| 0.001);
|
||||
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].gas_fee_config.1, GasFeeStrategyType::HighTipLowCuPrice);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_task_configs_without_tip_keeps_default_priority_fee_only() {
|
||||
let swqos_types = [SwqosType::Jito, SwqosType::Default];
|
||||
let configs = [
|
||||
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
|
||||
(SwqosType::Default, GasFeeStrategyType::Normal, value(700_000, 0.0)),
|
||||
];
|
||||
|
||||
let selected = select_swqos_task_configs(&swqos_types, &configs, false, false, |_| 0.0);
|
||||
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].gas_fee_config.0, SwqosType::Default);
|
||||
assert_eq!(selected[0].gas_fee_config.2.cu_price, 700_000);
|
||||
assert_eq!(selected[0].gas_fee_config.2.tip, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey,
|
||||
signature::Keypair, signature::Signature,
|
||||
};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -15,7 +14,7 @@ use tracing::{info, trace, warn};
|
||||
use super::{params::SwapParams, traits::InstructionBuilder};
|
||||
use crate::swqos::TradeType;
|
||||
use crate::{
|
||||
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
|
||||
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient, SwqosSubmitTiming},
|
||||
perf::syscall_bypass::SystemCallBypassManager,
|
||||
swqos::common::poll_any_transaction_confirmation,
|
||||
trading::core::{
|
||||
@@ -26,7 +25,6 @@ use crate::{
|
||||
trading::MiddlewareManager,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::swqos::{ SwqosType};
|
||||
|
||||
/// Global syscall bypass manager (reserved for future time/IO optimizations).
|
||||
/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
|
||||
@@ -57,7 +55,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
|
||||
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
|
||||
let timing_start_us: Option<i64> = if params.log_enabled {
|
||||
@@ -163,7 +161,6 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
let result = execute_parallel(
|
||||
params.swqos_clients.as_slice(),
|
||||
params.payer,
|
||||
params.rpc.as_ref(),
|
||||
final_instructions,
|
||||
params.address_lookup_table_account,
|
||||
params.recent_blockhash,
|
||||
@@ -189,7 +186,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e)), vec![]),
|
||||
};
|
||||
// submit_timings 为完成先后顺序(先完成的先 push),打印不排序、不增加延迟
|
||||
let submit_timings_ref: &[(crate::swqos::SwqosType, i64)] = submit_timings.as_slice();
|
||||
let submit_timings_ref: &[SwqosSubmitTiming] = submit_timings.as_slice();
|
||||
|
||||
let result = if need_confirm {
|
||||
let confirm_result = if let Some(rpc) = params.rpc.as_ref() {
|
||||
@@ -234,10 +231,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Ok((ok, signatures, err, submit_timings))
|
||||
|
||||
};
|
||||
|
||||
result
|
||||
@@ -262,7 +256,7 @@ async fn simulate_transaction(
|
||||
is_buy: bool,
|
||||
with_tip: bool,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
use crate::trading::common::build_transaction;
|
||||
use solana_client::rpc_config::RpcSimulateTransactionConfig;
|
||||
use solana_commitment_config::CommitmentLevel;
|
||||
@@ -286,7 +280,6 @@ async fn simulate_transaction(
|
||||
|
||||
let transaction = build_transaction(
|
||||
&payer,
|
||||
Some(&rpc),
|
||||
unit_limit,
|
||||
unit_price,
|
||||
&instructions,
|
||||
@@ -299,8 +292,7 @@ async fn simulate_transaction(
|
||||
&Pubkey::default(),
|
||||
tip,
|
||||
durable_nonce.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
// Simulate the transaction
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -358,6 +350,7 @@ async fn simulate_transaction(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::GasFeeStrategyType;
|
||||
use crate::swqos::SwqosType;
|
||||
|
||||
/// 运行 `cargo test -p sol-trade-sdk log_timing_preview -- --nocapture` 查看日志打印效果
|
||||
@@ -368,19 +361,33 @@ mod tests {
|
||||
let before_submit_ms = 15.67;
|
||||
let w = 12usize; // same as crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
println!("\n--- 1. 构建指令耗时 / 提交前耗时(各打印一次,统一 ms,保留 4 位小数)---\n");
|
||||
println!(" [SDK][{:width$}] {} build_instructions: {:.4} ms", "-", dir, build_ms, width = w);
|
||||
println!(" [SDK][{:width$}] {} before_submit: {:.4} ms", "-", dir, before_submit_ms, width = w);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
build_ms,
|
||||
width = w
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
before_submit_ms,
|
||||
width = w
|
||||
);
|
||||
|
||||
println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
|
||||
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
|
||||
(SwqosType::Jito, 45.12, 83.38, 128.50),
|
||||
(SwqosType::Helius, 52.30, 76.20, 128.50),
|
||||
(SwqosType::ZeroSlot, 48.90, 79.60, 128.50),
|
||||
for (swqos_type, strategy_type, submit_ms, confirmed_ms, total_ms) in [
|
||||
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 45.12, 83.38, 128.50),
|
||||
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 46.08, 82.42, 128.50),
|
||||
(SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, 52.30, 76.20, 128.50),
|
||||
(SwqosType::ZeroSlot, GasFeeStrategyType::Normal, 48.90, 79.60, 128.50),
|
||||
] {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
dir,
|
||||
strategy_type.as_str(),
|
||||
submit_ms,
|
||||
confirmed_ms,
|
||||
total_ms,
|
||||
@@ -388,14 +395,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n");
|
||||
for (swqos_type, submit_ms, total_ms) in
|
||||
[(SwqosType::Jito, 44.20, 44.20), (SwqosType::Helius, 51.80, 51.80)]
|
||||
{
|
||||
println!(
|
||||
"\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n"
|
||||
);
|
||||
for (swqos_type, strategy_type, submit_ms, total_ms) in [
|
||||
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 44.20, 44.20),
|
||||
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 45.10, 45.10),
|
||||
(SwqosType::Helius, GasFeeStrategyType::Normal, 51.80, 51.80),
|
||||
] {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
dir,
|
||||
strategy_type.as_str(),
|
||||
submit_ms,
|
||||
total_ms,
|
||||
width = w
|
||||
@@ -403,8 +415,20 @@ mod tests {
|
||||
}
|
||||
|
||||
println!("\n--- 4. Simulate 模式(build/before_submit 仍从 grpc_recv_us 起算)---\n");
|
||||
println!(" [SDK][{:width$}] {} build_instructions: {:.4} ms", "-", dir, build_ms, width = w);
|
||||
println!(" [SDK][{:width$}] {} before_submit: {:.4} ms", "-", dir, before_submit_ms, width = w);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
build_ms,
|
||||
width = w
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
before_submit_ms,
|
||||
width = w
|
||||
);
|
||||
println!(" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms", "-", dir, 8.50, width = w);
|
||||
println!(" [SDK][{:width$}] {} total: {:.4} ms", "-", dir, 36.51, width = w);
|
||||
println!();
|
||||
|
||||
@@ -75,6 +75,8 @@ pub struct SwapParams {
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
/// Fixed output amount. For protocols with exact-out instructions this selects exact-out
|
||||
/// semantics and treats `input_amount` as the maximum input budget.
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
pub simulate: bool,
|
||||
@@ -97,6 +99,9 @@ pub struct SwapParams {
|
||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||
pub use_exact_sol_amount: Option<bool>,
|
||||
/// Use PumpFun V2 instructions (buy_v2 / sell_v2 / buy_exact_quote_in_v2, 27/26-account metas, quote_mint support).
|
||||
/// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface.
|
||||
pub use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl SwapParams {
|
||||
|
||||
@@ -12,6 +12,11 @@ pub struct MeteoraDammV2Params {
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
pub referral_token_account: Option<Pubkey>,
|
||||
/// `swap2` mode: 0 exact-in, 1 partial-fill (recommended default), 2 exact-out.
|
||||
pub swap_mode: u8,
|
||||
/// Include the instructions sysvar remaining account when the pool's rate limiter applies.
|
||||
pub include_rate_limiter_sysvar: bool,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
@@ -32,18 +37,35 @@ impl MeteoraDammV2Params {
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
referral_token_account: None,
|
||||
swap_mode: crate::instruction::utils::meteora_damm_v2::SWAP_MODE_PARTIAL_FILL,
|
||||
include_rate_limiter_sysvar: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_referral_token_account(mut self, referral_token_account: Pubkey) -> Self {
|
||||
self.referral_token_account = Some(referral_token_account);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_swap_mode(mut self, swap_mode: u8) -> Self {
|
||||
self.swap_mode = swap_mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_rate_limiter_sysvar(mut self, include: bool) -> Self {
|
||||
self.include_rate_limiter_sysvar = include;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data =
|
||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||
let mint_accounts = rpc
|
||||
.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint])
|
||||
.await?;
|
||||
let mint_accounts =
|
||||
rpc.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint]).await?;
|
||||
let token_a_program = mint_accounts
|
||||
.get(0)
|
||||
.and_then(|a| a.as_ref())
|
||||
@@ -62,6 +84,9 @@ impl MeteoraDammV2Params {
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
referral_token_account: None,
|
||||
swap_mode: crate::instruction::utils::meteora_damm_v2::SWAP_MODE_PARTIAL_FILL,
|
||||
include_rate_limiter_sysvar: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,28 +5,47 @@ use crate::instruction::utils::pumpfun::reconcile_mayhem_mode_for_trade;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator vault**: Pump buy/sell pass `creator_vault` = `PDA(["creator-vault", authority])`.
|
||||
/// Usually `authority` is [`BondingCurveAccount::creator`]; with **Creator Rewards Sharing** it is
|
||||
/// `fee_sharing_config_pda(mint)` (see [`fetch_fee_sharing_creator_vault_if_active`](crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active)).
|
||||
/// Keep `bonding_curve.creator` in sync with chain; ix building uses [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing).
|
||||
/// **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.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing): ix vault when it matches `PDA(creator)`, fee-sharing vault, or RPC hint.
|
||||
/// 最新一笔可观测 trade 的 **`tradeEvent.creator`(日志)**。当 `Some` 且非 default 时,
|
||||
/// **优先于** `bonding_curve.creator` 用于链下 creator-fee 询价与 `creator_vault` 在缺省 ix 时的推导。
|
||||
/// Pump 上 creator 可能随交易推进,调用方应在每次解析到带 `creator` 的 trade 后更新(如 `.with_observed_trade_creator`)。
|
||||
pub observed_trade_creator: Option<Pubkey>,
|
||||
/// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing):
|
||||
/// **显式 `creator_vault`(非 default、非 phantom)永远优先并按原值使用**,不会再用 `creator` 重算覆盖;
|
||||
/// 未传时再按 `fee_sharing_creator_vault_if_active`、`PDA(effective_creator)`(见 [`Self::effective_creator_for_trade`])。
|
||||
pub creator_vault: Pubkey,
|
||||
/// `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 时严格按该值组装,不再用 mint 字符串后缀猜测。
|
||||
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).
|
||||
/// 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 {
|
||||
@@ -38,11 +57,14 @@ impl PumpFunParams {
|
||||
Self {
|
||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
observed_trade_creator: None,
|
||||
creator_vault: creator_vault,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
fee_recipient: Pubkey::default(),
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,24 +96,30 @@ impl PumpFunParams {
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve_account.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let creator_vault_resolved =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve_account.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(
|
||||
&bonding_curve_account.creator,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: (creator != Pubkey::default()).then_some(creator),
|
||||
creator_vault: creator_vault_resolved,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,27 +157,32 @@ impl PumpFunParams {
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let creator_vault_resolved =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: (creator != Pubkey::default()).then_some(creator),
|
||||
creator_vault: creator_vault_resolved,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
@@ -176,27 +209,44 @@ impl PumpFunParams {
|
||||
&mint_account.owner,
|
||||
);
|
||||
let fee_sharing_creator_vault_if_active =
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
|
||||
.await?;
|
||||
let creator_vault = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
.or_else(|| crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator))
|
||||
.unwrap_or_default();
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc, mint,
|
||||
)
|
||||
.await?;
|
||||
let creator_vault =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: None,
|
||||
creator_vault,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
fee_recipient: Pubkey::default(),
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// 链下公式与 **`creator_vault` 推导回退**:**日志/事件 `creator`**(若已写入 `observed_trade_creator`)
|
||||
/// 优先,否则使用 `bonding_curve.creator`。
|
||||
#[inline]
|
||||
pub fn effective_creator_for_trade(&self) -> Pubkey {
|
||||
self.observed_trade_creator
|
||||
.filter(|c| *c != Pubkey::default())
|
||||
.unwrap_or(self.bonding_curve.creator)
|
||||
}
|
||||
|
||||
/// One `getAccount` on pump-fees `SharingConfig` + re-resolves [`Self::creator_vault`]. Call before sell
|
||||
/// when params come from gRPC/cache so migrated fee-sharing mints do not hit Anchor 2006.
|
||||
pub async fn refresh_fee_sharing_creator_vault_from_rpc(
|
||||
@@ -205,9 +255,11 @@ impl PumpFunParams {
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
self.fee_sharing_creator_vault_if_active =
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
|
||||
.await?;
|
||||
let c = self.bonding_curve.creator;
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc, mint,
|
||||
)
|
||||
.await?;
|
||||
let c = self.effective_creator_for_trade();
|
||||
if let Some(v) =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&c,
|
||||
@@ -221,13 +273,29 @@ impl PumpFunParams {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
|
||||
/// Sets `quote_mint` and enables v2 instructions. Required for USDC-paired coins.
|
||||
/// For SOL-paired coins, pass `WSOL_TOKEN_ACCOUNT` or leave default.
|
||||
#[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();
|
||||
self
|
||||
}
|
||||
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`Self::effective_creator_for_trade`] + resolve.
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
self
|
||||
}
|
||||
|
||||
/// 覆盖 **最新一笔 trade 日志中的 `creator`**(`tradeEvent.creator`)。`None` 或 default 会清除覆盖。
|
||||
#[inline]
|
||||
pub fn with_observed_trade_creator(mut self, c: Option<Pubkey>) -> Self {
|
||||
self.observed_trade_creator = c.filter(|x| *x != Pubkey::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// Override fee-sharing vault hint (e.g. from an off-chain indexer). `None` clears the hint.
|
||||
#[inline]
|
||||
pub fn with_fee_sharing_creator_vault_if_active(
|
||||
|
||||
@@ -16,6 +16,26 @@ pub struct RaydiumAmmV4Params {
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// AMM open orders account
|
||||
pub amm_open_orders: Pubkey,
|
||||
/// AMM target orders account
|
||||
pub amm_target_orders: Pubkey,
|
||||
/// Serum/OpenBook program used by the AMM market
|
||||
pub serum_program: Pubkey,
|
||||
/// Serum/OpenBook market account
|
||||
pub serum_market: Pubkey,
|
||||
/// Serum/OpenBook bids account
|
||||
pub serum_bids: Pubkey,
|
||||
/// Serum/OpenBook asks account
|
||||
pub serum_asks: Pubkey,
|
||||
/// Serum/OpenBook event queue account
|
||||
pub serum_event_queue: Pubkey,
|
||||
/// Serum/OpenBook coin vault account
|
||||
pub serum_coin_vault_account: Pubkey,
|
||||
/// Serum/OpenBook pc vault account
|
||||
pub serum_pc_vault_account: Pubkey,
|
||||
/// Serum/OpenBook vault signer PDA
|
||||
pub serum_vault_signer: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
@@ -32,13 +52,68 @@ impl RaydiumAmmV4Params {
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
Self {
|
||||
amm,
|
||||
coin_mint,
|
||||
pc_mint,
|
||||
token_coin,
|
||||
token_pc,
|
||||
amm_open_orders: Pubkey::default(),
|
||||
amm_target_orders: Pubkey::default(),
|
||||
serum_program: Pubkey::default(),
|
||||
serum_market: Pubkey::default(),
|
||||
serum_bids: Pubkey::default(),
|
||||
serum_asks: Pubkey::default(),
|
||||
serum_event_queue: Pubkey::default(),
|
||||
serum_coin_vault_account: Pubkey::default(),
|
||||
serum_pc_vault_account: Pubkey::default(),
|
||||
serum_vault_signer: Pubkey::default(),
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_market_accounts(
|
||||
mut self,
|
||||
amm_open_orders: Pubkey,
|
||||
amm_target_orders: Pubkey,
|
||||
serum_program: Pubkey,
|
||||
serum_market: Pubkey,
|
||||
serum_bids: Pubkey,
|
||||
serum_asks: Pubkey,
|
||||
serum_event_queue: Pubkey,
|
||||
serum_coin_vault_account: Pubkey,
|
||||
serum_pc_vault_account: Pubkey,
|
||||
serum_vault_signer: Pubkey,
|
||||
) -> Self {
|
||||
self.amm_open_orders = amm_open_orders;
|
||||
self.amm_target_orders = amm_target_orders;
|
||||
self.serum_program = serum_program;
|
||||
self.serum_market = serum_market;
|
||||
self.serum_bids = serum_bids;
|
||||
self.serum_asks = serum_asks;
|
||||
self.serum_event_queue = serum_event_queue;
|
||||
self.serum_coin_vault_account = serum_coin_vault_account;
|
||||
self.serum_pc_vault_account = serum_pc_vault_account;
|
||||
self.serum_vault_signer = serum_vault_signer;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let market_state =
|
||||
crate::instruction::utils::raydium_amm_v4::fetch_market_state(rpc, amm_info.market)
|
||||
.await?;
|
||||
let serum_vault_signer =
|
||||
crate::instruction::utils::raydium_amm_v4::derive_serum_vault_signer(
|
||||
&amm_info.serum_dex,
|
||||
&amm_info.market,
|
||||
market_state.vault_signer_nonce,
|
||||
)?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
@@ -47,6 +122,16 @@ impl RaydiumAmmV4Params {
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
amm_open_orders: amm_info.open_orders,
|
||||
amm_target_orders: amm_info.target_orders,
|
||||
serum_program: amm_info.serum_dex,
|
||||
serum_market: amm_info.market,
|
||||
serum_bids: market_state.serum_bids,
|
||||
serum_asks: market_state.serum_asks,
|
||||
serum_event_queue: market_state.serum_event_queue,
|
||||
serum_coin_vault_account: market_state.serum_coin_vault_account,
|
||||
serum_pc_vault_account: market_state.serum_pc_vault_account,
|
||||
serum_vault_signer,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::common::SwqosSubmitTiming;
|
||||
use crate::trading::SwapParams;
|
||||
use anyhow::Result;
|
||||
use solana_sdk::{instruction::Instruction, signature::Signature};
|
||||
use crate::swqos::{SwqosType};
|
||||
/// 交易执行器trait - 定义了所有交易协议都需要实现的核心方法
|
||||
#[async_trait::async_trait]
|
||||
pub trait TradeExecutor: Send + Sync {
|
||||
@@ -9,7 +9,10 @@ pub trait TradeExecutor: Send + Sync {
|
||||
/// - bool: 是否至少有一个交易成功
|
||||
/// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
|
||||
/// - Option<anyhow::Error>: 最后一个错误(如果全部失败)
|
||||
async fn swap(&self, params: SwapParams) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)>;
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)>;
|
||||
/// 获取协议名称
|
||||
fn protocol_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
@@ -17,15 +17,16 @@ 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;
|
||||
use solana_sdk::{
|
||||
hash::Hash,
|
||||
instruction::Instruction,
|
||||
message::{v0, Message, VersionedMessage},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use std::sync::Arc;
|
||||
/// 预分配的交易构建器
|
||||
pub struct PreallocatedTxBuilder {
|
||||
@@ -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
|
||||
|
||||
@@ -13,11 +13,8 @@ pub(crate) fn creator_side_fee_basis_points(
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
let creator_bps = if *coin_creator == Pubkey::default() {
|
||||
0
|
||||
} else {
|
||||
COIN_CREATOR_FEE_BASIS_POINTS
|
||||
};
|
||||
let creator_bps =
|
||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
||||
creator_bps.saturating_add(cashback_fee_basis_points)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user