diff --git a/README.md b/README.md index 030d17f..7fc1853 100755 --- a/README.md +++ b/README.md @@ -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> { 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> { 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> { + 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> { } ``` -### 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 diff --git a/README_CN.md b/README_CN.md index 28a706b..acc4b0b 100755 --- a/README_CN.md +++ b/README_CN.md @@ -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> { 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> { 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> { + 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> { } ``` -### 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 # 示例程序 diff --git a/src/constants/mod.rs b/src/constants/mod.rs index 27ef1f4..9f9f1e0 100755 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -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"; } diff --git a/src/constants/raydium_cpmm.rs b/src/constants/raydium_cpmm.rs new file mode 100755 index 0000000..edf806e --- /dev/null +++ b/src/constants/raydium_cpmm.rs @@ -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]; diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index 80bb1a8..ccc2b16 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -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, diff --git a/src/instruction/raydium_cpmm.rs b/src/instruction/raydium_cpmm.rs new file mode 100755 index 0000000..f792249 --- /dev/null +++ b/src/instruction/raydium_cpmm.rs @@ -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> { + 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> { + self.build_sell_instructions_with_accounts(params).await + } +} + +impl RaydiumCpmmInstructionBuilder { + /// 使用提供的账户信息构建买入指令 + async fn build_buy_instructions_with_accounts( + &self, + params: &BuyParams, + ) -> Result> { + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .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, + ¶ms.mint, + ) + .unwrap() + }; + + let wsol_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + ); + let mint_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + ¶ms.mint, + ); + + // 获取池的代币账户 + let wsol_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); + let mint_vault_account = get_vault_pda(&pool_state, ¶ms.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( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + &accounts::TOKEN_PROGRAM, + ), + ); + instructions.push( + // 将SOL转入wSOL ATA账户 + solana_sdk::system_instruction::transfer( + ¶ms.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( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + ¶ms.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, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[], + ) + .unwrap(), + ); + } + + Ok(instructions) + } + + /// 使用提供的账户信息构建卖出指令 + async fn build_sell_instructions_with_accounts( + &self, + params: &SellParams, + ) -> Result> { + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .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(), ¶ms.payer.pubkey(), ¶ms.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, + ¶ms.mint, + ) + .unwrap() + }; + + let wsol_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + &accounts::WSOL_TOKEN_ACCOUNT, + ); + let mint_token_account = spl_associated_token_account::get_associated_token_address( + ¶ms.payer.pubkey(), + ¶ms.mint, + ); + + // 获取池的代币账户 + let wsol_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); + let mint_vault_account = get_vault_pda(&pool_state, ¶ms.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( + ¶ms.payer.pubkey(), + ¶ms.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, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[¶ms.payer.pubkey()], + ) + .unwrap(), + ); + + Ok(instructions) + } +} diff --git a/src/lib.rs b/src/lib.rs index 1d2b892..1c64f34 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 - } - DexType::PumpSwap => { - Box::new(PumpSwapParams::default()) as Box - } + DexType::PumpFun => Box::new(PumpFunParams::default()) as Box, + DexType::PumpSwap => Box::new(PumpSwapParams::default()) as Box, DexType::Bonk => Box::new(BonkParams::default()) as Box, + DexType::RaydiumCpmm => { + Box::new(RaydiumCpmmParams::default()) as Box + } } }; let buy_params = BuyParams { @@ -227,6 +227,10 @@ impl SolanaTrade { .as_any() .downcast_ref::() .is_some(), + DexType::RaydiumCpmm => protocol_params + .as_any() + .downcast_ref::() + .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 - } - DexType::PumpSwap => { - Box::new(PumpSwapParams::default()) as Box - } + DexType::PumpFun => Box::new(PumpFunParams::default()) as Box, + DexType::PumpSwap => Box::new(PumpSwapParams::default()) as Box, DexType::Bonk => Box::new(BonkParams::default()) as Box, + DexType::RaydiumCpmm => { + Box::new(RaydiumCpmmParams::default()) as Box + } } }; let sell_params = SellParams { @@ -348,6 +351,10 @@ impl SolanaTrade { .as_any() .downcast_ref::() .is_some(), + DexType::RaydiumCpmm => protocol_params + .as_any() + .downcast_ref::() + .is_some(), }; if !is_valid_params { diff --git a/src/main.rs b/src/main.rs index 695e948..c2660f7 100755 --- a/src/main.rs +++ b/src/main.rs @@ -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> { 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> { Ok(()) } + +async fn test_raydium_cpmm() -> Result<(), Box> { + 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> { println!("正在订阅 GRPC 事件..."); @@ -339,7 +390,7 @@ async fn test_grpc() -> Result<(), Box> { )?; 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) { PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| { println!("Withdraw event: {:?}", e); }, + RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| { + println!("RaydiumCpmmSwapEvent: {:?}", e); + }, }); } } diff --git a/src/streaming/event_parser/common/types.rs b/src/streaming/event_parser/common/types.rs index 5a21d3e..02aae00 100755 --- a/src/streaming/event_parser/common/types.rs +++ b/src/streaming/event_parser/common/types.rs @@ -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, } diff --git a/src/streaming/event_parser/factory.rs b/src/streaming/event_parser/factory.rs index 6d47011..f4f9a0f 100755 --- a/src/streaming/event_parser/factory.rs +++ b/src/streaming/event_parser/factory.rs @@ -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()), } } diff --git a/src/streaming/event_parser/protocols/mod.rs b/src/streaming/event_parser/protocols/mod.rs index dfe995e..523557b 100755 --- a/src/streaming/event_parser/protocols/mod.rs +++ b/src/streaming/event_parser/protocols/mod.rs @@ -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; \ No newline at end of file +pub use bonk::BonkEventParser; +pub use raydium_cpmm::RaydiumCpmmEventParser; \ No newline at end of file diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/events.rs b/src/streaming/event_parser/protocols/raydium_cpmm/events.rs new file mode 100755 index 0000000..2c3f770 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_cpmm/events.rs @@ -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]; +} diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/mod.rs b/src/streaming/event_parser/protocols/raydium_cpmm/mod.rs new file mode 100755 index 0000000..e5896a6 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_cpmm/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::RaydiumCpmmEventParser; diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs b/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs new file mode 100755 index 0000000..8b6984a --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs @@ -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> { + None + } + + /// 解析买入指令事件 + fn parse_swap_base_input_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + 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> { + 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> { + 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> { + 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 { + self.inner.supported_program_ids() + } +} diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index d9ad774..806253b 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -215,6 +215,34 @@ impl ProtocolParams for BonkParams { } } +/// RaydiumCpmm协议特定参数 +#[derive(Clone)] +pub struct RaydiumCpmmParams { + pub pool_state: Option, + pub minimum_amount_out: Option, + 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 { + Box::new(self.clone()) + } +} + impl BuyParams { /// 转换为BuyWithTipParams pub fn with_tip(self, swqos_clients: Vec>) -> BuyWithTipParams { diff --git a/src/trading/factory.rs b/src/trading/factory.rs index f0779b7..3db4704 100755 --- a/src/trading/factory.rs +++ b/src/trading/factory.rs @@ -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 { - vec![DexType::PumpFun, DexType::PumpSwap, DexType::Bonk] + vec![DexType::PumpFun, DexType::PumpSwap, DexType::Bonk, DexType::RaydiumCpmm] } /// 检查协议是否支持 diff --git a/src/trading/mod.rs b/src/trading/mod.rs index 566f7b1..318a478 100755 --- a/src/trading/mod.rs +++ b/src/trading/mod.rs @@ -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}; diff --git a/src/trading/raydium_cpmm/common.rs b/src/trading/raydium_cpmm/common.rs new file mode 100755 index 0000000..6a67058 --- /dev/null +++ b/src/trading/raydium_cpmm/common.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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::() + .map_err(|e| anyhow!("解析 token0 余额失败: {}", e))?; + + let token1_amount = token1_balance + .amount + .parse::() + .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 { + 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)); + } +} diff --git a/src/trading/raydium_cpmm/mod.rs b/src/trading/raydium_cpmm/mod.rs new file mode 100755 index 0000000..53868a7 --- /dev/null +++ b/src/trading/raydium_cpmm/mod.rs @@ -0,0 +1,2 @@ +pub mod common; +pub mod pool; diff --git a/src/trading/raydium_cpmm/pool.rs b/src/trading/raydium_cpmm/pool.rs new file mode 100755 index 0000000..939bffb --- /dev/null +++ b/src/trading/raydium_cpmm/pool.rs @@ -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 { + let pool = Pool::try_from_slice(&data[8..])?; + Ok(pool) + } + + pub async fn fetch( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + 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) + } +}