Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c76c8d6ac2 | |||
| dc3d2b8deb | |||
| fb1ff176d0 | |||
| dd41dd4f87 | |||
| 8bef655abb | |||
| 8413baa3d9 | |||
| 100156424c | |||
| 1d224770d8 | |||
| 1c78b3cab2 | |||
| ca306172ca | |||
| b09902c527 | |||
| 70d1da0b13 | |||
| 8c6557257e | |||
| 77232a2b70 |
+5
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.22"
|
||||
version = "5.0.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -75,6 +75,9 @@ futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
base64 = "0.22.1"
|
||||
bs58 = "0.5.1"
|
||||
# `five8` 1.0 permits five8_core 0.1.x, whose DecodeError implements
|
||||
# std::error::Error only with this feature. Solana keypair decoding requires it.
|
||||
five8_core = { version = "0.1.2", features = ["std"] }
|
||||
rand = "0.9.0"
|
||||
bincode = "1.3.3"
|
||||
anyhow = "1.0.90"
|
||||
@@ -112,6 +115,7 @@ sha2 = "0.10"
|
||||
tonic-prost = "0.14.2"
|
||||
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime found,QUIC(Speedlanding/Soyas)无法初始化
|
||||
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
|
||||
lunar-lander-quic-client = "0.4"
|
||||
rcgen = "0.13"
|
||||
uuid = "1.11"
|
||||
|
||||
|
||||
@@ -92,9 +92,9 @@ This SDK is available in multiple languages:
|
||||
|
||||
## 🔖 Current Release
|
||||
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.21"`
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.23"`
|
||||
|
||||
This release refreshes PumpFun native-SOL quote handling so SOL/WSOL sentinels prefer the smaller V1 hot path, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
|
||||
This release updates PumpSwap for the July 2026 virtual quote reserve rollout. Pool and event schemas include `virtual_quote_reserves`, and all PumpSwap buy, sell, pricing, and dynamic-fee calculations use `quote_vault_balance + virtual_quote_reserves`.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -104,7 +104,7 @@ This release refreshes PumpFun native-SOL quote handling so SOL/WSOL sentinels p
|
||||
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
|
||||
5. **Raydium AMM V4 Trading**: Support for Raydium AMM V4 (Automated Market Maker) trading operations
|
||||
6. **Meteora DAMM V2 Trading**: Support for Meteora DAMM V2 (Dynamic AMM) trading operations
|
||||
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane and other services
|
||||
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, LunarLander and other services
|
||||
8. **Concurrent Trading**: Submit through every configured SWQoS provider plus the default RPC lane; the first accepted result can return early while slower routes continue submitting
|
||||
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
|
||||
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
|
||||
@@ -126,14 +126,14 @@ Add the dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.21" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "4.0.21"
|
||||
sol-trade-sdk = "4.0.23"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -161,6 +161,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
// 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::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Lunar Lander: 4th param None = QUIC (default); Some(SwqosTransport::Http) = binary HTTP
|
||||
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None, None),
|
||||
SwqosConfig::LunarLander(
|
||||
"your_hellomoon_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Http),
|
||||
),
|
||||
];
|
||||
// Create TradeConfig instance
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -281,27 +289,33 @@ Optional builder methods:
|
||||
When using shred to subscribe to events, due to the nature of shreds, you cannot get complete information about transaction events.
|
||||
Please ensure that the parameters your trading logic depends on are available in shreds when using them.
|
||||
|
||||
See the [low-latency bot integration checklist](docs/LOW_LATENCY_BOTS.md) for client warmup, blockhash/nonce handling, trade intent, event-state freshness, and bounded requoting.
|
||||
|
||||
### 📊 Usage Examples Summary Table
|
||||
|
||||
| Description | Run Command | Source Code |
|
||||
The complete bilingual index and safety classification are available in [`examples/README.md`](examples/README.md).
|
||||
|
||||
| Description | Run Command | Guide |
|
||||
|-------------|-------------|-------------|
|
||||
| Simple buy/sell parameter API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
|
||||
| Create and configure TradingClient instance | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
||||
| Share infrastructure across multiple wallets | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
|
||||
| PumpFun token sniping trading | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
||||
| PumpFun token copy trading | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
|
||||
| PumpSwap trading operations | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
|
||||
| Raydium CPMM trading operations | `cargo run --package raydium_cpmm_trading` | [examples/raydium_cpmm_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_cpmm_trading/src/main.rs) |
|
||||
| Raydium AMM V4 trading operations | `cargo run --package raydium_amm_v4_trading` | [examples/raydium_amm_v4_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_amm_v4_trading/src/main.rs) |
|
||||
| Meteora DAMM V2 trading operations | `cargo run --package meteora_damm_v2_direct_trading` | [examples/meteora_damm_v2_direct_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/meteora_damm_v2_direct_trading/src/main.rs) |
|
||||
| Bonk token sniping trading | `cargo run --package bonk_sniper_trading` | [examples/bonk_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_sniper_trading/src/main.rs) |
|
||||
| Bonk token copy trading | `cargo run --package bonk_copy_trading` | [examples/bonk_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_copy_trading/src/main.rs) |
|
||||
| Custom instruction middleware example | `cargo run --package middleware_system` | [examples/middleware_system](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/middleware_system/src/main.rs) |
|
||||
| Address lookup table example | `cargo run --package address_lookup` | [examples/address_lookup](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/address_lookup/src/main.rs) |
|
||||
| Nonce example | `cargo run --package nonce_cache` | [examples/nonce_cache](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/nonce_cache/src/main.rs) |
|
||||
| Wrap/unwrap SOL to/from WSOL example | `cargo run --package wsol_wrapper` | [examples/wsol_wrapper](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/wsol_wrapper/src/main.rs) |
|
||||
| Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
||||
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||
| Simple buy/sell parameter API | `cargo run --package simple_trading` | [README](examples/simple_trading/README.md) |
|
||||
| Create and configure TradingClient | `cargo run --package trading_client` | [README](examples/trading_client/README.md) |
|
||||
| Share infrastructure across wallets | `cargo run --package shared_infrastructure` | [README](examples/shared_infrastructure/README.md) |
|
||||
| PumpFun sniper | `cargo run --package pumpfun_sniper_trading` | [README](examples/pumpfun_sniper_trading/README.md) |
|
||||
| PumpFun copy trading | `cargo run --package pumpfun_copy_trading` | [README](examples/pumpfun_copy_trading/README.md) |
|
||||
| PumpSwap low-latency stream | `cargo run --package pumpswap_trading` | [README](examples/pumpswap_trading/README.md) |
|
||||
| PumpSwap direct RPC flow | `cargo run --package pumpswap_direct_trading` | [README](examples/pumpswap_direct_trading/README.md) |
|
||||
| Raydium CPMM | `cargo run --package raydium_cpmm_trading` | [README](examples/raydium_cpmm_trading/README.md) |
|
||||
| Raydium AMM V4 | `cargo run --package raydium_amm_v4_trading` | [README](examples/raydium_amm_v4_trading/README.md) |
|
||||
| Meteora DAMM V2 | `cargo run --package meteora_damm_v2_direct_trading` | [README](examples/meteora_damm_v2_direct_trading/README.md) |
|
||||
| Bonk sniper | `cargo run --package bonk_sniper_trading` | [README](examples/bonk_sniper_trading/README.md) |
|
||||
| Bonk copy trading | `cargo run --package bonk_copy_trading` | [README](examples/bonk_copy_trading/README.md) |
|
||||
| Instruction middleware | `cargo run --package middleware_system` | [README](examples/middleware_system/README.md) |
|
||||
| Address lookup tables | `cargo run --package address_lookup` | [README](examples/address_lookup/README.md) |
|
||||
| Durable nonce | `cargo run --package nonce_cache` | [README](examples/nonce_cache/README.md) |
|
||||
| Wrap/unwrap SOL and WSOL | `cargo run --package wsol_wrapper` | [README](examples/wsol_wrapper/README.md) |
|
||||
| Seed optimization | `cargo run --package seed_trading` | [README](examples/seed_trading/README.md) |
|
||||
| Gas fee strategy | `cargo run --package gas_fee_strategy` | [README](examples/gas_fee_strategy/README.md) |
|
||||
| Multi-DEX CLI template | `cargo run --package cli_trading` | [README](examples/cli_trading/README.md) |
|
||||
|
||||
### ⚙️ SWQoS Service Configuration
|
||||
|
||||
@@ -492,6 +506,14 @@ For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_au
|
||||
- **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`.
|
||||
|
||||
#### PumpSwap: virtual quote reserves
|
||||
|
||||
PumpSwap quotes must use `effective_quote_reserves = pool_quote_token_account.amount + virtual_quote_reserves`. The Pool account and BuyEvent/SellEvent encode `virtual_quote_reserves` as `i128`.
|
||||
|
||||
- RPC constructors such as `PumpSwapParams::from_pool_address_by_rpc` read and apply the Pool field automatically.
|
||||
- Event fast paths must pass the event's raw `pool_quote_token_reserves` and `virtual_quote_reserves` separately to `PumpSwapParams::from_trade(...)` or `from_trade_with_fee_basis_points(...)`. Do not add them before calling the constructor.
|
||||
- The SDK uses effective reserves for buys, sells, prices, and dynamic fee-tier selection. Invalid signed sums return an error instead of wrapping.
|
||||
|
||||
## 🛡️ MEV Protection Services
|
||||
|
||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||
@@ -502,6 +524,8 @@ You can apply for a key through the official website: [Community Website](https:
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC)
|
||||
- **SpeedLanding**: High-speed transaction execution with API key authentication
|
||||
- **Node1**: High-speed transaction execution with API key authentication
|
||||
- **LunarLander**: HelloMoon transaction landing service (minimum tip: 0.001 SOL)
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
|
||||
+47
-23
@@ -92,9 +92,9 @@
|
||||
|
||||
## 🔖 当前版本
|
||||
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.21"`
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.23"`
|
||||
|
||||
本版本刷新 PumpFun native SOL quote 处理逻辑,SOL/WSOL sentinel 默认优先走更小的 V1 热路径,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
|
||||
本版本适配 PumpSwap 2026 年 7 月的虚拟 quote 储备升级。Pool 与事件 schema 新增 `virtual_quote_reserves`,PumpSwap 买入、卖出、报价和动态费率计算统一使用 `quote_vault_balance + virtual_quote_reserves`。
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
|
||||
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
|
||||
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
|
||||
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
|
||||
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、SpeedLanding、LunarLander 等服务
|
||||
8. **并发交易**: 所有已配置的 SWQoS 通道和默认 RPC 通道都会发出提交;首个成功只影响返回,较慢通道会继续提交
|
||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||
@@ -126,14 +126,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.21" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.23" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "4.0.21"
|
||||
sol-trade-sdk = "4.0.23"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -161,6 +161,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain(/iris)或 Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Lunar Lander:第4个参数 None 为 QUIC(默认);Some(SwqosTransport::Http) 为 binary HTTP
|
||||
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None, None),
|
||||
SwqosConfig::LunarLander(
|
||||
"your_hellomoon_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Http),
|
||||
),
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -280,27 +288,33 @@ client.buy_simple(buy_params).await?;
|
||||
当你使用 shred 订阅事件时,由于 shred 的特性,你无法获取到交易事件的完整信息。
|
||||
请你在使用时,确保你的交易逻辑依赖的参数,在shred中都能获取到。
|
||||
|
||||
客户端预热、blockhash/nonce、交易模式、事件状态新鲜度和 6040 重报价规则见 [低延迟 Bot 集成清单](docs/LOW_LATENCY_BOTS_CN.md)。
|
||||
|
||||
### 📊 使用示例汇总表格
|
||||
|
||||
| 描述 | 运行命令 | 源码路径 |
|
||||
完整双语索引和安全分类见 [`examples/README_CN.md`](examples/README_CN.md)。
|
||||
|
||||
| 描述 | 运行命令 | 使用说明 |
|
||||
|------|---------|----------|
|
||||
| 简化买卖参数 API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
|
||||
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
||||
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
|
||||
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
||||
| PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
|
||||
| PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
|
||||
| Raydium CPMM 交易操作 | `cargo run --package raydium_cpmm_trading` | [examples/raydium_cpmm_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_cpmm_trading/src/main.rs) |
|
||||
| Raydium AMM V4 交易操作 | `cargo run --package raydium_amm_v4_trading` | [examples/raydium_amm_v4_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/raydium_amm_v4_trading/src/main.rs) |
|
||||
| Meteora DAMM V2 交易操作 | `cargo run --package meteora_damm_v2_direct_trading` | [examples/meteora_damm_v2_direct_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/meteora_damm_v2_direct_trading/src/main.rs) |
|
||||
| Bonk 代币狙击交易 | `cargo run --package bonk_sniper_trading` | [examples/bonk_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_sniper_trading/src/main.rs) |
|
||||
| Bonk 代币跟单交易 | `cargo run --package bonk_copy_trading` | [examples/bonk_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/bonk_copy_trading/src/main.rs) |
|
||||
| 自定义指令中间件示例 | `cargo run --package middleware_system` | [examples/middleware_system](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/middleware_system/src/main.rs) |
|
||||
| 地址查找表示例 | `cargo run --package address_lookup` | [examples/address_lookup](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/address_lookup/src/main.rs) |
|
||||
| Nonce示例 | `cargo run --package nonce_cache` | [examples/nonce_cache](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/nonce_cache/src/main.rs) |
|
||||
| SOL与WSOL相互转换示例 | `cargo run --package wsol_wrapper` | [examples/wsol_wrapper](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/wsol_wrapper/src/main.rs) |
|
||||
| Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
||||
| Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||
| 简化买卖参数 API | `cargo run --package simple_trading` | [README](examples/simple_trading/README_CN.md) |
|
||||
| 创建和配置 TradingClient | `cargo run --package trading_client` | [README](examples/trading_client/README_CN.md) |
|
||||
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [README](examples/shared_infrastructure/README_CN.md) |
|
||||
| PumpFun 狙击 | `cargo run --package pumpfun_sniper_trading` | [README](examples/pumpfun_sniper_trading/README_CN.md) |
|
||||
| PumpFun 跟单 | `cargo run --package pumpfun_copy_trading` | [README](examples/pumpfun_copy_trading/README_CN.md) |
|
||||
| PumpSwap 低延迟事件流 | `cargo run --package pumpswap_trading` | [README](examples/pumpswap_trading/README_CN.md) |
|
||||
| PumpSwap 直接 RPC 流程 | `cargo run --package pumpswap_direct_trading` | [README](examples/pumpswap_direct_trading/README_CN.md) |
|
||||
| Raydium CPMM | `cargo run --package raydium_cpmm_trading` | [README](examples/raydium_cpmm_trading/README_CN.md) |
|
||||
| Raydium AMM V4 | `cargo run --package raydium_amm_v4_trading` | [README](examples/raydium_amm_v4_trading/README_CN.md) |
|
||||
| Meteora DAMM V2 | `cargo run --package meteora_damm_v2_direct_trading` | [README](examples/meteora_damm_v2_direct_trading/README_CN.md) |
|
||||
| Bonk 狙击 | `cargo run --package bonk_sniper_trading` | [README](examples/bonk_sniper_trading/README_CN.md) |
|
||||
| Bonk 跟单 | `cargo run --package bonk_copy_trading` | [README](examples/bonk_copy_trading/README_CN.md) |
|
||||
| 指令中间件 | `cargo run --package middleware_system` | [README](examples/middleware_system/README_CN.md) |
|
||||
| 地址查找表 | `cargo run --package address_lookup` | [README](examples/address_lookup/README_CN.md) |
|
||||
| Durable nonce | `cargo run --package nonce_cache` | [README](examples/nonce_cache/README_CN.md) |
|
||||
| SOL/WSOL 包装与解包 | `cargo run --package wsol_wrapper` | [README](examples/wsol_wrapper/README_CN.md) |
|
||||
| Seed 优化 | `cargo run --package seed_trading` | [README](examples/seed_trading/README_CN.md) |
|
||||
| Gas fee 策略 | `cargo run --package gas_fee_strategy` | [README](examples/gas_fee_strategy/README_CN.md) |
|
||||
| 多 DEX CLI 模板 | `cargo run --package cli_trading` | [README](examples/cli_trading/README_CN.md) |
|
||||
|
||||
### ⚙️ SWQoS 服务配置说明
|
||||
|
||||
@@ -489,6 +503,14 @@ legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quo
|
||||
| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) / `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | 优先旧版 `buy`/`sell`/`buy_exact_sol_in` | native SOL 配对;普通 SOL 结算走 V1,显式 WSOL 结算才走 V2 |
|
||||
| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) |
|
||||
|
||||
#### PumpSwap:虚拟 quote 储备
|
||||
|
||||
PumpSwap 报价必须使用 `effective_quote_reserves = pool_quote_token_account.amount + virtual_quote_reserves`。Pool 账户以及 BuyEvent/SellEvent 中的 `virtual_quote_reserves` 类型均为 `i128`。
|
||||
|
||||
- `PumpSwapParams::from_pool_address_by_rpc` 等 RPC 构造器会自动读取并应用 Pool 字段。
|
||||
- 事件热路径必须把事件中的原始 `pool_quote_token_reserves` 和 `virtual_quote_reserves` 分别传给 `PumpSwapParams::from_trade(...)` 或 `from_trade_with_fee_basis_points(...)`,不要在调用前自行相加。
|
||||
- SDK 在买入、卖出、报价和动态费率分层中统一使用有效储备;无效的有符号结果会返回错误,不会发生整数回绕。
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
||||
@@ -499,6 +521,8 @@ legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quo
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC)
|
||||
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
|
||||
- **Node1**: 高速交易执行,支持 API 密钥认证
|
||||
- **LunarLander**: HelloMoon 交易着陆服务(最低小费:0.001 SOL)
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Low-Latency Bot Integration Checklist
|
||||
|
||||
Before subscription, initialize and warm `SolanaTrade`, RPC and SWQoS clients, a background blockhash cache or durable nonce pool, known ATAs, and ALTs. Restore signature/instruction deduplication and position state before accepting events.
|
||||
|
||||
The event hot path should be limited to:
|
||||
|
||||
```text
|
||||
filter -> deduplicate -> reject stale event -> map post-trade state -> Simple*Params -> sign -> submit
|
||||
```
|
||||
|
||||
Do not initialize clients, synchronously fetch a blockhash, query balances, or search for pools in this path. An RPC fallback is valid for incomplete shred data but is no longer a purely low-latency path.
|
||||
|
||||
## Trade intent
|
||||
|
||||
| Goal | Parameter |
|
||||
|---|---|
|
||||
| Exact spend with minimum-output protection | `BuyAmount::ExactInput` |
|
||||
| Fill-priority sniping/arbitrage with maximum-cost protection | `BuyAmount::WithMaxInput` |
|
||||
| Exact token output with maximum-input protection | `BuyAmount::ExactOutput` |
|
||||
| Sell an exact token amount | `SellAmount::ExactInput` |
|
||||
|
||||
`WithMaxInput` still enforces slippage. Never use `min_out = 0` as routine error handling.
|
||||
Exact-output support is protocol- and pool-direction-specific. PumpSwap exposes exact output through its on-chain `buy` instruction, but its `sell` instruction accepts exact base input plus minimum quote output; the SDK rejects `SellAmount::ExactOutput` when that direction would require `sell`.
|
||||
|
||||
Use post-trade event reserves. Preserve PumpFun quote mint, creator/vault, token program, cashback, and mayhem fields. PumpSwap event integrations should use `from_trade_with_fee_basis_points`. Refresh delayed sells because the triggering trade and your own buy both change pool state. Durable nonce extends transaction validity, not quote validity.
|
||||
|
||||
For `BuySlippageBelowMinBaseAmountOut`, discard the old transaction, obtain newer reserves and fee rates, enforce a quote-age limit, and rebuild only within a bounded retry policy.
|
||||
After a submit timeout or ambiguous relay error, reconcile the signature and position before retrying. A retry policy may rebuild quotes automatically only when the previous transaction is known not to have been submitted.
|
||||
|
||||
Reference examples:
|
||||
|
||||
- `fnzero-examples/pumpfun_grpc_sniper`
|
||||
- `fnzero-examples/pumpfun_shredstream_sniper`
|
||||
- `examples/pumpswap_trading`
|
||||
@@ -0,0 +1,45 @@
|
||||
# 低延迟 Bot 集成清单
|
||||
|
||||
## 启动阶段
|
||||
|
||||
- 创建并预热 `SolanaTrade`、RPC 和全部 SWQoS 客户端。
|
||||
- 启动后台 blockhash cache,或准备并持续刷新 durable nonce pool。
|
||||
- 对已知 mint 准备 ATA、WSOL ATA 和 ALT。
|
||||
- 建立签名 + 指令索引去重,并恢复持仓状态。
|
||||
|
||||
## 事件热路径
|
||||
|
||||
```text
|
||||
过滤 -> 去重 -> 事件年龄检查 -> 映射成交后状态 -> Simple*Params -> 签名 -> 提交
|
||||
```
|
||||
|
||||
事件处理期间不要初始化客户端、同步查询 blockhash、查余额或搜索池。Shred 缺少必需参数时可以 RPC 回退,但要明确该路径不再是纯低延迟路径。
|
||||
|
||||
## 交易意图
|
||||
|
||||
| 目标 | 参数 |
|
||||
|---|---|
|
||||
| 固定花费,保护最小输出 | `BuyAmount::ExactInput` |
|
||||
| 狙击/套利优先成交,保护最大成本 | `BuyAmount::WithMaxInput` |
|
||||
| 固定买到数量,限制最大输入 | `BuyAmount::ExactOutput` |
|
||||
| 卖出固定 token 数量 | `SellAmount::ExactInput` |
|
||||
|
||||
`WithMaxInput` 仍有滑点保护。不要通过设置 `min_out = 0` 处理滑点错误。
|
||||
|
||||
## 状态和费率
|
||||
|
||||
- 使用解析事件中的成交后 reserves。
|
||||
- PumpFun 保留 quote mint、creator、creator vault、token program、cashback 和 mayhem 字段。
|
||||
- PumpSwap 使用 `from_trade_with_fee_basis_points` 传入最新储备和动态费率。
|
||||
- 自己的买入会改变池状态;延迟卖出前必须使用更新后的流缓存或 RPC 快照。
|
||||
- durable nonce 只延长交易有效期,不会延长报价有效期。
|
||||
|
||||
## 6040 重报价
|
||||
|
||||
发生 `BuySlippageBelowMinBaseAmountOut` 时,丢弃旧交易,取得更新的储备和费率,检查报价年龄,然后在业务配置的有限次数内重建。没有新状态时,不要通过不断放大滑点重发同一报价。
|
||||
|
||||
参考示例:
|
||||
|
||||
- `fnzero-examples/pumpfun_grpc_sniper`
|
||||
- `fnzero-examples/pumpfun_shredstream_sniper`
|
||||
- `examples/pumpswap_trading`
|
||||
@@ -0,0 +1,26 @@
|
||||
# sol-trade-sdk Examples
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Run commands from the repository root with `cargo run --package <name>`. Read the example's README before running it: many protocol examples contain placeholder keys or addresses and submit real mainnet transactions after configuration.
|
||||
|
||||
## Choose an example
|
||||
|
||||
| Category | Examples |
|
||||
|---|---|
|
||||
| Start here | `simple_trading`, `trading_client`, `shared_infrastructure` |
|
||||
| Low latency | `pumpswap_trading`; also read `../docs/LOW_LATENCY_BOTS.md` |
|
||||
| PumpFun streams | `pumpfun_sniper_trading`, `pumpfun_copy_trading` |
|
||||
| Other DEXs | `pumpswap_direct_trading`, `bonk_*`, `raydium_*`, `meteora_*` |
|
||||
| Transaction construction | `address_lookup`, `nonce_cache`, `middleware_system`, `seed_trading` |
|
||||
| Utilities | `gas_fee_strategy`, `wsol_wrapper`, `cli_trading` |
|
||||
|
||||
## Safety boundary
|
||||
|
||||
- Files containing `use_your_*` or `your_*_here` are templates. Replace every placeholder before running.
|
||||
- `simple_trading` and `gas_fee_strategy` do not submit a swap as shipped. Most other trading examples can.
|
||||
- Event-driven templates that create the client or fetch blockhash/state inside the callback demonstrate protocol mapping, not the final low-latency architecture.
|
||||
- Never commit private keys. Use environment variables or a secure keystore in real applications.
|
||||
- Use `SimpleBuyParams` / `SimpleSellParams` for new integrations unless low-level account flags are specifically required.
|
||||
|
||||
Each example directory contains matching English and Chinese documentation.
|
||||
@@ -0,0 +1,26 @@
|
||||
# sol-trade-sdk 示例索引
|
||||
|
||||
[English](README.md)
|
||||
|
||||
请在仓库根目录使用 `cargo run --package <name>`。运行前先阅读对应示例的 README:很多协议示例包含占位私钥或地址,配置完成后会提交真实主网交易。
|
||||
|
||||
## 如何选择
|
||||
|
||||
| 分类 | 示例 |
|
||||
|---|---|
|
||||
| 入门 | `simple_trading`、`trading_client`、`shared_infrastructure` |
|
||||
| 低延迟 | `pumpswap_trading`;同时阅读 `../docs/LOW_LATENCY_BOTS_CN.md` |
|
||||
| PumpFun 事件流 | `pumpfun_sniper_trading`、`pumpfun_copy_trading` |
|
||||
| 其他 DEX | `pumpswap_direct_trading`、`bonk_*`、`raydium_*`、`meteora_*` |
|
||||
| 交易构造 | `address_lookup`、`nonce_cache`、`middleware_system`、`seed_trading` |
|
||||
| 工具 | `gas_fee_strategy`、`wsol_wrapper`、`cli_trading` |
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 含 `use_your_*` 或 `your_*_here` 的源码都是模板,运行前必须替换全部占位值。
|
||||
- `simple_trading` 和 `gas_fee_strategy` 默认不会提交 swap;多数其他交易示例配置后会发真实交易。
|
||||
- 若事件回调内仍创建客户端或同步查询 blockhash/状态,该示例只展示协议参数映射,不代表最终低延迟架构。
|
||||
- 不要提交私钥。真实应用应使用环境变量或安全 keystore。
|
||||
- 新接入优先使用 `SimpleBuyParams` / `SimpleSellParams`,只有必须控制低层账户 flag 时才使用低层参数。
|
||||
|
||||
每个示例目录都提供内容对应的英文和中文文档。
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
sol-parser-sdk = "0.4.14"
|
||||
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Address Lookup Table
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Shows how to fetch an Address Lookup Table (ALT), attach it to a PumpFun transaction, and submit after receiving a `sol-parser-sdk` gRPC event.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY`, replace `use_your_lookup_table_key_here`, and configure RPC and gRPC before running.
|
||||
|
||||
```bash
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
export GRPC_AUTH_TOKEN=optional-token
|
||||
cargo run --package address_lookup
|
||||
```
|
||||
|
||||
An ALT reduces message size but does not make stale pool state valid. Fetch and cache known ALTs before the event hot path. The current example initializes the trading client after an event and is therefore an API demonstration, not the recommended low-latency structure.
|
||||
|
||||
See [Address Lookup Tables](../../docs/ADDRESS_LOOKUP_TABLE.md).
|
||||
@@ -0,0 +1,17 @@
|
||||
# 地址查找表(ALT)
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示如何获取 Address Lookup Table、附加到 PumpFun 交易,并在收到 `sol-parser-sdk` gRPC 事件后提交。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,替换 `use_your_lookup_table_key_here`,并配置 RPC 和 gRPC。
|
||||
|
||||
```bash
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
export GRPC_AUTH_TOKEN=optional-token
|
||||
cargo run --package address_lookup
|
||||
```
|
||||
|
||||
ALT 只能缩小消息体积,不能让过期池状态重新有效。已知 ALT 应在事件热路径前获取并缓存。当前示例在事件后初始化交易客户端,因此只用于展示 API,不代表推荐低延迟结构。
|
||||
|
||||
参见[地址查找表说明](../../docs/ADDRESS_LOOKUP_TABLE_CN.md)。
|
||||
@@ -24,7 +24,6 @@ use sol_trade_sdk::{
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -105,7 +104,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bonk Copy Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Consumes Bonk events through `solana-streamer-sdk`, maps event state to SDK protocol params, then performs one follow-up buy/sell flow.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY`, RPC/stream configuration, target policy, amounts, slippage, and fee limits before running.
|
||||
|
||||
```bash
|
||||
cargo run --package bonk_copy_trading
|
||||
```
|
||||
|
||||
Add signature/instruction deduplication, event-age checks, wallet and mint allowlists, position isolation, and maximum input/loss limits. Delayed sells require current state and a fresh blockhash; do not reuse the trigger snapshot.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bonk 跟单交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
通过 `solana-streamer-sdk` 消费 Bonk 事件,把事件状态映射为 SDK 协议参数,再执行一次跟随买卖流程。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、RPC/stream 配置、目标策略、金额、滑点和费用上限。
|
||||
|
||||
```bash
|
||||
cargo run --package bonk_copy_trading
|
||||
```
|
||||
|
||||
必须增加签名/指令去重、事件年龄检查、钱包和 mint allowlist、持仓隔离及最大输入/亏损限制。延迟卖出需要最新状态和新 blockhash,不能复用触发快照。
|
||||
@@ -3,10 +3,8 @@ use std::sync::{
|
||||
Arc,
|
||||
};
|
||||
|
||||
use sol_trade_sdk::common::GasFeeStrategy;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, GasFeeStrategy,
|
||||
};
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
@@ -17,15 +15,12 @@ use sol_trade_sdk::{
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::match_event;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::parser::BONK_PROGRAM_ID;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent;
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
||||
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
|
||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
||||
|
||||
// Global static flag to ensure transaction is executed only once
|
||||
@@ -56,24 +51,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
account_required,
|
||||
};
|
||||
|
||||
// Listen to account data belonging to owner programs -> account event monitoring
|
||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
||||
|
||||
// listen to specific event type
|
||||
let event_type_filter = EventTypeFilter {
|
||||
include: vec![
|
||||
EventType::BonkBuyExactIn,
|
||||
EventType::BonkSellExactIn,
|
||||
EventType::BonkBuyExactOut,
|
||||
EventType::BonkSellExactOut,
|
||||
],
|
||||
};
|
||||
let event_type_filter = EventTypeFilter::include_only(vec![
|
||||
EventType::BonkBuyExactIn,
|
||||
EventType::BonkSellExactIn,
|
||||
EventType::BonkBuyExactOut,
|
||||
EventType::BonkSellExactOut,
|
||||
]);
|
||||
|
||||
grpc.subscribe_events_immediate(
|
||||
protocols,
|
||||
None,
|
||||
vec![transaction_filter],
|
||||
vec![account_filter],
|
||||
vec![],
|
||||
Some(event_type_filter),
|
||||
None,
|
||||
callback,
|
||||
@@ -81,27 +71,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?;
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
grpc.stop().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
BonkTradeEvent => |e: BonkTradeEvent| {
|
||||
// Test code, only test one transaction
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = bonk_copy_trade_with_grpc(event_clone).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
fn create_event_callback() -> impl Fn(DexEvent) {
|
||||
|event: DexEvent| {
|
||||
let DexEvent::BonkTradeEvent(event) = event else {
|
||||
return;
|
||||
};
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = bonk_copy_trade_with_grpc(event).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +97,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
@@ -147,6 +135,9 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
} else {
|
||||
sol_trade_sdk::TradeTokenType::SOL
|
||||
};
|
||||
let balance_before = client
|
||||
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
|
||||
.await?;
|
||||
let buy_sol_amount = 100_000;
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::Bonk,
|
||||
@@ -182,22 +173,31 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from Bonk...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
let balance_after = client
|
||||
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
|
||||
.await?;
|
||||
let amount_token = balance_after
|
||||
.checked_sub(balance_before)
|
||||
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
|
||||
if amount_token == 0 {
|
||||
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
|
||||
}
|
||||
let sell_extension = BonkParams::from_mint_by_rpc(
|
||||
&client.infrastructure.rpc,
|
||||
&mint_pubkey,
|
||||
&trade_info.base_token_program,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
trade_info.quote_token_mint == sol_trade_sdk::constants::USD1_TOKEN_ACCOUNT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
@@ -206,21 +206,8 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::Bonk(BonkParams::from_trade(
|
||||
trade_info.virtual_base,
|
||||
trade_info.virtual_quote,
|
||||
trade_info.real_base_after,
|
||||
trade_info.real_quote_after,
|
||||
trade_info.pool_state,
|
||||
trade_info.base_vault,
|
||||
trade_info.quote_vault,
|
||||
trade_info.base_token_program,
|
||||
trade_info.platform_config,
|
||||
trade_info.platform_associated_account,
|
||||
trade_info.creator_associated_account,
|
||||
trade_info.global_config,
|
||||
)),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
extension_params: DexParamEnum::Bonk(sell_extension),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -234,7 +221,12 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Exit program
|
||||
std::process::exit(0);
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bonk Sniper Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Uses `solana-streamer-sdk` ShredStream events to detect Bonk launch activity and execute one buy/sell flow.
|
||||
|
||||
> This is a live-transaction template. Replace `use_your_shred_stream_url_here`, set `PRIVATE_KEY`, RPC, target filters, amounts, slippage, and fees before running.
|
||||
|
||||
```bash
|
||||
cargo run --package bonk_sniper_trading
|
||||
```
|
||||
|
||||
Shreds may not contain every log-derived field. Only trade when every required account, reserve, token-program, and fee field is present and current; otherwise use a correctness-first RPC fallback. Prewarm the trading client and blockhash cache before subscription in a production bot.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bonk 狙击交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
使用 `solana-streamer-sdk` ShredStream 事件识别 Bonk launch 活动,并执行一次买卖流程。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前替换 `use_your_shred_stream_url_here`,设置 `PRIVATE_KEY`、RPC、目标过滤、金额、滑点和费用。
|
||||
|
||||
```bash
|
||||
cargo run --package bonk_sniper_trading
|
||||
```
|
||||
|
||||
Shred 不一定包含所有日志字段。只有必需账户、储备、token program 和费率字段完整且新鲜时才能交易,否则应使用正确性优先的 RPC 回退。生产 Bot 应在订阅前预热交易客户端和 blockhash cache。
|
||||
@@ -1,4 +1,3 @@
|
||||
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
@@ -10,13 +9,11 @@ use sol_trade_sdk::{
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent;
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
|
||||
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
|
||||
use solana_streamer_sdk::streaming::ShredStreamGrpc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
@@ -32,16 +29,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?;
|
||||
let callback = create_event_callback();
|
||||
let protocols = vec![Protocol::Bonk];
|
||||
let event_type_filter = EventTypeFilter {
|
||||
include: vec![
|
||||
EventType::BonkBuyExactIn,
|
||||
EventType::BonkBuyExactOut,
|
||||
EventType::BonkSellExactIn,
|
||||
EventType::BonkSellExactOut,
|
||||
EventType::BonkInitialize,
|
||||
EventType::BonkInitializeV2,
|
||||
],
|
||||
};
|
||||
let event_type_filter = EventTypeFilter::include_only(vec![
|
||||
EventType::BonkBuyExactIn,
|
||||
EventType::BonkBuyExactOut,
|
||||
EventType::BonkSellExactIn,
|
||||
EventType::BonkSellExactOut,
|
||||
EventType::BonkInitialize,
|
||||
EventType::BonkInitializeV2,
|
||||
]);
|
||||
println!("Starting to listen for events, press Ctrl+C to stop...");
|
||||
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?;
|
||||
tokio::signal::ctrl_c().await?;
|
||||
@@ -49,27 +44,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
BonkTradeEvent => |e: BonkTradeEvent| {
|
||||
// Only process developer token creation events
|
||||
if !e.is_dev_create_token_trade {
|
||||
return;
|
||||
fn create_event_callback() -> impl Fn(DexEvent) {
|
||||
|event: DexEvent| {
|
||||
let DexEvent::BonkTradeEvent(event) = event else {
|
||||
return;
|
||||
};
|
||||
if !event.is_dev_create_token_trade {
|
||||
return;
|
||||
}
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = bonk_sniper_trade_with_shreds(event).await {
|
||||
eprintln!("Error in sniper trade: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
// Ensure we only execute the trade once using atomic compare-and-swap
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
// Spawn a new task to handle the trading operation
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = bonk_sniper_trade_with_shreds(event_clone).await {
|
||||
eprintln!("Error in sniper trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +67,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
@@ -113,6 +103,9 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
} else {
|
||||
sol_trade_sdk::TradeTokenType::SOL
|
||||
};
|
||||
let balance_before = client
|
||||
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
|
||||
.await?;
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from Bonk...");
|
||||
@@ -150,22 +143,25 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from Bonk...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&trade_info.base_token_program,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after = client
|
||||
.get_payer_token_balance_with_program(&mint_pubkey, &trade_info.base_token_program)
|
||||
.await?;
|
||||
let amount_token = balance_after
|
||||
.checked_sub(balance_before)
|
||||
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
|
||||
if amount_token == 0 {
|
||||
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
|
||||
}
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
@@ -174,7 +170,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
extension_params: DexParamEnum::Bonk(BonkParams::immediate_sell(
|
||||
trade_info.base_token_program,
|
||||
trade_info.platform_config,
|
||||
@@ -195,7 +191,12 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Exit program after completing the trade
|
||||
std::process::exit(0);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# SOL Trade CLI
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
A command-line tool for trading tokens on Solana, supporting multiple DEX trading and wallet management features.
|
||||
|
||||
> **Live transaction warning:** set `PRIVATE_KEY` (base58 or a 64-byte JSON array) and `RPC_URL` before running. The CLI no longer creates a temporary trading wallet. Commands below can submit real mainnet transactions.
|
||||
|
||||
## Features
|
||||
|
||||
This CLI tool supports the following operation modes:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# SOL Trade CLI
|
||||
|
||||
[English](README.md)
|
||||
|
||||
一个用于在 Solana 上交易代币的命令行工具,支持多种 DEX 交易和钱包管理功能。
|
||||
|
||||
> **真实交易警告:**运行前设置 `PRIVATE_KEY`(base58 或 64 字节 JSON 数组)和 `RPC_URL`。CLI 不再创建临时交易钱包;下列命令可能提交真实主网交易。
|
||||
|
||||
## 功能
|
||||
|
||||
此 CLI 工具支持以下操作模式:
|
||||
|
||||
@@ -24,22 +24,21 @@ use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
native_token::sol_str_to_lamports,
|
||||
pubkey::Pubkey,
|
||||
signature::Keypair,
|
||||
signer::Signer,
|
||||
};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
// 设置 payer
|
||||
static PAYER: LazyLock<Keypair> = LazyLock::new(|| Keypair::new());
|
||||
// 设置 rpc url
|
||||
static RPC_URL: &str = "https://api.mainnet-beta.solana.com";
|
||||
|
||||
static DEXS: &[&str] = &["pumpfun", "pumpswap", "bonk", "raydium_v4", "raydium_cpmm"];
|
||||
|
||||
fn rpc_url() -> String {
|
||||
std::env::var("RPC_URL")
|
||||
.or_else(|_| std::env::var("SOLANA_RPC_URL"))
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string())
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "sol-trade-cli")]
|
||||
#[command(about = "SOL Trade CLI - A command line interface for trading tokens on Solana")]
|
||||
@@ -197,7 +196,7 @@ async fn show_startup_info() {
|
||||
println!("\n📋 STARTUP INFORMATION");
|
||||
println!("══════════════════════════════════════");
|
||||
|
||||
println!("🌐 RPC URL: {}", RPC_URL);
|
||||
println!("🌐 RPC URL: {}", rpc_url());
|
||||
|
||||
// Try to initialize client to show wallet info
|
||||
match initialize_real_client().await {
|
||||
@@ -214,14 +213,9 @@ async fn show_startup_info() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Generate a temporary keypair to show the format
|
||||
let temp_keypair = solana_sdk::signature::Keypair::new();
|
||||
println!(
|
||||
"👛 Wallet Address: {} (temporary - set SOLANA_RPC_URL for real wallet)",
|
||||
temp_keypair.pubkey()
|
||||
);
|
||||
println!("💰 SOL Balance: Unable to fetch (no valid RPC connection)");
|
||||
Err(err) => {
|
||||
println!("👛 Wallet: unavailable ({})", err);
|
||||
println!("💰 SOL Balance: Unable to fetch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,21 +262,21 @@ async fn run_interactive_mode() -> Result<(), Box<dyn std::error::Error>> {
|
||||
handle_close_wsol().await?;
|
||||
}
|
||||
_ => {
|
||||
if input.starts_with("raydium_cpmm_buy ") {
|
||||
handle_interactive_raydium_cpmm_buy(&input[16..]).await?;
|
||||
} else if input.starts_with("raydium_cpmm_sell ") {
|
||||
handle_interactive_raydium_cpmm_sell(&input[17..]).await?;
|
||||
} else if input.starts_with("raydium_v4_buy ") {
|
||||
handle_interactive_raydium_v4_buy(&input[14..]).await?;
|
||||
} else if input.starts_with("raydium_v4_sell ") {
|
||||
handle_interactive_raydium_v4_sell(&input[15..]).await?;
|
||||
} else if input.starts_with("buy ") {
|
||||
handle_interactive_buy(&input[4..]).await?;
|
||||
} else if input.starts_with("sell ") {
|
||||
handle_interactive_sell(&input[5..]).await?;
|
||||
} else if input.starts_with("wrap_sol ") || input.starts_with("wrap-sol ") {
|
||||
let amount_str =
|
||||
if input.starts_with("wrap_sol ") { &input[9..] } else { &input[9..] };
|
||||
if let Some(args) = input.strip_prefix("raydium_cpmm_buy ") {
|
||||
handle_interactive_raydium_cpmm_buy(args).await?;
|
||||
} else if let Some(args) = input.strip_prefix("raydium_cpmm_sell ") {
|
||||
handle_interactive_raydium_cpmm_sell(args).await?;
|
||||
} else if let Some(args) = input.strip_prefix("raydium_v4_buy ") {
|
||||
handle_interactive_raydium_v4_buy(args).await?;
|
||||
} else if let Some(args) = input.strip_prefix("raydium_v4_sell ") {
|
||||
handle_interactive_raydium_v4_sell(args).await?;
|
||||
} else if let Some(args) = input.strip_prefix("buy ") {
|
||||
handle_interactive_buy(args).await?;
|
||||
} else if let Some(args) = input.strip_prefix("sell ") {
|
||||
handle_interactive_sell(args).await?;
|
||||
} else if let Some(amount_str) =
|
||||
input.strip_prefix("wrap_sol ").or_else(|| input.strip_prefix("wrap-sol "))
|
||||
{
|
||||
if let Ok(amount) = amount_str.parse::<f64>() {
|
||||
handle_wrap_sol(amount).await?;
|
||||
} else {
|
||||
@@ -477,23 +471,20 @@ async fn check_mint_ata(
|
||||
let mint_pubkey = Pubkey::from_str(mint).unwrap();
|
||||
|
||||
if let Ok(mint_info) = client.infrastructure.rpc.get_account(&mint_pubkey).await {
|
||||
let owner_pubkey = mint_info.owner.clone();
|
||||
let owner_pubkey = mint_info.owner;
|
||||
let mint_ata = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&client.get_payer_pubkey(),
|
||||
&mint_pubkey,
|
||||
&owner_pubkey,
|
||||
false,
|
||||
);
|
||||
match client.infrastructure.rpc.get_token_account_balance(&mint_ata).await {
|
||||
Ok(balance) => {
|
||||
let amount = balance.ui_amount.unwrap_or(0.0);
|
||||
decimals = balance.decimals;
|
||||
amount_f64 = amount as f64 * 10_f64.powi(decimals as i32);
|
||||
if let Ok(balance) = client.infrastructure.rpc.get_token_account_balance(&mint_ata).await {
|
||||
let amount = balance.ui_amount.unwrap_or(0.0);
|
||||
decimals = balance.decimals;
|
||||
amount_f64 = amount * 10_f64.powi(decimals as i32);
|
||||
|
||||
create_mint_ata = false;
|
||||
use_seed = false;
|
||||
}
|
||||
Err(_) => {}
|
||||
create_mint_ata = false;
|
||||
use_seed = false;
|
||||
}
|
||||
if !create_mint_ata {
|
||||
return Ok((create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals));
|
||||
@@ -505,16 +496,13 @@ async fn check_mint_ata(
|
||||
&owner_pubkey,
|
||||
true,
|
||||
);
|
||||
match client.infrastructure.rpc.get_token_account_balance(&mint_ata).await {
|
||||
Ok(_) => {
|
||||
create_mint_ata = false;
|
||||
use_seed = true;
|
||||
}
|
||||
Err(_) => {}
|
||||
if client.infrastructure.rpc.get_token_account_balance(&mint_ata).await.is_ok() {
|
||||
create_mint_ata = false;
|
||||
use_seed = true;
|
||||
}
|
||||
return Ok((create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals));
|
||||
}
|
||||
return Err("Mint account not found".to_string().into());
|
||||
Err("Mint account not found".to_string().into())
|
||||
}
|
||||
|
||||
// Buy and sell functions - currently in demo mode since trading logic is complex
|
||||
@@ -606,8 +594,8 @@ async fn handle_buy_pumpfun(
|
||||
println!("🔥 BUY PUMPFUN COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" SOL Amount: {} SOL", sol_amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}", slippage);
|
||||
}
|
||||
let client = initialize_real_client().await?;
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
@@ -631,10 +619,10 @@ async fn handle_buy_pumpfun(
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: false,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
create_mint_ata,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
@@ -664,8 +652,8 @@ async fn handle_buy_pumpswap(
|
||||
println!("🔥 BUY PUMPSWAP COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" SOL Amount: {} SOL", sol_amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}%", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}%", slippage);
|
||||
}
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let param = PumpSwapParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey).await?;
|
||||
@@ -688,10 +676,10 @@ async fn handle_buy_pumpswap(
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
create_mint_ata,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
@@ -720,8 +708,8 @@ async fn handle_buy_bonk(
|
||||
println!("🔥 BUY BONK COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" SOL Amount: {} SOL", sol_amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}%", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}%", slippage);
|
||||
}
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let param =
|
||||
@@ -745,10 +733,10 @@ async fn handle_buy_bonk(
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
create_mint_ata,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
@@ -779,8 +767,8 @@ async fn handle_buy_raydium_v4(
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" AMM: {}", amm);
|
||||
println!(" SOL Amount: {} SOL", sol_amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}%", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}%", slippage);
|
||||
}
|
||||
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
@@ -806,10 +794,10 @@ async fn handle_buy_raydium_v4(
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
create_mint_ata,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
@@ -840,8 +828,8 @@ async fn handle_buy_raydium_cpmm(
|
||||
println!(" Pool Address: {}", pool_address);
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" SOL Amount: {} SOL", sol_amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}%", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}%", slippage);
|
||||
}
|
||||
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
@@ -868,10 +856,10 @@ async fn handle_buy_raydium_cpmm(
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: true,
|
||||
close_input_token_ata: false,
|
||||
create_mint_ata: create_mint_ata,
|
||||
create_mint_ata,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
@@ -1008,13 +996,13 @@ async fn handle_sell_pumpfun(
|
||||
amount_f64: f64,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
let amount = token_amount.unwrap_or(amount_f64);
|
||||
|
||||
println!("🔥 SELL PUMPFUN COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" Token Amount: {} ", amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}%", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}%", slippage);
|
||||
}
|
||||
|
||||
let client = initialize_real_client().await?;
|
||||
@@ -1042,7 +1030,7 @@ async fn handle_sell_pumpfun(
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
@@ -1070,12 +1058,12 @@ async fn handle_sell_pumpswap(
|
||||
amount_f64: f64,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
let amount = token_amount.unwrap_or(amount_f64);
|
||||
println!("🔥 SELL PUMPSWAP COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" Token Amount: {}", amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}", slippage);
|
||||
}
|
||||
let client = initialize_real_client().await?;
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
@@ -1102,7 +1090,7 @@ async fn handle_sell_pumpswap(
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
@@ -1129,12 +1117,12 @@ async fn handle_sell_bonk(
|
||||
amount_f64: f64,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
let amount = token_amount.unwrap_or(amount_f64);
|
||||
println!("🔥 SELL PUMPSWAP COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" Token Amount: {}", amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}", slippage);
|
||||
}
|
||||
let client = initialize_real_client().await?;
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
@@ -1162,7 +1150,7 @@ async fn handle_sell_bonk(
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
@@ -1190,13 +1178,13 @@ async fn handle_sell_raydium_v4(
|
||||
amount_f64: f64,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
let amount = token_amount.unwrap_or(amount_f64);
|
||||
println!("🔥 SELL RAYDIUM V4 COMMAND");
|
||||
println!(" AMM: {}", amm);
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" Token Amount: {}", amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}", slippage);
|
||||
}
|
||||
let client = initialize_real_client().await?;
|
||||
let amm_pubkey = Pubkey::from_str(amm)?;
|
||||
@@ -1225,7 +1213,7 @@ async fn handle_sell_raydium_v4(
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
@@ -1253,13 +1241,13 @@ async fn handle_sell_raydium_cpmm(
|
||||
amount_f64: f64,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
let amount = token_amount.unwrap_or(amount_f64);
|
||||
println!("🔥 SELL RAYDIUM CPMM COMMAND");
|
||||
println!(" Pool Address: {}", pool_address);
|
||||
println!(" Token Mint: {}", mint);
|
||||
println!(" Token Amount: {}", amount);
|
||||
if slippage.is_some() {
|
||||
println!(" Slippage: {}", slippage.unwrap());
|
||||
if let Some(slippage) = slippage {
|
||||
println!(" Slippage: {}", slippage);
|
||||
}
|
||||
let client = initialize_real_client().await?;
|
||||
let pool_pubkey = Pubkey::from_str(pool_address)?;
|
||||
@@ -1289,7 +1277,7 @@ async fn handle_sell_raydium_cpmm(
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
@@ -1396,10 +1384,9 @@ async fn handle_wallet() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Real implementation functions
|
||||
async fn initialize_real_client() -> AnyResult<SolanaTrade> {
|
||||
// You need to update this with a real RPC URL
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Arc::new(Keypair::try_from(&PAYER.to_bytes()[..]).unwrap());
|
||||
let rpc_url = RPC_URL.to_string();
|
||||
let payer = Arc::new(sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?);
|
||||
let rpc_url = rpc_url();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Gas Fee Strategy
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Local, non-transaction demo of global, per-route, and high/low `GasFeeStrategy` configuration. It also shows replacement, deletion, and inspection of strategies.
|
||||
|
||||
```bash
|
||||
cargo run --package gas_fee_strategy
|
||||
```
|
||||
|
||||
The example does not connect to Solana or submit a transaction. Fee values are illustrative; production values must come from current network conditions and explicit cost limits.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Gas Fee 策略
|
||||
|
||||
[English](README.md)
|
||||
|
||||
本地演示全局、按提交通道和高低两档 `GasFeeStrategy` 配置,并展示策略覆盖、删除和查看。
|
||||
|
||||
```bash
|
||||
cargo run --package gas_fee_strategy
|
||||
```
|
||||
|
||||
示例不会连接 Solana,也不会提交交易。代码中的费用仅用于说明,生产值应根据实时网络情况和明确成本上限设置。
|
||||
@@ -5,8 +5,9 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Meteora DAMM V2 Direct Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Demonstrates a direct Meteora DAMM V2 buy/sell flow using pool data supplied by `solana-streamer-sdk` types and SDK trading params.
|
||||
|
||||
> This is a live mainnet template. Set `PRIVATE_KEY`, `RPC_URL`, and real raw-unit
|
||||
> `MIN_BUY_OUTPUT_AMOUNT` / `MIN_SELL_OUTPUT_AMOUNT` quotes before running.
|
||||
|
||||
```bash
|
||||
cargo run --package meteora_damm_v2_direct_trading
|
||||
```
|
||||
|
||||
Token program selection, pool orientation, dynamic fee state, and remaining accounts must be current. Refresh pool state and blockhash before a delayed sell. Use Token-2022-aware balance/account handling where required.
|
||||
|
||||
`MIN_*_OUTPUT_AMOUNT` is the on-chain minimum output, not a token UI amount. Derive it from a current quote and your slippage tolerance. The example intentionally refuses to use `1`, because that provides almost no price protection in partial-fill mode.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Meteora DAMM V2 直接交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示使用 `solana-streamer-sdk` 类型提供的池数据和 SDK 参数执行 Meteora DAMM V2 直接买卖流程。
|
||||
|
||||
> 这是会发送真实主网交易的模板。运行前必须设置 `PRIVATE_KEY`、`RPC_URL`,以及以原始单位表示的真实报价 `MIN_BUY_OUTPUT_AMOUNT` / `MIN_SELL_OUTPUT_AMOUNT`。
|
||||
|
||||
```bash
|
||||
cargo run --package meteora_damm_v2_direct_trading
|
||||
```
|
||||
|
||||
Token program、池方向、动态费率状态和 remaining accounts 必须保持最新。延迟卖出前刷新池状态和 blockhash;需要时使用兼容 Token-2022 的余额与账户处理。
|
||||
|
||||
`MIN_*_OUTPUT_AMOUNT` 是链上的最低输出原始数量,不是 UI 数量。应根据当前报价和滑点容忍度计算。示例会拒绝使用 `1`,因为在 partial-fill 模式下它几乎不提供价格保护。
|
||||
@@ -1,7 +1,5 @@
|
||||
use sol_trade_sdk::{
|
||||
common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
|
||||
},
|
||||
common::{AnyResult, TradeConfig},
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{DexParamEnum, MeteoraDammV2Params},
|
||||
@@ -10,12 +8,11 @@ use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{pubkey::Pubkey, signer::Signer};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn main() -> AnyResult<()> {
|
||||
println!("Testing Metaora Damm V2 trading...");
|
||||
|
||||
let client = create_solana_trade_client().await?;
|
||||
@@ -23,9 +20,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let pool = Pubkey::from_str("7dVri3qjYD3uobSZL3Zth8vSCgU6r6R2nvFsh7uVfDte").unwrap();
|
||||
let mint_pubkey = Pubkey::from_str("PRVT6TB7uss3FrUd2D9xs2zqDBsa3GbMJMwCQsgmeta").unwrap();
|
||||
let min_buy_output = required_u64_env("MIN_BUY_OUTPUT_AMOUNT")?;
|
||||
let min_sell_output = required_u64_env("MIN_SELL_OUTPUT_AMOUNT")?;
|
||||
|
||||
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);
|
||||
let pool_params =
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?;
|
||||
let token_program = if pool_params.token_a_mint == mint_pubkey {
|
||||
pool_params.token_a_program
|
||||
} else if pool_params.token_b_mint == mint_pubkey {
|
||||
pool_params.token_b_program
|
||||
} else {
|
||||
anyhow::bail!("target mint does not belong to the configured Meteora pool");
|
||||
};
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from Metaora Damm V2...");
|
||||
@@ -37,10 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
input_token_amount: input_token_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::MeteoraDammV2(
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
|
||||
.await?,
|
||||
),
|
||||
extension_params: DexParamEnum::MeteoraDammV2(pool_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -48,36 +55,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: Some(1),
|
||||
fixed_output_token_amount: Some(min_buy_output),
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from Metaora Damm V2...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM;
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
println!("Token balance: {}", amount_token);
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
|
||||
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
|
||||
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
|
||||
})?;
|
||||
if amount_token == 0 {
|
||||
anyhow::bail!("confirmed buy did not increase token balance");
|
||||
}
|
||||
println!("Position acquired by this run: {}", amount_token);
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::MeteoraDammV2,
|
||||
output_token_type: TradeTokenType::USDC,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::MeteoraDammV2(
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
|
||||
@@ -90,23 +97,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: Some(1),
|
||||
fixed_output_token_amount: Some(min_sell_output),
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Exit program
|
||||
std::process::exit(0);
|
||||
fn required_u64_env(name: &str) -> AnyResult<u64> {
|
||||
let value = std::env::var(name).map_err(|_| {
|
||||
anyhow::anyhow!("{} is required and must be a real raw-unit minimum output quote", name)
|
||||
})?;
|
||||
let amount = value.parse::<u64>().map_err(|_| anyhow::anyhow!("{} must be u64", name))?;
|
||||
if amount == 0 {
|
||||
anyhow::bail!("{} must be greater than zero", name);
|
||||
}
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
/// Create SolanaTrade client
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("your_payer_keypair_here");
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
|
||||
@@ -5,8 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
anyhow = "1.0.79"
|
||||
anyhow = "1.0.79"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Instruction Middleware
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Demonstrates implementing `InstructionMiddleware`, installing it in a `MiddlewareManager`, and observing or modifying instructions before submission.
|
||||
|
||||
> The example loads `PRIVATE_KEY` and calls `client.buy`. After configuration it can submit a real PumpSwap mainnet transaction.
|
||||
|
||||
```bash
|
||||
cargo run --package middleware_system
|
||||
```
|
||||
|
||||
Middleware must preserve signer, account-order, compute-budget, tip, nonce, and transaction-size semantics. Keep latency-sensitive middleware deterministic and free of blocking I/O.
|
||||
@@ -0,0 +1,13 @@
|
||||
# 指令中间件
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示如何实现 `InstructionMiddleware`、安装到 `MiddlewareManager`,并在提交前观察或修改指令。
|
||||
|
||||
> 示例从 `PRIVATE_KEY` 加载钱包并调用 `client.buy`;配置完成后可能提交真实 PumpSwap 主网交易。
|
||||
|
||||
```bash
|
||||
cargo run --package middleware_system
|
||||
```
|
||||
|
||||
中间件必须保持 signer、账户顺序、compute budget、tip、nonce 和交易体积语义。低延迟中间件应确定性执行,不能包含阻塞 I/O。
|
||||
@@ -10,7 +10,7 @@ use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Keypair};
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
#[tokio::main]
|
||||
@@ -59,7 +59,7 @@ impl InstructionMiddleware for CustomMiddleware {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
sol-parser-sdk = "0.4.14"
|
||||
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
spl-associated-token-account = "7.0.0"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Durable Nonce
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Shows how to fetch a durable nonce and attach it to a PumpFun transaction triggered by a `sol-parser-sdk` gRPC event.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY`, replace `use_your_nonce_account_here`, and configure RPC and gRPC before running. The nonce authority must match the signer.
|
||||
|
||||
```bash
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
export GRPC_AUTH_TOKEN=optional-token
|
||||
cargo run --package nonce_cache
|
||||
```
|
||||
|
||||
Do not set both a recent blockhash and a durable nonce in new code; use `SimpleBuyParams::with_durable_nonce`. Refresh nonce state immediately before construction, and never reuse consumed nonce state. A nonce extends transaction validity but does not preserve quote freshness.
|
||||
|
||||
See [Nonce Cache](../../docs/NONCE_CACHE.md).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Durable Nonce
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示如何获取 durable nonce,并附加到由 `sol-parser-sdk` gRPC 事件触发的 PumpFun 交易。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,替换 `use_your_nonce_account_here`,并配置 RPC 和 gRPC;nonce authority 必须与签名者匹配。
|
||||
|
||||
```bash
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
export GRPC_AUTH_TOKEN=optional-token
|
||||
cargo run --package nonce_cache
|
||||
```
|
||||
|
||||
新代码不要同时设置 recent blockhash 和 durable nonce,应使用 `SimpleBuyParams::with_durable_nonce`。构建前刷新 nonce,已消费状态不可复用。Nonce 只延长交易有效期,不会保持报价新鲜。
|
||||
|
||||
参见[Nonce Cache 说明](../../docs/NONCE_CACHE_CN.md)。
|
||||
@@ -23,7 +23,7 @@ use sol_trade_sdk::{
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -104,7 +104,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
sol-parser-sdk = "0.4.14"
|
||||
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# PumpFun Copy Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Consumes PumpFun buy/sell events through `sol-parser-sdk` Yellowstone gRPC and maps the event's post-trade protocol fields into one follow-up buy/sell flow.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY` and configure RPC, gRPC, target filtering, amounts, slippage, and fee limits before running.
|
||||
|
||||
```bash
|
||||
export RPC_URL=https://your-rpc.example
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
cargo run --package pumpfun_copy_trading
|
||||
```
|
||||
|
||||
Copying every event is unsafe. Add wallet/mint allowlists, signature plus instruction-index deduplication, event-age limits, position isolation, and maximum input/loss controls. Prewarm the client and blockhash cache before subscription for low latency.
|
||||
@@ -0,0 +1,15 @@
|
||||
# PumpFun 跟单交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
通过 `sol-parser-sdk` Yellowstone gRPC 消费 PumpFun 买卖事件,把事件中的成交后协议字段映射到一次跟随买卖流程。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`,并配置 RPC、gRPC、目标过滤、金额、滑点和费用上限。
|
||||
|
||||
```bash
|
||||
export RPC_URL=https://your-rpc.example
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
cargo run --package pumpfun_copy_trading
|
||||
```
|
||||
|
||||
无条件复制全部事件是不安全的。必须增加钱包/mint allowlist、签名加指令索引去重、事件年龄上限、持仓隔离和最大输入/亏损控制。低延迟场景应在订阅前预热客户端和 blockhash cache。
|
||||
@@ -12,9 +12,7 @@ use sol_parser_sdk::grpc::{
|
||||
TransactionFilter, YellowstoneGrpc,
|
||||
};
|
||||
use sol_parser_sdk::DexEvent;
|
||||
use sol_trade_sdk::common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
|
||||
};
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
@@ -26,8 +24,6 @@ use sol_trade_sdk::{
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -109,7 +105,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
@@ -140,6 +136,8 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
|
||||
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);
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
|
||||
|
||||
// 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析)
|
||||
let buy_sol_amount = 100_000u64;
|
||||
@@ -180,19 +178,25 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// 卖出:查询余额后卖出,同样传入 is_cashback_coin
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&e.token_program,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
|
||||
let amount_token = balance_after
|
||||
.checked_sub(balance_before)
|
||||
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
|
||||
if amount_token == 0 {
|
||||
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
|
||||
}
|
||||
let sell_extension = PumpFunParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey)
|
||||
.await?
|
||||
.with_creator_vault(e.creator_vault);
|
||||
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
@@ -200,25 +204,9 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||
e.bonding_curve,
|
||||
e.associated_bonding_curve,
|
||||
e.mint,
|
||||
e.quote_mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
e.real_token_reserves,
|
||||
real_quote_reserves,
|
||||
Some(true),
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
extension_params: DexParamEnum::PumpFun(sell_extension),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -231,7 +219,12 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
println!("跟单一次买+卖完成");
|
||||
Ok(())
|
||||
|
||||
@@ -5,7 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
sol-parser-sdk = "0.4.14"
|
||||
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# PumpFun Sniper Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Uses `sol-parser-sdk` Yellowstone gRPC and selects creator-first-buy events (`is_created_buy`). Event fields are mapped into PumpFun params, including quote reserves, token program, cashback, and mayhem state, before one buy/sell flow.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY`, `RPC_URL`, `GRPC_ENDPOINT`, optional `GRPC_AUTH_TOKEN`, target policy, amounts, and fees.
|
||||
|
||||
```bash
|
||||
export RPC_URL=https://your-rpc.example
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
cargo run --package pumpfun_sniper_trading
|
||||
```
|
||||
|
||||
The current source creates `SolanaTrade` and fetches blockhash inside event handling. For production latency, initialize both before subscription as shown in the [low-latency guide](../../docs/LOW_LATENCY_BOTS.md). Deduplicate signatures and refresh state before delayed sells.
|
||||
@@ -0,0 +1,15 @@
|
||||
# PumpFun 狙击交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
使用 `sol-parser-sdk` Yellowstone gRPC,只选择创建者首次买入事件 `is_created_buy`。示例将 quote 储备、token program、cashback 和 mayhem 等事件字段映射到 PumpFun 参数,再执行一次买卖流程。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、`RPC_URL`、`GRPC_ENDPOINT`、可选 `GRPC_AUTH_TOKEN`、目标策略、金额和费用。
|
||||
|
||||
```bash
|
||||
export RPC_URL=https://your-rpc.example
|
||||
export GRPC_ENDPOINT=https://your-yellowstone.example
|
||||
cargo run --package pumpfun_sniper_trading
|
||||
```
|
||||
|
||||
当前源码在事件处理中创建 `SolanaTrade` 并查询 blockhash。生产低延迟实现应按[低延迟指南](../../docs/LOW_LATENCY_BOTS_CN.md)在订阅前完成预热,同时进行签名去重并刷新延迟卖出状态。
|
||||
@@ -13,7 +13,6 @@ use sol_parser_sdk::grpc::{
|
||||
TransactionFilter, YellowstoneGrpc,
|
||||
};
|
||||
use sol_parser_sdk::DexEvent;
|
||||
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
@@ -26,8 +25,6 @@ use sol_trade_sdk::{
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -99,7 +96,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
@@ -118,15 +115,10 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
let mint_pubkey = e.mint;
|
||||
let virtual_quote_reserves = if e.virtual_quote_reserves != 0 {
|
||||
e.virtual_quote_reserves
|
||||
} else {
|
||||
e.virtual_sol_reserves
|
||||
};
|
||||
let real_quote_reserves =
|
||||
if e.virtual_quote_reserves != 0 { e.real_quote_reserves } else { e.real_sol_reserves };
|
||||
let slippage_basis_points = Some(300u64);
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
|
||||
|
||||
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);
|
||||
@@ -169,13 +161,22 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address(&payer, &mint_pubkey);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &e.token_program).await?;
|
||||
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
|
||||
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
|
||||
})?;
|
||||
if amount_token == 0 {
|
||||
anyhow::bail!("confirmed buy did not increase token balance");
|
||||
}
|
||||
let sell_extension = PumpFunParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey)
|
||||
.await?
|
||||
.with_creator_vault(e.creator_vault);
|
||||
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
@@ -183,25 +184,9 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||
e.bonding_curve,
|
||||
e.associated_bonding_curve,
|
||||
e.mint,
|
||||
e.quote_mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
e.real_token_reserves,
|
||||
real_quote_reserves,
|
||||
Some(true),
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
extension_params: DexParamEnum::PumpFun(sell_extension),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -214,7 +199,10 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
println!("狙击一次买+卖完成");
|
||||
Ok(())
|
||||
|
||||
@@ -5,7 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# PumpSwap Direct Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
RPC-driven PumpSwap buy/wait/sell example without an event stream. It resolves pool state through RPC, buys, reads the wallet token balance, refreshes pool state, and sells.
|
||||
|
||||
> This template submits real mainnet transactions. Set `PRIVATE_KEY` and `RPC_URL`, replace mint and pool, then review amounts, slippage, fee strategy, ATA flags, and whether WSOL accounts should be closed.
|
||||
|
||||
```bash
|
||||
cargo run --package pumpswap_direct_trading
|
||||
```
|
||||
|
||||
For new code, prefer `SimpleBuyParams` with `BuyAmount::WithMaxInput` or an explicitly selected intent. Fetch a fresh blockhash before the delayed sell; do not reuse pre-buy reserves.
|
||||
|
||||
For an event-driven version, see [`pumpswap_trading`](../pumpswap_trading/README.md).
|
||||
@@ -0,0 +1,15 @@
|
||||
# PumpSwap 直接交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
不依赖事件流的 RPC 驱动 PumpSwap 买入/等待/卖出示例:通过 RPC 读取池状态,买入后查询钱包 token 余额,刷新池状态再卖出。
|
||||
|
||||
> 模板会提交真实主网交易。运行前设置 `PRIVATE_KEY` 和 `RPC_URL`,替换 mint 和 pool,并检查金额、滑点、费用策略、ATA flag 及是否关闭 WSOL 账户。
|
||||
|
||||
```bash
|
||||
cargo run --package pumpswap_direct_trading
|
||||
```
|
||||
|
||||
新代码优先使用 `SimpleBuyParams`,并明确选择 `BuyAmount::WithMaxInput` 或其他交易意图。延迟卖出前必须获取新 blockhash,不能复用买入前储备。
|
||||
|
||||
事件驱动版本见 [`pumpswap_trading`](../pumpswap_trading/README_CN.md)。
|
||||
@@ -1,7 +1,5 @@
|
||||
use sol_trade_sdk::{
|
||||
common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
|
||||
},
|
||||
common::{AnyResult, TradeConfig},
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
@@ -10,12 +8,11 @@ use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{pubkey::Pubkey, signer::Signer};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn main() -> AnyResult<()> {
|
||||
println!("Testing PumpSwap trading...");
|
||||
|
||||
let client = create_solana_trade_client().await?;
|
||||
@@ -26,6 +23,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
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);
|
||||
let pool_params =
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?;
|
||||
let token_program = if pool_params.base_mint == mint_pubkey {
|
||||
pool_params.base_token_program
|
||||
} else if pool_params.quote_mint == mint_pubkey {
|
||||
pool_params.quote_token_program
|
||||
} else {
|
||||
anyhow::bail!("target mint does not belong to the configured pool");
|
||||
};
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
@@ -37,9 +45,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
input_token_amount: buy_sol_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpSwap(
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
),
|
||||
extension_params: DexParamEnum::PumpSwap(pool_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -53,29 +59,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from PumpSwap...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM_2022;
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
|
||||
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
|
||||
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
|
||||
})?;
|
||||
if amount_token == 0 {
|
||||
anyhow::bail!("confirmed buy did not increase token balance");
|
||||
}
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpSwap,
|
||||
output_token_type: TradeTokenType::SOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpSwap(
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
@@ -92,9 +98,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -102,8 +109,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
PRIVATE_KEY=
|
||||
RPC_URL=https://api.mainnet-beta.solana.com
|
||||
GRPC_ENDPOINT=https://solana-yellowstone-grpc.publicnode.com:443
|
||||
GRPC_AUTH_TOKEN=
|
||||
|
||||
# At least one target is required. If both are set, both must match.
|
||||
TARGET_MINT=
|
||||
TARGET_POOL=
|
||||
MAX_EVENT_AGE_MS=1000
|
||||
@@ -5,8 +5,9 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
|
||||
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# PumpSwap Low-Latency gRPC Example
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
This example consumes PumpSwap events through `solana-streamer-sdk` and builds one follow-up buy from the event's post-trade reserves and dynamic fee rates. The trading client and blockhash cache are initialized before subscription, so the event hot path does not synchronously fetch a blockhash.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env, then load it into the current shell:
|
||||
set -a; source .env; set +a
|
||||
export PRIVATE_KEY=your_base58_private_key
|
||||
export RPC_URL=https://your-rpc.example
|
||||
cargo run --release --package pumpswap_trading
|
||||
```
|
||||
|
||||
`GRPC_ENDPOINT` and `GRPC_AUTH_TOKEN` are optional. `TARGET_MINT` or `TARGET_POOL` is required; when both are set, both must match. `MAX_EVENT_AGE_MS` defaults to 1000. The binary reads environment variables but does not load `.env` itself, so source the file first when using it.
|
||||
|
||||
## Trade semantics
|
||||
|
||||
- The buy uses `BuyAmount::WithMaxInput`, which applies slippage to maximum quote cost and is appropriate when fill priority matters.
|
||||
- Buy parameters use post-trade reserves and LP/protocol/creator fee bps from the event.
|
||||
- The event's raw and virtual quote reserves come from the same transaction snapshot. The hot path does not fetch the Pool account, avoiding both added latency and mixed-slot quotes.
|
||||
- The first matching event asynchronously records the pre-buy balance; the next fresh event performs the trade without a balance RPC in the submission hot path. It sells only the confirmed balance increase and refreshes pool state and blockhash before selling.
|
||||
- Use `BuyAmount::ExactInput` when the quote spend must be exact. That mode protects minimum output and can fail more often in an active pool.
|
||||
- If baseline warmup fails, the example waits for another event. Once transaction execution starts, an error keeps the one-shot guard locked because submission or position state may be uncertain; inspect the signatures and account state before retrying.
|
||||
|
||||
Production bots should also add durable signature deduplication, a position state machine, SWQoS configuration, and bounded requoting. Do not solve slippage errors by setting `min_out` to zero.
|
||||
@@ -0,0 +1,27 @@
|
||||
# PumpSwap 低延迟 gRPC 示例
|
||||
|
||||
[English](README.md)
|
||||
|
||||
该示例通过 `solana-streamer-sdk` 监听 PumpSwap 买卖事件,并用事件中的成交后储备和动态费率构建一次跟随买入。交易客户端和 blockhash cache 会在订阅前初始化,事件热路径不会同步查询 blockhash。
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env,然后载入当前 shell:
|
||||
set -a; source .env; set +a
|
||||
cargo run --release --package pumpswap_trading
|
||||
```
|
||||
|
||||
也可以直接导出环境变量。`TARGET_MINT` 或 `TARGET_POOL` 至少设置一个;两者同时设置时必须都匹配。`MAX_EVENT_AGE_MS` 默认 1000。程序只读取环境变量,不会自行加载 `.env`。
|
||||
|
||||
## 交易语义
|
||||
|
||||
- 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。
|
||||
- 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。
|
||||
- 原始 quote 储备和虚拟 quote 储备均来自同一笔交易的事件快照;热路径不再查询 Pool 账户,避免额外延迟和跨 slot 混合报价。
|
||||
- 第一个匹配事件用于异步记录买前余额,下一条新鲜事件才会交易,因此提交热路径不再查询余额;示例只卖出确认后的余额增量,并在卖出前重新获取池状态和 blockhash。
|
||||
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
|
||||
- 基线预热失败时会等待后续事件;一旦进入交易阶段,错误会保持单次执行锁定,因为提交状态或持仓可能不确定,必须先核对签名和账户状态再重试。
|
||||
|
||||
生产机器人还应增加持久化签名去重、持仓状态机、SWQoS 配置和有限次数的重新报价。不要通过把 `min_out` 设为零来处理滑点错误。
|
||||
@@ -1,5 +1,4 @@
|
||||
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::common::{clock::now_micros, SolanaRpcClient, TradeConfig};
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
@@ -8,41 +7,160 @@ use sol_trade_sdk::{
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
AccountPolicy, BuyAmount, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{pubkey::Pubkey, signer::Signer};
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey};
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID;
|
||||
use solana_streamer_sdk::streaming::event_parser::{
|
||||
common::filter::EventTypeFilter, protocols::pumpswap::PumpSwapBuyEvent,
|
||||
};
|
||||
use solana_streamer_sdk::streaming::event_parser::{
|
||||
common::EventType, protocols::pumpswap::PumpSwapSellEvent,
|
||||
};
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
||||
use solana_streamer_sdk::{
|
||||
match_event, streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
Arc, RwLock,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::watch;
|
||||
|
||||
// Global static flag to ensure transaction is executed only once
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
const BLOCKHASH_REFRESH_INTERVAL: Duration = Duration::from_millis(400);
|
||||
const MAX_BLOCKHASH_AGE: Duration = Duration::from_secs(20);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct EventSelection {
|
||||
target_mint: Option<Pubkey>,
|
||||
target_pool: Option<Pubkey>,
|
||||
max_event_age_ms: u64,
|
||||
}
|
||||
|
||||
impl EventSelection {
|
||||
fn from_env() -> AnyResult<Self> {
|
||||
let target_mint = parse_optional_pubkey("TARGET_MINT")?;
|
||||
let target_pool = parse_optional_pubkey("TARGET_POOL")?;
|
||||
if target_mint.is_none() && target_pool.is_none() {
|
||||
anyhow::bail!("set TARGET_MINT or TARGET_POOL before running this live example");
|
||||
}
|
||||
let max_event_age_ms = std::env::var("MAX_EVENT_AGE_MS")
|
||||
.unwrap_or_else(|_| "1000".to_string())
|
||||
.parse::<u64>()
|
||||
.map_err(|_| anyhow::anyhow!("MAX_EVENT_AGE_MS must be a positive integer"))?;
|
||||
if max_event_age_ms == 0 || max_event_age_ms > i64::MAX as u64 / 1_000 {
|
||||
anyhow::bail!("MAX_EVENT_AGE_MS is outside the supported range");
|
||||
}
|
||||
Ok(Self { target_mint, target_pool, max_event_age_ms })
|
||||
}
|
||||
|
||||
fn matches(self, pool: Pubkey, base_mint: Pubkey, quote_mint: Pubkey, recv_us: i64) -> bool {
|
||||
if self.target_pool.is_some_and(|target| target != pool) {
|
||||
return false;
|
||||
}
|
||||
if self.target_mint.is_some_and(|target| target != base_mint && target != quote_mint) {
|
||||
return false;
|
||||
}
|
||||
is_event_fresh(recv_us, now_micros(), self.max_event_age_ms)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_optional_pubkey(key: &str) -> AnyResult<Option<Pubkey>> {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| Pubkey::from_str(&value).map_err(anyhow::Error::from))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn is_event_fresh(recv_us: i64, now_us: i64, max_age_ms: u64) -> bool {
|
||||
recv_us > 0
|
||||
&& now_us >= recv_us
|
||||
&& now_us.saturating_sub(recv_us) <= (max_age_ms as i64).saturating_mul(1_000)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedBlockhash {
|
||||
hash: Hash,
|
||||
fetched_at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PositionBaseline {
|
||||
mint: Pubkey,
|
||||
token_program: Pubkey,
|
||||
amount: u64,
|
||||
}
|
||||
|
||||
enum EventAction {
|
||||
BaselineWarmed,
|
||||
TradeCompleted,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BlockhashCache {
|
||||
receiver: watch::Receiver<CachedBlockhash>,
|
||||
}
|
||||
|
||||
impl BlockhashCache {
|
||||
async fn start(rpc: Arc<SolanaRpcClient>) -> AnyResult<Self> {
|
||||
let initial =
|
||||
CachedBlockhash { hash: rpc.get_latest_blockhash().await?, fetched_at: Instant::now() };
|
||||
let (sender, receiver) = watch::channel(initial);
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(BLOCKHASH_REFRESH_INTERVAL);
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match rpc.get_latest_blockhash().await {
|
||||
Ok(hash) => {
|
||||
if sender
|
||||
.send(CachedBlockhash { hash, fetched_at: Instant::now() })
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => eprintln!("warning: blockhash refresh failed: {err}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(Self { receiver })
|
||||
}
|
||||
|
||||
fn latest(&self) -> AnyResult<Hash> {
|
||||
let cached = self.receiver.borrow().clone();
|
||||
if cached.fetched_at.elapsed() > MAX_BLOCKHASH_AGE {
|
||||
anyhow::bail!("cached blockhash is older than {} seconds", MAX_BLOCKHASH_AGE.as_secs());
|
||||
}
|
||||
Ok(cached.hash)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Subscribing to GRPC events...");
|
||||
|
||||
let selection = EventSelection::from_env()?;
|
||||
|
||||
let trade_client = Arc::new(create_solana_trade_client().await?);
|
||||
let blockhash_cache = BlockhashCache::start(trade_client.infrastructure.rpc.clone()).await?;
|
||||
let position_baseline = Arc::new(RwLock::new(None));
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
std::env::var("GRPC_ENDPOINT")
|
||||
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()),
|
||||
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||
)?;
|
||||
|
||||
let callback = create_event_callback();
|
||||
let callback =
|
||||
create_event_callback(trade_client, blockhash_cache, position_baseline, selection);
|
||||
let protocols = vec![Protocol::PumpSwap];
|
||||
// Filter accounts
|
||||
let account_include = vec![
|
||||
@@ -63,8 +181,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
||||
|
||||
// listen to specific event type
|
||||
let event_type_filter =
|
||||
EventTypeFilter { include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell] };
|
||||
let event_type_filter = EventTypeFilter {
|
||||
include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
grpc.subscribe_events_immediate(
|
||||
protocols,
|
||||
@@ -83,53 +203,120 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
||||
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return;
|
||||
}
|
||||
// Test code, only test one transaction
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpswap_trade_with_grpc_buy_event(event_clone).await {
|
||||
eprintln!("Error in trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
||||
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return;
|
||||
}
|
||||
// Test code, only test one transaction
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpswap_trade_with_grpc_sell_event(event_clone).await {
|
||||
eprintln!("Error in trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
fn create_event_callback(
|
||||
client: Arc<SolanaTrade>,
|
||||
blockhash_cache: BlockhashCache,
|
||||
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
|
||||
selection: EventSelection,
|
||||
) -> impl Fn(DexEvent) {
|
||||
move |event: DexEvent| match event {
|
||||
DexEvent::PumpSwapBuyEvent(e) => {
|
||||
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return;
|
||||
}
|
||||
});
|
||||
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
|
||||
return;
|
||||
}
|
||||
// Test code, only test one transaction
|
||||
if ALREADY_EXECUTED
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
let client = client.clone();
|
||||
let blockhash_cache = blockhash_cache.clone();
|
||||
let position_baseline = position_baseline.clone();
|
||||
let was_preparing =
|
||||
position_baseline.read().map(|baseline| baseline.is_none()).unwrap_or(true);
|
||||
tokio::spawn(async move {
|
||||
match pumpswap_trade_with_grpc_buy_event(
|
||||
client,
|
||||
blockhash_cache,
|
||||
position_baseline,
|
||||
selection,
|
||||
e,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(EventAction::BaselineWarmed) => {
|
||||
ALREADY_EXECUTED.store(false, Ordering::Release);
|
||||
}
|
||||
Ok(EventAction::TradeCompleted) => {}
|
||||
Err(err) if was_preparing => {
|
||||
eprintln!("baseline warmup failed; waiting for a later event: {err:?}");
|
||||
ALREADY_EXECUTED.store(false, Ordering::Release);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"trade failed after entering execution state: {err:?}; automatic retry is disabled because submission status or position state may be uncertain"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
DexEvent::PumpSwapSellEvent(e) => {
|
||||
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return;
|
||||
}
|
||||
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
|
||||
return;
|
||||
}
|
||||
// Test code, only test one transaction
|
||||
if ALREADY_EXECUTED
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
let client = client.clone();
|
||||
let blockhash_cache = blockhash_cache.clone();
|
||||
let position_baseline = position_baseline.clone();
|
||||
let was_preparing =
|
||||
position_baseline.read().map(|baseline| baseline.is_none()).unwrap_or(true);
|
||||
tokio::spawn(async move {
|
||||
match pumpswap_trade_with_grpc_sell_event(
|
||||
client,
|
||||
blockhash_cache,
|
||||
position_baseline,
|
||||
selection,
|
||||
e,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(EventAction::BaselineWarmed) => {
|
||||
ALREADY_EXECUTED.store(false, Ordering::Release);
|
||||
}
|
||||
Ok(EventAction::TradeCompleted) => {}
|
||||
Err(err) if was_preparing => {
|
||||
eprintln!("baseline warmup failed; waiting for a later event: {err:?}");
|
||||
ALREADY_EXECUTED.store(false, Ordering::Release);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"trade failed after entering execution state: {err:?}; automatic retry is disabled because submission status or position state may be uncertain"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SolanaTrade client
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("your_payer_keypair_here");
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
println!("Initializing SolanaTrade client...");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -141,12 +328,17 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
println!("SolanaTrade client initialized successfully");
|
||||
Ok(solana_trade)
|
||||
}
|
||||
|
||||
async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
async fn pumpswap_trade_with_grpc_buy_event(
|
||||
client: Arc<SolanaTrade>,
|
||||
blockhash_cache: BlockhashCache,
|
||||
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
|
||||
selection: EventSelection,
|
||||
trade_info: PumpSwapBuyEvent,
|
||||
) -> AnyResult<EventAction> {
|
||||
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
||||
trade_info.pool,
|
||||
trade_info.base_mint,
|
||||
@@ -155,6 +347,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
trade_info.pool_quote_token_account,
|
||||
trade_info.pool_base_token_reserves,
|
||||
trade_info.pool_quote_token_reserves,
|
||||
trade_info.virtual_quote_reserves,
|
||||
trade_info.coin_creator_vault_ata,
|
||||
trade_info.coin_creator_vault_authority,
|
||||
trade_info.base_token_program,
|
||||
@@ -162,8 +355,8 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
trade_info.protocol_fee_recipient,
|
||||
Pubkey::default(),
|
||||
trade_info.coin_creator,
|
||||
false,
|
||||
0,
|
||||
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
trade_info.lp_fee_basis_points,
|
||||
trade_info.protocol_fee_basis_points,
|
||||
trade_info.coin_creator_fee_basis_points,
|
||||
@@ -175,12 +368,25 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
} else {
|
||||
trade_info.base_mint
|
||||
};
|
||||
pumpswap_trade_with_grpc(&client, mint, params).await?;
|
||||
Ok(())
|
||||
pumpswap_trade_with_grpc(
|
||||
&client,
|
||||
&blockhash_cache,
|
||||
&position_baseline,
|
||||
trade_info.metadata.recv_us,
|
||||
selection.max_event_age_ms,
|
||||
mint,
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
async fn pumpswap_trade_with_grpc_sell_event(
|
||||
client: Arc<SolanaTrade>,
|
||||
blockhash_cache: BlockhashCache,
|
||||
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
|
||||
selection: EventSelection,
|
||||
trade_info: PumpSwapSellEvent,
|
||||
) -> AnyResult<EventAction> {
|
||||
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
||||
trade_info.pool,
|
||||
trade_info.base_mint,
|
||||
@@ -189,6 +395,7 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
trade_info.pool_quote_token_account,
|
||||
trade_info.pool_base_token_reserves,
|
||||
trade_info.pool_quote_token_reserves,
|
||||
trade_info.virtual_quote_reserves,
|
||||
trade_info.coin_creator_vault_ata,
|
||||
trade_info.coin_creator_vault_authority,
|
||||
trade_info.base_token_program,
|
||||
@@ -196,8 +403,8 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
trade_info.protocol_fee_recipient,
|
||||
Pubkey::default(),
|
||||
trade_info.coin_creator,
|
||||
false,
|
||||
0,
|
||||
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
trade_info.lp_fee_basis_points,
|
||||
trade_info.protocol_fee_basis_points,
|
||||
trade_info.coin_creator_fee_basis_points,
|
||||
@@ -209,92 +416,166 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
} else {
|
||||
trade_info.base_mint
|
||||
};
|
||||
pumpswap_trade_with_grpc(&client, mint, params).await?;
|
||||
Ok(())
|
||||
pumpswap_trade_with_grpc(
|
||||
&client,
|
||||
&blockhash_cache,
|
||||
&position_baseline,
|
||||
trade_info.metadata.recv_us,
|
||||
selection.max_event_age_ms,
|
||||
mint,
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn pumpswap_trade_with_grpc(
|
||||
client: &SolanaTrade,
|
||||
blockhash_cache: &BlockhashCache,
|
||||
position_baseline: &Arc<RwLock<Option<PositionBaseline>>>,
|
||||
grpc_recv_us: i64,
|
||||
max_event_age_ms: u64,
|
||||
mint_pubkey: Pubkey,
|
||||
params: PumpSwapParams,
|
||||
) -> AnyResult<()> {
|
||||
) -> AnyResult<EventAction> {
|
||||
println!("Testing PumpSwap trading...");
|
||||
validate_pumpswap_snapshot(¶ms)?;
|
||||
if !is_event_fresh(grpc_recv_us, now_micros(), max_event_age_ms) {
|
||||
anyhow::bail!("event became stale before transaction construction");
|
||||
}
|
||||
let slippage_basis_points = Some(500);
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
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);
|
||||
|
||||
let is_sol = params.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| params.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let program_id = if params.base_mint == mint_pubkey {
|
||||
params.base_token_program
|
||||
} else if params.quote_mint == mint_pubkey {
|
||||
params.quote_token_program
|
||||
} else {
|
||||
anyhow::bail!("target mint {} does not belong to pool {}", mint_pubkey, params.pool);
|
||||
};
|
||||
let baseline = position_baseline
|
||||
.read()
|
||||
.map_err(|_| anyhow::anyhow!("position baseline lock is poisoned"))?
|
||||
.as_ref()
|
||||
.copied();
|
||||
let balance_before = if let Some(baseline) = baseline {
|
||||
if baseline.mint != mint_pubkey || baseline.token_program != program_id {
|
||||
anyhow::bail!("cached position baseline belongs to a different mint or token program");
|
||||
}
|
||||
baseline.amount
|
||||
} else {
|
||||
let amount = client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
|
||||
let mut baseline = position_baseline
|
||||
.write()
|
||||
.map_err(|_| anyhow::anyhow!("position baseline lock is poisoned"))?;
|
||||
*baseline = Some(PositionBaseline { mint: mint_pubkey, token_program: program_id, amount });
|
||||
println!(
|
||||
"Position baseline warmed at {} base units; waiting for the next fresh matching event",
|
||||
amount
|
||||
);
|
||||
return Ok(EventAction::BaselineWarmed);
|
||||
};
|
||||
|
||||
let recent_blockhash = blockhash_cache.latest()?;
|
||||
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);
|
||||
if !is_event_fresh(grpc_recv_us, now_micros(), max_event_age_ms) {
|
||||
anyhow::bail!("event became stale while preparing the transaction");
|
||||
}
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
let buy_token_amount = 300_000;
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::PumpSwap,
|
||||
input_token_type: if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: buy_token_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_input_token_ata: is_sol,
|
||||
close_input_token_ata: is_sol,
|
||||
create_mint_ata: true,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpSwap,
|
||||
if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_token_amount },
|
||||
DexParamEnum::PumpSwap(params.clone()),
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.slippage_basis_points(slippage_basis_points.unwrap_or(500))
|
||||
.account_policy(AccountPolicy::Auto)
|
||||
.wait_tx_confirmed(true)
|
||||
.grpc_recv_us(grpc_recv_us);
|
||||
let (ok, sigs, err, _) = client.buy_simple(buy_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from PumpSwap...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let program_id = if params.base_mint == mint_pubkey {
|
||||
params.base_token_program
|
||||
} else {
|
||||
params.quote_token_program
|
||||
};
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpSwap,
|
||||
output_token_type: if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpSwap(params.clone()),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
create_output_token_ata: is_sol,
|
||||
close_output_token_ata: is_sol,
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
|
||||
let position_amount = balance_after.checked_sub(balance_before).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"token balance decreased from {} to {}; refusing to sell existing holdings",
|
||||
balance_before,
|
||||
balance_after
|
||||
)
|
||||
})?;
|
||||
if position_amount == 0 {
|
||||
anyhow::bail!("confirmed buy did not increase token balance; refusing to sell");
|
||||
}
|
||||
let sell_params_from_rpc =
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, ¶ms.pool).await?;
|
||||
let sell_params = SimpleSellParams::new(
|
||||
DexType::PumpSwap,
|
||||
if is_sol { TradeTokenType::SOL } else { TradeTokenType::USDC },
|
||||
mint_pubkey,
|
||||
SellAmount::ExactInput(position_amount),
|
||||
DexParamEnum::PumpSwap(sell_params_from_rpc),
|
||||
blockhash_cache.latest()?,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.slippage_basis_points(slippage_basis_points.unwrap_or(500))
|
||||
.account_policy(AccountPolicy::Auto)
|
||||
.wait_tx_confirmed(true)
|
||||
.with_tip(false);
|
||||
let (ok, sigs, err, _) = client.sell_simple(sell_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
// Exit program
|
||||
std::process::exit(0);
|
||||
println!("Round-trip example completed; further matching events remain locked out");
|
||||
Ok(EventAction::TradeCompleted)
|
||||
}
|
||||
|
||||
fn validate_pumpswap_snapshot(params: &PumpSwapParams) -> AnyResult<()> {
|
||||
let required = [
|
||||
("pool", params.pool),
|
||||
("base_mint", params.base_mint),
|
||||
("quote_mint", params.quote_mint),
|
||||
("pool_base_token_account", params.pool_base_token_account),
|
||||
("pool_quote_token_account", params.pool_quote_token_account),
|
||||
("coin_creator_vault_ata", params.coin_creator_vault_ata),
|
||||
("coin_creator_vault_authority", params.coin_creator_vault_authority),
|
||||
("base_token_program", params.base_token_program),
|
||||
("quote_token_program", params.quote_token_program),
|
||||
];
|
||||
for (name, value) in required {
|
||||
if value == Pubkey::default() {
|
||||
anyhow::bail!("event snapshot is missing {name}");
|
||||
}
|
||||
}
|
||||
if params.pool_base_token_reserves == 0 || params.pool_quote_token_reserves == 0 {
|
||||
anyhow::bail!("event snapshot has an empty raw pool reserve");
|
||||
}
|
||||
params.effective_quote_reserves()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_event_fresh;
|
||||
|
||||
#[test]
|
||||
fn event_freshness_has_a_strict_boundary() {
|
||||
assert!(!is_event_fresh(0, 1_000_000, 100));
|
||||
assert!(!is_event_fresh(1_000_001, 1_000_000, 100));
|
||||
assert!(!is_event_fresh(899_999, 1_000_000, 100));
|
||||
assert!(is_event_fresh(900_000, 1_000_000, 100));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Raydium AMM V4 Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Consumes Raydium AMM V4 events from `solana-streamer-sdk`, builds AMM params, and executes one buy/sell flow.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY`, RPC/stream configuration, target filters, token amounts, and fees before running.
|
||||
|
||||
```bash
|
||||
cargo run --package raydium_amm_v4_trading
|
||||
```
|
||||
|
||||
AMM address, market accounts, vaults, token programs, and reserve direction must match the event. For low latency, prewarm the client and blockhash cache and refresh delayed-sell state rather than reusing the triggering snapshot.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Raydium AMM V4 交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
通过 `solana-streamer-sdk` 消费 Raydium AMM V4 事件,构建 AMM 参数并执行一次买卖流程。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、RPC/stream 配置、目标过滤、token 数量和费用。
|
||||
|
||||
```bash
|
||||
cargo run --package raydium_amm_v4_trading
|
||||
```
|
||||
|
||||
AMM 地址、market 账户、vault、token program 和储备方向必须与事件一致。低延迟场景应预热客户端和 blockhash cache,并刷新延迟卖出状态,不能复用触发快照。
|
||||
@@ -1,4 +1,3 @@
|
||||
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
@@ -10,17 +9,13 @@ use sol_trade_sdk::{
|
||||
};
|
||||
use sol_trade_sdk::{common::TradeConfig, TradeTokenType};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID;
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_amm_v4::RaydiumAmmV4SwapEvent;
|
||||
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
|
||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
||||
use solana_streamer_sdk::{
|
||||
match_event, streaming::event_parser::protocols::raydium_amm_v4::RaydiumAmmV4SwapEvent,
|
||||
};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
@@ -54,19 +49,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
account_required,
|
||||
};
|
||||
|
||||
// Listen to account data belonging to owner programs -> account event monitoring
|
||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
||||
|
||||
// listen to specific event type
|
||||
let event_type_filter = EventTypeFilter {
|
||||
include: vec![EventType::RaydiumAmmV4SwapBaseIn, EventType::RaydiumAmmV4SwapBaseOut],
|
||||
};
|
||||
let event_type_filter = EventTypeFilter::include_only(vec![
|
||||
EventType::RaydiumAmmV4SwapBaseIn,
|
||||
EventType::RaydiumAmmV4SwapBaseOut,
|
||||
]);
|
||||
|
||||
grpc.subscribe_events_immediate(
|
||||
protocols,
|
||||
None,
|
||||
vec![transaction_filter],
|
||||
vec![account_filter],
|
||||
vec![],
|
||||
Some(event_type_filter),
|
||||
None,
|
||||
callback,
|
||||
@@ -74,27 +67,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?;
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
grpc.stop().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumAmmV4SwapEvent => |e: RaydiumAmmV4SwapEvent| {
|
||||
// Test code, only test one transaction
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = raydium_amm_v4_copy_trade_with_grpc(event_clone).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
fn create_event_callback() -> impl Fn(DexEvent) {
|
||||
|event: DexEvent| {
|
||||
let DexEvent::RaydiumAmmV4SwapEvent(event) = event else {
|
||||
return;
|
||||
};
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = raydium_amm_v4_copy_trade_with_grpc(event).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +93,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
@@ -147,6 +138,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
let input_token_amount = 100_000;
|
||||
let is_wsol = params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| params.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let mint_token_program = client.infrastructure.rpc.get_account(&mint_pubkey).await?.owner;
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::RaydiumAmmV4,
|
||||
input_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
|
||||
@@ -168,22 +162,24 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from Raydium_amm_v4...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&trade_info.token_program,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
|
||||
let amount_token = balance_after
|
||||
.checked_sub(balance_before)
|
||||
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
|
||||
if amount_token == 0 {
|
||||
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
|
||||
}
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let params =
|
||||
@@ -195,7 +191,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::RaydiumAmmV4(params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
@@ -210,7 +206,12 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Exit program
|
||||
std::process::exit(0);
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Raydium CPMM Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Consumes Raydium CPMM events from `solana-streamer-sdk`, maps event pool data into SDK params, then performs one buy and sell.
|
||||
|
||||
> This is a live-transaction template. Set `PRIVATE_KEY`, RPC/stream configuration, target filters, amounts, and fee settings before running.
|
||||
|
||||
```bash
|
||||
cargo run --package raydium_cpmm_trading
|
||||
```
|
||||
|
||||
Production bots must initialize the trading client and blockhash cache before subscription, filter and deduplicate events, reject stale events, and refresh state before delayed sells. The current code focuses on CPMM parameter mapping.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Raydium CPMM 交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
通过 `solana-streamer-sdk` 消费 Raydium CPMM 事件,将事件池数据映射为 SDK 参数,然后执行一次买入和卖出。
|
||||
|
||||
> 这是会发送真实交易的模板。运行前设置 `PRIVATE_KEY`、RPC/stream 配置、目标过滤、金额和费用设置。
|
||||
|
||||
```bash
|
||||
cargo run --package raydium_cpmm_trading
|
||||
```
|
||||
|
||||
生产 Bot 必须在订阅前初始化交易客户端和 blockhash cache,过滤并去重事件、拒绝过期事件,并在延迟卖出前刷新状态。当前代码重点展示 CPMM 参数映射。
|
||||
@@ -1,4 +1,3 @@
|
||||
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::constants::{USDC_TOKEN_ACCOUNT, WSOL_TOKEN_ACCOUNT};
|
||||
use sol_trade_sdk::trading::core::params::{DexParamEnum, RaydiumCpmmParams};
|
||||
@@ -6,17 +5,13 @@ use sol_trade_sdk::trading::factory::DexType;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{common::AnyResult, swqos::SwqosConfig, SolanaTrade};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID;
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_cpmm::RaydiumCpmmSwapEvent;
|
||||
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
|
||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
||||
use solana_streamer_sdk::{
|
||||
match_event, streaming::event_parser::protocols::raydium_cpmm::RaydiumCpmmSwapEvent,
|
||||
};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
@@ -50,19 +45,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
account_required,
|
||||
};
|
||||
|
||||
// Listen to account data belonging to owner programs -> account event monitoring
|
||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
||||
|
||||
// listen to specific event type
|
||||
let event_type_filter = EventTypeFilter {
|
||||
include: vec![EventType::RaydiumCpmmSwapBaseInput, EventType::RaydiumCpmmSwapBaseOutput],
|
||||
};
|
||||
let event_type_filter = EventTypeFilter::include_only(vec![
|
||||
EventType::RaydiumCpmmSwapBaseInput,
|
||||
EventType::RaydiumCpmmSwapBaseOutput,
|
||||
]);
|
||||
|
||||
grpc.subscribe_events_immediate(
|
||||
protocols,
|
||||
None,
|
||||
vec![transaction_filter],
|
||||
vec![account_filter],
|
||||
vec![],
|
||||
Some(event_type_filter),
|
||||
None,
|
||||
callback,
|
||||
@@ -70,32 +63,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?;
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
grpc.stop().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
|
||||
let is_wsol = e.input_token_mint == WSOL_TOKEN_ACCOUNT || e.output_token_mint == WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = e.input_token_mint == USDC_TOKEN_ACCOUNT || e.output_token_mint == USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return;
|
||||
fn create_event_callback() -> impl Fn(DexEvent) {
|
||||
|event: DexEvent| {
|
||||
let DexEvent::RaydiumCpmmSwapEvent(event) = event else {
|
||||
return;
|
||||
};
|
||||
let is_wsol = event.input_token_mint == WSOL_TOKEN_ACCOUNT
|
||||
|| event.output_token_mint == WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = event.input_token_mint == USDC_TOKEN_ACCOUNT
|
||||
|| event.output_token_mint == USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return;
|
||||
}
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = raydium_cpmm_copy_trade_with_grpc(event).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
// Test code, only test one transaction
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = raydium_cpmm_copy_trade_with_grpc(event_clone).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +96,7 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
@@ -147,6 +140,15 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
|
||||
let is_wsol = trade_info.input_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| trade_info.output_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let mint_token_program = if buy_params.base_mint == mint_pubkey {
|
||||
buy_params.base_token_program
|
||||
} else if buy_params.quote_mint == mint_pubkey {
|
||||
buy_params.quote_token_program
|
||||
} else {
|
||||
return Err(std::io::Error::other("target mint does not belong to pool").into());
|
||||
};
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from Raydium_cpmm...");
|
||||
@@ -172,17 +174,24 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("buy failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from Raydium_cpmm...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address(&payer, &mint_pubkey);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &mint_token_program).await?;
|
||||
let amount_token = balance_after
|
||||
.checked_sub(balance_before)
|
||||
.ok_or_else(|| std::io::Error::other("token balance decreased after buy"))?;
|
||||
if amount_token == 0 {
|
||||
return Err(std::io::Error::other("confirmed buy did not increase token balance").into());
|
||||
}
|
||||
|
||||
let sell_params = RaydiumCpmmParams::from_pool_address_by_rpc(
|
||||
&client.infrastructure.rpc,
|
||||
@@ -197,7 +206,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
@@ -212,7 +221,12 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
return Err(
|
||||
std::io::Error::other(format!("sell failed: {:?}; sigs: {:?}", err, sigs)).into()
|
||||
);
|
||||
}
|
||||
|
||||
// Exit program
|
||||
std::process::exit(0);
|
||||
|
||||
@@ -5,7 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Seed-Optimized Trading
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Demonstrates PumpSwap buy/sell account derivation with seed optimization enabled, including the matching token-account lookup used before selling.
|
||||
|
||||
> This is a live mainnet transaction template. Set `PRIVATE_KEY` and `RPC_URL`, replace mint and pool values, and review amounts, slippage, fees, and account-close flags before running.
|
||||
|
||||
```bash
|
||||
cargo run --package seed_trading
|
||||
```
|
||||
|
||||
The same `use_seed_optimize` policy must be used when deriving accounts and building transactions. The example reuses a blockhash across a delay; production code should refresh it before the sell.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Seed 优化交易
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示开启 seed 优化后的 PumpSwap 买卖账户派生,以及卖出前使用相同规则查询 token 账户余额。
|
||||
|
||||
> 这是会发送真实主网交易的模板。运行前设置 `PRIVATE_KEY` 和 `RPC_URL`,替换 mint 和 pool,并检查金额、滑点、费用及账户关闭 flag。
|
||||
|
||||
```bash
|
||||
cargo run --package seed_trading
|
||||
```
|
||||
|
||||
派生账户和构建交易必须使用一致的 `use_seed_optimize` 策略。示例在等待后复用 blockhash;生产代码应在卖出前刷新。
|
||||
@@ -1,7 +1,5 @@
|
||||
use sol_trade_sdk::{
|
||||
common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
|
||||
},
|
||||
common::{AnyResult, TradeConfig},
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
@@ -10,12 +8,11 @@ use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{pubkey::Pubkey, signer::Signer};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn main() -> AnyResult<()> {
|
||||
println!("Testing PumpSwap trading...");
|
||||
|
||||
let client = create_solana_trade_client().await?;
|
||||
@@ -26,6 +23,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
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);
|
||||
let pool_params =
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?;
|
||||
let token_program = if pool_params.base_mint == mint_pubkey {
|
||||
pool_params.base_token_program
|
||||
} else if pool_params.quote_mint == mint_pubkey {
|
||||
pool_params.quote_token_program
|
||||
} else {
|
||||
anyhow::bail!("target mint does not belong to the configured pool");
|
||||
};
|
||||
let balance_before =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from PumpSwap...");
|
||||
@@ -37,9 +45,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
input_token_amount: buy_sol_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpSwap(
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
),
|
||||
extension_params: DexParamEnum::PumpSwap(pool_params),
|
||||
address_lookup_table_accounts: Vec::new(),
|
||||
wait_tx_confirmed: true,
|
||||
wait_for_all_submits: false,
|
||||
@@ -53,32 +59,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use_exact_sol_amount: None,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
let (ok, sigs, err, _) = client.buy(buy_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("buy failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from PumpSwap...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM;
|
||||
// ❗️❗️❗️❗️ Must use the 'use seed' method to get the ATA account, otherwise the transaction will fail
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
true,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let balance_after =
|
||||
client.get_payer_token_balance_with_program(&mint_pubkey, &token_program).await?;
|
||||
let amount_token = balance_after.checked_sub(balance_before).ok_or_else(|| {
|
||||
anyhow::anyhow!("token balance decreased after buy; refusing to sell existing holdings")
|
||||
})?;
|
||||
if amount_token == 0 {
|
||||
anyhow::bail!("confirmed buy did not increase token balance");
|
||||
}
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpSwap,
|
||||
output_token_type: TradeTokenType::WSOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
recent_blockhash: Some(client.infrastructure.rpc.get_latest_blockhash().await?),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpSwap(
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
@@ -95,9 +100,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
simulate: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
let (ok, sigs, err, _) = client.sell(sell_params).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -105,8 +111,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Shared Infrastructure
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Demonstrates sharing one `TradingInfrastructure` across multiple wallet-specific `TradingClient` values. This reduces connection, memory, and initialization overhead in multi-wallet services.
|
||||
|
||||
Replace the RPC, SWQoS credentials, and all wallet placeholders in `src/main.rs`. The example initializes clients and prints sharing information; it does not submit a trade.
|
||||
|
||||
```bash
|
||||
cargo run --package shared_infrastructure
|
||||
```
|
||||
|
||||
Only share infrastructure when wallets use the same RPC, commitment, and submission routes. Keep wallet signing and position state isolated.
|
||||
@@ -0,0 +1,13 @@
|
||||
# 多钱包共享基础设施
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示多个钱包专属 `TradingClient` 共享一份 `TradingInfrastructure`,降低多钱包服务的连接数、内存和初始化开销。
|
||||
|
||||
运行前替换 `src/main.rs` 中的 RPC、SWQoS 凭证和全部钱包占位值。示例只初始化客户端并打印共享信息,不提交交易。
|
||||
|
||||
```bash
|
||||
cargo run --package shared_infrastructure
|
||||
```
|
||||
|
||||
只有钱包使用相同 RPC、commitment 和提交通道时才共享基础设施;签名权限和持仓状态仍必须按钱包隔离。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Simple Trading API
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
This is the recommended starting point for `SimpleBuyParams`, `SimpleSellParams`, `BuyAmount`, `SellAmount`, account policies, recent blockhashes, and durable nonces.
|
||||
|
||||
The example constructs parameters but does not submit the commented buy/sell calls. Set `PRIVATE_KEY` and replace protocol-state placeholders before adapting it to a live integration.
|
||||
|
||||
```bash
|
||||
export RPC_URL=https://your-rpc.example
|
||||
cargo run --package simple_trading
|
||||
```
|
||||
|
||||
`WithMaxInput` protects maximum quote cost and is generally appropriate when fill priority matters. `ExactInput` protects minimum output and may fail more often in active markets. `HotPathMinimal` requires all needed token accounts to exist; normal integrations should use `AccountPolicy::Auto`.
|
||||
|
||||
See [Trading Parameters](../../docs/TRADING_PARAMETERS.md) and [Low-Latency Bots](../../docs/LOW_LATENCY_BOTS.md).
|
||||
@@ -0,0 +1,16 @@
|
||||
# 简化交易 API
|
||||
|
||||
[English](README.md)
|
||||
|
||||
这是学习 `SimpleBuyParams`、`SimpleSellParams`、`BuyAmount`、`SellAmount`、账户策略、recent blockhash 和 durable nonce 的推荐入口。
|
||||
|
||||
示例只构造参数,买卖调用保持注释状态。用于真实集成前必须设置 `PRIVATE_KEY` 并替换协议状态占位值。
|
||||
|
||||
```bash
|
||||
export RPC_URL=https://your-rpc.example
|
||||
cargo run --package simple_trading
|
||||
```
|
||||
|
||||
`WithMaxInput` 保护最大 quote 成本,适合优先成交;`ExactInput` 保护最小输出,在活跃市场更容易失败。`HotPathMinimal` 要求所需 token 账户均已存在,普通接入应使用 `AccountPolicy::Auto`。
|
||||
|
||||
参见[交易参数说明](../../docs/TRADING_PARAMETERS_CN.md)和[低延迟 Bot 指南](../../docs/LOW_LATENCY_BOTS_CN.md)。
|
||||
@@ -18,11 +18,11 @@ use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signer::Signer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::processed();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Trading Client Construction
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Shows two client-construction patterns: `TradingClient::new` for one wallet and `TradingClient::from_infrastructure` for wallets sharing RPC and SWQoS clients.
|
||||
|
||||
This is a configuration template. Set `PRIVATE_KEY`, RPC, and every enabled provider credential; it initializes network clients but does not submit a trade.
|
||||
|
||||
```bash
|
||||
cargo run --package trading_client
|
||||
```
|
||||
|
||||
Initialize the client before subscribing to events. Do not construct it in a trading callback.
|
||||
@@ -0,0 +1,13 @@
|
||||
# TradingClient 创建方式
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示两种客户端创建模式:单钱包使用 `TradingClient::new`,多钱包通过 `TradingClient::from_infrastructure` 共享 RPC 和 SWQoS 客户端。
|
||||
|
||||
这是配置模板。运行前设置 `PRIVATE_KEY`、RPC 和所有已启用服务商凭证;程序会初始化网络客户端,但不会提交交易。
|
||||
|
||||
```bash
|
||||
cargo run --package trading_client
|
||||
```
|
||||
|
||||
客户端应在事件订阅前完成初始化,不要在交易回调中创建。
|
||||
@@ -13,7 +13,6 @@ use sol_trade_sdk::{
|
||||
AstralaneTransport, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -35,7 +34,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
///
|
||||
/// Use this when you have a single wallet or don't need to share infrastructure.
|
||||
async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let commitment = CommitmentConfig::processed();
|
||||
|
||||
@@ -73,7 +72,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
/// Use this when you have multiple wallets sharing the same configuration.
|
||||
/// The infrastructure (RPC client, SWQOS clients) is created once and shared.
|
||||
async fn create_trading_client_from_infrastructure() -> AnyResult<TradingClient> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let commitment = CommitmentConfig::processed();
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# SOL / WSOL Operations
|
||||
|
||||
[中文](README_CN.md)
|
||||
|
||||
Demonstrates wrapping SOL, partially unwrapping WSOL through the SDK seed account, and closing the WSOL account to recover the remaining SOL.
|
||||
|
||||
> All three steps submit real transactions. Set `PRIVATE_KEY` and RPC, use a test wallet first, and reduce amounts as appropriate.
|
||||
|
||||
```bash
|
||||
cargo run --package wsol_wrapper
|
||||
```
|
||||
|
||||
Closing WSOL unwraps the account's full remaining balance and removes the account. Do not run that step when another workflow expects the same WSOL account to remain available.
|
||||
@@ -0,0 +1,13 @@
|
||||
# SOL / WSOL 操作
|
||||
|
||||
[English](README.md)
|
||||
|
||||
展示包装 SOL、通过 SDK seed 账户部分解包 WSOL,以及关闭 WSOL 账户取回剩余 SOL。
|
||||
|
||||
> 三个步骤都会提交真实交易。运行前设置 `PRIVATE_KEY` 和 RPC,先使用测试钱包,并按需降低金额。
|
||||
|
||||
```bash
|
||||
cargo run --package wsol_wrapper
|
||||
```
|
||||
|
||||
关闭 WSOL 会解包账户中的全部剩余余额并删除该账户。如果其他流程仍依赖该 WSOL 账户,不要执行关闭步骤。
|
||||
@@ -1,6 +1,5 @@
|
||||
use sol_trade_sdk::{common::TradeConfig, swqos::SwqosConfig, SolanaTrade};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -80,7 +79,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// Create and initialize SolanaTrade client
|
||||
async fn create_solana_trade_client() -> Result<SolanaTrade, Box<dyn std::error::Error>> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let payer = sol_trade_sdk::common::keypair::load_keypair_from_env("PRIVATE_KEY")?;
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
|
||||
@@ -288,6 +288,213 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "boost_buy_and_burn",
|
||||
"discriminator": [
|
||||
105,
|
||||
68,
|
||||
6,
|
||||
175,
|
||||
0,
|
||||
7,
|
||||
35,
|
||||
162
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "pool"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "global_config",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108,
|
||||
95,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
102,
|
||||
105,
|
||||
103
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "base_mint",
|
||||
"writable": true,
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "quote_mint",
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pool_base_token_account",
|
||||
"writable": true,
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pool_quote_token_account",
|
||||
"writable": true,
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "boost_vault_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
98,
|
||||
111,
|
||||
111,
|
||||
115,
|
||||
116,
|
||||
95,
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "pool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "boost_vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "boost_vault_authority"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "quote_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": "base_token_program"
|
||||
},
|
||||
{
|
||||
"name": "quote_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": "quote_amount_in",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "min_base_amount_burned",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "buy",
|
||||
"docs": [
|
||||
@@ -2587,6 +2794,208 @@
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "init_boost",
|
||||
"discriminator": [
|
||||
140,
|
||||
233,
|
||||
33,
|
||||
94,
|
||||
132,
|
||||
90,
|
||||
194,
|
||||
143
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "global_config",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108,
|
||||
95,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
102,
|
||||
105,
|
||||
103
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "creator",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "base_mint",
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "quote_mint",
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pool_base_token_account",
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pool_quote_token_account",
|
||||
"writable": true,
|
||||
"relations": [
|
||||
"pool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "boost_vault_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
98,
|
||||
111,
|
||||
111,
|
||||
115,
|
||||
116,
|
||||
95,
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "pool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "boost_vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "boost_vault_authority"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "quote_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": "quote_token_program"
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "associated_token_program",
|
||||
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
},
|
||||
{
|
||||
"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": "init_user_volume_accumulator",
|
||||
"discriminator": [
|
||||
@@ -3175,6 +3584,72 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "set_boost_authority",
|
||||
"discriminator": [
|
||||
227,
|
||||
149,
|
||||
76,
|
||||
42,
|
||||
130,
|
||||
39,
|
||||
234,
|
||||
205
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "admin",
|
||||
"signer": true,
|
||||
"relations": [
|
||||
"global_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "global_config",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "boost_authority"
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "event_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
95,
|
||||
95,
|
||||
101,
|
||||
118,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "program"
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "set_coin_creator",
|
||||
"docs": [
|
||||
@@ -3610,6 +4085,38 @@
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "toggle_boost",
|
||||
"discriminator": [
|
||||
117,
|
||||
161,
|
||||
160,
|
||||
74,
|
||||
223,
|
||||
137,
|
||||
118,
|
||||
99
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "admin",
|
||||
"signer": true,
|
||||
"relations": [
|
||||
"global_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "global_config",
|
||||
"writable": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "toggle_cashback_enabled",
|
||||
"discriminator": [
|
||||
@@ -4672,6 +5179,19 @@
|
||||
222
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "BoostBuyAndBurnEvent",
|
||||
"discriminator": [
|
||||
63,
|
||||
69,
|
||||
28,
|
||||
22,
|
||||
48,
|
||||
92,
|
||||
194,
|
||||
185
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "BuyEvent",
|
||||
"discriminator": [
|
||||
@@ -4802,6 +5322,19 @@
|
||||
124
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "InitBoostEvent",
|
||||
"discriminator": [
|
||||
174,
|
||||
124,
|
||||
74,
|
||||
249,
|
||||
4,
|
||||
81,
|
||||
246,
|
||||
17
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "InitUserVolumeAccumulatorEvent",
|
||||
"discriminator": [
|
||||
@@ -4867,6 +5400,19 @@
|
||||
211
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SetBoostAuthorityEvent",
|
||||
"discriminator": [
|
||||
89,
|
||||
128,
|
||||
240,
|
||||
141,
|
||||
91,
|
||||
202,
|
||||
71,
|
||||
105
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SetMetaplexCoinCreatorEvent",
|
||||
"discriminator": [
|
||||
@@ -5177,6 +5723,51 @@
|
||||
{
|
||||
"code": 6058,
|
||||
"name": "BuybackFeeRecipientMissing"
|
||||
},
|
||||
{
|
||||
"code": 6059,
|
||||
"name": "MissingCashbackAccounts",
|
||||
"msg": "Cashback trade is missing the required remaining accounts"
|
||||
},
|
||||
{
|
||||
"code": 6060,
|
||||
"name": "InvalidCashbackAccumulator",
|
||||
"msg": "Cashback user_volume_accumulator account is invalid"
|
||||
},
|
||||
{
|
||||
"code": 6061,
|
||||
"name": "InvalidCashbackAccumulatorAta",
|
||||
"msg": "Cashback user_volume_accumulator ATA is missing or invalid"
|
||||
},
|
||||
{
|
||||
"code": 6062,
|
||||
"name": "InvalidPoolV2",
|
||||
"msg": "pool_v2 remaining account is missing or invalid"
|
||||
},
|
||||
{
|
||||
"code": 6063,
|
||||
"name": "InsufficientRealQuoteReserves",
|
||||
"msg": "BOOST: sell output exceeds the real quote vault. effective = real + virtual is pricing-only; payout is capped at real_vault, so quote min(out, real_vault)"
|
||||
},
|
||||
{
|
||||
"code": 6064,
|
||||
"name": "BoostPoolLiquidityUnsupported",
|
||||
"msg": "BOOST: deposit/withdraw don't apply to boost pools"
|
||||
},
|
||||
{
|
||||
"code": 6065,
|
||||
"name": "PoolCannotBoost",
|
||||
"msg": "BOOST: pool cannot be boosted (no virtual reserves)"
|
||||
},
|
||||
{
|
||||
"code": 6066,
|
||||
"name": "BoostDisabled",
|
||||
"msg": "BOOST: boost is disabled"
|
||||
},
|
||||
{
|
||||
"code": 6067,
|
||||
"name": "SeedLockViolation",
|
||||
"msg": "BOOST: lp_supply must never drop below the circulating LP mint supply"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
@@ -5292,6 +5883,62 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "BoostBuyAndBurnEvent",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "mint",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "bonding_curve",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "quote_amount_in_requested",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "quote_amount_in_used",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "base_amount_burned",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "virtual_quote_reserves",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "real_quote_reserves_after",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "base_reserves_after",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "boost_vault_remaining",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "BuyEvent",
|
||||
"docs": [
|
||||
@@ -5435,6 +6082,18 @@
|
||||
{
|
||||
"name": "buyback_fee",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "virtual_quote_reserves",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "can_boost",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "base_supply",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6018,6 +6677,14 @@
|
||||
{
|
||||
"name": "buyback_basis_points",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "boost_authority",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "boost_enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6064,6 +6731,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "InitBoostEvent",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "mint",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "bonding_curve",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "virtual_quote_reserves",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "real_quote_reserves_after",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "InitUserVolumeAccumulatorEvent",
|
||||
"type": {
|
||||
@@ -6180,6 +6879,13 @@
|
||||
{
|
||||
"name": "is_cashback_coin",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "virtual_quote_reserves",
|
||||
"docs": [
|
||||
"For non-boost pools, value is 0, so the behavior is identical to legacy pools."
|
||||
],
|
||||
"type": "i128"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6321,6 +7027,18 @@
|
||||
{
|
||||
"name": "buyback_fee",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "virtual_quote_reserves",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "can_boost",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "base_supply",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6353,6 +7071,30 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SetBoostAuthorityEvent",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "old_boost_authority",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "new_boost_authority",
|
||||
"type": "pubkey"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SetMetaplexCoinCreatorEvent",
|
||||
"type": {
|
||||
|
||||
@@ -1348,6 +1348,12 @@ impl TradingClient {
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
validate_trade_safety(
|
||||
"buy",
|
||||
params.input_token_amount,
|
||||
params.fixed_output_token_amount,
|
||||
params.slippage_basis_points,
|
||||
)?;
|
||||
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)"
|
||||
@@ -1476,6 +1482,12 @@ impl TradingClient {
|
||||
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
|
||||
anyhow::Error,
|
||||
> {
|
||||
validate_trade_safety(
|
||||
"sell",
|
||||
params.input_token_amount,
|
||||
params.fixed_output_token_amount,
|
||||
params.slippage_basis_points,
|
||||
)?;
|
||||
#[cfg(feature = "perf-trace")]
|
||||
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||
debug!(
|
||||
@@ -1820,6 +1832,30 @@ impl TradingClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_trade_safety(
|
||||
side: &str,
|
||||
input_amount: u64,
|
||||
fixed_output_amount: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if input_amount == 0 {
|
||||
return Err(anyhow::anyhow!("{} input amount must be greater than zero", side));
|
||||
}
|
||||
if fixed_output_amount == Some(0) {
|
||||
return Err(anyhow::anyhow!("{} fixed output amount must be greater than zero", side));
|
||||
}
|
||||
if let Some(bps) = slippage_basis_points {
|
||||
if bps >= 10_000 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} slippage_basis_points must be below 10000, got {}",
|
||||
side,
|
||||
bps
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1841,6 +1877,20 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trade_safety_rejects_zero_amounts_and_unbounded_slippage() {
|
||||
assert!(validate_trade_safety("buy", 0, None, Some(100)).is_err());
|
||||
assert!(validate_trade_safety("buy", 1, Some(0), Some(100)).is_err());
|
||||
assert!(validate_trade_safety("sell", 1, None, Some(10_000)).is_err());
|
||||
assert!(validate_trade_safety("sell", 1, None, Some(u64::MAX)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trade_safety_accepts_bounded_values() {
|
||||
assert!(validate_trade_safety("buy", 1, None, None).is_ok());
|
||||
assert!(validate_trade_safety("buy", 1, Some(1), Some(9_999)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_swqos_configs_adds_default_rpc_route() {
|
||||
let configs = vec![SwqosConfig::Jito("uuid".to_string(), SwqosRegion::Frankfurt, None)];
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use anyhow::{Context, Result};
|
||||
use solana_sdk::signature::Keypair;
|
||||
|
||||
/// Load a Solana keypair from a base58 string or a 64-byte JSON array in an environment variable.
|
||||
pub fn load_keypair_from_env(name: &str) -> Result<Keypair> {
|
||||
let value = std::env::var(name).with_context(|| format!("{} is required", name))?;
|
||||
load_keypair_from_string(&value).with_context(|| format!("invalid {}", name))
|
||||
}
|
||||
|
||||
/// Parse a Solana keypair without the panic behavior of `Keypair::from_base58_string`.
|
||||
pub fn load_keypair_from_string(value: &str) -> Result<Keypair> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
anyhow::bail!("keypair is empty");
|
||||
}
|
||||
|
||||
let bytes = if value.starts_with('[') {
|
||||
serde_json::from_str::<Vec<u8>>(value).context("keypair JSON must be a byte array")?
|
||||
} else {
|
||||
bs58::decode(value)
|
||||
.into_vec()
|
||||
.context("keypair must be valid base58 or a 64-byte JSON array")?
|
||||
};
|
||||
if bytes.len() != 64 {
|
||||
anyhow::bail!("keypair must contain 64 bytes, got {}", bytes.len());
|
||||
}
|
||||
Keypair::try_from(bytes.as_slice()).context("invalid 64-byte Solana keypair")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::signature::Signer;
|
||||
|
||||
#[test]
|
||||
fn parses_base58_and_json_keypairs() {
|
||||
let keypair = Keypair::new();
|
||||
let base58 = bs58::encode(keypair.to_bytes()).into_string();
|
||||
let json = serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap();
|
||||
|
||||
assert_eq!(load_keypair_from_string(&base58).unwrap().pubkey(), keypair.pubkey());
|
||||
assert_eq!(load_keypair_from_string(&json).unwrap().pubkey(), keypair.pubkey());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_placeholders_and_wrong_lengths() {
|
||||
assert!(load_keypair_from_string("use_your_payer_keypair_here").is_err());
|
||||
assert!(load_keypair_from_string("[1,2,3]").is_err());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod fast_fn;
|
||||
pub mod fast_timing;
|
||||
pub mod gas_fee_strategy;
|
||||
pub mod global;
|
||||
pub mod keypair;
|
||||
pub mod nonce_cache;
|
||||
pub mod sdk_log;
|
||||
pub mod seed;
|
||||
|
||||
+47
-1
@@ -184,6 +184,20 @@ pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("uiuaQsxA47JybQAVN4FTfYuoEDkMiXV1r591Aewbeam"),
|
||||
];
|
||||
|
||||
// LunarLander (HelloMoon) tip accounts
|
||||
pub const LUNARLANDER_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"),
|
||||
pubkey!("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"),
|
||||
pubkey!("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"),
|
||||
pubkey!("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"),
|
||||
pubkey!("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"),
|
||||
pubkey!("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"),
|
||||
pubkey!("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"),
|
||||
pubkey!("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"),
|
||||
pubkey!("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"),
|
||||
pubkey!("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"),
|
||||
];
|
||||
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
@@ -433,6 +447,36 @@ pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 10] = [
|
||||
"fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// Lunar Lander HTTP endpoints. Binary tx via POST /send-bin with x-api-key header.
|
||||
/// `SwqosRegion` order; regions without a dedicated published PoP use the nearest available host.
|
||||
pub const SWQOS_ENDPOINTS_LUNARLANDER: [&str; 10] = [
|
||||
"http://nyc-1.prod.lunar-lander.hellomoon.io",
|
||||
"http://fra-1.prod.lunar-lander.hellomoon.io",
|
||||
"http://ams-1.prod.lunar-lander.hellomoon.io",
|
||||
"http://ams-1.prod.lunar-lander.hellomoon.io", // Dublin -> Amsterdam
|
||||
"http://ash-2.prod.lunar-lander.hellomoon.io", // SLC -> Ashburn
|
||||
"http://tyo-1.prod.lunar-lander.hellomoon.io",
|
||||
"http://tyo-1.prod.lunar-lander.hellomoon.io", // Singapore -> Tokyo
|
||||
"http://fra-1.prod.lunar-lander.hellomoon.io", // London -> Frankfurt
|
||||
"http://nyc-1.prod.lunar-lander.hellomoon.io", // LosAngeles -> NYC
|
||||
"http://nyc-1.prod.lunar-lander.hellomoon.io", // Default -> NYC
|
||||
];
|
||||
|
||||
/// Lunar Lander QUIC endpoints (direct, port 16888). Auth via client cert CN = API key.
|
||||
/// ALPN: b"lunar-lander-tpu". Fire-and-forget unidirectional streams.
|
||||
pub const SWQOS_ENDPOINTS_LUNARLANDER_QUIC: [&str; 10] = [
|
||||
"nyc-1.prod.lunar-lander.hellomoon.io:16888",
|
||||
"fra-1.prod.lunar-lander.hellomoon.io:16888",
|
||||
"ams-1.prod.lunar-lander.hellomoon.io:16888",
|
||||
"ams-1.prod.lunar-lander.hellomoon.io:16888", // Dublin -> Amsterdam
|
||||
"ash-2.prod.lunar-lander.hellomoon.io:16888", // SLC -> Ashburn
|
||||
"tyo-1.prod.lunar-lander.hellomoon.io:16888",
|
||||
"tyo-1.prod.lunar-lander.hellomoon.io:16888", // Singapore -> Tokyo
|
||||
"fra-1.prod.lunar-lander.hellomoon.io:16888", // London -> Frankfurt
|
||||
"nyc-1.prod.lunar-lander.hellomoon.io:16888", // LosAngeles -> NYC
|
||||
"nyc-1.prod.lunar-lander.hellomoon.io:16888", // Default -> NYC
|
||||
];
|
||||
|
||||
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"http://ewr-sender.helius-rpc.com/fast",
|
||||
@@ -477,9 +521,9 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
|
||||
/// Helius Sender: 0.0002 SOL when not swqos_only; use SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY when swqos_only=true.
|
||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||
pub const SWQOS_MIN_TIP_SOLAMI: f64 = 0.0001;
|
||||
pub const SWQOS_MIN_TIP_LUNARLANDER: f64 = 0.001; // LunarLander 最低小费 0.001 SOL
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -503,6 +547,8 @@ mod tests {
|
||||
&SWQOS_ENDPOINTS_SOYAS,
|
||||
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
&SWQOS_ENDPOINTS_LUNARLANDER,
|
||||
&SWQOS_ENDPOINTS_LUNARLANDER_QUIC,
|
||||
&SWQOS_ENDPOINTS_SOLAMI,
|
||||
];
|
||||
|
||||
|
||||
+343
-45
@@ -2,7 +2,7 @@ use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
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,
|
||||
encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
@@ -34,6 +34,38 @@ use solana_sdk::{
|
||||
/// Instruction builder for PumpSwap protocol
|
||||
pub struct PumpSwapInstructionBuilder;
|
||||
|
||||
#[inline]
|
||||
fn request_mint_matches_pool(actual: Pubkey, expected: Pubkey) -> bool {
|
||||
actual == expected
|
||||
|| (expected == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
&& actual == crate::constants::SOL_TOKEN_ACCOUNT)
|
||||
}
|
||||
|
||||
fn push_cashback_remaining_accounts(
|
||||
accounts: &mut Vec<AccountMeta>,
|
||||
user: &Pubkey,
|
||||
quote_mint: &Pubkey,
|
||||
quote_token_program: &Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
is_buy_instruction: bool,
|
||||
) -> Result<()> {
|
||||
if !is_cashback_coin {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let quote_ata = get_user_volume_accumulator_quote_ata(user, quote_mint, quote_token_program)
|
||||
.ok_or_else(|| anyhow!("user volume accumulator quote ATA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(quote_ata, false));
|
||||
|
||||
if !is_buy_instruction {
|
||||
let accumulator = get_user_volume_accumulator_pda(user)
|
||||
.ok_or_else(|| anyhow!("user volume accumulator PDA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(accumulator, false));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
@@ -49,12 +81,17 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params.input_amount.unwrap_or(0) == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
if params.fixed_output_amount == Some(0) {
|
||||
return Err(anyhow!("Fixed output amount cannot be zero"));
|
||||
}
|
||||
|
||||
let pool = protocol_params.pool;
|
||||
let base_mint = protocol_params.base_mint;
|
||||
let quote_mint = protocol_params.quote_mint;
|
||||
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let virtual_quote_reserves = protocol_params.virtual_quote_reserves;
|
||||
protocol_params.effective_quote_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_input_ata = params.create_input_mint_ata;
|
||||
@@ -81,15 +118,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if params.fixed_output_amount.is_some() && !quote_is_wsol_or_usdc {
|
||||
return Err(anyhow!(
|
||||
"PumpSwap exact-output buy is unsupported when the pool requires a sell instruction"
|
||||
));
|
||||
}
|
||||
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 };
|
||||
if !request_mint_matches_pool(params.input_mint, input_stable_mint)
|
||||
|| !request_mint_matches_pool(params.output_mint, output_trade_mint)
|
||||
{
|
||||
return Err(anyhow!("PumpSwap buy request mints do not match the supplied pool"));
|
||||
}
|
||||
let fee_basis_points = protocol_params.fee_basis_points;
|
||||
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
if output_amount >= pool_base_token_reserves {
|
||||
return Err(anyhow!("Exact base output must be below the pool base reserve"));
|
||||
}
|
||||
(output_amount, params.input_amount.unwrap_or(0))
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal_with_fees(
|
||||
@@ -97,9 +147,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// base_amount_out, max_quote_amount_in
|
||||
(result.base, result.max_quote)
|
||||
} else {
|
||||
@@ -108,9 +159,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// min_quote_amount_out, base_amount_in
|
||||
(result.min_quote, params.input_amount.unwrap_or(0))
|
||||
};
|
||||
@@ -138,7 +190,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
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 fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint, quote_token_program);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
@@ -206,12 +258,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
accounts.push(accounts::FEE_CONFIG_META);
|
||||
accounts.push(accounts::FEE_PROGRAM_META);
|
||||
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator (after named accounts per IDL)
|
||||
if protocol_params.is_cashback_coin {
|
||||
if let Some(wsol_ata) = get_user_volume_accumulator_wsol_ata(¶ms.payer.pubkey()) {
|
||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||
}
|
||||
}
|
||||
push_cashback_remaining_accounts(
|
||||
&mut accounts,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
protocol_params.is_cashback_coin,
|
||||
quote_is_wsol_or_usdc,
|
||||
)?;
|
||||
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts);
|
||||
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
@@ -224,12 +278,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(
|
||||
protocol_extra,
|
||||
quote_mint,
|
||||
quote_token_program,
|
||||
),
|
||||
false,
|
||||
));
|
||||
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
let track_volume = 1_u8;
|
||||
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)
|
||||
@@ -284,6 +342,8 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let quote_mint = protocol_params.quote_mint;
|
||||
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let virtual_quote_reserves = protocol_params.virtual_quote_reserves;
|
||||
protocol_params.effective_quote_reserves()?;
|
||||
let pool_base_token_account = protocol_params.pool_base_token_account;
|
||||
let pool_quote_token_account = protocol_params.pool_quote_token_account;
|
||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||
@@ -305,8 +365,11 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
||||
}
|
||||
|
||||
if params.input_amount.is_none() {
|
||||
return Err(anyhow!("Token amount is not set"));
|
||||
if params.input_amount.unwrap_or_default() == 0 {
|
||||
return Err(anyhow!("Token amount must be greater than zero"));
|
||||
}
|
||||
if params.fixed_output_amount == Some(0) {
|
||||
return Err(anyhow!("Fixed output amount cannot be zero"));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -314,12 +377,26 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if params.fixed_output_amount.is_some() && quote_is_wsol_or_usdc {
|
||||
return Err(anyhow!(
|
||||
"PumpSwap exact-output sell is unsupported when the pool requires a sell instruction"
|
||||
));
|
||||
}
|
||||
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 input_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
|
||||
if !request_mint_matches_pool(params.input_mint, input_trade_mint)
|
||||
|| !request_mint_matches_pool(params.output_mint, output_stable_mint)
|
||||
{
|
||||
return Err(anyhow!("PumpSwap sell request mints do not match the supplied pool"));
|
||||
}
|
||||
let fee_basis_points = protocol_params.fee_basis_points;
|
||||
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
if output_amount >= pool_base_token_reserves {
|
||||
return Err(anyhow!("Exact base output must be below the pool base reserve"));
|
||||
}
|
||||
(params.input_amount.unwrap(), output_amount)
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal_with_fees(
|
||||
@@ -327,9 +404,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// base_amount_in, min_quote_amount_out
|
||||
(params.input_amount.unwrap(), result.min_quote)
|
||||
} else {
|
||||
@@ -338,9 +416,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// max_quote_amount_in, base_amount_out
|
||||
(result.max_quote, result.base)
|
||||
};
|
||||
@@ -353,7 +432,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
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 fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint, quote_token_program);
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
@@ -416,20 +495,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
accounts.push(accounts::FEE_CONFIG_META);
|
||||
accounts.push(accounts::FEE_PROGRAM_META);
|
||||
// Cashback sell: 官方 remainingAccounts = [accumulator 的 quote_mint ATA, accumulator PDA, poolV2](用 quote_mint 非固定 WSOL)
|
||||
if protocol_params.is_cashback_coin {
|
||||
if let (Some(quote_ata), Some(accumulator)) = (
|
||||
get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
),
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey()),
|
||||
) {
|
||||
accounts.push(AccountMeta::new(quote_ata, false));
|
||||
accounts.push(AccountMeta::new(accumulator, false));
|
||||
}
|
||||
}
|
||||
push_cashback_remaining_accounts(
|
||||
&mut accounts,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
protocol_params.is_cashback_coin,
|
||||
!quote_is_wsol_or_usdc,
|
||||
)?;
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
@@ -439,18 +512,46 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(
|
||||
protocol_extra,
|
||||
quote_mint,
|
||||
quote_token_program,
|
||||
),
|
||||
false,
|
||||
));
|
||||
|
||||
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
|
||||
let ix_data = if quote_is_wsol_or_usdc {
|
||||
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
|
||||
let track_volume = 1_u8;
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = encode_pumpswap_sell_ix_data(token_amount, sol_amount);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else if params.fixed_output_amount.is_some() {
|
||||
let ix_data = encode_pumpswap_buy_ix_data(sol_amount, token_amount, track_volume);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else {
|
||||
encode_pumpswap_buy_two_args(sol_amount, token_amount)
|
||||
};
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
sol_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let ix_data = encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
min_base_amount_out,
|
||||
track_volume,
|
||||
);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
}
|
||||
|
||||
if close_wsol_ata {
|
||||
push_close_wsol_if_needed(
|
||||
@@ -535,6 +636,28 @@ mod tests {
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
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 reverse_pumpswap_params() -> PumpSwapParams {
|
||||
PumpSwapParams::new(
|
||||
pk(1),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
pk(2),
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
@@ -604,16 +727,164 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_fixed_output_uses_min_quote_directly() {
|
||||
let instructions = PumpSwapInstructionBuilder
|
||||
async fn pumpswap_sell_fixed_output_rejects_unsupported_sell_instruction() {
|
||||
let error = PumpSwapInstructionBuilder
|
||||
.build_sell_instructions(&swap_params(TradeType::Sell, Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"PumpSwap exact-output sell is unsupported when the pool requires a sell instruction"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_reverse_sell_fixed_output_uses_current_buy_layout() {
|
||||
let mut params = swap_params(TradeType::Sell, Some(42));
|
||||
params.protocol_params = DexParamEnum::PumpSwap(reverse_pumpswap_params());
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
|
||||
assert_eq!(ix.data.len(), 25);
|
||||
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);
|
||||
assert_eq!(ix.data[24], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_reverse_sell_exact_input_never_increases_token_spend() {
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
params.protocol_params = DexParamEnum::PumpSwap(reverse_pumpswap_params());
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpswap::BUY_EXACT_QUOTE_IN_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.data.len(), 25);
|
||||
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);
|
||||
|
||||
let quote = crate::utils::calc::pumpswap::buy_quote_input_internal_with_fees(
|
||||
100_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(25, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
let expected_min =
|
||||
crate::utils::calc::common::calculate_with_slippage_sell(quote.base, 100);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), expected_min);
|
||||
assert_eq!(ix.data[24], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_buy_cashback_accounts_match_actual_instruction_direction() {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
let mut protocol_params = pumpswap_params();
|
||||
protocol_params.is_cashback_coin = true;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_ata = get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpswap::BUY_EXACT_QUOTE_IN_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.accounts[23].pubkey, expected_ata);
|
||||
assert_eq!(ix.data[24], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_reverse_buy_uses_sell_cashback_account_layout() {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
let mut protocol_params = reverse_pumpswap_params();
|
||||
protocol_params.is_cashback_coin = true;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_ata = get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
&pk(2),
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()).unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts[21].pubkey, expected_ata);
|
||||
assert_eq!(ix.accounts[22].pubkey, expected_accumulator);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_fee_atas_use_the_quote_token_program() {
|
||||
let quote_token_program = pk(99);
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
let mut protocol_params = reverse_pumpswap_params();
|
||||
protocol_params.quote_token_program = quote_token_program;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_fee_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&ix.accounts[9].pubkey,
|
||||
&pk(2),
|
||||
"e_token_program,
|
||||
);
|
||||
let buyback_recipient_index = ix.accounts.len() - 2;
|
||||
let expected_buyback_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&ix.accounts[buyback_recipient_index].pubkey,
|
||||
&pk(2),
|
||||
"e_token_program,
|
||||
);
|
||||
|
||||
assert_eq!(ix.accounts[10].pubkey, expected_fee_ata);
|
||||
assert_eq!(ix.accounts[buyback_recipient_index + 1].pubkey, expected_buyback_ata);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_rejects_request_mints_from_another_pool() {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
params.output_mint = pk(88);
|
||||
|
||||
let error = PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "PumpSwap buy request mints do not match the supplied pool");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_rejects_zero_input() {
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
params.input_amount = Some(0);
|
||||
|
||||
let error = PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "Token amount must be greater than zero");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -627,6 +898,7 @@ mod tests {
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
@@ -653,8 +925,9 @@ mod tests {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
params.input_amount = Some(1_000_000);
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
params.protocol_params =
|
||||
DexParamEnum::PumpSwap(pumpswap_params().with_fee_basis_points(20, 5, 75));
|
||||
let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 75);
|
||||
protocol_params.virtual_quote_reserves = 500_000_000;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
@@ -668,9 +941,34 @@ mod tests {
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
500_000_000,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(base_amount_out, expected.base);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_prices_with_effective_quote_reserves() {
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 0);
|
||||
protocol_params.virtual_quote_reserves = 500_000_000;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let min_quote_amount_out = u64::from_le_bytes(ix.data[16..24].try_into().unwrap());
|
||||
|
||||
let expected = crate::utils::calc::pumpswap::sell_base_input_internal_with_fees(
|
||||
100_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
500_000_000,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(min_quote_amount_out, expected.min_quote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,6 @@ 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,
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::{
|
||||
common::{
|
||||
spl_associated_token_account::get_associated_token_address_with_program_id, SolanaRpcClient,
|
||||
},
|
||||
constants::{TOKEN_PROGRAM, WSOL_TOKEN_ACCOUNT},
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool},
|
||||
constants::WSOL_TOKEN_ACCOUNT,
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool, POOL_DISCRIMINATOR},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -18,7 +18,7 @@ use std::sync::{
|
||||
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)
|
||||
// Pool account sizes are handled by find_by_base_mint/find_by_quote_mint.
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
@@ -241,6 +241,8 @@ const U8_LEN: usize = 1;
|
||||
const BOOL_LEN: usize = 1;
|
||||
const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8;
|
||||
const FEE_CONFIG_DISCRIMINATOR_LEN: usize = 8;
|
||||
const GLOBAL_CONFIG_DISCRIMINATOR: [u8; 8] = [149, 8, 156, 202, 160, 252, 176, 217];
|
||||
const FEE_CONFIG_DISCRIMINATOR: [u8; 8] = [143, 52, 146, 187, 219, 123, 76, 155];
|
||||
const FEE_CONFIG_BUMP_LEN: usize = 1;
|
||||
const FEE_TIER_LEN: usize = 16 + U64_LEN * 3;
|
||||
|
||||
@@ -301,6 +303,9 @@ fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
|
||||
}
|
||||
|
||||
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
|
||||
if data.get(..GLOBAL_CONFIG_DISCRIMINATOR_LEN)? != GLOBAL_CONFIG_DISCRIMINATOR {
|
||||
return None;
|
||||
}
|
||||
let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN;
|
||||
offset += PUBKEY_LEN; // admin
|
||||
let lp_fee_basis_points = read_u64(data, offset)?;
|
||||
@@ -364,6 +369,9 @@ fn decode_fee_tiers(data: &[u8], offset: &mut usize) -> Option<Vec<PumpSwapFeeTi
|
||||
}
|
||||
|
||||
pub fn decode_fee_config(data: &[u8]) -> Option<PumpSwapFeeConfig> {
|
||||
if data.get(..FEE_CONFIG_DISCRIMINATOR_LEN)? != FEE_CONFIG_DISCRIMINATOR {
|
||||
return None;
|
||||
}
|
||||
let mut offset = FEE_CONFIG_DISCRIMINATOR_LEN;
|
||||
offset += FEE_CONFIG_BUMP_LEN;
|
||||
offset += PUBKEY_LEN; // admin
|
||||
@@ -399,6 +407,14 @@ async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfi
|
||||
}
|
||||
};
|
||||
|
||||
if account.owner != accounts::AMM_PROGRAM {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
owner = %account.owner,
|
||||
"PumpSwap GlobalConfig owner 无效"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let Some(config) = decode_global_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
@@ -435,6 +451,14 @@ async fn refresh_fee_config_once(rpc: &SolanaRpcClient) -> Option<PumpSwapFeeCon
|
||||
}
|
||||
};
|
||||
|
||||
if account.owner != accounts::FEE_PROGRAM {
|
||||
warn!(
|
||||
target: "pumpswap_fee_config",
|
||||
owner = %account.owner,
|
||||
"PumpSwap FeeConfig owner 无效"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let Some(config) = decode_fee_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_fee_config",
|
||||
@@ -665,22 +689,30 @@ pub(crate) fn coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey {
|
||||
pump_pool_authority
|
||||
}
|
||||
|
||||
pub(crate) fn coin_creator_vault_ata(coin_creator: Pubkey, quote_mint: Pubkey) -> Pubkey {
|
||||
pub(crate) fn coin_creator_vault_ata(
|
||||
coin_creator: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
) -> Pubkey {
|
||||
let creator_vault_authority = coin_creator_vault_authority(coin_creator);
|
||||
let associated_token_creator_vault_authority = get_associated_token_address_with_program_id(
|
||||
&creator_vault_authority,
|
||||
"e_mint,
|
||||
&TOKEN_PROGRAM,
|
||||
"e_token_program,
|
||||
);
|
||||
associated_token_creator_vault_authority
|
||||
}
|
||||
|
||||
pub(crate) fn fee_recipient_ata(fee_recipient: Pubkey, quote_mint: Pubkey) -> Pubkey {
|
||||
pub(crate) fn fee_recipient_ata(
|
||||
fee_recipient: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
) -> Pubkey {
|
||||
let associated_token_fee_recipient =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&fee_recipient,
|
||||
"e_mint,
|
||||
&TOKEN_PROGRAM,
|
||||
"e_token_program,
|
||||
);
|
||||
associated_token_fee_recipient
|
||||
}
|
||||
@@ -733,19 +765,32 @@ pub async fn fetch_pool(
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Pool, anyhow::Error> {
|
||||
let account = rpc.get_account(pool_address).await?;
|
||||
if account.owner != accounts::AMM_PROGRAM {
|
||||
return Err(anyhow!("Account is not owned by PumpSwap program"));
|
||||
}
|
||||
let pool = pool_decode(&account.data[8..]).ok_or_else(|| anyhow!("Failed to decode pool"))?;
|
||||
Ok(pool)
|
||||
decode_pool_account(&account).map_err(anyhow::Error::msg)
|
||||
}
|
||||
|
||||
/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022)
|
||||
const POOL_DATA_LEN_SPL: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_T22: u64 = 643;
|
||||
fn decode_pool_account(account: &solana_sdk::account::Account) -> Result<Pool, String> {
|
||||
if account.owner != accounts::AMM_PROGRAM {
|
||||
return Err("Account is not owned by PumpSwap program".to_string());
|
||||
}
|
||||
let discriminator = account
|
||||
.data
|
||||
.get(..8)
|
||||
.ok_or_else(|| "Pool account is shorter than its discriminator".to_string())?;
|
||||
if discriminator != POOL_DISCRIMINATOR {
|
||||
return Err("Account discriminator is not PumpSwap Pool".to_string());
|
||||
}
|
||||
pool_decode(&account.data[8..]).ok_or_else(|| "Failed to decode pool".to_string())
|
||||
}
|
||||
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel.
|
||||
async fn get_program_accounts_both_sizes(
|
||||
/// Known allocated Pool account sizes. Current accounts may be serialized to
|
||||
/// exactly 261 bytes or retain a larger historical allocation.
|
||||
const POOL_DATA_LEN_LEGACY: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_CURRENT: u64 = 8 + 253;
|
||||
const POOL_DATA_LEN_PADDED: u64 = 300;
|
||||
const POOL_DATA_LEN_EXTENDED: u64 = 643;
|
||||
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel.
|
||||
async fn get_program_accounts_known_sizes(
|
||||
rpc: &SolanaRpcClient,
|
||||
memcmp_offset: usize,
|
||||
mint: &Pubkey,
|
||||
@@ -753,6 +798,9 @@ async fn get_program_accounts_both_sizes(
|
||||
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(0, &POOL_DISCRIMINATOR),
|
||||
),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
|
||||
),
|
||||
@@ -768,12 +816,28 @@ async fn get_program_accounts_both_sizes(
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let (spl_result, t22_result) = tokio::join!(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)),
|
||||
let (legacy_result, current_result, padded_result, extended_result) = tokio::join!(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_LEGACY)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_CURRENT)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_PADDED)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)),
|
||||
);
|
||||
let mut all = spl_result.unwrap_or_default();
|
||||
all.extend(t22_result.unwrap_or_default());
|
||||
let results = [legacy_result, current_result, padded_result, extended_result];
|
||||
let mut all = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
for (size, result) in
|
||||
[POOL_DATA_LEN_LEGACY, POOL_DATA_LEN_CURRENT, POOL_DATA_LEN_PADDED, POOL_DATA_LEN_EXTENDED]
|
||||
.into_iter()
|
||||
.zip(results)
|
||||
{
|
||||
match result {
|
||||
Ok(accounts) => all.extend(accounts),
|
||||
Err(error) => errors.push(format!("dataSize={size}: {error}")),
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(anyhow!("Incomplete PumpSwap pool query: {}", errors.join("; ")));
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
@@ -782,13 +846,7 @@ fn decode_pool_accounts(
|
||||
) -> Vec<(Pubkey, Pool)> {
|
||||
accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
if acc.data.len() > 8 {
|
||||
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|(addr, acc)| decode_pool_account(&acc).ok().map(|pool| (addr, pool)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -797,7 +855,7 @@ pub async fn find_by_base_mint(
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?;
|
||||
let accounts = get_program_accounts_known_sizes(rpc, 43, base_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||
}
|
||||
@@ -814,7 +872,7 @@ pub async fn find_by_quote_mint(
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?;
|
||||
let accounts = get_program_accounts_known_sizes(rpc, 75, quote_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", quote_mint));
|
||||
}
|
||||
@@ -878,17 +936,111 @@ pub async fn find_by_mint(
|
||||
Err(anyhow!("No pool found for mint {}. diag: {}", mint, diag_str))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PoolRpcSnapshot {
|
||||
pub base_reserve: u64,
|
||||
pub quote_reserve: u64,
|
||||
pub base_token_program: Pubkey,
|
||||
pub quote_token_program: Pubkey,
|
||||
pub base_mint_supply: u64,
|
||||
}
|
||||
|
||||
const TOKEN_ACCOUNT_MINT_END: usize = 32;
|
||||
const TOKEN_ACCOUNT_AMOUNT_OFFSET: usize = 64;
|
||||
const TOKEN_ACCOUNT_AMOUNT_END: usize = 72;
|
||||
const TOKEN_ACCOUNT_STATE_OFFSET: usize = 108;
|
||||
const MINT_SUPPLY_OFFSET: usize = 36;
|
||||
const MINT_SUPPLY_END: usize = 44;
|
||||
const MINT_INITIALIZED_OFFSET: usize = 45;
|
||||
|
||||
fn supported_token_program(program: &Pubkey) -> bool {
|
||||
*program == crate::constants::TOKEN_PROGRAM || *program == crate::constants::TOKEN_PROGRAM_2022
|
||||
}
|
||||
|
||||
fn decode_token_account_amount(
|
||||
account: &solana_sdk::account::Account,
|
||||
expected_mint: &Pubkey,
|
||||
) -> Result<(u64, Pubkey), anyhow::Error> {
|
||||
if !supported_token_program(&account.owner) {
|
||||
return Err(anyhow!("Pool vault is not owned by a supported token program"));
|
||||
}
|
||||
let mint = account
|
||||
.data
|
||||
.get(..TOKEN_ACCOUNT_MINT_END)
|
||||
.ok_or_else(|| anyhow!("Pool vault data is too short"))?;
|
||||
if mint != expected_mint.as_ref() {
|
||||
return Err(anyhow!("Pool vault mint does not match Pool account"));
|
||||
}
|
||||
if account.data.get(TOKEN_ACCOUNT_STATE_OFFSET).copied() != Some(1) {
|
||||
return Err(anyhow!("Pool vault is not initialized"));
|
||||
}
|
||||
let amount = account
|
||||
.data
|
||||
.get(TOKEN_ACCOUNT_AMOUNT_OFFSET..TOKEN_ACCOUNT_AMOUNT_END)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.map(u64::from_le_bytes)
|
||||
.ok_or_else(|| anyhow!("Pool vault amount is missing"))?;
|
||||
Ok((amount, account.owner))
|
||||
}
|
||||
|
||||
fn decode_mint_supply(
|
||||
account: &solana_sdk::account::Account,
|
||||
expected_token_program: &Pubkey,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
if account.owner != *expected_token_program {
|
||||
return Err(anyhow!("Base mint and base vault use different token programs"));
|
||||
}
|
||||
if account.data.get(MINT_INITIALIZED_OFFSET).copied() != Some(1) {
|
||||
return Err(anyhow!("Base mint is not initialized"));
|
||||
}
|
||||
account
|
||||
.data
|
||||
.get(MINT_SUPPLY_OFFSET..MINT_SUPPLY_END)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.map(u64::from_le_bytes)
|
||||
.ok_or_else(|| anyhow!("Base mint supply is missing"))
|
||||
}
|
||||
|
||||
pub async fn get_pool_rpc_snapshot(
|
||||
pool: &Pool,
|
||||
rpc: &SolanaRpcClient,
|
||||
) -> Result<PoolRpcSnapshot, anyhow::Error> {
|
||||
let addresses = [pool.pool_base_token_account, pool.pool_quote_token_account, pool.base_mint];
|
||||
let accounts = rpc.get_multiple_accounts(&addresses).await?;
|
||||
let base_vault = accounts
|
||||
.first()
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| anyhow!("PumpSwap base vault account was not found"))?;
|
||||
let quote_vault = accounts
|
||||
.get(1)
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| anyhow!("PumpSwap quote vault account was not found"))?;
|
||||
let base_mint = accounts
|
||||
.get(2)
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| anyhow!("PumpSwap base mint account was not found"))?;
|
||||
let (base_reserve, base_token_program) =
|
||||
decode_token_account_amount(base_vault, &pool.base_mint)?;
|
||||
let (quote_reserve, quote_token_program) =
|
||||
decode_token_account_amount(quote_vault, &pool.quote_mint)?;
|
||||
let base_mint_supply = decode_mint_supply(base_mint, &base_token_program)?;
|
||||
|
||||
Ok(PoolRpcSnapshot {
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
base_mint_supply,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
pool: &Pool,
|
||||
rpc: &SolanaRpcClient,
|
||||
) -> Result<(u64, u64), anyhow::Error> {
|
||||
let base_balance = rpc.get_token_account_balance(&pool.pool_base_token_account).await?;
|
||||
let quote_balance = rpc.get_token_account_balance(&pool.pool_quote_token_account).await?;
|
||||
let snapshot = get_pool_rpc_snapshot(pool, rpc).await?;
|
||||
|
||||
let base_amount = base_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
let quote_amount = quote_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok((base_amount, quote_amount))
|
||||
Ok((snapshot.base_reserve, snapshot.quote_reserve))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -902,7 +1054,40 @@ pub fn get_fee_config_pda() -> Option<Pubkey> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use crate::instruction::utils::pumpswap_types;
|
||||
use solana_sdk::{account::Account, pubkey::Pubkey};
|
||||
|
||||
fn pool_account(virtual_quote_reserves: i128) -> Account {
|
||||
let mut data = Vec::with_capacity(8 + pumpswap_types::POOL_SIZE);
|
||||
data.extend_from_slice(&POOL_DISCRIMINATOR);
|
||||
data.push(7);
|
||||
data.extend_from_slice(&42u16.to_le_bytes());
|
||||
for seed in 1..=6 {
|
||||
data.extend_from_slice(Pubkey::new_from_array([seed; 32]).as_ref());
|
||||
}
|
||||
data.extend_from_slice(&123_456u64.to_le_bytes());
|
||||
data.extend_from_slice(Pubkey::new_from_array([7; 32]).as_ref());
|
||||
data.push(1);
|
||||
data.push(0);
|
||||
data.extend_from_slice(&virtual_quote_reserves.to_le_bytes());
|
||||
Account { data, owner: accounts::AMM_PROGRAM, ..Account::default() }
|
||||
}
|
||||
|
||||
fn token_account(mint: Pubkey, owner: Pubkey, amount: u64) -> Account {
|
||||
let mut data = vec![0; 165];
|
||||
data[..32].copy_from_slice(mint.as_ref());
|
||||
data[TOKEN_ACCOUNT_AMOUNT_OFFSET..TOKEN_ACCOUNT_AMOUNT_END]
|
||||
.copy_from_slice(&amount.to_le_bytes());
|
||||
data[TOKEN_ACCOUNT_STATE_OFFSET] = 1;
|
||||
Account { data, owner, ..Account::default() }
|
||||
}
|
||||
|
||||
fn mint_account(owner: Pubkey, supply: u64) -> Account {
|
||||
let mut data = vec![0; 82];
|
||||
data[MINT_SUPPLY_OFFSET..MINT_SUPPLY_END].copy_from_slice(&supply.to_le_bytes());
|
||||
data[MINT_INITIALIZED_OFFSET] = 1;
|
||||
Account { data, owner, ..Account::default() }
|
||||
}
|
||||
|
||||
fn fee_config_fixture() -> PumpSwapFeeConfig {
|
||||
PumpSwapFeeConfig {
|
||||
@@ -929,6 +1114,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_decoders_require_official_account_discriminators() {
|
||||
let global_len = 8 + 32 + 8 + 8 + 1 + 32 * 8 + 8 + 32 + 32 + 32 + 1 + 32 * 7 + 1 + 32 * 8;
|
||||
let mut global_data = vec![0; global_len];
|
||||
global_data[..8].copy_from_slice(&GLOBAL_CONFIG_DISCRIMINATOR);
|
||||
assert!(decode_global_config(&global_data).is_some());
|
||||
global_data[0] ^= 0xff;
|
||||
assert!(decode_global_config(&global_data).is_none());
|
||||
|
||||
let mut fee_data = Vec::with_capacity(8 + 1 + 32 + 24 + 4 + 4);
|
||||
fee_data.extend_from_slice(&FEE_CONFIG_DISCRIMINATOR);
|
||||
fee_data.extend_from_slice(&[0; 1 + 32 + 24]);
|
||||
fee_data.extend_from_slice(&0_u32.to_le_bytes());
|
||||
fee_data.extend_from_slice(&0_u32.to_le_bytes());
|
||||
assert!(decode_fee_config(&fee_data).is_some());
|
||||
fee_data[0] ^= 0xff;
|
||||
assert!(decode_fee_config(&fee_data).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpswap_user_volume_accumulator_pda_deterministic() {
|
||||
let user = Pubkey::new_unique();
|
||||
@@ -986,4 +1190,78 @@ mod tests {
|
||||
);
|
||||
assert_eq!(fees, PumpSwapFeeBasisPoints::new(20, 5, 75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpswap_pool_queries_cover_current_serialized_and_padded_sizes() {
|
||||
assert_eq!(POOL_DATA_LEN_LEGACY, 252);
|
||||
assert_eq!(POOL_DATA_LEN_CURRENT, 261);
|
||||
assert_eq!(POOL_DATA_LEN_PADDED, 300);
|
||||
assert_eq!(POOL_DATA_LEN_EXTENDED, 643);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_account_validation_checks_owner_length_and_discriminator() {
|
||||
let account = pool_account(-123_456);
|
||||
assert_eq!(decode_pool_account(&account).unwrap().virtual_quote_reserves, -123_456);
|
||||
|
||||
let mut wrong_owner = account.clone();
|
||||
wrong_owner.owner = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
decode_pool_account(&wrong_owner).unwrap_err(),
|
||||
"Account is not owned by PumpSwap program"
|
||||
);
|
||||
|
||||
let mut short = account.clone();
|
||||
short.data.truncate(7);
|
||||
assert_eq!(
|
||||
decode_pool_account(&short).unwrap_err(),
|
||||
"Pool account is shorter than its discriminator"
|
||||
);
|
||||
|
||||
let mut wrong_discriminator = account;
|
||||
wrong_discriminator.data[0] ^= 0xff;
|
||||
assert_eq!(
|
||||
decode_pool_account(&wrong_discriminator).unwrap_err(),
|
||||
"Account discriminator is not PumpSwap Pool"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_snapshot_decoders_validate_token_ownership_and_layout() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let token_program = crate::constants::TOKEN_PROGRAM_2022;
|
||||
let vault = token_account(mint, token_program, 987_654_321);
|
||||
assert_eq!(
|
||||
decode_token_account_amount(&vault, &mint).unwrap(),
|
||||
(987_654_321, token_program)
|
||||
);
|
||||
assert_eq!(
|
||||
decode_mint_supply(&mint_account(token_program, 42), &token_program).unwrap(),
|
||||
42
|
||||
);
|
||||
|
||||
let wrong_mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
decode_token_account_amount(&vault, &wrong_mint).unwrap_err().to_string(),
|
||||
"Pool vault mint does not match Pool account"
|
||||
);
|
||||
|
||||
let unsupported = token_account(mint, Pubkey::new_unique(), 1);
|
||||
assert_eq!(
|
||||
decode_token_account_amount(&unsupported, &mint).unwrap_err().to_string(),
|
||||
"Pool vault is not owned by a supported token program"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coin_creator_vault_ata_uses_quote_token_program() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let token_program = crate::constants::TOKEN_PROGRAM_2022;
|
||||
let authority = coin_creator_vault_authority(creator);
|
||||
let expected =
|
||||
get_associated_token_address_with_program_id(&authority, &mint, &token_program);
|
||||
|
||||
assert_eq!(coin_creator_vault_ata(creator, mint, token_program), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub const POOL_DISCRIMINATOR: [u8; 8] = [241, 154, 109, 4, 17, 177, 109, 188];
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct Pool {
|
||||
pub pool_bump: u8,
|
||||
@@ -17,16 +19,135 @@ pub struct Pool {
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Whether this pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
/// Reserved for future fields (pump-public-docs: pool structure = 244 bytes total)
|
||||
pub _reserved: [u8; 7],
|
||||
/// Virtual quote reserves appended to the Pool account.
|
||||
///
|
||||
/// Quotes must use `quote_vault_balance + virtual_quote_reserves`.
|
||||
pub virtual_quote_reserves: i128,
|
||||
}
|
||||
|
||||
/// Borsh 解码用的 Pool 长度。链上池为 244 字节(pump-public-docs Breaking Change),与 POOL_SIZE 一致。
|
||||
pub const POOL_SIZE: usize = 244;
|
||||
/// Minimum Borsh payload length for the current Pool layout, excluding the
|
||||
/// 8-byte Anchor account discriminator.
|
||||
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 + 16;
|
||||
const LEGACY_POOL_FIELDS_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||
/// Legacy Pool accounts were allocated with seven trailing padding bytes.
|
||||
pub const LEGACY_POOL_SIZE: usize = LEGACY_POOL_FIELDS_SIZE + 7;
|
||||
|
||||
#[derive(BorshDeserialize)]
|
||||
struct LegacyPool {
|
||||
pool_bump: u8,
|
||||
index: u16,
|
||||
creator: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
lp_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
lp_supply: u64,
|
||||
coin_creator: Pubkey,
|
||||
is_mayhem_mode: bool,
|
||||
is_cashback_coin: bool,
|
||||
}
|
||||
|
||||
impl From<LegacyPool> for Pool {
|
||||
fn from(pool: LegacyPool) -> Self {
|
||||
Self {
|
||||
pool_bump: pool.pool_bump,
|
||||
index: pool.index,
|
||||
creator: pool.creator,
|
||||
base_mint: pool.base_mint,
|
||||
quote_mint: pool.quote_mint,
|
||||
lp_mint: pool.lp_mint,
|
||||
pool_base_token_account: pool.pool_base_token_account,
|
||||
pool_quote_token_account: pool.pool_quote_token_account,
|
||||
lp_supply: pool.lp_supply,
|
||||
coin_creator: pool.coin_creator,
|
||||
is_mayhem_mode: pool.is_mayhem_mode,
|
||||
is_cashback_coin: pool.is_cashback_coin,
|
||||
virtual_quote_reserves: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||
if data.len() < POOL_SIZE {
|
||||
return None;
|
||||
if data.len() >= POOL_SIZE {
|
||||
return borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok();
|
||||
}
|
||||
|
||||
if data.len() == LEGACY_POOL_SIZE {
|
||||
return borsh::from_slice::<LegacyPool>(&data[..LEGACY_POOL_FIELDS_SIZE])
|
||||
.ok()
|
||||
.map(Into::into);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Compute the quote reserves used by PumpSwap pricing.
|
||||
///
|
||||
/// Returns `None` when the signed sum is non-positive or cannot fit in a `u64`.
|
||||
#[inline]
|
||||
pub fn effective_quote_reserves(
|
||||
quote_vault_balance: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
) -> Option<u64> {
|
||||
i128::from(quote_vault_balance)
|
||||
.checked_add(virtual_quote_reserves)
|
||||
.and_then(|reserves| u64::try_from(reserves).ok())
|
||||
.filter(|reserves| *reserves != 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pool_payload(virtual_quote_reserves: i128) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(POOL_SIZE);
|
||||
data.push(7);
|
||||
data.extend_from_slice(&42u16.to_le_bytes());
|
||||
for seed in 1..=6 {
|
||||
data.extend_from_slice(Pubkey::new_from_array([seed; 32]).as_ref());
|
||||
}
|
||||
data.extend_from_slice(&123_456u64.to_le_bytes());
|
||||
data.extend_from_slice(Pubkey::new_from_array([7; 32]).as_ref());
|
||||
data.push(1);
|
||||
data.push(0);
|
||||
data.extend_from_slice(&virtual_quote_reserves.to_le_bytes());
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_current_pool_virtual_quote_reserves() {
|
||||
let pool = pool_decode(&pool_payload(987_654_321)).unwrap();
|
||||
|
||||
assert_eq!(pool.virtual_quote_reserves, 987_654_321);
|
||||
assert!(pool.is_mayhem_mode);
|
||||
assert!(!pool.is_cashback_coin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_legacy_pool_with_zero_virtual_quote_reserves() {
|
||||
let mut data = pool_payload(0);
|
||||
data.truncate(LEGACY_POOL_FIELDS_SIZE);
|
||||
data.extend_from_slice(&[0; 7]);
|
||||
|
||||
let pool = pool_decode(&data).unwrap();
|
||||
assert_eq!(pool.virtual_quote_reserves, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_partial_current_pool_layout() {
|
||||
let current = pool_payload(987_654_321);
|
||||
for len in (LEGACY_POOL_SIZE + 1)..POOL_SIZE {
|
||||
assert!(pool_decode(¤t[..len]).is_none(), "accepted body length {len}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
|
||||
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
|
||||
assert_eq!(effective_quote_reserves(1_000, -250), Some(750));
|
||||
assert_eq!(effective_quote_reserves(1_000, -1_000), None);
|
||||
assert_eq!(effective_quote_reserves(100, -101), None);
|
||||
assert_eq!(effective_quote_reserves(u64::MAX, 1), None);
|
||||
}
|
||||
borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use bincode::serialize as bincode_serialize;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::LUNARLANDER_TIP_ACCOUNTS};
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use lunar_lander_quic_client::{ClientOptions, LunarLanderQuicClient};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LunarLanderBackend {
|
||||
Http {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
http_client: Client,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
},
|
||||
Quic(Arc<LunarLanderQuicClient>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LunarLanderClient {
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
backend: LunarLanderBackend,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for LunarLanderClient {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *LUNARLANDER_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| LUNARLANDER_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
fn get_swqos_type(&self) -> SwqosType {
|
||||
SwqosType::LunarLander
|
||||
}
|
||||
}
|
||||
|
||||
impl LunarLanderClient {
|
||||
/// Create an HTTP binary client (POST /send-bin with bincode body).
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
backend: LunarLanderBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
},
|
||||
};
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
client
|
||||
}
|
||||
|
||||
/// Create a QUIC client (port 16888, cert CN = api_key, fire-and-forget unidirectional streams).
|
||||
pub async fn new_quic(
|
||||
rpc_url: String,
|
||||
quic_endpoint: &str,
|
||||
api_key: String,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let quic_client = LunarLanderQuicClient::connect_with_options(
|
||||
quic_endpoint,
|
||||
api_key,
|
||||
quic_client_options(mev_protection),
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
backend: LunarLanderBackend::Quic(Arc::new(quic_client)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn start_ping_task(&self) {
|
||||
match &self.backend {
|
||||
LunarLanderBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
let http_client = http_client.clone();
|
||||
let ping_handle = ping_handle.clone();
|
||||
let stop_ping = stop_ping.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
// Immediate first ping to warm connection.
|
||||
let _ = Self::send_ping_request(&http_client, &endpoint, &auth_token).await;
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) =
|
||||
Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("LunarLander ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut guard = ping_handle.lock().await;
|
||||
if let Some(old) = guard.as_ref() {
|
||||
old.abort();
|
||||
}
|
||||
*guard = Some(handle);
|
||||
}
|
||||
LunarLanderBackend::Quic(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// GET {endpoint}/ping for HTTP keepalive.
|
||||
async fn send_ping_request(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
auth_token: &str,
|
||||
) -> Result<()> {
|
||||
let url = format!("{}/ping", endpoint.trim_end_matches('/'));
|
||||
let response = http_client
|
||||
.get(&url)
|
||||
.header("x-api-key", auth_token)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
let _ = response.bytes().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_transaction_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let signature = *transaction
|
||||
.signatures
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("LunarLander transaction has no signature"))?;
|
||||
let body_bytes = bincode_serialize(transaction)
|
||||
.map_err(|e| anyhow::anyhow!("LunarLander binary serialize failed: {}", e))?;
|
||||
|
||||
match &self.backend {
|
||||
LunarLanderBackend::Http { endpoint, auth_token, http_client, .. } => {
|
||||
let url = format!("{}/send-bin", endpoint.trim_end_matches('/'));
|
||||
let response = http_client
|
||||
.post(&url)
|
||||
.header("x-api-key", auth_token)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
.await?;
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if status.is_success() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"LunarLander",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"LunarLander",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {}", status),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("LunarLander sendTransaction failed: {}", status));
|
||||
}
|
||||
}
|
||||
LunarLanderBackend::Quic(quic) => {
|
||||
quic.send_transaction(&body_bytes).await?;
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"LunarLander",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"LunarLander",
|
||||
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: {:?}",
|
||||
"LunarLander",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LunarLanderClient {
|
||||
fn drop(&mut self) {
|
||||
match &self.backend {
|
||||
LunarLanderBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
stop_ping.store(true, Ordering::Relaxed);
|
||||
if let Ok(mut guard) = ping_handle.try_lock() {
|
||||
if let Some(handle) = guard.take() {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
LunarLanderBackend::Quic(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quic_client_options(mev_protection: bool) -> ClientOptions {
|
||||
ClientOptions { mev_protect: mev_protection, ..ClientOptions::default() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quic_options_follow_sdk_mev_protection() {
|
||||
assert!(!quic_client_options(false).mev_protect);
|
||||
assert!(quic_client_options(true).mev_protect);
|
||||
}
|
||||
}
|
||||
+83
-11
@@ -7,6 +7,7 @@ pub mod flashblock;
|
||||
pub mod helius;
|
||||
pub mod jito;
|
||||
pub mod lightspeed;
|
||||
pub mod lunarlander;
|
||||
pub mod nextblock;
|
||||
pub mod node1;
|
||||
pub mod node1_quic;
|
||||
@@ -33,22 +34,23 @@ use crate::{
|
||||
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_SOLAMI, SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT,
|
||||
SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE,
|
||||
SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1,
|
||||
SWQOS_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SWQOS_ENDPOINTS_LUNARLANDER, SWQOS_ENDPOINTS_LUNARLANDER_QUIC, SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOLAMI,
|
||||
SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
|
||||
SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
|
||||
SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
|
||||
SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_LUNARLANDER, SWQOS_MIN_TIP_NEXTBLOCK,
|
||||
SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
},
|
||||
swqos::{
|
||||
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
|
||||
flashblock::FlashBlockClient, helius::HeliusClient, jito::JitoClient,
|
||||
lightspeed::LightspeedClient, nextblock::NextBlockClient, node1::Node1Client,
|
||||
node1_quic::Node1QuicClient, solami::SolamiClient, solana_rpc::SolRpcClient,
|
||||
soyas::SoyasClient, speedlanding::SpeedlandingClient, stellium::StelliumClient,
|
||||
temporal::TemporalClient, zeroslot::ZeroSlotClient,
|
||||
lightspeed::LightspeedClient, lunarlander::LunarLanderClient, nextblock::NextBlockClient,
|
||||
node1::Node1Client, node1_quic::Node1QuicClient, solami::SolamiClient,
|
||||
solana_rpc::SolRpcClient, soyas::SoyasClient, speedlanding::SpeedlandingClient,
|
||||
stellium::StelliumClient, temporal::TemporalClient, zeroslot::ZeroSlotClient,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,6 +125,7 @@ pub enum SwqosType {
|
||||
Speedlanding,
|
||||
Helius,
|
||||
Solami,
|
||||
LunarLander,
|
||||
Default,
|
||||
}
|
||||
|
||||
@@ -146,6 +149,7 @@ impl SwqosType {
|
||||
Self::Speedlanding => "Speedlanding",
|
||||
Self::Helius => "Helius",
|
||||
Self::Solami => "Solami",
|
||||
Self::LunarLander => "LunarLander",
|
||||
Self::Default => "Default",
|
||||
}
|
||||
}
|
||||
@@ -167,6 +171,7 @@ impl SwqosType {
|
||||
Self::Speedlanding,
|
||||
Self::Helius,
|
||||
Self::Solami,
|
||||
Self::LunarLander,
|
||||
Self::Default,
|
||||
]
|
||||
}
|
||||
@@ -209,6 +214,7 @@ pub trait SwqosClientTrait {
|
||||
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SwqosType::Helius => SWQOS_MIN_TIP_HELIUS,
|
||||
SwqosType::Solami => SWQOS_MIN_TIP_SOLAMI,
|
||||
SwqosType::LunarLander => SWQOS_MIN_TIP_LUNARLANDER,
|
||||
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
|
||||
}
|
||||
}
|
||||
@@ -271,6 +277,10 @@ pub enum SwqosConfig {
|
||||
Helius(String, SwqosRegion, Option<String>, Option<bool>),
|
||||
/// Solami(api_key, region, custom_url)
|
||||
Solami(String, SwqosRegion, Option<String>),
|
||||
/// Lunar Lander (HelloMoon): binary tx via QUIC (port 16888) or HTTP POST /send-bin.
|
||||
/// (api_key, region, custom_url, transport). transport=None => QUIC; Some(Http) => HTTP.
|
||||
/// Minimum tip: 0.001 SOL. Apply for API key: https://docs.hellomoon.io/reference/lunar-lander
|
||||
LunarLander(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
}
|
||||
|
||||
impl SwqosConfig {
|
||||
@@ -292,6 +302,7 @@ impl SwqosConfig {
|
||||
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
|
||||
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
|
||||
SwqosConfig::Solami(_, _, _) => SwqosType::Solami,
|
||||
SwqosConfig::LunarLander(_, _, _, _) => SwqosType::LunarLander,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +332,7 @@ impl SwqosConfig {
|
||||
SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(),
|
||||
SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(),
|
||||
SwqosType::Solami => SWQOS_ENDPOINTS_SOLAMI[region as usize].to_string(),
|
||||
SwqosType::LunarLander => SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string(),
|
||||
SwqosType::Default => "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -354,6 +366,14 @@ impl SwqosConfig {
|
||||
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
SwqosType::LunarLander => {
|
||||
let use_quic = transport.unwrap_or(SwqosTransport::Quic) == SwqosTransport::Quic;
|
||||
if use_quic {
|
||||
SWQOS_ENDPOINTS_LUNARLANDER_QUIC[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
_ => Self::get_endpoint(swqos_type, region, None),
|
||||
}
|
||||
}
|
||||
@@ -527,6 +547,27 @@ impl SwqosConfig {
|
||||
SolamiClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
|
||||
Ok(Arc::new(solami_client))
|
||||
}
|
||||
SwqosConfig::LunarLander(api_key, region, url, transport) => {
|
||||
let use_quic = transport.unwrap_or(SwqosTransport::Quic) == SwqosTransport::Quic;
|
||||
if use_quic {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_LUNARLANDER_QUIC[region as usize].to_string()
|
||||
});
|
||||
let lunarlander_client = LunarLanderClient::new_quic(
|
||||
rpc_url.clone(),
|
||||
&quic_endpoint,
|
||||
api_key,
|
||||
mev_protection,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(lunarlander_client))
|
||||
} else {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::LunarLander, region, url);
|
||||
let lunarlander_client =
|
||||
LunarLanderClient::new(rpc_url.clone(), endpoint, api_key);
|
||||
Ok(Arc::new(lunarlander_client))
|
||||
}
|
||||
}
|
||||
SwqosConfig::Default(endpoint) => {
|
||||
let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment);
|
||||
let rpc_client = SolRpcClient::new(Arc::new(rpc));
|
||||
@@ -535,3 +576,34 @@ impl SwqosConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn lunarlander_defaults_to_quic_endpoint() {
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::LunarLander,
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(endpoint, SWQOS_ENDPOINTS_LUNARLANDER_QUIC[SwqosRegion::Frankfurt as usize]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lunarlander_http_transport_uses_binary_http_endpoint() {
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::LunarLander,
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Http),
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(endpoint, SWQOS_ENDPOINTS_LUNARLANDER[SwqosRegion::Frankfurt as usize]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::instruction::utils::pumpswap::{
|
||||
accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP, PumpSwapFeeBasisPoints,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const SPL_MINT_SUPPLY_OFFSET: usize = 36;
|
||||
const SPL_MINT_SUPPLY_LEN: usize = 8;
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
@@ -32,8 +28,10 @@ pub struct PumpSwapParams {
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
/// Raw quote-vault token balance. Pricing uses this plus [`Self::virtual_quote_reserves`].
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Signed virtual quote reserves from the PumpSwap Pool account or trade event.
|
||||
pub virtual_quote_reserves: i128,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
@@ -75,6 +73,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
@@ -99,6 +98,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
@@ -127,6 +127,21 @@ impl PumpSwapParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Quote reserves used by PumpSwap pricing and fee-tier selection.
|
||||
pub fn effective_quote_reserves(&self) -> Result<u64, anyhow::Error> {
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
self.pool_quote_token_reserves,
|
||||
self.virtual_quote_reserves,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Invalid PumpSwap effective quote reserves: vault={} virtual={}",
|
||||
self.pool_quote_token_reserves,
|
||||
self.virtual_quote_reserves
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_fee_basis_points(
|
||||
mut self,
|
||||
lp_fee_basis_points: u64,
|
||||
@@ -161,6 +176,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
@@ -178,6 +194,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
@@ -202,6 +219,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
@@ -223,6 +241,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
@@ -244,17 +263,9 @@ impl PumpSwapParams {
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
let (pool_address, pool) =
|
||||
crate::instruction::utils::pumpswap::find_by_mint(rpc, mint).await?;
|
||||
Self::from_pool_data(rpc, &pool_address, &pool).await
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
@@ -275,9 +286,23 @@ impl PumpSwapParams {
|
||||
pool_address: &Pubkey,
|
||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||
let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok();
|
||||
let snapshot =
|
||||
crate::instruction::utils::pumpswap::get_pool_rpc_snapshot(pool_data, rpc).await?;
|
||||
let pool_base_token_reserves = snapshot.base_reserve;
|
||||
let pool_quote_token_reserves = snapshot.quote_reserve;
|
||||
let effective_quote_token_reserves =
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
pool_quote_token_reserves,
|
||||
pool_data.virtual_quote_reserves,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Invalid PumpSwap effective quote reserves: vault={} virtual={}",
|
||||
pool_quote_token_reserves,
|
||||
pool_data.virtual_quote_reserves
|
||||
)
|
||||
})?;
|
||||
let base_mint_supply = Some(snapshot.base_mint_supply);
|
||||
let fee_config = crate::instruction::utils::pumpswap::fetch_fee_config(rpc).await;
|
||||
let raw_fee_basis_points = crate::instruction::utils::pumpswap::compute_fee_basis_points(
|
||||
fee_config.as_ref(),
|
||||
@@ -285,7 +310,7 @@ impl PumpSwapParams {
|
||||
pool_data.base_mint,
|
||||
base_mint_supply,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
effective_quote_token_reserves,
|
||||
);
|
||||
let creator_fee_basis_points = if pool_data.coin_creator == Pubkey::default() {
|
||||
0
|
||||
@@ -296,21 +321,11 @@ impl PumpSwapParams {
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
snapshot.quote_token_program,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
base_mint: pool_data.base_mint,
|
||||
@@ -319,19 +334,12 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves: pool_data.virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
base_token_program: snapshot.base_token_program,
|
||||
is_cashback_coin: pool_data.is_cashback_coin,
|
||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
quote_token_program: snapshot.quote_token_program,
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
pool_creator: pool_data.creator,
|
||||
coin_creator: pool_data.coin_creator,
|
||||
@@ -345,13 +353,3 @@ impl PumpSwapParams {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_mint_supply(data: &[u8]) -> Option<u64> {
|
||||
let bytes = data.get(SPL_MINT_SUPPLY_OFFSET..SPL_MINT_SUPPLY_OFFSET + SPL_MINT_SUPPLY_LEN)?;
|
||||
Some(u64::from_le_bytes(bytes.try_into().ok()?))
|
||||
}
|
||||
|
||||
async fn fetch_mint_supply(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<u64, anyhow::Error> {
|
||||
let account = rpc.get_account(mint).await?;
|
||||
decode_mint_supply(&account.data).ok_or_else(|| anyhow::anyhow!("Failed to decode mint supply"))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,18 @@
|
||||
/// * fee_basis_points = 100 -> 1% fee
|
||||
#[inline(always)]
|
||||
pub const fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 {
|
||||
ceil_div(amount * fee_basis_points, 10_000)
|
||||
let whole = match (amount / 10_000).checked_mul(fee_basis_points) {
|
||||
Some(value) => value,
|
||||
None => return u128::MAX,
|
||||
};
|
||||
let remainder_product = match (amount % 10_000).checked_mul(fee_basis_points) {
|
||||
Some(value) => value,
|
||||
None => return u128::MAX,
|
||||
};
|
||||
match whole.checked_add(ceil_div(remainder_product, 10_000)) {
|
||||
Some(value) => value,
|
||||
None => u128::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ceiling division implementation
|
||||
@@ -25,7 +36,12 @@ pub const fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 {
|
||||
/// Returns the ceiling result of a/b
|
||||
#[inline(always)]
|
||||
pub const fn ceil_div(a: u128, b: u128) -> u128 {
|
||||
(a + b - 1) / b
|
||||
let quotient = a / b;
|
||||
if a % b == 0 {
|
||||
quotient
|
||||
} else {
|
||||
quotient + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum slippage in basis points (99.99% = 9999 bps)
|
||||
@@ -55,7 +71,12 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
|
||||
} else {
|
||||
basis_points
|
||||
};
|
||||
amount + (amount * bps / 10000)
|
||||
let result = amount as u128 + (amount as u128 * bps as u128 / 10_000);
|
||||
if result > u64::MAX as u128 {
|
||||
u64::MAX
|
||||
} else {
|
||||
result as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate sell amount with slippage protection
|
||||
@@ -75,9 +96,35 @@ pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
if amount <= basis_points / 10000 {
|
||||
1
|
||||
let bps = if basis_points > MAX_SLIPPAGE_BASIS_POINTS {
|
||||
MAX_SLIPPAGE_BASIS_POINTS
|
||||
} else {
|
||||
amount - (amount * basis_points / 10000)
|
||||
basis_points
|
||||
};
|
||||
amount - (amount as u128 * bps as u128 / 10_000) as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ceil_div_handles_u128_max_without_addition_overflow() {
|
||||
assert_eq!(ceil_div(u128::MAX, u128::MAX), 1);
|
||||
assert_eq!(ceil_div(u128::MAX, 2), u128::MAX / 2 + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_fee_handles_large_amounts_without_multiplication_overflow() {
|
||||
assert_eq!(compute_fee(u128::MAX, 1), ceil_div(u128::MAX, 10_000));
|
||||
assert_eq!(compute_fee(u128::MAX, 10_000), u128::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slippage_helpers_are_deterministic_at_numeric_boundaries() {
|
||||
assert_eq!(calculate_with_slippage_buy(u64::MAX, 100), u64::MAX);
|
||||
assert_eq!(calculate_with_slippage_sell(u64::MAX, 100), 18_262_276_632_972_456_099);
|
||||
assert_eq!(calculate_with_slippage_sell(10_000, u64::MAX), 1);
|
||||
assert_eq!(calculate_with_slippage_sell(1, u64::MAX), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ pub fn get_sell_sol_amount_from_token_amount(
|
||||
let amount_128 = amount as u128;
|
||||
|
||||
// Calculate SOL amount received from selling tokens using constant product formula
|
||||
let numerator = amount_128.checked_mul(virtual_sol_reserves).unwrap_or(0);
|
||||
let Some(numerator) = amount_128.checked_mul(virtual_sol_reserves) else {
|
||||
return u64::MAX;
|
||||
};
|
||||
let denominator = virtual_token_reserves.checked_add(amount_128).unwrap_or(1);
|
||||
|
||||
let sol_cost = numerator.checked_div(denominator).unwrap_or(0);
|
||||
@@ -101,5 +103,5 @@ pub fn get_sell_sol_amount_from_token_amount(
|
||||
// Calculate transaction fee
|
||||
let fee = compute_fee(sol_cost, total_fee_basis_points_128);
|
||||
|
||||
sol_cost.saturating_sub(fee) as u64
|
||||
sol_cost.saturating_sub(fee).min(u64::MAX as u128) as u64
|
||||
}
|
||||
|
||||
+348
-52
@@ -7,16 +7,45 @@ use crate::instruction::utils::pumpswap::accounts::{
|
||||
use crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[inline]
|
||||
fn effective_quote_reserve(
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
) -> Result<u64, String> {
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
)
|
||||
.filter(|reserve| *reserve != 0)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Invalid effective quote reserves: raw={quote_reserve}, virtual={virtual_quote_reserves}."
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
|
||||
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
|
||||
#[inline]
|
||||
pub(crate) fn creator_side_fee_basis_points(
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
) -> Result<u64, String> {
|
||||
let creator_bps =
|
||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
||||
creator_bps.saturating_add(cashback_fee_basis_points)
|
||||
creator_bps
|
||||
.checked_add(cashback_fee_basis_points)
|
||||
.ok_or_else(|| "Coin creator fee basis points overflow.".to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn checked_u64(value: u128, name: &str) -> Result<u64, String> {
|
||||
u64::try_from(value).map_err(|_| format!("Calculated {name} exceeds u64."))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn checked_fee(amount: u64, basis_points: u64, name: &str) -> Result<u64, String> {
|
||||
checked_u64(compute_fee(amount as u128, basis_points as u128), name)
|
||||
}
|
||||
|
||||
/// Result for buying base tokens with base amount input
|
||||
@@ -69,7 +98,8 @@ pub struct SellQuoteInputResult {
|
||||
/// * `base` - Amount of base tokens to buy
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
||||
///
|
||||
@@ -80,6 +110,7 @@ pub fn buy_base_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyBaseInputResult, String> {
|
||||
@@ -88,10 +119,11 @@ pub fn buy_base_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -101,36 +133,42 @@ pub fn buy_base_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<BuyBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
if base > base_reserve {
|
||||
return Err("Cannot buy more base tokens than the pool reserves.".to_string());
|
||||
}
|
||||
|
||||
// Calculate required quote amount using constant product formula
|
||||
let numerator = (quote_reserve as u128) * (base as u128);
|
||||
let numerator = (effective_quote_reserve as u128) * (base as u128);
|
||||
let denominator = base_reserve - base;
|
||||
|
||||
if denominator == 0 {
|
||||
return Err("Pool would be depleted; denominator is zero.".to_string());
|
||||
}
|
||||
|
||||
let quote_amount_in = ceil_div(numerator, denominator as u128) as u64;
|
||||
let quote_amount_in =
|
||||
checked_u64(ceil_div(numerator, denominator as u128), "raw quote amount")?;
|
||||
|
||||
// Calculate fees
|
||||
let lp_fee =
|
||||
compute_fee(quote_amount_in as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
|
||||
let lp_fee = checked_fee(quote_amount_in, fee_basis_points.lp_fee_basis_points, "LP fee")?;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_in as u128, fee_basis_points.protocol_fee_basis_points as u128)
|
||||
as u64;
|
||||
let coin_creator_fee = compute_fee(
|
||||
quote_amount_in as u128,
|
||||
fee_basis_points.coin_creator_fee_basis_points as u128,
|
||||
) as u64;
|
||||
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
|
||||
checked_fee(quote_amount_in, fee_basis_points.protocol_fee_basis_points, "protocol fee")?;
|
||||
let coin_creator_fee = checked_fee(
|
||||
quote_amount_in,
|
||||
fee_basis_points.coin_creator_fee_basis_points,
|
||||
"coin creator fee",
|
||||
)?;
|
||||
let total_quote = quote_amount_in
|
||||
.checked_add(lp_fee)
|
||||
.and_then(|amount| amount.checked_add(protocol_fee))
|
||||
.and_then(|amount| amount.checked_add(coin_creator_fee))
|
||||
.ok_or_else(|| "Total quote amount exceeds u64.".to_string())?;
|
||||
|
||||
// Calculate max quote with slippage
|
||||
let max_quote = calculate_with_slippage_buy(total_quote, slippage_basis_points);
|
||||
@@ -148,7 +186,8 @@ pub fn buy_base_input_internal_with_fees(
|
||||
/// * `quote` - Amount of quote tokens to spend
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
@@ -159,6 +198,7 @@ pub fn buy_quote_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyQuoteInputResult, String> {
|
||||
@@ -167,10 +207,11 @@ pub fn buy_quote_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -180,18 +221,23 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<BuyQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate total fee basis points
|
||||
let total_fee_bps = fee_basis_points
|
||||
.lp_fee_basis_points
|
||||
.saturating_add(fee_basis_points.protocol_fee_basis_points)
|
||||
.saturating_add(fee_basis_points.coin_creator_fee_basis_points);
|
||||
let denominator = 10_000 + total_fee_bps;
|
||||
.checked_add(fee_basis_points.protocol_fee_basis_points)
|
||||
.and_then(|fees| fees.checked_add(fee_basis_points.coin_creator_fee_basis_points))
|
||||
.ok_or_else(|| "Fee basis points overflow.".to_string())?;
|
||||
let denominator = 10_000_u64
|
||||
.checked_add(total_fee_bps)
|
||||
.ok_or_else(|| "Fee denominator overflow.".to_string())?;
|
||||
|
||||
// Calculate effective quote amount after fees
|
||||
let mut effective_quote = (quote as u128 * 10_000) / denominator as u128;
|
||||
@@ -200,28 +246,36 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
compute_fee(effective_quote, fee_basis_points.protocol_fee_basis_points as u128);
|
||||
let coin_creator_fee =
|
||||
compute_fee(effective_quote, fee_basis_points.coin_creator_fee_basis_points as u128);
|
||||
let total_with_fees = effective_quote + lp_fee + protocol_fee + coin_creator_fee;
|
||||
let total_with_fees = effective_quote
|
||||
.checked_add(lp_fee)
|
||||
.and_then(|amount| amount.checked_add(protocol_fee))
|
||||
.and_then(|amount| amount.checked_add(coin_creator_fee))
|
||||
.ok_or_else(|| "Total quote amount exceeds u128.".to_string())?;
|
||||
if total_with_fees > quote as u128 {
|
||||
effective_quote = effective_quote.saturating_sub(total_with_fees - quote as u128);
|
||||
effective_quote = effective_quote
|
||||
.checked_sub(total_with_fees - quote as u128)
|
||||
.ok_or_else(|| "Quote input is too small to cover fees.".to_string())?;
|
||||
}
|
||||
let input_amount = effective_quote.saturating_sub(1);
|
||||
let input_amount = effective_quote
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| "Quote input is too small after fees.".to_string())?;
|
||||
|
||||
// Calculate base amount out using constant product formula
|
||||
let numerator = (base_reserve as u128) * input_amount;
|
||||
let denominator_effective = (quote_reserve as u128) + input_amount;
|
||||
let denominator_effective = (effective_quote_reserve as u128) + input_amount;
|
||||
|
||||
if denominator_effective == 0 {
|
||||
return Err("Pool would be depleted; denominator is zero.".to_string());
|
||||
}
|
||||
|
||||
let base_amount_out = (numerator / denominator_effective) as u64;
|
||||
let base_amount_out = checked_u64(numerator / denominator_effective, "base amount")?;
|
||||
|
||||
// Calculate max quote with slippage
|
||||
let max_quote = calculate_with_slippage_buy(quote, slippage_basis_points);
|
||||
|
||||
Ok(BuyQuoteInputResult {
|
||||
base: base_amount_out,
|
||||
internal_quote_without_fees: effective_quote as u64,
|
||||
internal_quote_without_fees: checked_u64(effective_quote, "effective quote amount")?,
|
||||
max_quote,
|
||||
})
|
||||
}
|
||||
@@ -232,7 +286,8 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
/// * `base` - Amount of base tokens to sell
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
@@ -243,6 +298,7 @@ pub fn sell_base_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellBaseInputResult, String> {
|
||||
@@ -251,10 +307,11 @@ pub fn sell_base_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -264,32 +321,43 @@ pub fn sell_base_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<SellBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate quote amount out using constant product formula
|
||||
let quote_amount_out = ((quote_reserve as u128) * (base as u128)
|
||||
/ ((base_reserve as u128) + (base as u128))) as u64;
|
||||
let quote_amount_out = checked_u64(
|
||||
(effective_quote_reserve as u128) * (base as u128)
|
||||
/ ((base_reserve as u128) + (base as u128)),
|
||||
"raw quote amount",
|
||||
)?;
|
||||
|
||||
// Calculate fees
|
||||
let lp_fee =
|
||||
compute_fee(quote_amount_out as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
|
||||
let lp_fee = checked_fee(quote_amount_out, fee_basis_points.lp_fee_basis_points, "LP fee")?;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_out as u128, fee_basis_points.protocol_fee_basis_points as u128)
|
||||
as u64;
|
||||
let coin_creator_fee = compute_fee(
|
||||
quote_amount_out as u128,
|
||||
fee_basis_points.coin_creator_fee_basis_points as u128,
|
||||
) as u64;
|
||||
checked_fee(quote_amount_out, fee_basis_points.protocol_fee_basis_points, "protocol fee")?;
|
||||
let coin_creator_fee = checked_fee(
|
||||
quote_amount_out,
|
||||
fee_basis_points.coin_creator_fee_basis_points,
|
||||
"coin creator fee",
|
||||
)?;
|
||||
|
||||
// Calculate final quote after fees
|
||||
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
|
||||
let total_fees = lp_fee
|
||||
.checked_add(protocol_fee)
|
||||
.and_then(|fees| fees.checked_add(coin_creator_fee))
|
||||
.ok_or_else(|| "Total fees exceed u64.".to_string())?;
|
||||
if total_fees > quote_amount_out {
|
||||
return Err("Fees exceed total output; final quote is negative.".to_string());
|
||||
}
|
||||
let quote_vault_outflow = quote_amount_out - lp_fee;
|
||||
if quote_vault_outflow > quote_reserve {
|
||||
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||
}
|
||||
let final_quote = quote_amount_out - total_fees;
|
||||
|
||||
// Calculate min quote with slippage
|
||||
@@ -310,12 +378,22 @@ fn calculate_quote_amount_out(
|
||||
lp_fee_basis_points: u64,
|
||||
protocol_fee_basis_points: u64,
|
||||
coin_creator_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
let total_fee_basis_points =
|
||||
lp_fee_basis_points + protocol_fee_basis_points + coin_creator_fee_basis_points;
|
||||
let denominator = MAX_FEE_BASIS_POINTS - total_fee_basis_points;
|
||||
ceil_div((user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128), denominator as u128)
|
||||
as u64
|
||||
) -> Result<u64, String> {
|
||||
let total_fee_basis_points = lp_fee_basis_points
|
||||
.checked_add(protocol_fee_basis_points)
|
||||
.and_then(|fees| fees.checked_add(coin_creator_fee_basis_points))
|
||||
.ok_or_else(|| "Fee basis points overflow.".to_string())?;
|
||||
let denominator = MAX_FEE_BASIS_POINTS
|
||||
.checked_sub(total_fee_basis_points)
|
||||
.ok_or_else(|| "Total fee basis points must be less than 10,000.".to_string())?;
|
||||
if denominator == 0 {
|
||||
return Err("Total fee basis points must be less than 10,000.".to_string());
|
||||
}
|
||||
let raw_quote = ceil_div(
|
||||
(user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128),
|
||||
denominator as u128,
|
||||
);
|
||||
u64::try_from(raw_quote).map_err(|_| "Calculated quote amount exceeds u64.".to_string())
|
||||
}
|
||||
|
||||
/// Calculate base tokens needed to receive a specific amount of quote tokens
|
||||
@@ -324,7 +402,8 @@ fn calculate_quote_amount_out(
|
||||
/// * `quote` - Desired amount of quote tokens to receive
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
@@ -335,6 +414,7 @@ pub fn sell_quote_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellQuoteInputResult, String> {
|
||||
@@ -343,10 +423,11 @@ pub fn sell_quote_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -356,6 +437,7 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<SellQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
@@ -364,6 +446,7 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
if quote > quote_reserve {
|
||||
return Err("Cannot receive more quote tokens than the pool quote reserves.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate raw quote amount including fees
|
||||
let raw_quote = calculate_quote_amount_out(
|
||||
@@ -371,19 +454,232 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
fee_basis_points.lp_fee_basis_points,
|
||||
fee_basis_points.protocol_fee_basis_points,
|
||||
fee_basis_points.coin_creator_fee_basis_points,
|
||||
);
|
||||
)?;
|
||||
|
||||
let lp_fee = checked_fee(raw_quote, fee_basis_points.lp_fee_basis_points, "LP fee")?;
|
||||
let quote_vault_outflow = raw_quote
|
||||
.checked_sub(lp_fee)
|
||||
.ok_or_else(|| "LP fee exceeds raw quote output.".to_string())?;
|
||||
if quote_vault_outflow > quote_reserve {
|
||||
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||
}
|
||||
|
||||
// Calculate base amount needed using inverse constant product formula
|
||||
if raw_quote >= quote_reserve {
|
||||
if raw_quote >= effective_quote_reserve {
|
||||
return Err("Invalid input: Desired quote amount exceeds available reserve.".to_string());
|
||||
}
|
||||
|
||||
let base_amount_in =
|
||||
ceil_div((base_reserve as u128) * (raw_quote as u128), (quote_reserve - raw_quote) as u128)
|
||||
as u64;
|
||||
let base_amount_in = checked_u64(
|
||||
ceil_div(
|
||||
(base_reserve as u128) * (raw_quote as u128),
|
||||
(effective_quote_reserve - raw_quote) as u128,
|
||||
),
|
||||
"base amount",
|
||||
)?;
|
||||
|
||||
// Calculate min quote with slippage
|
||||
let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points);
|
||||
|
||||
Ok(SellQuoteInputResult { internal_raw_quote: raw_quote, base: base_amount_in, min_quote })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fees() -> PumpSwapFeeBasisPoints {
|
||||
PumpSwapFeeBasisPoints::new(20, 5, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_uses_effective_quote_reserves() {
|
||||
let result =
|
||||
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 500_000, &fees())
|
||||
.unwrap();
|
||||
let without_virtual =
|
||||
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 0, &fees())
|
||||
.unwrap();
|
||||
|
||||
assert!(result.base < without_virtual.base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_rejects_output_not_covered_by_real_quote_vault() {
|
||||
let error = sell_base_input_internal_with_fees(
|
||||
1_000_000,
|
||||
100,
|
||||
1_000_000,
|
||||
1_000,
|
||||
1_000_000,
|
||||
&fees(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, "Insufficient real quote reserves to cover the sell output.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_quote_sell_uses_effective_reserve_for_denominator() {
|
||||
let result =
|
||||
sell_quote_input_internal_with_fees(500, 100, 1_000_000, 1_000, 1_000_000, &fees())
|
||||
.unwrap();
|
||||
|
||||
assert!(result.base < 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_quote_sell_rejects_output_above_real_quote_vault() {
|
||||
let error =
|
||||
sell_quote_input_internal_with_fees(1_001, 100, 1_000_000, 1_000, 1_000_000, &fees())
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, "Cannot receive more quote tokens than the pool quote reserves.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_virtual_reserves_are_applied() {
|
||||
let result = buy_quote_input_internal_with_fees(
|
||||
10_000,
|
||||
100,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
-500_000,
|
||||
&fees(),
|
||||
)
|
||||
.unwrap();
|
||||
let without_virtual =
|
||||
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 0, &fees())
|
||||
.unwrap();
|
||||
|
||||
assert!(result.base > without_virtual.base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_effective_quote_reserves_are_rejected() {
|
||||
let error = buy_quote_input_internal_with_fees(
|
||||
10_000,
|
||||
100,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
-1_000_000,
|
||||
&fees(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, "Invalid effective quote reserves: raw=1000000, virtual=-1000000.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_modes_match_official_integer_formulas() {
|
||||
let fees = PumpSwapFeeBasisPoints::new(20, 5, 30);
|
||||
let base_reserve = 800_000_000_000_000;
|
||||
let quote_reserve = 100_000_000_000;
|
||||
let virtual_quote_reserves = 5_000_000_000;
|
||||
let slippage_basis_points = 125;
|
||||
|
||||
let buy_base = buy_base_input_internal_with_fees(
|
||||
123_456_789_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(buy_base.internal_quote_amount, 16_206_205);
|
||||
assert_eq!(buy_base.ui_quote, 16_295_341);
|
||||
assert_eq!(buy_base.max_quote, 16_499_032);
|
||||
|
||||
let buy_quote = buy_quote_input_internal_with_fees(
|
||||
1_500_000_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(buy_quote.internal_quote_without_fees, 1_491_795_125);
|
||||
assert_eq!(buy_quote.base, 11_206_836_149_304);
|
||||
assert_eq!(buy_quote.max_quote, 1_518_750_000);
|
||||
|
||||
let sell_base = sell_base_input_internal_with_fees(
|
||||
123_456_789_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sell_base.internal_quote_amount_out, 16_201_203);
|
||||
assert_eq!(sell_base.ui_quote, 16_112_095);
|
||||
assert_eq!(sell_base.min_quote, 15_910_694);
|
||||
|
||||
let sell_quote = sell_quote_input_internal_with_fees(
|
||||
500_000_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sell_quote.internal_raw_quote, 502_765_209);
|
||||
assert_eq!(sell_quote.base, 3_849_022_110_532);
|
||||
assert_eq!(sell_quote.min_quote, 493_750_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_quote_results_return_errors_instead_of_truncating() {
|
||||
let no_fees = PumpSwapFeeBasisPoints::new(0, 0, 0);
|
||||
|
||||
let buy_error =
|
||||
buy_base_input_internal_with_fees(u64::MAX - 1, 0, u64::MAX, u64::MAX, 0, &no_fees)
|
||||
.unwrap_err();
|
||||
assert_eq!(buy_error, "Calculated raw quote amount exceeds u64.");
|
||||
|
||||
let sell_error =
|
||||
sell_quote_input_internal_with_fees(u64::MAX - 1, 0, u64::MAX, u64::MAX, 0, &no_fees)
|
||||
.unwrap_err();
|
||||
assert_eq!(sell_error, "Calculated base amount exceeds u64.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_fee_boundaries_return_errors() {
|
||||
let overflowing_fees = PumpSwapFeeBasisPoints::new(u64::MAX, 1, 0);
|
||||
let error = buy_quote_input_internal_with_fees(
|
||||
10_000,
|
||||
0,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
0,
|
||||
&overflowing_fees,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error, "Fee basis points overflow.");
|
||||
|
||||
let oversized_fee = PumpSwapFeeBasisPoints::new(u64::MAX, 0, 0);
|
||||
let error = sell_base_input_internal_with_fees(
|
||||
1_000_000,
|
||||
0,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
0,
|
||||
&oversized_fee,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error, "Calculated LP fee exceeds u64.");
|
||||
|
||||
let error = creator_side_fee_basis_points(&Pubkey::new_unique(), u64::MAX).unwrap_err();
|
||||
assert_eq!(error, "Coin creator fee basis points overflow.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_quote_rejects_amount_too_small_after_fees() {
|
||||
let error =
|
||||
buy_quote_input_internal_with_fees(1, 0, 1_000_000, 1_000_000, 0, &fees()).unwrap_err();
|
||||
|
||||
assert_eq!(error, "Quote input is too small after fees.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user