diff --git a/Cargo.toml b/Cargo.toml index 4ab14d7..f2f8c48 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "0.2.0" +version = "0.2.1" edition = "2021" authors = ["William ", "sgxiang ", "wei <1415121722@qq.com>"] repository = "https://github.com/0xfnzero/sol-trade-sdk" diff --git a/README.md b/README.md index f2d9636..7113004 100755 --- a/README.md +++ b/README.md @@ -29,11 +29,23 @@ Add the dependency to your `Cargo.toml`: ```toml # Add to your Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.2.0" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.2.1" } ``` ## Usage Examples +### Important Parameter Description + +#### auto_handle_wsol Parameter + +In PumpSwap, Bonk, and Raydium CPMM trading, the `auto_handle_wsol` parameter is used to automatically handle wSOL (Wrapped SOL): + +- **Mechanism**: + - When `auto_handle_wsol: true`, the SDK automatically handles the conversion between SOL and wSOL + - When buying: automatically wraps SOL to wSOL for trading + - When selling: automatically unwraps the received wSOL to SOL + - Default value is `true` + ### 1. Event Subscription - Monitor Token Trading #### 1.1 Subscribe to Events Using Yellowstone gRPC @@ -410,8 +422,9 @@ async fn test_raydium_cpmm() -> Result<(), Box> { recent_blockhash, None, Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // If not provided, will auto-calculate wsol-mint direction pool + pool_state: Some(pool_state), // If not provided, will auto-calculate mint_token_program: Some(spl_token::ID), // Support spl_token or spl_token_2022::ID + mint_token_in_pool_state_index: Some(1), // Index of mint_token in pool_state, default is at index 1 minimum_amount_out: Some(buy_amount_out), // If not provided, defaults to 0 auto_handle_wsol: true, // Automatically handle wSOL wrapping/unwrapping })), @@ -430,8 +443,9 @@ async fn test_raydium_cpmm() -> Result<(), Box> { recent_blockhash, None, Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // If not provided, will auto-calculate wsol-mint direction pool + pool_state: Some(pool_state), // If not provided, will auto-calculate mint_token_program: Some(spl_token::ID), // Support spl_token or spl_token_2022::ID + mint_token_in_pool_state_index: Some(1), // Index of mint_token in pool_state, default is at index 1 minimum_amount_out: Some(sell_sol_amount), // If not provided, defaults to 0 auto_handle_wsol: true, // Automatically handle wSOL wrapping/unwrapping })), diff --git a/README_CN.md b/README_CN.md index 915b01e..accb9d9 100755 --- a/README_CN.md +++ b/README_CN.md @@ -29,11 +29,23 @@ git clone https://github.com/0xfnzero/sol-trade-sdk ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.2.0" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.2.1" } ``` ## 使用示例 +### 重要参数说明 + +#### auto_handle_wsol 参数 + +在 PumpSwap、Bonk、Raydium CPMM 交易中,`auto_handle_wsol` 参数用于自动处理 wSOL(Wrapped SOL): + +- **作用机制**: + - 当 `auto_handle_wsol: true` 时,SDK 会自动处理 SOL 与 wSOL 之间的转换 + - 买入时:自动将 SOL 包装为 wSOL 进行交易 + - 卖出时:自动将获得的 wSOL 解包装为 SOL + - 默认值为 `true` + ### 1. 事件订阅 - 监听代币交易 #### 1.1 使用 Yellowstone gRPC 订阅事件 @@ -408,8 +420,9 @@ async fn test_raydium_cpmm() -> Result<(), Box> { recent_blockhash, None, Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子 + pool_state: Some(pool_state), // 如果不传,会自动计算 mint_token_program: Some(spl_token::ID), // 支持 spl_token 或 spl_token_2022::ID + mint_token_in_pool_state_index: Some(1), // mint_token 在 pool_state 中的索引,默认在索引1 minimum_amount_out: Some(buy_amount_out), // 如果不传,默认为0 auto_handle_wsol: true, // 自动处理 wSOL 包装/解包装 })), @@ -428,8 +441,9 @@ async fn test_raydium_cpmm() -> Result<(), Box> { recent_blockhash, None, Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子 + pool_state: Some(pool_state), // 如果不传,会自动计算 mint_token_program: Some(spl_token::ID), // 支持 spl_token 或 spl_token_2022::ID + mint_token_in_pool_state_index: Some(1), // mint_token 在 pool_state 中的索引,默认在索引1 minimum_amount_out: Some(sell_sol_amount), // 如果不传,默认为0 auto_handle_wsol: true, // 自动处理 wSOL 包装/解包装 })), diff --git a/src/instruction/bonk.rs b/src/instruction/bonk.rs index 5ee4741..ddbe045 100755 --- a/src/instruction/bonk.rs +++ b/src/instruction/bonk.rs @@ -4,9 +4,7 @@ use spl_associated_token_account::instruction::create_associated_token_account_i use spl_token::instruction::close_account; use crate::{ - constants::bonk::{ - accounts, BUY_EXECT_IN_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR, - }, + constants::bonk::{accounts, BUY_EXECT_IN_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR}, constants::trade::trade::DEFAULT_SLIPPAGE, trading::bonk::{ common::{get_amount_out, get_pool_pda, get_vault_pda}, @@ -14,7 +12,7 @@ use crate::{ }, trading::common::utils::get_token_balance, trading::core::{ - params::{BuyParams, BonkParams, SellParams}, + params::{BonkParams, BuyParams, SellParams}, traits::InstructionBuilder, }, }; @@ -70,11 +68,7 @@ impl BonkInstructionBuilder { let mut real_base = protocol_params.real_base.unwrap_or(0); let mut real_quote = protocol_params.real_quote.unwrap_or(0); - if virtual_base == 0 - || virtual_quote == 0 - || real_base == 0 - || real_quote == 0 - { + if virtual_base == 0 || virtual_quote == 0 || real_base == 0 || real_quote == 0 { let pool = Pool::fetch(params.rpc.as_ref().unwrap(), &pool_state).await?; virtual_base = pool.virtual_base as u128; virtual_quote = pool.virtual_quote as u128; @@ -147,7 +141,7 @@ impl BonkInstructionBuilder { solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // User Quote Token solana_sdk::instruction::AccountMeta::new(base_vault_account, false), // Base Vault solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault - solana_sdk::instruction::AccountMeta::new(params.mint, false), // Base Token Mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Base Token Mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly) @@ -253,7 +247,7 @@ impl BonkInstructionBuilder { solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // User Quote Token solana_sdk::instruction::AccountMeta::new(base_vault_account, false), // Base Vault solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault - solana_sdk::instruction::AccountMeta::new(params.mint, false), // Base Token Mint (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Base Token Mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly) @@ -274,7 +268,24 @@ impl BonkInstructionBuilder { data, }); - instructions.push(close_account(&accounts::TOKEN_PROGRAM, &user_quote_token_account, ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), &[¶ms.payer.pubkey()]).unwrap()); + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for Bonk"))?; + + if protocol_params.auto_handle_wsol { + instructions.push( + close_account( + &accounts::TOKEN_PROGRAM, + &user_quote_token_account, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[¶ms.payer.pubkey()], + ) + .unwrap(), + ); + } Ok(instructions) } diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index ef6c503..aa3d92f 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer}; use spl_associated_token_account::instruction::create_associated_token_account_idempotent; +use spl_token::instruction::close_account; use crate::{ constants::pumpswap::{accounts, BUY_DISCRIMINATOR, SELL_DISCRIMINATOR}, @@ -361,6 +362,24 @@ impl PumpSwapInstructionBuilder { data, }); + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpSwap"))?; + + if protocol_params.auto_handle_wsol { + instructions.push( + close_account( + &accounts::TOKEN_PROGRAM, + &user_quote_token_account, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[¶ms.payer.pubkey()], + ) + .unwrap(), + ); + } Ok(instructions) } } diff --git a/src/instruction/raydium_cpmm.rs b/src/instruction/raydium_cpmm.rs index f63569c..e65610f 100755 --- a/src/instruction/raydium_cpmm.rs +++ b/src/instruction/raydium_cpmm.rs @@ -49,10 +49,21 @@ impl RaydiumCpmmInstructionBuilder { let pool_state = if protocol_params.pool_state.is_some() { protocol_params.pool_state.unwrap() } else { + let mint_token_in_pool_state_index = protocol_params + .mint_token_in_pool_state_index + .unwrap_or(1); get_pool_pda( &accounts::AMM_CONFIG, - &accounts::WSOL_TOKEN_ACCOUNT, - ¶ms.mint, + if mint_token_in_pool_state_index == 1 { + &accounts::WSOL_TOKEN_ACCOUNT + } else { + ¶ms.mint + }, + if mint_token_in_pool_state_index == 1 { + ¶ms.mint + } else { + &accounts::WSOL_TOKEN_ACCOUNT + }, ) .unwrap() }; @@ -124,7 +135,9 @@ impl RaydiumCpmmInstructionBuilder { ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), ¶ms.mint, - &protocol_params.mint_token_program.unwrap_or(accounts::TOKEN_PROGRAM), + &protocol_params + .mint_token_program + .unwrap_or(accounts::TOKEN_PROGRAM), )); // 创建买入指令 @@ -138,7 +151,12 @@ impl RaydiumCpmmInstructionBuilder { 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(protocol_params.mint_token_program.unwrap_or(accounts::TOKEN_PROGRAM), false), // Output Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly( + protocol_params + .mint_token_program + .unwrap_or(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 @@ -221,10 +239,21 @@ impl RaydiumCpmmInstructionBuilder { let pool_state = if protocol_params.pool_state.is_some() { protocol_params.pool_state.unwrap() } else { + let mint_token_in_pool_state_index = protocol_params + .mint_token_in_pool_state_index + .unwrap_or(1); get_pool_pda( &accounts::AMM_CONFIG, - &accounts::WSOL_TOKEN_ACCOUNT, - ¶ms.mint, + if mint_token_in_pool_state_index == 1 { + &accounts::WSOL_TOKEN_ACCOUNT + } else { + ¶ms.mint + }, + if mint_token_in_pool_state_index == 1 { + ¶ms.mint + } else { + &accounts::WSOL_TOKEN_ACCOUNT + }, ) .unwrap() }; @@ -267,7 +296,12 @@ impl RaydiumCpmmInstructionBuilder { 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(protocol_params.mint_token_program.unwrap_or(accounts::TOKEN_PROGRAM), false), // Input Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly( + protocol_params + .mint_token_program + .unwrap_or(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) @@ -285,16 +319,18 @@ impl RaydiumCpmmInstructionBuilder { data, }); - instructions.push( - close_account( - &accounts::TOKEN_PROGRAM, - &wsol_token_account, - ¶ms.payer.pubkey(), - ¶ms.payer.pubkey(), - &[¶ms.payer.pubkey()], - ) - .unwrap(), - ); + if protocol_params.auto_handle_wsol { + 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/main.rs b/src/main.rs index c204f95..13764f7 100755 --- a/src/main.rs +++ b/src/main.rs @@ -353,8 +353,9 @@ async fn test_raydium_cpmm() -> Result<(), Box> { recent_blockhash, None, Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子 + pool_state: Some(pool_state), // 如果不传,会自动计算 mint_token_program: Some(spl_token::ID), // spl_token_2022::ID + mint_token_in_pool_state_index: Some(1), // mint_token 在 pool_state 中的索引,默认在索引1 minimum_amount_out: Some(buy_amount_out), // 如果不传、默认为0 auto_handle_wsol: true, })), @@ -373,8 +374,9 @@ async fn test_raydium_cpmm() -> Result<(), Box> { recent_blockhash, None, Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // 如果不传,会自动计算 wsol-mint 方向的池子 + pool_state: Some(pool_state), // 如果不传,会自动计算 mint_token_program: Some(spl_token::ID), // spl_token_2022::ID + mint_token_in_pool_state_index: Some(1), // mint_token 在 pool_state 中的索引,默认在索引1 minimum_amount_out: Some(sell_sol_amount), // 如果不传、默认为0 auto_handle_wsol: true, })), diff --git a/src/streaming/event_parser/common/types.rs b/src/streaming/event_parser/common/types.rs index 478f4ea..0ad95f9 100755 --- a/src/streaming/event_parser/common/types.rs +++ b/src/streaming/event_parser/common/types.rs @@ -13,6 +13,7 @@ pub enum ProtocolType { PumpFun, Bonk, RaydiumCpmm, + RaydiumClmm, } /// 事件类型枚举 @@ -44,6 +45,10 @@ pub enum EventType { RaydiumCpmmSwapBaseInput, RaydiumCpmmSwapBaseOutput, + // Raydium CLMM 事件 + RaydiumClmmSwap, + RaydiumClmmSwapV2, + // 通用事件 Unknown, } @@ -66,6 +71,8 @@ impl EventType { EventType::BonkInitialize => "BonkInitialize".to_string(), EventType::RaydiumCpmmSwapBaseInput => "RaydiumCpmmSwapBaseInput".to_string(), EventType::RaydiumCpmmSwapBaseOutput => "RaydiumCpmmSwapBaseOutput".to_string(), + EventType::RaydiumClmmSwap => "RaydiumClmmSwap".to_string(), + EventType::RaydiumClmmSwapV2 => "RaydiumClmmSwapV2".to_string(), EventType::Unknown => "Unknown".to_string(), } } diff --git a/src/streaming/event_parser/common/utils.rs b/src/streaming/event_parser/common/utils.rs index 4c5fee9..5452cfc 100755 --- a/src/streaming/event_parser/common/utils.rs +++ b/src/streaming/event_parser/common/utils.rs @@ -56,6 +56,22 @@ pub fn read_u64_le(data: &[u8], offset: usize) -> Option { Some(u64::from_le_bytes(bytes)) } +pub fn read_u128_le(data: &[u8], offset: usize) -> Option { + if data.len() < offset + 16 { + return None; + } + let bytes: [u8; 16] = data[offset..offset + 16].try_into().ok()?; + Some(u128::from_le_bytes(bytes)) +} + +pub fn read_u8_le(data: &[u8], offset: usize) -> Option { + if data.len() < offset + 1 { + return None; + } + let bytes: [u8; 1] = data[offset..offset + 1].try_into().ok()?; + Some(u8::from_le_bytes(bytes)) +} + /// 安全地从字节数组中读取u32 pub fn read_u32_le(data: &[u8], offset: usize) -> Option { if data.len() < offset + 4 { diff --git a/src/streaming/event_parser/core/traits.rs b/src/streaming/event_parser/core/traits.rs index 20a8bbb..331fe43 100755 --- a/src/streaming/event_parser/core/traits.rs +++ b/src/streaming/event_parser/core/traits.rs @@ -436,6 +436,10 @@ impl EventParser for GenericEventParser { signature: &str, slot: u64, ) -> Vec> { + let program_id = accounts[instruction.program_id_index as usize]; + if !self.should_handle(&program_id) { + return Vec::new(); + } let mut events = Vec::new(); for (disc, configs) in &self.instruction_configs { if instruction.data.len() < disc.len() { diff --git a/src/streaming/event_parser/factory.rs b/src/streaming/event_parser/factory.rs index f4f9a0f..b7830d5 100755 --- a/src/streaming/event_parser/factory.rs +++ b/src/streaming/event_parser/factory.rs @@ -5,7 +5,8 @@ use std::sync::Arc; use crate::streaming::event_parser::protocols::{ bonk::parser::BONK_PROGRAM_ID, pumpfun::parser::PUMPFUN_PROGRAM_ID, pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID, - BonkEventParser, RaydiumCpmmEventParser, + raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, BonkEventParser, RaydiumCpmmEventParser, + RaydiumClmmEventParser, }; use super::{ @@ -20,6 +21,7 @@ pub enum Protocol { PumpFun, Bonk, RaydiumCpmm, + RaydiumClmm, } impl Protocol { @@ -29,6 +31,7 @@ impl Protocol { Protocol::PumpFun => vec![PUMPFUN_PROGRAM_ID], Protocol::Bonk => vec![BONK_PROGRAM_ID], Protocol::RaydiumCpmm => vec![RAYDIUM_CPMM_PROGRAM_ID], + Protocol::RaydiumClmm => vec![RAYDIUM_CLMM_PROGRAM_ID], } } } @@ -40,6 +43,7 @@ impl std::fmt::Display for Protocol { Protocol::PumpFun => write!(f, "PumpFun"), Protocol::Bonk => write!(f, "Bonk"), Protocol::RaydiumCpmm => write!(f, "RaydiumCpmm"), + Protocol::RaydiumClmm => write!(f, "RaydiumClmm"), } } } @@ -53,6 +57,7 @@ impl std::str::FromStr for Protocol { "pumpfun" => Ok(Protocol::PumpFun), "bonk" => Ok(Protocol::Bonk), "raydiumcpmm" => Ok(Protocol::RaydiumCpmm), + "raydiumclmm" => Ok(Protocol::RaydiumClmm), _ => Err(anyhow!("Unsupported protocol: {}", s)), } } @@ -69,6 +74,7 @@ impl EventParserFactory { Protocol::PumpFun => Arc::new(PumpFunEventParser::new()), Protocol::Bonk => Arc::new(BonkEventParser::new()), Protocol::RaydiumCpmm => Arc::new(RaydiumCpmmEventParser::new()), + Protocol::RaydiumClmm => Arc::new(RaydiumClmmEventParser::new()), } } diff --git a/src/streaming/event_parser/protocols/mod.rs b/src/streaming/event_parser/protocols/mod.rs index 523557b..e0d8aa5 100755 --- a/src/streaming/event_parser/protocols/mod.rs +++ b/src/streaming/event_parser/protocols/mod.rs @@ -2,8 +2,10 @@ pub mod pumpfun; pub mod pumpswap; pub mod bonk; pub mod raydium_cpmm; +pub mod raydium_clmm; pub use pumpfun::PumpFunEventParser; pub use pumpswap::PumpSwapEventParser; pub use bonk::BonkEventParser; -pub use raydium_cpmm::RaydiumCpmmEventParser; \ No newline at end of file +pub use raydium_cpmm::RaydiumCpmmEventParser; +pub use raydium_clmm::RaydiumClmmEventParser; \ No newline at end of file diff --git a/src/streaming/event_parser/protocols/raydium_clmm/events.rs b/src/streaming/event_parser/protocols/raydium_clmm/events.rs new file mode 100755 index 0000000..cd0c0b1 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_clmm/events.rs @@ -0,0 +1,59 @@ +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)] +pub struct RaydiumClmmSwapEvent { + pub metadata: EventMetadata, + pub amount: u64, + pub other_amount_threshold: u64, + pub sqrt_price_limit_x64: u128, + pub is_base_input: bool, + pub payer: 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 observation_state: Pubkey, + pub token_program: Pubkey, + pub tick_array: Pubkey, + pub remaining_accounts: Vec, +} + +impl_unified_event!(RaydiumClmmSwapEvent,); + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct RaydiumClmmSwapV2Event { + pub metadata: EventMetadata, + pub amount: u64, + pub other_amount_threshold: u64, + pub sqrt_price_limit_x64: u128, + pub is_base_input: bool, + pub payer: 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 observation_state: Pubkey, + pub token_program: Pubkey, + pub token_program2022: Pubkey, + pub memo_program: Pubkey, + pub input_vault_mint: Pubkey, + pub output_vault_mint: Pubkey, + pub remaining_accounts: Vec, +} +impl_unified_event!(RaydiumClmmSwapV2Event,); + +/// 事件鉴别器常量 +pub mod discriminators { + // 指令鉴别器 + pub const SWAP: &[u8] = &[248, 198, 158, 145, 225, 117, 135, 200]; + pub const SWAP_V2: &[u8] = &[43, 4, 237, 11, 26, 201, 30, 98]; +} diff --git a/src/streaming/event_parser/protocols/raydium_clmm/mod.rs b/src/streaming/event_parser/protocols/raydium_clmm/mod.rs new file mode 100755 index 0000000..89b867a --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_clmm/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::RaydiumClmmEventParser; diff --git a/src/streaming/event_parser/protocols/raydium_clmm/parser.rs b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs new file mode 100755 index 0000000..09bc0a6 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs @@ -0,0 +1,170 @@ +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_transaction_status::UiCompiledInstruction; + +use crate::streaming::event_parser::{ + common::{read_u128_le, read_u64_le, read_u8_le, EventMetadata, EventType, ProtocolType}, + core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent}, + protocols::raydium_clmm::{discriminators, RaydiumClmmSwapEvent, RaydiumClmmSwapV2Event}, +}; + +/// Raydium CLMM程序ID +pub const RAYDIUM_CLMM_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"); + +/// Raydium CLMM事件解析器 +pub struct RaydiumClmmEventParser { + inner: GenericEventParser, +} + +impl RaydiumClmmEventParser { + pub fn new() -> Self { + // 配置所有事件类型 + let configs = vec![ + GenericEventParseConfig { + inner_instruction_discriminator: "", + instruction_discriminator: discriminators::SWAP, + event_type: EventType::RaydiumClmmSwap, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_swap_instruction, + }, + GenericEventParseConfig { + inner_instruction_discriminator: "", + instruction_discriminator: discriminators::SWAP_V2, + event_type: EventType::RaydiumClmmSwapV2, + inner_instruction_parser: Self::parse_trade_inner_instruction, + instruction_parser: Self::parse_swap_v2_instruction, + }, + ]; + + let inner = + GenericEventParser::new(RAYDIUM_CLMM_PROGRAM_ID, ProtocolType::RaydiumClmm, configs); + + Self { inner } + } + + /// 解析交易事件 + fn parse_trade_inner_instruction( + _data: &[u8], + _metadata: EventMetadata, + ) -> Option> { + None + } + + /// 解析交易指令事件 + fn parse_swap_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 10 { + return None; + } + + let amount = read_u64_le(data, 0)?; + let other_amount_threshold = read_u64_le(data, 8)?; + let sqrt_price_limit_x64 = read_u128_le(data, 16)?; + let is_base_input = read_u8_le(data, 32)?; + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[2], accounts[3], accounts[4] + )); + + Some(Box::new(RaydiumClmmSwapEvent { + metadata, + amount, + other_amount_threshold, + sqrt_price_limit_x64, + is_base_input: is_base_input == 1, + payer: accounts[0], + amm_config: accounts[1], + pool_state: accounts[2], + input_token_account: accounts[3], + output_token_account: accounts[4], + input_vault: accounts[5], + output_vault: accounts[6], + observation_state: accounts[7], + token_program: accounts[8], + tick_array: accounts[9], + remaining_accounts: accounts[10..].to_vec(), + ..Default::default() + })) + } + + fn parse_swap_v2_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + if data.len() < 16 || accounts.len() < 13 { + return None; + } + + let amount = read_u64_le(data, 0)?; + let other_amount_threshold = read_u64_le(data, 8)?; + let sqrt_price_limit_x64 = read_u128_le(data, 16)?; + let is_base_input = read_u8_le(data, 32)?; + + let mut metadata = metadata; + metadata.set_id(format!( + "{}-{}-{}-{}", + metadata.signature, accounts[2], accounts[3], accounts[4] + )); + + Some(Box::new(RaydiumClmmSwapV2Event { + metadata, + amount, + other_amount_threshold, + sqrt_price_limit_x64, + is_base_input: is_base_input == 1, + payer: accounts[0], + amm_config: accounts[1], + pool_state: accounts[2], + input_token_account: accounts[3], + output_token_account: accounts[4], + input_vault: accounts[5], + output_vault: accounts[6], + observation_state: accounts[7], + token_program: accounts[8], + token_program2022: accounts[9], + memo_program: accounts[10], + input_vault_mint: accounts[11], + output_vault_mint: accounts[12], + remaining_accounts: accounts[13..].to_vec(), + ..Default::default() + })) + } +} + +#[async_trait::async_trait] +impl EventParser for RaydiumClmmEventParser { + 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 0145789..ca05c55 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -218,8 +218,14 @@ impl ProtocolParams for BonkParams { /// RaydiumCpmm协议特定参数 #[derive(Clone)] pub struct RaydiumCpmmParams { + /// 池子状态账户地址 pub pool_state: Option, - pub mint_token_program: Option, // spl_token_2022::ID + /// 代币程序ID + /// 指定代币使用的程序,通常为 spl_token::ID 或 spl_token_2022::ID + pub mint_token_program: Option, + /// 指定 mint_token 在 pool_state 账户数据中的索引位置 + /// 默认值为1,表示在索引1的位置 + pub mint_token_in_pool_state_index: Option, pub minimum_amount_out: Option, pub auto_handle_wsol: bool, } @@ -229,6 +235,7 @@ impl RaydiumCpmmParams { Self { pool_state: None, mint_token_program: Some(spl_token::ID), + mint_token_in_pool_state_index: Some(1), minimum_amount_out: None, auto_handle_wsol: true, }