feat: integrate Raydium CPMM trading protocol support

- Add Raydium CPMM constants, instruction builder and pool implementation
- Implement unified buy/sell trading interface and event parsing system
- Update documentation and examples to support new protocol
This commit is contained in:
ysq
2025-07-16 22:54:23 +08:00
parent aef9fe6b14
commit 75a29cd7ee
20 changed files with 1119 additions and 50 deletions
+89 -14
View File
@@ -1,18 +1,19 @@
# Sol Trade SDK
A comprehensive Rust SDK for seamless interaction with Solana DEX trading programs. This SDK provides a robust set of tools and interfaces to integrate PumpFun, PumpSwap, and Bonk functionality into your applications.
A comprehensive Rust SDK for seamless interaction with Solana DEX trading programs. This SDK provides a robust set of tools and interfaces to integrate PumpFun, PumpSwap, Bonk, and Raydium CPMM functionality into your applications.
## Project Features
1. **PumpFun Trading**: Support for `buy` and `sell` operations
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
3. **Bonk Trading**: Support for Bonk trading operations
4. **Event Subscription**: Subscribe to PumpFun, PumpSwap, and Bonk program trading events
5. **Yellowstone gRPC**: Subscribe to program events using Yellowstone gRPC
6. **ShredStream Support**: Subscribe to program events using ShredStream
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, and other services
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
5. **Event Subscription**: Subscribe to PumpFun, PumpSwap, Bonk, and Raydium CPMM program trading events
6. **Yellowstone gRPC**: Subscribe to program events using Yellowstone gRPC
7. **ShredStream Support**: Subscribe to program events using ShredStream
8. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, and other services
9. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
10. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
## Installation
@@ -47,6 +48,7 @@ use sol_trade_sdk::{
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
},
raydium_cpmm::RaydiumCpmmSwapEvent,
},
Protocol, UnifiedEvent,
},
@@ -94,12 +96,15 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("Raydium CPMM Swap event: {:?}", e);
},
});
};
// Subscribe to events from multiple protocols
println!("Starting to listen for events, press Ctrl+C to stop...");
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk];
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm];
grpc.subscribe_events(protocols, None, None, None, callback)
.await?;
@@ -148,12 +153,15 @@ async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("Raydium CPMM Swap event: {:?}", e);
},
});
};
// Subscribe to events
println!("Starting to listen for events, press Ctrl+C to stop...");
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk];
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm];
shred_stream
.shredstream_subscribe(protocols, None, callback)
.await?;
@@ -364,7 +372,71 @@ async fn test_pumpswap() -> AnyResult<()> {
}
```
### 5. Bonk Trading Operations
### 5. Raydium CPMM Trading Operations
```rust
use sol_trade_sdk::{
trading::{
core::params::RaydiumCpmmParams,
factory::DexType,
raydium_cpmm::common::{get_buy_token_amount, get_sell_sol_amount}
},
};
async fn test_raydium_cpmm() -> Result<(), Box<dyn std::error::Error>> {
println!("Testing Raydium CPMM trading...");
let trade_client = test_create_solana_trade_client().await?;
let mint_pubkey = Pubkey::from_str("xxxxxxxx")?; // Token address
let buy_sol_cost = 100_000; // 0.0001 SOL (in lamports)
let slippage_basis_points = Some(100); // 1% slippage
let recent_blockhash = trade_client.rpc.get_latest_blockhash().await?;
let pool_state = Pubkey::from_str("xxxxxxx")?; // Pool state address
// Calculate expected token amount when buying
let buy_amount_out = get_buy_token_amount(&trade_client.rpc, &pool_state, buy_sol_cost).await?;
println!("Buying tokens from Raydium CPMM...");
trade_client.buy(
DexType::RaydiumCpmm,
mint_pubkey,
None,
buy_sol_cost,
slippage_basis_points,
recent_blockhash,
None,
Some(Box::new(RaydiumCpmmParams {
pool_state: Some(pool_state), // If not provided, will auto-calculate wsol-mint direction pool
minimum_amount_out: Some(buy_amount_out), // If not provided, defaults to 0
auto_handle_wsol: true, // Automatically handle wSOL wrapping/unwrapping
})),
).await?;
println!("Selling tokens from Raydium CPMM...");
let amount_token = 100_000_000; // Token amount to sell
let sell_sol_amount = get_sell_sol_amount(&trade_client.rpc, &pool_state, amount_token).await?;
trade_client.sell(
DexType::RaydiumCpmm,
mint_pubkey,
None,
amount_token,
slippage_basis_points,
recent_blockhash,
None,
Some(Box::new(RaydiumCpmmParams {
pool_state: Some(pool_state), // If not provided, will auto-calculate wsol-mint direction pool
minimum_amount_out: Some(sell_sol_amount), // If not provided, defaults to 0
auto_handle_wsol: true, // Automatically handle wSOL wrapping/unwrapping
})),
).await?;
Ok(())
}
```
### 6. Bonk Trading Operations
```rust
@@ -496,7 +568,7 @@ async fn test_bonk() -> Result<(), Box<dyn std::error::Error>> {
}
```
### 6. Custom Priority Fee Configuration
### 7. Custom Priority Fee Configuration
```rust
use sol_trade_sdk::common::PriorityFee;
@@ -527,6 +599,7 @@ let trade_config = TradeConfig {
- **PumpFun**: Primary meme coin trading platform
- **PumpSwap**: PumpFun's swap protocol
- **Bonk**: Token launch platform (letsbonk.fun)
- **Raydium CPMM**: Raydium's Concentrated Pool Market Maker protocol
## MEV Protection Services
@@ -540,9 +613,9 @@ let trade_config = TradeConfig {
### Unified Trading Interface
- **TradingProtocol Enum**: Use unified protocol enums (PumpFun, PumpSwap, Bonk)
- **TradingProtocol Enum**: Use unified protocol enums (PumpFun, PumpSwap, Bonk, RaydiumCpmm)
- **Unified buy/sell Methods**: All protocols use the same trading method signatures
- **Protocol-specific Parameters**: Each protocol has its own parameter structure (PumpFunParams, etc.)
- **Protocol-specific Parameters**: Each protocol has its own parameter structure (PumpFunParams, RaydiumCpmmParams, etc.)
### Event Parsing System
@@ -570,7 +643,8 @@ src/
│ │ ├── protocols/# Protocol-specific parsers
│ │ │ ├── bonk/ # Bonk event parsing
│ │ │ ├── pumpfun/ # PumpFun event parsing
│ │ │ ── pumpswap/ # PumpSwap event parsing
│ │ │ ── pumpswap/ # PumpSwap event parsing
│ │ │ └── raydium_cpmm/ # Raydium CPMM event parsing
│ │ └── factory.rs # Parser factory
│ ├── shred_stream.rs # ShredStream client
│ └── yellowstone_grpc.rs # Yellowstone gRPC client
@@ -581,6 +655,7 @@ src/
│ ├── bonk/ # Bonk trading implementation
│ ├── pumpfun/ # PumpFun trading implementation
│ ├── pumpswap/ # PumpSwap trading implementation
│ ├── raydium_cpmm/ # Raydium CPMM trading implementation
│ └── factory.rs # Trading factory
├── lib.rs # Main library file
└── main.rs # Example program
+88 -13
View File
@@ -7,12 +7,13 @@
1. **PumpFun 交易**: 支持`购买``卖出`功能
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
3. **Bonk 交易**: 支持 Bonk 的交易操作
4. **事件订阅**: 订阅 PumpFun、PumpSwap 和 Bonk 程序的交易事件
5. **Yellowstone gRPC**: 使用 Yellowstone gRPC 订阅程序事件
6. **ShredStream 支持**: 使用 ShredStream 订阅程序事件
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute 等服务
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
5. **事件订阅**: 订阅 PumpFun、PumpSwap、Bonk 和 Raydium CPMM 程序的交易事件
6. **Yellowstone gRPC**: 使用 Yellowstone gRPC 订阅程序事件
7. **ShredStream 支持**: 使用 ShredStream 订阅程序事件
8. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute 等服务
9. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
10. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
## 安装
@@ -47,6 +48,7 @@ use sol_trade_sdk::{
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
},
raydium_cpmm::RaydiumCpmmSwapEvent,
},
Protocol, UnifiedEvent,
},
@@ -94,12 +96,15 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("Raydium CPMM Swap event: {:?}", e);
},
});
};
// 订阅多个协议的事件
println!("开始监听事件,按 Ctrl+C 停止...");
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk];
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm];
grpc.subscribe_events(protocols, None, None, None, callback)
.await?;
@@ -148,12 +153,15 @@ async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("Raydium CPMM Swap event: {:?}", e);
},
});
};
// 订阅事件
println!("开始监听事件,按 Ctrl+C 停止...");
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk];
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm];
shred_stream
.shredstream_subscribe(protocols, None, callback)
.await?;
@@ -362,7 +370,71 @@ async fn test_pumpswap() -> AnyResult<()> {
}
```
### 5. Bonk 交易操作
### 5. Raydium CPMM 交易操作
```rust
use sol_trade_sdk::{
trading::{
core::params::RaydiumCpmmParams,
factory::DexType,
raydium_cpmm::common::{get_buy_token_amount, get_sell_sol_amount}
},
};
async fn test_raydium_cpmm() -> Result<(), Box<dyn std::error::Error>> {
println!("Testing Raydium CPMM trading...");
let trade_client = test_create_solana_trade_client().await?;
let mint_pubkey = Pubkey::from_str("xxxxxxxx")?; // 代币地址
let buy_sol_cost = 100_000; // 0.0001 SOL(以lamports为单位)
let slippage_basis_points = Some(100); // 1% 滑点
let recent_blockhash = trade_client.rpc.get_latest_blockhash().await?;
let pool_state = Pubkey::from_str("xxxxxxx")?; // 池状态地址
// 计算买入时预期获得的代币数量
let buy_amount_out = get_buy_token_amount(&trade_client.rpc, &pool_state, buy_sol_cost).await?;
println!("Buying tokens from Raydium CPMM...");
trade_client.buy(
DexType::RaydiumCpmm,
mint_pubkey,
None,
buy_sol_cost,
slippage_basis_points,
recent_blockhash,
None,
Some(Box::new(RaydiumCpmmParams {
pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子
minimum_amount_out: Some(buy_amount_out), // 如果不传,默认为0
auto_handle_wsol: true, // 自动处理 wSOL 包装/解包装
})),
).await?;
println!("Selling tokens from Raydium CPMM...");
let amount_token = 100_000_000; // 卖出代币数量
let sell_sol_amount = get_sell_sol_amount(&trade_client.rpc, &pool_state, amount_token).await?;
trade_client.sell(
DexType::RaydiumCpmm,
mint_pubkey,
None,
amount_token,
slippage_basis_points,
recent_blockhash,
None,
Some(Box::new(RaydiumCpmmParams {
pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子
minimum_amount_out: Some(sell_sol_amount), // 如果不传,默认为0
auto_handle_wsol: true, // 自动处理 wSOL 包装/解包装
})),
).await?;
Ok(())
}
```
### 6. Bonk 交易操作
```rust
@@ -494,7 +566,7 @@ async fn test_bonk() -> Result<(), Box<dyn std::error::Error>> {
}
```
### 6. 自定义优先费用配置
### 7. 自定义优先费用配置
```rust
use sol_trade_sdk::common::PriorityFee;
@@ -525,6 +597,7 @@ let trade_config = TradeConfig {
- **PumpFun**: 主要的 meme 币交易平台
- **PumpSwap**: PumpFun 的交换协议
- **Bonk**: 代币发行平台(letsbonk.fun
- **Raydium CPMM**: Raydium 的集中流动性做市商协议
## MEV 保护服务
@@ -538,9 +611,9 @@ let trade_config = TradeConfig {
### 统一交易接口
- **TradingProtocol 枚举**: 使用统一的协议枚举(PumpFun、PumpSwap、Bonk
- **TradingProtocol 枚举**: 使用统一的协议枚举(PumpFun、PumpSwap、Bonk、RaydiumCpmm
- **统一的 buy/sell 方法**: 所有协议都使用相同的交易方法签名
- **协议特定参数**: 每个协议都有自己的参数结构(PumpFunParams 等)
- **协议特定参数**: 每个协议都有自己的参数结构(PumpFunParams、RaydiumCpmmParams 等)
### 事件解析系统
@@ -568,7 +641,8 @@ src/
│ │ ├── protocols/# 协议特定解析器
│ │ │ ├── bonk/ # Bonk事件解析
│ │ │ ├── pumpfun/ # PumpFun事件解析
│ │ │ ── pumpswap/ # PumpSwap事件解析
│ │ │ ── pumpswap/ # PumpSwap事件解析
│ │ │ └── raydium_cpmm/ # Raydium CPMM事件解析
│ │ └── factory.rs # 解析器工厂
│ ├── shred_stream.rs # ShredStream客户端
│ └── yellowstone_grpc.rs # Yellowstone gRPC客户端
@@ -579,6 +653,7 @@ src/
│ ├── bonk/ # Bonk交易实现
│ ├── pumpfun/ # PumpFun交易实现
│ ├── pumpswap/ # PumpSwap交易实现
│ ├── raydium_cpmm/ # Raydium CPMM交易实现
│ └── factory.rs # 交易工厂
├── lib.rs # 主库文件
└── main.rs # 示例程序
+2
View File
@@ -3,9 +3,11 @@ pub mod pumpfun;
pub mod pumpswap;
pub mod swqos;
pub mod trade;
pub mod raydium_cpmm;
pub mod trade_platform {
pub const PUMPFUN: &'static str = "pumpfun";
pub const PUMPFUN_SWAP: &'static str = "pumpswap";
pub const BONK: &'static str = "bonk";
pub const RAYDIUM_CPMM: &'static str = "raydium_cpmm";
}
+31
View File
@@ -0,0 +1,31 @@
//! Constants used by the crate.
//!
//! This module contains various constants used throughout the crate, including:
//!
//! - Seeds for deriving Program Derived Addresses (PDAs)
//! - Program account addresses and public keys
//!
//! The constants are organized into submodules for better organization:
//!
//! - `seeds`: Contains seed values used for PDA derivation
//! - `accounts`: Contains important program account addresses
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
pub mod seeds {
pub const POOL_SEED: &[u8] = b"pool";
pub const POOL_VAULT_SEED: &[u8] = b"pool_vault";
pub const OBSERVATION_STATE_SEED: &[u8] = b"observation";
}
/// Constants related to program accounts and authorities
pub mod accounts {
use solana_sdk::{pubkey, pubkey::Pubkey};
pub const AUTHORITY: Pubkey = pubkey!("GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL");
pub const AMM_CONFIG: Pubkey = pubkey!("D4FPEruKEHrG5TenZ2mpDGEfu1iUvTiqBxvpU8HLBvC2");
pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
pub const WSOL_TOKEN_ACCOUNT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
pub const RAYDIUM_CPMM: Pubkey = pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C");
}
pub const SWAP_BASE_IN_DISCRIMINATOR: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222];
pub const SWAP_BASE_OUT_DISCRIMINATOR: &[u8] = &[55, 217, 98, 86, 163, 74, 180, 173];
+1
View File
@@ -27,6 +27,7 @@ use solana_sdk::{
pub mod pumpfun;
pub mod pumpswap;
pub mod bonk;
pub mod raydium_cpmm;
pub struct Create {
pub _name: String,
+301
View File
@@ -0,0 +1,301 @@
use anyhow::{anyhow, Result};
use solana_sdk::{instruction::Instruction, signer::Signer};
use spl_associated_token_account::instruction::create_associated_token_account_idempotent;
use spl_token::instruction::close_account;
use crate::{
constants::raydium_cpmm::{accounts, SWAP_BASE_IN_DISCRIMINATOR},
constants::trade::trade::DEFAULT_SLIPPAGE,
trading::common::utils::get_token_balance,
trading::core::{
params::{BuyParams, RaydiumCpmmParams, SellParams},
traits::InstructionBuilder,
},
trading::raydium_cpmm::{
common::{get_observation_state_pda, get_pool_pda, get_vault_pda},
// pool::Pool,
},
};
/// RaydiumCpmm协议的指令构建器
pub struct RaydiumCpmmInstructionBuilder;
#[async_trait::async_trait]
impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
async fn build_buy_instructions(&self, params: &BuyParams) -> Result<Vec<Instruction>> {
if params.sol_amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
self.build_buy_instructions_with_accounts(params).await
}
async fn build_sell_instructions(&self, params: &SellParams) -> Result<Vec<Instruction>> {
self.build_sell_instructions_with_accounts(params).await
}
}
impl RaydiumCpmmInstructionBuilder {
/// 使用提供的账户信息构建买入指令
async fn build_buy_instructions_with_accounts(
&self,
params: &BuyParams,
) -> Result<Vec<Instruction>> {
let protocol_params = params
.protocol_params
.as_any()
.downcast_ref::<RaydiumCpmmParams>()
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?;
let pool_state = if protocol_params.pool_state.is_some() {
protocol_params.pool_state.unwrap()
} else {
get_pool_pda(
&accounts::AMM_CONFIG,
&accounts::WSOL_TOKEN_ACCOUNT,
&params.mint,
)
.unwrap()
};
let wsol_token_account = spl_associated_token_account::get_associated_token_address(
&params.payer.pubkey(),
&accounts::WSOL_TOKEN_ACCOUNT,
);
let mint_token_account = spl_associated_token_account::get_associated_token_address(
&params.payer.pubkey(),
&params.mint,
);
// 获取池的代币账户
let wsol_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap();
let mint_vault_account = get_vault_pda(&pool_state, &params.mint).unwrap();
let observation_state_account = get_observation_state_pda(&pool_state).unwrap();
let amount_in: u64 = params.sol_amount;
let mut minimum_amount_out: u64 = if protocol_params.minimum_amount_out.is_some() {
protocol_params.minimum_amount_out.unwrap()
} else {
println!("未提供minimum_amount_out,使用默认值0");
0
};
if minimum_amount_out != 0 {
let slippage_basis_points: u64 = if params.slippage_basis_points.is_some() {
params.slippage_basis_points.unwrap()
} else {
DEFAULT_SLIPPAGE
} as u64;
minimum_amount_out = minimum_amount_out * (10000 - slippage_basis_points) / 10000;
println!("slippage_basis_points: {}", slippage_basis_points);
}
println!("minimum_amount_out: {}", minimum_amount_out);
let mut instructions = vec![];
if protocol_params.auto_handle_wsol {
// 插入wsol
instructions.push(
// 创建wSOL ATA账户,如果不存在
create_associated_token_account_idempotent(
&params.payer.pubkey(),
&params.payer.pubkey(),
&accounts::WSOL_TOKEN_ACCOUNT,
&accounts::TOKEN_PROGRAM,
),
);
instructions.push(
// 将SOL转入wSOL ATA账户
solana_sdk::system_instruction::transfer(
&params.payer.pubkey(),
&wsol_token_account,
amount_in,
),
);
// 同步wSOL余额
instructions.push(
spl_token::instruction::sync_native(&accounts::TOKEN_PROGRAM, &wsol_token_account)
.unwrap(),
);
}
// 创建用户的基础代币账户
instructions.push(create_associated_token_account_idempotent(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.mint,
&accounts::TOKEN_PROGRAM,
));
// 创建买入指令
let accounts = vec![
solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_CONFIG, false), // Amm Config (readonly)
solana_sdk::instruction::AccountMeta::new(pool_state, false), // Pool State
solana_sdk::instruction::AccountMeta::new(wsol_token_account, false), // Input Token Account
solana_sdk::instruction::AccountMeta::new(mint_token_account, false), // Output Token Account
solana_sdk::instruction::AccountMeta::new(wsol_vault_account, false), // Input Vault Account
solana_sdk::instruction::AccountMeta::new(mint_vault_account, false), // Output Vault Account
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Input Token Program (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Output Token Program (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Input token mint (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Output token mint (readonly)
solana_sdk::instruction::AccountMeta::new(observation_state_account, false), // Observation State Account
];
// 创建指令数据
let mut data = vec![];
data.extend_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data.extend_from_slice(&amount_in.to_le_bytes());
data.extend_from_slice(&minimum_amount_out.to_le_bytes());
instructions.push(Instruction {
program_id: accounts::RAYDIUM_CPMM,
accounts,
data,
});
if protocol_params.auto_handle_wsol {
// 关闭wSOL ATA账户,回收租金
instructions.push(
spl_token::instruction::close_account(
&accounts::TOKEN_PROGRAM,
&wsol_token_account,
&params.payer.pubkey(),
&params.payer.pubkey(),
&[],
)
.unwrap(),
);
}
Ok(instructions)
}
/// 使用提供的账户信息构建卖出指令
async fn build_sell_instructions_with_accounts(
&self,
params: &SellParams,
) -> Result<Vec<Instruction>> {
let protocol_params = params
.protocol_params
.as_any()
.downcast_ref::<RaydiumCpmmParams>()
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?;
if params.rpc.is_none() {
return Err(anyhow!("RPC is not set"));
}
let rpc = params.rpc.as_ref().unwrap().clone();
// 获取代币余额
let mut amount = params.token_amount;
if params.token_amount.is_none() || params.token_amount.unwrap_or(0) == 0 {
let balance_u64 =
get_token_balance(rpc.as_ref(), &params.payer.pubkey(), &params.mint).await?;
amount = Some(balance_u64);
}
let amount = amount.unwrap_or(0);
if amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let mut minimum_amount_out: u64 = if protocol_params.minimum_amount_out.is_some() {
protocol_params.minimum_amount_out.unwrap()
} else {
println!("未提供minimum_amount_out,使用默认值0");
0
};
if minimum_amount_out != 0 {
let slippage_basis_points: u64 = if params.slippage_basis_points.is_some() {
params.slippage_basis_points.unwrap()
} else {
DEFAULT_SLIPPAGE
} as u64;
minimum_amount_out = minimum_amount_out * (10000 - slippage_basis_points) / 10000;
println!("slippage_basis_points: {}", slippage_basis_points);
}
println!("minimum_amount_out: {}", minimum_amount_out);
let pool_state = if protocol_params.pool_state.is_some() {
protocol_params.pool_state.unwrap()
} else {
get_pool_pda(
&accounts::AMM_CONFIG,
&accounts::WSOL_TOKEN_ACCOUNT,
&params.mint,
)
.unwrap()
};
let wsol_token_account = spl_associated_token_account::get_associated_token_address(
&params.payer.pubkey(),
&accounts::WSOL_TOKEN_ACCOUNT,
);
let mint_token_account = spl_associated_token_account::get_associated_token_address(
&params.payer.pubkey(),
&params.mint,
);
// 获取池的代币账户
let wsol_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap();
let mint_vault_account = get_vault_pda(&pool_state, &params.mint).unwrap();
let observation_state_account = get_observation_state_pda(&pool_state).unwrap();
let mut instructions = vec![];
// 插入wsol
instructions.push(
// 创建wSOL ATA账户,如果不存在
create_associated_token_account_idempotent(
&params.payer.pubkey(),
&params.payer.pubkey(),
&accounts::WSOL_TOKEN_ACCOUNT,
&accounts::TOKEN_PROGRAM,
),
);
// 创建卖出指令
let accounts = vec![
solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_CONFIG, false), // Amm Config (readonly)
solana_sdk::instruction::AccountMeta::new(pool_state, false), // Pool State
solana_sdk::instruction::AccountMeta::new(mint_token_account, false), // Input Token Account
solana_sdk::instruction::AccountMeta::new(wsol_token_account, false), // Output Token Account
solana_sdk::instruction::AccountMeta::new(mint_vault_account, false), // Input Vault Account
solana_sdk::instruction::AccountMeta::new(wsol_vault_account, false), // Output Vault Account
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Input Token Program (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Output Token Program (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Input token mint (readonly)
solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Output token mint (readonly)
solana_sdk::instruction::AccountMeta::new(observation_state_account, false), // Observation State Account
];
// 创建指令数据
let mut data = vec![];
data.extend_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
data.extend_from_slice(&amount.to_le_bytes());
data.extend_from_slice(&minimum_amount_out.to_le_bytes());
instructions.push(Instruction {
program_id: accounts::RAYDIUM_CPMM,
accounts,
data,
});
instructions.push(
close_account(
&accounts::TOKEN_PROGRAM,
&wsol_token_account,
&params.payer.pubkey(),
&params.payer.pubkey(),
&[&params.payer.pubkey()],
)
.unwrap(),
);
Ok(instructions)
}
}
+19 -12
View File
@@ -11,6 +11,7 @@ use crate::swqos::SwqosConfig;
use crate::trading::core::params::BonkParams;
use crate::trading::core::params::PumpFunParams;
use crate::trading::core::params::PumpSwapParams;
use crate::trading::core::params::RaydiumCpmmParams;
use crate::trading::core::traits::ProtocolParams;
use crate::trading::factory::DexType;
use crate::trading::BuyParams;
@@ -179,13 +180,12 @@ impl SolanaTrade {
params
} else {
match dex_type {
DexType::PumpFun => {
Box::new(PumpFunParams::default()) as Box<dyn ProtocolParams>
}
DexType::PumpSwap => {
Box::new(PumpSwapParams::default()) as Box<dyn ProtocolParams>
}
DexType::PumpFun => Box::new(PumpFunParams::default()) as Box<dyn ProtocolParams>,
DexType::PumpSwap => Box::new(PumpSwapParams::default()) as Box<dyn ProtocolParams>,
DexType::Bonk => Box::new(BonkParams::default()) as Box<dyn ProtocolParams>,
DexType::RaydiumCpmm => {
Box::new(RaydiumCpmmParams::default()) as Box<dyn ProtocolParams>
}
}
};
let buy_params = BuyParams {
@@ -227,6 +227,10 @@ impl SolanaTrade {
.as_any()
.downcast_ref::<BonkParams>()
.is_some(),
DexType::RaydiumCpmm => protocol_params
.as_any()
.downcast_ref::<RaydiumCpmmParams>()
.is_some(),
};
if !is_valid_params {
@@ -301,13 +305,12 @@ impl SolanaTrade {
params
} else {
match dex_type {
DexType::PumpFun => {
Box::new(PumpFunParams::default()) as Box<dyn ProtocolParams>
}
DexType::PumpSwap => {
Box::new(PumpSwapParams::default()) as Box<dyn ProtocolParams>
}
DexType::PumpFun => Box::new(PumpFunParams::default()) as Box<dyn ProtocolParams>,
DexType::PumpSwap => Box::new(PumpSwapParams::default()) as Box<dyn ProtocolParams>,
DexType::Bonk => Box::new(BonkParams::default()) as Box<dyn ProtocolParams>,
DexType::RaydiumCpmm => {
Box::new(RaydiumCpmmParams::default()) as Box<dyn ProtocolParams>
}
}
};
let sell_params = SellParams {
@@ -348,6 +351,10 @@ impl SolanaTrade {
.as_any()
.downcast_ref::<BonkParams>()
.is_some(),
DexType::RaydiumCpmm => protocol_params
.as_any()
.downcast_ref::<RaydiumCpmmParams>()
.is_some(),
};
if !is_valid_params {
+57 -3
View File
@@ -11,14 +11,14 @@ use sol_trade_sdk::{
pumpswap::{
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
},
}, raydium_cpmm::RaydiumCpmmSwapEvent,
},
Protocol, UnifiedEvent,
},
ShredStreamGrpc, YellowstoneGrpc,
},
swqos::{SwqosConfig, SwqosRegion},
trading::{core::params::{BonkParams, PumpFunParams}, factory::DexType},
trading::{core::params::{BonkParams, PumpFunParams, RaydiumCpmmParams}, factory::DexType, raydium_cpmm::{common::{get_buy_token_amount, get_sell_sol_amount}}},
SolanaTrade,
};
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair};
@@ -28,6 +28,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
test_create_solana_trade_client().await?;
test_pumpswap().await?;
test_bonk().await?;
test_raydium_cpmm().await?;
test_grpc().await?;
test_shreds().await?;
Ok(())
@@ -330,6 +331,56 @@ async fn test_bonk() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
async fn test_raydium_cpmm() -> Result<(), Box<dyn std::error::Error>> {
println!("Testing Raydium Cpmm trading...");
let client = test_create_solana_trade_client().await?;
let mint_pubkey = Pubkey::from_str("xxxxxxxx")?;
let buy_sol_cost = 100_000;
let slippage_basis_points = Some(100);
let recent_blockhash = client.rpc.get_latest_blockhash().await?;
let pool_state = Pubkey::from_str("xxxxxxx")?;
let buy_amount_out = get_buy_token_amount(&client.rpc, &pool_state, buy_sol_cost).await?;
// Buy tokens
println!("Buying tokens from Raydium Cpmm...");
client.buy(
DexType::RaydiumCpmm,
mint_pubkey,
None,
buy_sol_cost,
slippage_basis_points,
recent_blockhash,
None,
Some(Box::new(RaydiumCpmmParams {
pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子
minimum_amount_out: Some(buy_amount_out), // 如果不传、默认为0
auto_handle_wsol: true,
})),
).await?;
// Sell tokens
println!("Selling tokens from Raydium Cpmm...");
let amount_token = 0;
let sell_sol_amount = get_sell_sol_amount(&client.rpc, &pool_state, amount_token).await?;
client.sell(
DexType::RaydiumCpmm,
mint_pubkey,
None,
amount_token,
slippage_basis_points,
recent_blockhash,
None,
Some(Box::new(RaydiumCpmmParams {
pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子
minimum_amount_out: Some(sell_sol_amount), // 如果不传、默认为0
auto_handle_wsol: true,
})),
).await?;
Ok(())
}
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
println!("正在订阅 GRPC 事件...");
@@ -339,7 +390,7 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
)?;
let callback = create_event_callback();
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk];
let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm];
println!("开始监听事件,按 Ctrl+C 停止...");
grpc.subscribe_events(protocols, None, None, None, callback).await?;
@@ -390,6 +441,9 @@ fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("RaydiumCpmmSwapEvent: {:?}", e);
},
});
}
}
@@ -12,6 +12,7 @@ pub enum ProtocolType {
PumpSwap,
PumpFun,
Bonk,
RaydiumCpmm,
}
/// 事件类型枚举
@@ -39,6 +40,10 @@ pub enum EventType {
BonkSellExactOut,
BonkInitialize,
// Raydium CPMM 事件
RaydiumCpmmSwapBaseInput,
RaydiumCpmmSwapBaseOutput,
// 通用事件
Unknown,
}
+8 -1
View File
@@ -3,7 +3,9 @@ use solana_sdk::pubkey::Pubkey;
use std::sync::Arc;
use crate::streaming::event_parser::protocols::{
pumpfun::parser::PUMPFUN_PROGRAM_ID, pumpswap::parser::PUMPSWAP_PROGRAM_ID, bonk::parser::BONK_PROGRAM_ID, BonkEventParser,
bonk::parser::BONK_PROGRAM_ID, pumpfun::parser::PUMPFUN_PROGRAM_ID,
pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID,
BonkEventParser, RaydiumCpmmEventParser,
};
use super::{
@@ -17,6 +19,7 @@ pub enum Protocol {
PumpSwap,
PumpFun,
Bonk,
RaydiumCpmm,
}
impl Protocol {
@@ -25,6 +28,7 @@ impl Protocol {
Protocol::PumpSwap => vec![PUMPSWAP_PROGRAM_ID],
Protocol::PumpFun => vec![PUMPFUN_PROGRAM_ID],
Protocol::Bonk => vec![BONK_PROGRAM_ID],
Protocol::RaydiumCpmm => vec![RAYDIUM_CPMM_PROGRAM_ID],
}
}
}
@@ -35,6 +39,7 @@ impl std::fmt::Display for Protocol {
Protocol::PumpSwap => write!(f, "PumpSwap"),
Protocol::PumpFun => write!(f, "PumpFun"),
Protocol::Bonk => write!(f, "Bonk"),
Protocol::RaydiumCpmm => write!(f, "RaydiumCpmm"),
}
}
}
@@ -47,6 +52,7 @@ impl std::str::FromStr for Protocol {
"pumpswap" => Ok(Protocol::PumpSwap),
"pumpfun" => Ok(Protocol::PumpFun),
"bonk" => Ok(Protocol::Bonk),
"raydiumcpmm" => Ok(Protocol::RaydiumCpmm),
_ => Err(anyhow!("Unsupported protocol: {}", s)),
}
}
@@ -62,6 +68,7 @@ impl EventParserFactory {
Protocol::PumpSwap => Arc::new(PumpSwapEventParser::new()),
Protocol::PumpFun => Arc::new(PumpFunEventParser::new()),
Protocol::Bonk => Arc::new(BonkEventParser::new()),
Protocol::RaydiumCpmm => Arc::new(RaydiumCpmmEventParser::new()),
}
}
+3 -1
View File
@@ -1,7 +1,9 @@
pub mod pumpfun;
pub mod pumpswap;
pub mod bonk;
pub mod raydium_cpmm;
pub use pumpfun::PumpFunEventParser;
pub use pumpswap::PumpSwapEventParser;
pub use bonk::BonkEventParser;
pub use bonk::BonkEventParser;
pub use raydium_cpmm::RaydiumCpmmEventParser;
@@ -0,0 +1,35 @@
use crate::impl_unified_event;
use crate::streaming::event_parser::common::EventMetadata;
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
/// 交易
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct RaydiumCpmmSwapEvent {
pub metadata: EventMetadata,
pub amount_in: u64,
pub minimum_amount_out: u64,
pub max_amount_in: u64,
pub amount_out: u64,
pub payer: Pubkey,
pub authority: Pubkey,
pub amm_config: Pubkey,
pub pool_state: Pubkey,
pub input_token_account: Pubkey,
pub output_token_account: Pubkey,
pub input_vault: Pubkey,
pub output_vault: Pubkey,
pub input_token_mint: Pubkey,
pub output_token_mint: Pubkey,
pub observation_state: Pubkey,
}
impl_unified_event!(RaydiumCpmmSwapEvent,);
/// 事件鉴别器常量
pub mod discriminators {
// 指令鉴别器
pub const SWAP_BASE_IN: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222];
pub const SWAP_BASE_OUT: &[u8] = &[55, 217, 98, 86, 163, 74, 180, 173];
}
+5
View File
@@ -0,0 +1,5 @@
pub mod events;
pub mod parser;
pub use events::*;
pub use parser::RaydiumCpmmEventParser;
+159
View File
@@ -0,0 +1,159 @@
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_transaction_status::UiCompiledInstruction;
use crate::streaming::event_parser::{
common::{read_u64_le, EventMetadata, EventType, ProtocolType},
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
protocols::raydium_cpmm::{discriminators, RaydiumCpmmSwapEvent},
};
/// Raydium CPMM程序ID
pub const RAYDIUM_CPMM_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C");
/// Raydium CPMM事件解析器
pub struct RaydiumCpmmEventParser {
inner: GenericEventParser,
}
impl RaydiumCpmmEventParser {
pub fn new() -> Self {
// 配置所有事件类型
let configs = vec![
GenericEventParseConfig {
inner_instruction_discriminator: "",
instruction_discriminator: discriminators::SWAP_BASE_IN,
event_type: EventType::RaydiumCpmmSwapBaseInput,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_swap_base_input_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: "",
instruction_discriminator: discriminators::SWAP_BASE_OUT,
event_type: EventType::RaydiumCpmmSwapBaseOutput,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_swap_base_output_instruction,
},
];
let inner =
GenericEventParser::new(RAYDIUM_CPMM_PROGRAM_ID, ProtocolType::RaydiumCpmm, configs);
Self { inner }
}
/// 解析交易事件
fn parse_trade_inner_instruction(
_data: &[u8],
_metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
None
}
/// 解析买入指令事件
fn parse_swap_base_input_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[3], accounts[10], accounts[11]
));
Some(Box::new(RaydiumCpmmSwapEvent {
metadata,
amount_in,
minimum_amount_out,
payer: accounts[0],
authority: accounts[1],
amm_config: accounts[2],
pool_state: accounts[3],
input_token_account: accounts[4],
output_token_account: accounts[5],
input_vault: accounts[6],
output_vault: accounts[7],
input_token_mint: accounts[10],
output_token_mint: accounts[11],
observation_state: accounts[12],
..Default::default()
}))
}
fn parse_swap_base_output_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let max_amount_in = read_u64_le(data, 0)?;
let amount_out = read_u64_le(data, 8)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[3], accounts[10], accounts[11]
));
Some(Box::new(RaydiumCpmmSwapEvent {
metadata,
max_amount_in,
amount_out,
payer: accounts[0],
authority: accounts[1],
amm_config: accounts[2],
pool_state: accounts[3],
input_token_account: accounts[4],
output_token_account: accounts[5],
input_vault: accounts[6],
output_vault: accounts[7],
input_token_mint: accounts[10],
output_token_mint: accounts[11],
observation_state: accounts[12],
..Default::default()
}))
}
}
#[async_trait::async_trait]
impl EventParser for RaydiumCpmmEventParser {
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
}
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_instruction(instruction, accounts, signature, slot)
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
self.inner.should_handle(program_id)
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
self.inner.supported_program_ids()
}
}
+28
View File
@@ -215,6 +215,34 @@ impl ProtocolParams for BonkParams {
}
}
/// RaydiumCpmm协议特定参数
#[derive(Clone)]
pub struct RaydiumCpmmParams {
pub pool_state: Option<Pubkey>,
pub minimum_amount_out: Option<u64>,
pub auto_handle_wsol: bool,
}
impl RaydiumCpmmParams {
pub fn default() -> Self {
Self {
pool_state: None,
minimum_amount_out: None,
auto_handle_wsol: true,
}
}
}
impl ProtocolParams for RaydiumCpmmParams {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn clone_box(&self) -> Box<dyn ProtocolParams> {
Box::new(self.clone())
}
}
impl BuyParams {
/// 转换为BuyWithTipParams
pub fn with_tip(self, swqos_clients: Vec<Arc<SwqosClient>>) -> BuyWithTipParams {
+14 -6
View File
@@ -1,18 +1,20 @@
use anyhow::{anyhow, Result};
use std::sync::Arc;
use crate::instruction::{bonk::BonkInstructionBuilder, pumpfun::PumpFunInstructionBuilder, pumpswap::PumpSwapInstructionBuilder};
use super::{
core::{executor::GenericTradeExecutor, traits::TradeExecutor},
use crate::instruction::{
bonk::BonkInstructionBuilder, pumpfun::PumpFunInstructionBuilder,
pumpswap::PumpSwapInstructionBuilder, raydium_cpmm::RaydiumCpmmInstructionBuilder,
};
use super::core::{executor::GenericTradeExecutor, traits::TradeExecutor};
/// 支持的交易协议
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DexType {
PumpFun,
PumpSwap,
Bonk,
RaydiumCpmm,
}
impl std::fmt::Display for DexType {
@@ -21,6 +23,7 @@ impl std::fmt::Display for DexType {
DexType::PumpFun => write!(f, "PumpFun"),
DexType::PumpSwap => write!(f, "PumpSwap"),
DexType::Bonk => write!(f, "Bonk"),
DexType::RaydiumCpmm => write!(f, "RaydiumCpmm"),
}
}
}
@@ -33,6 +36,7 @@ impl std::str::FromStr for DexType {
"pumpfun" => Ok(DexType::PumpFun),
"pumpswap" => Ok(DexType::PumpSwap),
"bonk" => Ok(DexType::Bonk),
"raydiumcpmm" => Ok(DexType::RaydiumCpmm),
_ => Err(anyhow!("Unsupported protocol: {}", s)),
}
}
@@ -55,9 +59,13 @@ impl TradeFactory {
}
DexType::Bonk => {
let instruction_builder = Arc::new(BonkInstructionBuilder);
Arc::new(GenericTradeExecutor::new(instruction_builder, "Bonk"))
}
DexType::RaydiumCpmm => {
let instruction_builder = Arc::new(RaydiumCpmmInstructionBuilder);
Arc::new(GenericTradeExecutor::new(
instruction_builder,
"Bonk",
"RaydiumCpmm",
))
}
}
@@ -65,7 +73,7 @@ impl TradeFactory {
/// 获取所有支持的协议
pub fn supported_dex_types() -> Vec<DexType> {
vec![DexType::PumpFun, DexType::PumpSwap, DexType::Bonk]
vec![DexType::PumpFun, DexType::PumpSwap, DexType::Bonk, DexType::RaydiumCpmm]
}
/// 检查协议是否支持
+1
View File
@@ -4,6 +4,7 @@ pub mod factory;
pub mod bonk;
pub mod pumpfun;
pub mod pumpswap;
pub mod raydium_cpmm;
pub use core::params::{BuyParams, BuyWithTipParams, SellParams, SellWithTipParams};
pub use core::traits::{InstructionBuilder, TradeExecutor};
+220
View File
@@ -0,0 +1,220 @@
use crate::{
common::SolanaRpcClient,
constants::{self, raydium_cpmm::accounts::WSOL_TOKEN_ACCOUNT},
trading::raydium_cpmm::pool::Pool,
};
use anyhow::anyhow;
use solana_sdk::pubkey::Pubkey;
pub fn get_pool_pda(amm_config: &Pubkey, mint1: &Pubkey, mint2: &Pubkey) -> Option<Pubkey> {
let seeds: &[&[u8]; 4] = &[
constants::raydium_cpmm::seeds::POOL_SEED,
amm_config.as_ref(),
mint1.as_ref(),
mint2.as_ref(),
];
let program_id: &Pubkey = &constants::raydium_cpmm::accounts::RAYDIUM_CPMM;
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
pda.map(|pubkey| pubkey.0)
}
pub fn get_vault_pda(pool_state: &Pubkey, mint: &Pubkey) -> Option<Pubkey> {
let seeds: &[&[u8]; 3] = &[
constants::raydium_cpmm::seeds::POOL_VAULT_SEED,
pool_state.as_ref(),
mint.as_ref(),
];
let program_id: &Pubkey = &constants::raydium_cpmm::accounts::RAYDIUM_CPMM;
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
pda.map(|pubkey| pubkey.0)
}
pub fn get_observation_state_pda(pool_state: &Pubkey) -> Option<Pubkey> {
let seeds: &[&[u8]; 2] = &[
constants::raydium_cpmm::seeds::OBSERVATION_STATE_SEED,
pool_state.as_ref(),
];
let program_id: &Pubkey = &constants::raydium_cpmm::accounts::RAYDIUM_CPMM;
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
pda.map(|pubkey| pubkey.0)
}
pub async fn get_buy_token_amount(
rpc: &SolanaRpcClient,
pool_state: &Pubkey,
sol_amount: u64,
) -> Result<u64, anyhow::Error> {
let pool = Pool::fetch(rpc, pool_state).await?;
let is_token0_input = if pool.token0_mint == WSOL_TOKEN_ACCOUNT {
true
} else {
false
};
let (token0_balance, token1_balance) =
get_pool_token_balances(rpc, pool_state, &pool.token0_mint, &pool.token1_mint).await?;
// 使用恒定乘积公式计算
let (reserve_in, reserve_out) = if is_token0_input {
(token0_balance, token1_balance)
} else {
(token1_balance, token0_balance)
};
if reserve_in == 0 || reserve_out == 0 {
return Err(anyhow!("池子储备金为零,无法进行交换"));
}
// 使用 u128 防止溢出
let amount_in_128 = sol_amount as u128;
let reserve_in_128 = reserve_in as u128;
let reserve_out_128 = reserve_out as u128;
// 恒定乘积公式: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in)
let numerator = amount_in_128 * reserve_out_128;
let denominator = reserve_in_128 + amount_in_128;
if denominator == 0 {
return Err(anyhow!("分母为零,计算错误"));
}
let amount_out = numerator / denominator;
// 检查是否超出储备金
if amount_out >= reserve_out_128 {
return Err(anyhow!("输出数量超过池子储备金"));
}
Ok(amount_out as u64)
}
pub async fn get_sell_sol_amount(
rpc: &SolanaRpcClient,
pool_state: &Pubkey,
token_amount: u64,
) -> Result<u64, anyhow::Error> {
let pool = Pool::fetch(rpc, pool_state).await?;
let is_token0_sol = if pool.token0_mint == WSOL_TOKEN_ACCOUNT {
true
} else {
false
};
let (token0_balance, token1_balance) =
get_pool_token_balances(rpc, pool_state, &pool.token0_mint, &pool.token1_mint).await?;
let (reserve_in, reserve_out) = if is_token0_sol {
(token1_balance, token0_balance)
} else {
(token0_balance, token1_balance)
};
if reserve_in == 0 || reserve_out == 0 {
return Err(anyhow!("池子储备金为零,无法进行交换"));
}
// 使用 u128 防止溢出
let amount_in_128 = token_amount as u128;
let reserve_in_128 = reserve_in as u128;
let reserve_out_128 = reserve_out as u128;
// 恒定乘积公式: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in)
let numerator = amount_in_128 * reserve_out_128;
let denominator = reserve_in_128 + amount_in_128;
if denominator == 0 {
return Err(anyhow!("分母为零,计算错误"));
}
let amount_out = numerator / denominator;
// 检查是否超出储备金
if amount_out >= reserve_out_128 {
return Err(anyhow!("输出数量超过池子储备金"));
}
Ok(amount_out as u64)
}
/// 获取池子中两个代币的余额
///
/// # 返回值
/// 返回 token0_balance, token1_balance
pub async fn get_pool_token_balances(
rpc: &SolanaRpcClient,
pool_state: &Pubkey,
token0_mint: &Pubkey,
token1_mint: &Pubkey,
) -> Result<(u64, u64), anyhow::Error> {
let token0_vault = get_vault_pda(pool_state, token0_mint).unwrap();
let token0_balance = rpc.get_token_account_balance(&token0_vault).await?;
let token1_vault = get_vault_pda(pool_state, token1_mint).unwrap();
let token1_balance = rpc.get_token_account_balance(&token1_vault).await?;
// 解析余额字符串为 u64
let token0_amount = token0_balance
.amount
.parse::<u64>()
.map_err(|e| anyhow!("解析 token0 余额失败: {}", e))?;
let token1_amount = token1_balance
.amount
.parse::<u64>()
.map_err(|e| anyhow!("解析 token1 余额失败: {}", e))?;
Ok((token0_amount, token1_amount))
}
/// 计算代币价格 (token1/token0)
///
/// # 返回值
/// 返回 token1 相对于 token0 的价格
pub async fn calculate_price(
token0_amount: u64,
token1_amount: u64,
mint0_decimals: u8,
mint1_decimals: u8,
) -> Result<f64, anyhow::Error> {
if token0_amount == 0 {
return Err(anyhow!("Token0 余额为零,无法计算价格"));
}
// 考虑小数位精度
let token0_adjusted = token0_amount as f64 / 10_f64.powi(mint0_decimals as i32);
let token1_adjusted = token1_amount as f64 / 10_f64.powi(mint1_decimals as i32);
let price = token1_adjusted / token0_adjusted;
Ok(price)
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::pubkey;
#[test]
fn test_get_pool_pda() {
// 测试get_pool_pda函数
let amm_config = constants::raydium_cpmm::accounts::AMM_CONFIG;
let input_mint = pubkey!("So11111111111111111111111111111111111111112"); // WSOL
let output_mint = pubkey!("BnwbwoqPm5ZNx7YTJ8g9jR2qCpYeHBC7xxpU8zEtbonk"); // USDC
let pool_state = pubkey!("E9rRRpcdsKAseeLFbwC1Ewxd3aYG27meqwTTrMfCTbSG");
let result = get_pool_pda(&amm_config, &input_mint, &output_mint);
assert_eq!(result, Some(pool_state));
}
#[test]
fn test_get_vault_pda() {
// 测试get_vault_pda函数
let pool_state = pubkey!("HBMkgQvt4NAFx6XzNav23bNcv6K3oiC5UfY3JsE22scY");
let mint = pubkey!("DeESECsL3cLXno1LFquss98kNQSno1xpQC2ERCqSbonk"); // WSOL
let vault_pda = pubkey!("7rkgNG3A8z636DuzhchKeqAJTaH3H5ZFWmBQeStydovA");
let result = get_vault_pda(&pool_state, &mint);
assert_eq!(result, Some(vault_pda));
}
#[test]
fn test_get_observation_state_pda() {
let pool_state = pubkey!("HBMkgQvt4NAFx6XzNav23bNcv6K3oiC5UfY3JsE22scY");
let observation_state_pda = pubkey!("Gq8u9N18ASjq3AK2gCk6RtGSNyjXZf9EZDb6vTtB9JRs");
let result = get_observation_state_pda(&pool_state);
assert_eq!(result, Some(observation_state_pda));
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod common;
pub mod pool;
+51
View File
@@ -0,0 +1,51 @@
use crate::{common::SolanaRpcClient, constants::raydium_cpmm::accounts};
use anyhow::anyhow;
use borsh::BorshDeserialize;
use solana_sdk::pubkey::Pubkey;
#[derive(Debug, Clone, BorshDeserialize)]
pub struct Pool {
pub amm_config: Pubkey,
pub pool_creator: Pubkey,
pub token0_vault: Pubkey,
pub token1_vault: Pubkey,
pub lp_mint: Pubkey,
pub token0_mint: Pubkey,
pub token1_mint: Pubkey,
pub token0_program: Pubkey,
pub token1_program: Pubkey,
pub observation_key: Pubkey,
pub auth_bump: u8,
pub status: u8,
pub lp_mint_decimals: u8,
pub mint0_decimals: u8,
pub mint1_decimals: u8,
pub lp_supply: u64,
pub protocol_fees_token0: u64,
pub protocol_fees_token1: u64,
pub fund_fees_token0: u64,
pub fund_fees_token1: u64,
pub open_time: u64,
pub recent_epoch: u64,
pub padding: [u64; 31],
}
impl Pool {
pub fn from_bytes(data: &[u8]) -> Result<Self, anyhow::Error> {
let pool = Pool::try_from_slice(&data[8..])?;
Ok(pool)
}
pub async fn fetch(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let account = rpc.get_account(pool_address).await?;
if account.owner != accounts::RAYDIUM_CPMM {
return Err(anyhow!("Account is not owned by Raydium Cpmm program"));
}
Self::from_bytes(&account.data)
}
}