diff --git a/examples/meteora_dlmm_account_subscription.rs b/examples/meteora_dlmm_account_subscription.rs new file mode 100644 index 0000000..1638275 --- /dev/null +++ b/examples/meteora_dlmm_account_subscription.rs @@ -0,0 +1,117 @@ +use solana_streamer_sdk::streaming::{ + event_parser::{ + protocols::meteora_dlmm::parser::METEORA_DLMM_PROGRAM_ID, DexEvent, Protocol, + }, + grpc::ClientConfig, + yellowstone_grpc::{AccountFilter, TransactionFilter}, + YellowstoneGrpc, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("开始 Meteora DLMM 账户数据订阅示例..."); + subscribe_meteora_dlmm_accounts().await?; + Ok(()) +} + +async fn subscribe_meteora_dlmm_accounts() -> Result<(), Box> { + println!("订阅 Meteora DLMM 账户数据..."); + + // 创建客户端配置 + let mut config: ClientConfig = ClientConfig::default(); + // 启用性能监控(可选,有性能开销) + config.enable_metrics = true; + let grpc = YellowstoneGrpc::new_with_config( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + config, + )?; + + println!("gRPC 客户端创建成功"); + + let callback = create_event_callback(); + + // 只订阅 Meteora DLMM 协议 + let protocols = vec![Protocol::MeteoraDlmm]; + + println!("监控协议: {:?}", protocols); + + // 账户过滤器 - 订阅 Meteora DLMM 程序拥有的账户 + let account_filter = AccountFilter { + account: vec![], + owner: vec![METEORA_DLMM_PROGRAM_ID.to_string()], + filters: vec![], + }; + + // 交易过滤器(可选,如果只想订阅账户数据,可以留空) + let transaction_filter = TransactionFilter { + account_include: vec![METEORA_DLMM_PROGRAM_ID.to_string()], + account_exclude: vec![], + account_required: vec![], + }; + + // 事件类型过滤器 - 只订阅账户事件 + use solana_streamer_sdk::streaming::event_parser::common::EventType; + use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter; + let event_type_filter = Some(EventTypeFilter { + include: vec![EventType::AccountMeteoraDlmmLbPair], + }); + + println!("开始监听事件,按 Ctrl+C 停止..."); + println!("监控程序: {}", METEORA_DLMM_PROGRAM_ID); + + println!("开始订阅..."); + + grpc.subscribe_events_immediate( + protocols, + None, + vec![transaction_filter], + vec![account_filter], + event_type_filter, + None, + callback, + ) + .await?; + + // 支持 stop 方法,测试代码 - 异步1000秒之后停止 + let grpc_clone = grpc.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(1000)).await; + grpc_clone.stop().await; + }); + + println!("等待 Ctrl+C 停止..."); + tokio::signal::ctrl_c().await?; + + Ok(()) +} + +fn create_event_callback() -> impl Fn(DexEvent) { + |event: DexEvent| { + println!( + "🎉 事件接收! 类型: {:?}, slot: {:?}", + event.metadata().event_type, + event.metadata().slot + ); + match event { + DexEvent::MeteoraDlmmLbPairAccountEvent(e) => { + println!("=== Meteora DLMM LbPair 账户更新 ==="); + println!("账户地址: {}", e.pubkey); + println!("Token X Mint: {}", e.lb_pair.token_x_mint); + println!("Token Y Mint: {}", e.lb_pair.token_y_mint); + println!("Active ID: {}", e.lb_pair.active_id); + println!("Bin Step: {}", e.lb_pair.bin_step); + println!("Status: {}", e.lb_pair.status); + println!("Reserve X: {}", e.lb_pair.reserve_x); + println!("Reserve Y: {}", e.lb_pair.reserve_y); + println!("Protocol Fee X: {}", e.lb_pair.protocol_fee.amount_x); + println!("Protocol Fee Y: {}", e.lb_pair.protocol_fee.amount_y); + println!("Last Updated At: {}", e.lb_pair.last_updated_at); + println!("====================================="); + } + _ => { + println!("其他事件: {:?}", event); + } + } + } +} diff --git a/examples/raydium_clmm_bitmap_extension_subscription.rs b/examples/raydium_clmm_bitmap_extension_subscription.rs new file mode 100644 index 0000000..8ac0453 --- /dev/null +++ b/examples/raydium_clmm_bitmap_extension_subscription.rs @@ -0,0 +1,158 @@ +use solana_streamer_sdk::streaming::{ + event_parser::{ + protocols::raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, DexEvent, Protocol, + }, + grpc::ClientConfig, + yellowstone_grpc::{AccountFilter, TransactionFilter}, + YellowstoneGrpc, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 初始化日志系统(可选) + // env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + println!("开始 Raydium CLMM TickArrayBitmapExtension 账户数据订阅示例..."); + subscribe_raydium_clmm_bitmap_extension().await?; + Ok(()) +} + +async fn subscribe_raydium_clmm_bitmap_extension() -> Result<(), Box> { + println!("订阅 Raydium CLMM TickArrayBitmapExtension 账户数据..."); + + // 创建客户端配置 + let mut config: ClientConfig = ClientConfig::default(); + // 启用性能监控(可选,有性能开销) + config.enable_metrics = true; + let grpc = YellowstoneGrpc::new_with_config( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + config, + )?; + + println!("gRPC 客户端创建成功"); + + let callback = create_event_callback(); + + // 只订阅 Raydium CLMM 协议 + let protocols = vec![Protocol::RaydiumClmm]; + + println!("监控协议: {:?}", protocols); + + // 账户过滤器 - 订阅 Raydium CLMM 程序拥有的账户 + let account_filter = AccountFilter { + account: vec![], + owner: vec![RAYDIUM_CLMM_PROGRAM_ID.to_string()], + filters: vec![], + }; + + // 交易过滤器(可选,如果只想订阅账户数据,可以留空) + let transaction_filter = TransactionFilter { + account_include: vec![], + account_exclude: vec![], + account_required: vec![], + }; + + // 事件类型过滤器 - 只订阅 TickArrayBitmapExtension 账户事件 + use solana_streamer_sdk::streaming::event_parser::common::EventType; + use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter; + let event_type_filter = Some(EventTypeFilter { + include: vec![EventType::AccountRaydiumClmmTickArrayBitmapExtension], + }); + + println!("开始监听事件,按 Ctrl+C 停止..."); + println!("监控程序: {}", RAYDIUM_CLMM_PROGRAM_ID); + + println!("开始订阅..."); + + grpc.subscribe_events_immediate( + protocols, + None, + vec![transaction_filter], + vec![account_filter], + event_type_filter, + None, + callback, + ) + .await?; + + // 支持 stop 方法,测试代码 - 异步1000秒之后停止 + let grpc_clone = grpc.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(1000)).await; + grpc_clone.stop().await; + }); + + println!("等待 Ctrl+C 停止..."); + tokio::signal::ctrl_c().await?; + + Ok(()) +} + +fn create_event_callback() -> impl Fn(DexEvent) { + |event: DexEvent| { + match event { + DexEvent::RaydiumClmmTickArrayBitmapExtensionAccountEvent(e) => { + println!("=== Raydium CLMM TickArrayBitmapExtension 账户更新 ==="); + println!("账户地址: {}", e.pubkey); + println!("Pool ID: {}", e.tick_array_bitmap_extension.pool_id); + println!("Executable: {}", e.executable); + println!("Lamports: {}", e.lamports); + println!("Owner: {}", e.owner); + println!("Rent Epoch: {}", e.rent_epoch); + println!("Slot: {}", e.metadata.slot); + println!("Signature: {}", e.metadata.signature); + + // 由于使用了 #[repr(C, packed)],需要先复制数据到本地变量 + let positive_bitmap = e.tick_array_bitmap_extension.positive_tick_array_bitmap; + let negative_bitmap = e.tick_array_bitmap_extension.negative_tick_array_bitmap; + + // 打印 positive_tick_array_bitmap 的统计信息 + let positive_non_zero_count = positive_bitmap + .iter() + .flatten() + .filter(|&&x| x != 0) + .count(); + println!("Positive Tick Array Bitmap: {} 个非零值", positive_non_zero_count); + + // 打印 negative_tick_array_bitmap 的统计信息 + let negative_non_zero_count = negative_bitmap + .iter() + .flatten() + .filter(|&&x| x != 0) + .count(); + println!("Negative Tick Array Bitmap: {} 个非零值", negative_non_zero_count); + + // 可选:打印前几个非零值作为示例 + println!("\nPositive Bitmap 前 5 个非零值:"); + let mut count = 0; + for (i, row) in positive_bitmap.iter().enumerate() { + for (j, &value) in row.iter().enumerate() { + if value != 0 && count < 5 { + println!(" [{}][{}] = {}", i, j, value); + count += 1; + } + } + } + + println!("\nNegative Bitmap 前 5 个非零值:"); + let mut count = 0; + for (i, row) in negative_bitmap.iter().enumerate() { + for (j, &value) in row.iter().enumerate() { + if value != 0 && count < 5 { + println!(" [{}][{}] = {}", i, j, value); + count += 1; + } + } + } + + println!("=====================================\n"); + } + _ => { + // 其他事件类型,可以忽略或记录 + // println!("其他事件类型: {:?}", event.metadata().event_type); + } + } + } +} + diff --git a/examples/whirlpool_account_subscription.rs b/examples/whirlpool_account_subscription.rs new file mode 100644 index 0000000..b565fb1 --- /dev/null +++ b/examples/whirlpool_account_subscription.rs @@ -0,0 +1,134 @@ +use solana_streamer_sdk::streaming::{ + event_parser::{ + protocols::whirlpool::parser::WHIRLPOOL_PROGRAM_ID, DexEvent, Protocol, + }, + grpc::ClientConfig, + yellowstone_grpc::{AccountFilter, TransactionFilter}, + YellowstoneGrpc, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 初始化日志系统,设置日志级别为 debug 以便查看详细信息 + //env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); + + println!("开始 Whirlpool 账户数据订阅示例..."); + subscribe_whirlpool_accounts().await?; + Ok(()) +} + +async fn subscribe_whirlpool_accounts() -> Result<(), Box> { + println!("订阅 Whirlpool 账户数据..."); + + // 创建客户端配置 + let mut config: ClientConfig = ClientConfig::default(); + // 启用性能监控(可选,有性能开销) + config.enable_metrics = true; + let grpc = YellowstoneGrpc::new_with_config( + "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), + None, + config, + )?; + + println!("gRPC 客户端创建成功"); + + let callback = create_event_callback(); + + // 只订阅 Whirlpool 协议 + let protocols = vec![Protocol::Whirlpool]; + + println!("监控协议: {:?}", protocols); + + // 账户过滤器 - 订阅 Whirlpool 程序拥有的账户 + let account_filter = AccountFilter { + account: vec![], + owner: vec![WHIRLPOOL_PROGRAM_ID.to_string()], + filters: vec![], + }; + + // 交易过滤器(可选,如果只想订阅账户数据,可以留空) + let transaction_filter = TransactionFilter { + account_include: vec![WHIRLPOOL_PROGRAM_ID.to_string()], + account_exclude: vec![], + account_required: vec![], + }; + + // 事件类型过滤器 - 只订阅账户事件 + use solana_streamer_sdk::streaming::event_parser::common::EventType; + use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter; + let event_type_filter = Some(EventTypeFilter { + include: vec![EventType::AccountWhirlpool], + }); + + println!("开始监听事件,按 Ctrl+C 停止..."); + println!("监控程序: {}", WHIRLPOOL_PROGRAM_ID); + + println!("开始订阅..."); + + grpc.subscribe_events_immediate( + protocols, + None, + vec![transaction_filter], + vec![account_filter], + event_type_filter, + None, + callback, + ) + .await?; + + // 支持 stop 方法,测试代码 - 异步1000秒之后停止 + let grpc_clone = grpc.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(1000)).await; + grpc_clone.stop().await; + }); + + println!("等待 Ctrl+C 停止..."); + tokio::signal::ctrl_c().await?; + + Ok(()) +} + +fn create_event_callback() -> impl Fn(DexEvent) { + |event: DexEvent| { + // println!( + // "🎉 事件接收! 类型: {:?}, slot: {:?}", + // event.metadata().event_type, + // event.metadata().slot + // ); + match event { + DexEvent::WhirlpoolAccountEvent(e) => { + println!("=== Whirlpool 账户更新 ==="); + println!("账户地址: {}", e.pubkey); + println!("Whirlpools Config: {}", e.whirlpool.whirlpools_config); + println!("Token Mint A: {}", e.whirlpool.token_mint_a); + println!("Token Mint B: {}", e.whirlpool.token_mint_b); + println!("Token Vault A: {}", e.whirlpool.token_vault_a); + println!("Token Vault B: {}", e.whirlpool.token_vault_b); + println!("Tick Spacing: {}", e.whirlpool.tick_spacing); + println!("Fee Rate: {}", e.whirlpool.fee_rate); + println!("Protocol Fee Rate: {}", e.whirlpool.protocol_fee_rate); + println!("Liquidity: {}", e.whirlpool.liquidity); + println!("Sqrt Price: {}", e.whirlpool.sqrt_price); + println!("Tick Current Index: {}", e.whirlpool.tick_current_index); + println!("Protocol Fee Owed A: {}", e.whirlpool.protocol_fee_owed_a); + println!("Protocol Fee Owed B: {}", e.whirlpool.protocol_fee_owed_b); + println!("Fee Growth Global A: {}", e.whirlpool.fee_growth_global_a); + println!("Fee Growth Global B: {}", e.whirlpool.fee_growth_global_b); + println!("Reward Last Updated Timestamp: {}", e.whirlpool.reward_last_updated_timestamp); + println!("奖励信息数量: {}", e.whirlpool.reward_infos.len()); + for (i, reward_info) in e.whirlpool.reward_infos.iter().enumerate() { + if reward_info.mint != solana_sdk::pubkey::Pubkey::default() { + println!(" 奖励 {}: Mint={}, Vault={}, Authority={}, Emissions={}, Growth={}", + i, reward_info.mint, reward_info.vault, reward_info.authority, + reward_info.emissions_per_second_x64, reward_info.growth_global_x64); + } + } + println!("====================================="); + } + _ => { + //println!("其他事件: {:?}", event); + } + } + } +} diff --git a/src/streaming/event_parser/common/types.rs b/src/streaming/event_parser/common/types.rs index 7bcaa96..b646d04 100755 --- a/src/streaming/event_parser/common/types.rs +++ b/src/streaming/event_parser/common/types.rs @@ -52,6 +52,8 @@ pub enum ProtocolType { RaydiumClmm, RaydiumAmmV4, MeteoraDammV2, + MeteoraDlmm, + Whirlpool, Common, } @@ -131,8 +133,11 @@ pub enum EventType { AccountRaydiumClmmAmmConfig, AccountRaydiumClmmPoolState, AccountRaydiumClmmTickArrayState, + AccountRaydiumClmmTickArrayBitmapExtension, AccountRaydiumCpmmAmmConfig, AccountRaydiumCpmmPoolState, + AccountMeteoraDlmmLbPair, + AccountWhirlpool, NonceAccount, TokenAccount, @@ -157,8 +162,11 @@ pub const ACCOUNT_EVENT_TYPES: &[EventType] = &[ EventType::AccountRaydiumClmmAmmConfig, EventType::AccountRaydiumClmmPoolState, EventType::AccountRaydiumClmmTickArrayState, + EventType::AccountRaydiumClmmTickArrayBitmapExtension, EventType::AccountRaydiumCpmmAmmConfig, EventType::AccountRaydiumCpmmPoolState, + EventType::AccountMeteoraDlmmLbPair, + EventType::AccountWhirlpool, EventType::TokenAccount, EventType::NonceAccount, ]; @@ -230,8 +238,13 @@ impl fmt::Display for EventType { EventType::AccountRaydiumClmmTickArrayState => { write!(f, "AccountRaydiumClmmTickArrayState") } + EventType::AccountRaydiumClmmTickArrayBitmapExtension => { + write!(f, "AccountRaydiumClmmTickArrayBitmapExtension") + } EventType::AccountRaydiumCpmmAmmConfig => write!(f, "AccountRaydiumCpmmAmmConfig"), EventType::AccountRaydiumCpmmPoolState => write!(f, "AccountRaydiumCpmmPoolState"), + EventType::AccountMeteoraDlmmLbPair => write!(f, "AccountMeteoraDlmmLbPair"), + EventType::AccountWhirlpool => write!(f, "AccountWhirlpool"), EventType::TokenAccount => write!(f, "TokenAccount"), EventType::NonceAccount => write!(f, "NonceAccount"), EventType::BlockMeta => write!(f, "BlockMeta"), diff --git a/src/streaming/event_parser/core/dispatcher.rs b/src/streaming/event_parser/core/dispatcher.rs index 77691ce..fed511e 100644 --- a/src/streaming/event_parser/core/dispatcher.rs +++ b/src/streaming/event_parser/core/dispatcher.rs @@ -11,9 +11,11 @@ use crate::streaming::event_parser::{ common::EventMetadata, core::common_event_parser::{CommonEventParser, COMPUTE_BUDGET_PROGRAM_ID}, protocols::{ - bonk::parser as bonk, meteora_damm_v2::parser as meteora_damm_v2, pumpfun::parser as pumpfun, + bonk::parser as bonk, meteora_damm_v2::parser as meteora_damm_v2, + meteora_dlmm::parser as meteora_dlmm, pumpfun::parser as pumpfun, pumpswap::parser as pumpswap, raydium_amm_v4::parser as raydium_amm_v4, raydium_clmm::parser as raydium_clmm, raydium_cpmm::parser as raydium_cpmm, + whirlpool::parser as whirlpool, }, DexEvent, Protocol, }; @@ -54,6 +56,8 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, + Protocol::Whirlpool => ProtocolType::Whirlpool, }; match protocol { @@ -99,6 +103,14 @@ impl EventDispatcher { accounts, metadata, ), + Protocol::MeteoraDlmm => { + // Meteora DLMM 目前不需要解析指令数据,返回 None + None + } + Protocol::Whirlpool => { + // Whirlpool 目前不需要解析指令数据,返回 None + None + } } } @@ -129,6 +141,8 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, + Protocol::Whirlpool => ProtocolType::Whirlpool, }; match protocol { @@ -167,6 +181,14 @@ impl EventDispatcher { inner_instruction_data, metadata, ), + Protocol::MeteoraDlmm => { + // Meteora DLMM 目前不需要解析 inner instruction 数据,返回 None + None + } + Protocol::Whirlpool => { + // Whirlpool 目前不需要解析 inner instruction 数据,返回 None + None + } } } @@ -187,6 +209,10 @@ impl EventDispatcher { Some(Protocol::RaydiumAmmV4) } else if program_id == &meteora_damm_v2::METEORA_DAMM_V2_PROGRAM_ID { Some(Protocol::MeteoraDammV2) + } else if program_id == &meteora_dlmm::METEORA_DLMM_PROGRAM_ID { + Some(Protocol::MeteoraDlmm) + } else if program_id == &whirlpool::WHIRLPOOL_PROGRAM_ID { + Some(Protocol::Whirlpool) } else { None } @@ -225,6 +251,8 @@ impl EventDispatcher { Protocol::RaydiumClmm => raydium_clmm::RAYDIUM_CLMM_PROGRAM_ID, Protocol::RaydiumAmmV4 => raydium_amm_v4::RAYDIUM_AMM_V4_PROGRAM_ID, Protocol::MeteoraDammV2 => meteora_damm_v2::METEORA_DAMM_V2_PROGRAM_ID, + Protocol::MeteoraDlmm => meteora_dlmm::METEORA_DLMM_PROGRAM_ID, + Protocol::Whirlpool => whirlpool::WHIRLPOOL_PROGRAM_ID, } } @@ -261,6 +289,8 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, + Protocol::Whirlpool => ProtocolType::Whirlpool, }; match protocol { @@ -284,6 +314,12 @@ impl EventDispatcher { // Meteora DAMM 目前不需要解析账户数据,返回 None None } + Protocol::MeteoraDlmm => { + meteora_dlmm::parse_meteora_dlmm_account_data(discriminator, account, metadata) + } + Protocol::Whirlpool => { + whirlpool::parse_whirlpool_account_data(discriminator, account, metadata) + } } } } diff --git a/src/streaming/event_parser/core/traits.rs b/src/streaming/event_parser/core/traits.rs index 0dc95f8..64251b1 100755 --- a/src/streaming/event_parser/core/traits.rs +++ b/src/streaming/event_parser/core/traits.rs @@ -8,11 +8,13 @@ use crate::streaming::event_parser::core::common_event_parser::{ use crate::streaming::event_parser::protocols::block::block_meta_event::BlockMetaEvent; use crate::streaming::event_parser::protocols::bonk::events::*; use crate::streaming::event_parser::protocols::meteora_damm_v2::events::*; +use crate::streaming::event_parser::protocols::meteora_dlmm::events::*; use crate::streaming::event_parser::protocols::pumpfun::events::*; use crate::streaming::event_parser::protocols::pumpswap::events::*; use crate::streaming::event_parser::protocols::raydium_amm_v4::events::*; use crate::streaming::event_parser::protocols::raydium_clmm::events::*; use crate::streaming::event_parser::protocols::raydium_cpmm::events::*; +use crate::streaming::event_parser::protocols::whirlpool::events::*; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -65,6 +67,7 @@ pub enum DexEvent { RaydiumClmmAmmConfigAccountEvent(RaydiumClmmAmmConfigAccountEvent), RaydiumClmmPoolStateAccountEvent(RaydiumClmmPoolStateAccountEvent), RaydiumClmmTickArrayStateAccountEvent(RaydiumClmmTickArrayStateAccountEvent), + RaydiumClmmTickArrayBitmapExtensionAccountEvent(RaydiumClmmTickArrayBitmapExtensionAccountEvent), // Raydium CPMM events RaydiumCpmmSwapEvent(RaydiumCpmmSwapEvent), @@ -81,6 +84,12 @@ pub enum DexEvent { MeteoraDammV2InitializeCustomizablePoolEvent(MeteoraDammV2InitializeCustomizablePoolEvent), MeteoraDammV2InitializePoolWithDynamicConfigEvent(MeteoraDammV2InitializePoolWithDynamicConfigEvent), + // Meteora DLMM events + MeteoraDlmmLbPairAccountEvent(MeteoraDlmmLbPairAccountEvent), + + // Whirlpool events + WhirlpoolAccountEvent(WhirlpoolAccountEvent), + // Common events TokenAccountEvent(TokenAccountEvent), NonceAccountEvent(NonceAccountEvent), @@ -130,6 +139,7 @@ impl DexEvent { DexEvent::RaydiumClmmAmmConfigAccountEvent(e) => &e.metadata, DexEvent::RaydiumClmmPoolStateAccountEvent(e) => &e.metadata, DexEvent::RaydiumClmmTickArrayStateAccountEvent(e) => &e.metadata, + DexEvent::RaydiumClmmTickArrayBitmapExtensionAccountEvent(e) => &e.metadata, DexEvent::RaydiumCpmmSwapEvent(e) => &e.metadata, DexEvent::RaydiumCpmmDepositEvent(e) => &e.metadata, DexEvent::RaydiumCpmmWithdrawEvent(e) => &e.metadata, @@ -141,6 +151,8 @@ impl DexEvent { DexEvent::MeteoraDammV2InitializePoolEvent(e) => &e.metadata, DexEvent::MeteoraDammV2InitializeCustomizablePoolEvent(e) => &e.metadata, DexEvent::MeteoraDammV2InitializePoolWithDynamicConfigEvent(e) => &e.metadata, + DexEvent::MeteoraDlmmLbPairAccountEvent(e) => &e.metadata, + DexEvent::WhirlpoolAccountEvent(e) => &e.metadata, DexEvent::TokenAccountEvent(e) => &e.metadata, DexEvent::NonceAccountEvent(e) => &e.metadata, DexEvent::TokenInfoEvent(e) => &e.metadata, @@ -189,6 +201,7 @@ impl DexEvent { DexEvent::RaydiumClmmAmmConfigAccountEvent(e) => &mut e.metadata, DexEvent::RaydiumClmmPoolStateAccountEvent(e) => &mut e.metadata, DexEvent::RaydiumClmmTickArrayStateAccountEvent(e) => &mut e.metadata, + DexEvent::RaydiumClmmTickArrayBitmapExtensionAccountEvent(e) => &mut e.metadata, DexEvent::RaydiumCpmmSwapEvent(e) => &mut e.metadata, DexEvent::RaydiumCpmmDepositEvent(e) => &mut e.metadata, DexEvent::RaydiumCpmmWithdrawEvent(e) => &mut e.metadata, @@ -200,6 +213,8 @@ impl DexEvent { DexEvent::MeteoraDammV2InitializePoolEvent(e) => &mut e.metadata, DexEvent::MeteoraDammV2InitializeCustomizablePoolEvent(e) => &mut e.metadata, DexEvent::MeteoraDammV2InitializePoolWithDynamicConfigEvent(e) => &mut e.metadata, + DexEvent::MeteoraDlmmLbPairAccountEvent(e) => &mut e.metadata, + DexEvent::WhirlpoolAccountEvent(e) => &mut e.metadata, DexEvent::TokenAccountEvent(e) => &mut e.metadata, DexEvent::NonceAccountEvent(e) => &mut e.metadata, DexEvent::TokenInfoEvent(e) => &mut e.metadata, diff --git a/src/streaming/event_parser/protocols/meteora_dlmm/events.rs b/src/streaming/event_parser/protocols/meteora_dlmm/events.rs new file mode 100644 index 0000000..31f670a --- /dev/null +++ b/src/streaming/event_parser/protocols/meteora_dlmm/events.rs @@ -0,0 +1,22 @@ +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::meteora_dlmm::types::LbPair; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +/// LbPair 账户事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmLbPairAccountEvent { + pub metadata: EventMetadata, + pub pubkey: Pubkey, + pub executable: bool, + pub lamports: u64, + pub owner: Pubkey, + pub rent_epoch: u64, + pub lb_pair: LbPair, +} + +/// 事件鉴别器常量 +pub mod discriminators { + // 账户鉴别器 + pub const LB_PAIR: &[u8] = &[33, 11, 49, 98, 181, 101, 177, 13]; +} diff --git a/src/streaming/event_parser/protocols/meteora_dlmm/mod.rs b/src/streaming/event_parser/protocols/meteora_dlmm/mod.rs new file mode 100644 index 0000000..86156fa --- /dev/null +++ b/src/streaming/event_parser/protocols/meteora_dlmm/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; +pub mod types; + +pub use events::*; diff --git a/src/streaming/event_parser/protocols/meteora_dlmm/parser.rs b/src/streaming/event_parser/protocols/meteora_dlmm/parser.rs new file mode 100644 index 0000000..77dc4d3 --- /dev/null +++ b/src/streaming/event_parser/protocols/meteora_dlmm/parser.rs @@ -0,0 +1,25 @@ +use crate::streaming::event_parser::protocols::meteora_dlmm::discriminators; +use solana_sdk::pubkey::Pubkey; + +/// Meteora DLMM 程序ID +pub const METEORA_DLMM_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"); + +/// 解析 Meteora DLMM 账户数据 +/// +/// 根据判别器路由到具体的账户解析函数 +pub fn parse_meteora_dlmm_account_data( + discriminator: &[u8], + account: &crate::streaming::grpc::AccountPretty, + metadata: crate::streaming::event_parser::common::EventMetadata, +) -> Option { + match discriminator { + discriminators::LB_PAIR => { + crate::streaming::event_parser::protocols::meteora_dlmm::types::lb_pair_parser( + account, + metadata, + ) + } + _ => None, + } +} diff --git a/src/streaming/event_parser/protocols/meteora_dlmm/types.rs b/src/streaming/event_parser/protocols/meteora_dlmm/types.rs new file mode 100644 index 0000000..1e7432c --- /dev/null +++ b/src/streaming/event_parser/protocols/meteora_dlmm/types.rs @@ -0,0 +1,129 @@ +use borsh::BorshDeserialize; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +use crate::streaming::{ + event_parser::{ + common::{EventMetadata, EventType}, + protocols::meteora_dlmm::MeteoraDlmmLbPairAccountEvent, + DexEvent, + }, + grpc::AccountPretty, +}; + +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct StaticParameters { + pub base_factor: u16, + pub filter_period: u16, + pub decay_period: u16, + pub reduction_factor: u16, + pub variable_fee_control: u32, + pub max_volatility_accumulator: u32, + pub min_bin_id: i32, + pub max_bin_id: i32, + pub protocol_share: u16, + pub base_fee_power_factor: u8, + pub padding: [u8; 5], +} + +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct VariableParameters { + pub volatility_accumulator: u32, + pub volatility_reference: u32, + pub index_reference: i32, + pub padding: [u8; 4], + pub last_update_timestamp: i64, + pub padding1: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct ProtocolFee { + pub amount_x: u64, + pub amount_y: u64, +} + +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct RewardInfo { + pub mint: Pubkey, + pub vault: Pubkey, + pub funder: Pubkey, + pub reward_duration: u64, + pub reward_duration_end: u64, + pub reward_rate: u128, + pub last_update_time: u64, + pub cumulative_seconds_with_empty_liquidity_reward: u64, +} + +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct LbPair { + pub parameters: StaticParameters, + pub v_parameters: VariableParameters, + pub bump_seed: [u8; 1], + pub bin_step_seed: [u8; 2], + pub pair_type: u8, + pub active_id: i32, + pub bin_step: u16, + pub status: u8, + pub require_base_factor_seed: u8, + pub base_factor_seed: [u8; 2], + pub activation_type: u8, + pub creator_pool_on_off_control: u8, + pub token_x_mint: Pubkey, + pub token_y_mint: Pubkey, + pub reserve_x: Pubkey, + pub reserve_y: Pubkey, + pub protocol_fee: ProtocolFee, + pub padding1: [u8; 32], + pub reward_infos: [RewardInfo; 2], + pub oracle: Pubkey, + pub bin_array_bitmap: [u64; 16], + pub last_updated_at: i64, + pub padding2: [u8; 32], + pub pre_activation_swap_address: Pubkey, + pub base_key: Pubkey, + pub activation_point: u64, + pub pre_activation_duration: u64, + pub padding3: [u8; 8], + pub padding4: u64, + pub creator: Pubkey, + pub token_mint_x_program_flag: u8, + pub token_mint_y_program_flag: u8, + pub reserved: [u8; 22], +} + +pub const LB_PAIR_SIZE: usize = std::mem::size_of::(); + +pub fn lb_pair_decode(data: &[u8]) -> Option { + if data.len() < LB_PAIR_SIZE { + return None; + } + borsh::from_slice::(&data[..LB_PAIR_SIZE]).ok() +} + +pub fn lb_pair_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option { + metadata.event_type = EventType::AccountMeteoraDlmmLbPair; + + if account.data.len() < LB_PAIR_SIZE + 8 { + return None; + } + if let Some(lb_pair) = lb_pair_decode(&account.data[8..LB_PAIR_SIZE + 8]) { + Some(DexEvent::MeteoraDlmmLbPairAccountEvent( + MeteoraDlmmLbPairAccountEvent { + metadata, + pubkey: account.pubkey, + executable: account.executable, + lamports: account.lamports, + owner: account.owner, + rent_epoch: account.rent_epoch, + lb_pair, + }, + )) + } else { + None + } +} diff --git a/src/streaming/event_parser/protocols/mod.rs b/src/streaming/event_parser/protocols/mod.rs index 0d44048..7f13df0 100755 --- a/src/streaming/event_parser/protocols/mod.rs +++ b/src/streaming/event_parser/protocols/mod.rs @@ -1,11 +1,13 @@ pub mod block; pub mod bonk; pub mod meteora_damm_v2; +pub mod meteora_dlmm; pub mod pumpfun; pub mod pumpswap; pub mod raydium_amm_v4; pub mod raydium_clmm; pub mod raydium_cpmm; pub mod types; +pub mod whirlpool; pub use block::block_meta_event::BlockMetaEvent; pub use types::Protocol; diff --git a/src/streaming/event_parser/protocols/raydium_clmm/events.rs b/src/streaming/event_parser/protocols/raydium_clmm/events.rs index 492fa57..d70a295 100755 --- a/src/streaming/event_parser/protocols/raydium_clmm/events.rs +++ b/src/streaming/event_parser/protocols/raydium_clmm/events.rs @@ -1,5 +1,5 @@ use crate::streaming::event_parser::common::EventMetadata; -use crate::streaming::event_parser::protocols::raydium_clmm::types::{PoolState, TickArrayState}; +use crate::streaming::event_parser::protocols::raydium_clmm::types::{PoolState, TickArrayBitmapExtension, TickArrayState}; use crate::{ streaming::event_parser::protocols::raydium_clmm::types::AmmConfig, }; @@ -247,6 +247,18 @@ pub struct RaydiumClmmTickArrayStateAccountEvent { pub tick_array_state: TickArrayState, } +/// TickArrayBitmapExtension 账户事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct RaydiumClmmTickArrayBitmapExtensionAccountEvent { + pub metadata: EventMetadata, + pub pubkey: Pubkey, + pub executable: bool, + pub lamports: u64, + pub owner: Pubkey, + pub rent_epoch: u64, + pub tick_array_bitmap_extension: TickArrayBitmapExtension, +} + /// 事件鉴别器常量 pub mod discriminators { // 指令鉴别器 @@ -263,4 +275,5 @@ pub mod discriminators { pub const AMM_CONFIG: &[u8] = &[218, 244, 33, 104, 203, 203, 43, 111]; pub const POOL_STATE: &[u8] = &[247, 237, 227, 245, 215, 195, 222, 70]; pub const TICK_ARRAY_STATE: &[u8] = &[192, 155, 85, 205, 49, 249, 129, 42]; + pub const TICK_ARRAY_BITMAP_EXTENSION: &[u8] = &[60, 150, 36, 219, 97, 128, 139, 153]; } diff --git a/src/streaming/event_parser/protocols/raydium_clmm/parser.rs b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs index b127644..7901104 100755 --- a/src/streaming/event_parser/protocols/raydium_clmm/parser.rs +++ b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs @@ -79,6 +79,9 @@ pub fn parse_raydium_clmm_account_data( discriminators::TICK_ARRAY_STATE => { crate::streaming::event_parser::protocols::raydium_clmm::types::tick_array_state_parser(account, metadata) } + discriminators::TICK_ARRAY_BITMAP_EXTENSION => { + crate::streaming::event_parser::protocols::raydium_clmm::types::tick_array_bitmap_extension_parser(account, metadata) + } _ => None, } } diff --git a/src/streaming/event_parser/protocols/raydium_clmm/types.rs b/src/streaming/event_parser/protocols/raydium_clmm/types.rs index 5f3695c..bb0aa2e 100644 --- a/src/streaming/event_parser/protocols/raydium_clmm/types.rs +++ b/src/streaming/event_parser/protocols/raydium_clmm/types.rs @@ -7,7 +7,7 @@ use crate::streaming::{ common::{EventMetadata, EventType}, protocols::raydium_clmm::{ RaydiumClmmAmmConfigAccountEvent, RaydiumClmmPoolStateAccountEvent, - RaydiumClmmTickArrayStateAccountEvent, + RaydiumClmmTickArrayBitmapExtensionAccountEvent, RaydiumClmmTickArrayStateAccountEvent, }, DexEvent, }, @@ -231,3 +231,106 @@ pub fn tick_array_state_parser( None } } + +// EXTENSION_TICKARRAY_BITMAP_SIZE 常量,根据 Raydium CLMM 实现,通常为 14 +pub const EXTENSION_TICKARRAY_BITMAP_SIZE: usize = 14; + +#[repr(C, packed)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct TickArrayBitmapExtension { + pub pool_id: Pubkey, + /// Packed initialized tick array state for start_tick_index is positive + pub positive_tick_array_bitmap: [[u64; 8]; EXTENSION_TICKARRAY_BITMAP_SIZE], + /// Packed initialized tick array state for start_tick_index is negative + pub negative_tick_array_bitmap: [[u64; 8]; EXTENSION_TICKARRAY_BITMAP_SIZE], +} + +impl Default for TickArrayBitmapExtension { + fn default() -> Self { + Self { + pool_id: Pubkey::default(), + positive_tick_array_bitmap: [[0u64; 8]; EXTENSION_TICKARRAY_BITMAP_SIZE], + negative_tick_array_bitmap: [[0u64; 8]; EXTENSION_TICKARRAY_BITMAP_SIZE], + } + } +} + +pub const TICK_ARRAY_BITMAP_EXTENSION_SIZE: usize = 32 + (EXTENSION_TICKARRAY_BITMAP_SIZE * 8 * 8) + (EXTENSION_TICKARRAY_BITMAP_SIZE * 8 * 8); + +pub fn tick_array_bitmap_extension_decode(data: &[u8]) -> Option { + if data.len() < TICK_ARRAY_BITMAP_EXTENSION_SIZE { + return None; + } + + // 由于使用了 #[repr(C, packed)],我们需要手动解析 + let mut offset = 0; + + // 读取 pool_id (32 bytes) + if data.len() < offset + 32 { + return None; + } + let pool_id = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // 读取 positive_tick_array_bitmap + let mut positive_tick_array_bitmap = [[0u64; 8]; EXTENSION_TICKARRAY_BITMAP_SIZE]; + for i in 0..EXTENSION_TICKARRAY_BITMAP_SIZE { + for j in 0..8 { + if data.len() < offset + 8 { + return None; + } + positive_tick_array_bitmap[i][j] = u64::from_le_bytes( + data[offset..offset + 8].try_into().ok()? + ); + offset += 8; + } + } + + // 读取 negative_tick_array_bitmap + let mut negative_tick_array_bitmap = [[0u64; 8]; EXTENSION_TICKARRAY_BITMAP_SIZE]; + for i in 0..EXTENSION_TICKARRAY_BITMAP_SIZE { + for j in 0..8 { + if data.len() < offset + 8 { + return None; + } + negative_tick_array_bitmap[i][j] = u64::from_le_bytes( + data[offset..offset + 8].try_into().ok()? + ); + offset += 8; + } + } + + Some(TickArrayBitmapExtension { + pool_id, + positive_tick_array_bitmap, + negative_tick_array_bitmap, + }) +} + +pub fn tick_array_bitmap_extension_parser( + account: &AccountPretty, + mut metadata: EventMetadata, +) -> Option { + metadata.event_type = EventType::AccountRaydiumClmmTickArrayBitmapExtension; + + if account.data.len() < TICK_ARRAY_BITMAP_EXTENSION_SIZE + 8 { + return None; + } + if let Some(tick_array_bitmap_extension) = + tick_array_bitmap_extension_decode(&account.data[8..TICK_ARRAY_BITMAP_EXTENSION_SIZE + 8]) + { + Some(DexEvent::RaydiumClmmTickArrayBitmapExtensionAccountEvent( + RaydiumClmmTickArrayBitmapExtensionAccountEvent { + metadata, + pubkey: account.pubkey, + executable: account.executable, + lamports: account.lamports, + owner: account.owner, + rent_epoch: account.rent_epoch, + tick_array_bitmap_extension, + }, + )) + } else { + None + } +} diff --git a/src/streaming/event_parser/protocols/types.rs b/src/streaming/event_parser/protocols/types.rs index 3d864dc..86f41ec 100755 --- a/src/streaming/event_parser/protocols/types.rs +++ b/src/streaming/event_parser/protocols/types.rs @@ -1,8 +1,9 @@ use crate::streaming::event_parser::protocols::{ bonk::parser::BONK_PROGRAM_ID, meteora_damm_v2::parser::METEORA_DAMM_V2_PROGRAM_ID, - pumpfun::parser::PUMPFUN_PROGRAM_ID, pumpswap::parser::PUMPSWAP_PROGRAM_ID, - raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID, raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, - raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID, + meteora_dlmm::parser::METEORA_DLMM_PROGRAM_ID, pumpfun::parser::PUMPFUN_PROGRAM_ID, + pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID, + raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID, + whirlpool::parser::WHIRLPOOL_PROGRAM_ID, }; use anyhow::{anyhow, Result}; use solana_sdk::pubkey::Pubkey; @@ -17,6 +18,8 @@ pub enum Protocol { RaydiumClmm, RaydiumAmmV4, MeteoraDammV2, + MeteoraDlmm, + Whirlpool, } impl Protocol { @@ -29,6 +32,8 @@ impl Protocol { Protocol::RaydiumClmm => vec![RAYDIUM_CLMM_PROGRAM_ID], Protocol::RaydiumAmmV4 => vec![RAYDIUM_AMM_V4_PROGRAM_ID], Protocol::MeteoraDammV2 => vec![METEORA_DAMM_V2_PROGRAM_ID], + Protocol::MeteoraDlmm => vec![METEORA_DLMM_PROGRAM_ID], + Protocol::Whirlpool => vec![WHIRLPOOL_PROGRAM_ID], } } } @@ -43,6 +48,8 @@ impl std::fmt::Display for Protocol { Protocol::RaydiumClmm => write!(f, "RaydiumClmm"), Protocol::RaydiumAmmV4 => write!(f, "RaydiumAmmV4"), Protocol::MeteoraDammV2 => write!(f, "MeteoraDammV2"), + Protocol::MeteoraDlmm => write!(f, "MeteoraDlmm"), + Protocol::Whirlpool => write!(f, "Whirlpool"), } } } @@ -59,6 +66,8 @@ impl std::str::FromStr for Protocol { "raydiumclmm" => Ok(Protocol::RaydiumClmm), "raydiumammv4" => Ok(Protocol::RaydiumAmmV4), "meteoradamm_v2" => Ok(Protocol::MeteoraDammV2), + "meteoradlmm" => Ok(Protocol::MeteoraDlmm), + "whirlpool" => Ok(Protocol::Whirlpool), _ => Err(anyhow!("Unsupported protocol: {}", s)), } } diff --git a/src/streaming/event_parser/protocols/whirlpool/events.rs b/src/streaming/event_parser/protocols/whirlpool/events.rs new file mode 100644 index 0000000..9d5c561 --- /dev/null +++ b/src/streaming/event_parser/protocols/whirlpool/events.rs @@ -0,0 +1,23 @@ +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::whirlpool::types::Whirlpool; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +/// Whirlpool 账户事件 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct WhirlpoolAccountEvent { + pub metadata: EventMetadata, + pub pubkey: Pubkey, + pub executable: bool, + pub lamports: u64, + pub owner: Pubkey, + pub rent_epoch: u64, + pub whirlpool: Whirlpool, +} + +/// 事件鉴别器常量 +pub mod discriminators { + // 账户鉴别器 - Anchor discriminator for "Whirlpool" account + // 这是通过 Anchor 的账户名称 "account:Whirlpool" 计算得出的 8 字节哈希 + pub const WHIRLPOOL: &[u8] = &[63, 149, 209, 12, 225, 128, 99, 9]; +} diff --git a/src/streaming/event_parser/protocols/whirlpool/mod.rs b/src/streaming/event_parser/protocols/whirlpool/mod.rs new file mode 100644 index 0000000..86156fa --- /dev/null +++ b/src/streaming/event_parser/protocols/whirlpool/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; +pub mod types; + +pub use events::*; diff --git a/src/streaming/event_parser/protocols/whirlpool/parser.rs b/src/streaming/event_parser/protocols/whirlpool/parser.rs new file mode 100644 index 0000000..c022e72 --- /dev/null +++ b/src/streaming/event_parser/protocols/whirlpool/parser.rs @@ -0,0 +1,25 @@ +use crate::streaming::event_parser::protocols::whirlpool::discriminators; +use solana_sdk::pubkey::Pubkey; + +/// Whirlpool 程序ID +pub const WHIRLPOOL_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"); + +/// 解析 Whirlpool 账户数据 +/// +/// 根据判别器路由到具体的账户解析函数 +pub fn parse_whirlpool_account_data( + discriminator: &[u8], + account: &crate::streaming::grpc::AccountPretty, + metadata: crate::streaming::event_parser::common::EventMetadata, +) -> Option { + match discriminator { + discriminators::WHIRLPOOL => { + crate::streaming::event_parser::protocols::whirlpool::types::whirlpool_parser( + account, + metadata, + ) + } + _ => None, + } +} diff --git a/src/streaming/event_parser/protocols/whirlpool/types.rs b/src/streaming/event_parser/protocols/whirlpool/types.rs new file mode 100644 index 0000000..579b468 --- /dev/null +++ b/src/streaming/event_parser/protocols/whirlpool/types.rs @@ -0,0 +1,291 @@ +use borsh::BorshDeserialize; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +use crate::streaming::{ + event_parser::{ + common::{EventMetadata, EventType}, + protocols::whirlpool::WhirlpoolAccountEvent, + DexEvent, + }, + grpc::AccountPretty, +}; + +// Number of rewards supported by Whirlpools +pub const NUM_REWARDS: usize = 3; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct WhirlpoolRewardInfo { + /// Reward token mint. + pub mint: Pubkey, + /// Reward vault token account. + pub vault: Pubkey, + /// Authority account that has permission to initialize the reward and set emissions. + pub authority: Pubkey, + /// Q64.64 number that indicates how many tokens per second are earned per unit of liquidity. + pub emissions_per_second_x64: u128, + /// Q64.64 number that tracks the total tokens earned per unit of liquidity since the reward + /// emissions were turned on. + pub growth_global_x64: u128, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Whirlpool { + pub whirlpools_config: Pubkey, + pub whirlpool_bump: [u8; 1], + pub tick_spacing: u16, + pub fee_tier_index_seed: [u8; 2], + pub fee_rate: u16, + pub protocol_fee_rate: u16, + pub liquidity: u128, + pub sqrt_price: u128, + pub tick_current_index: i32, + pub protocol_fee_owed_a: u64, + pub protocol_fee_owed_b: u64, + pub token_mint_a: Pubkey, + pub token_vault_a: Pubkey, + pub fee_growth_global_a: u128, + pub token_mint_b: Pubkey, + pub token_vault_b: Pubkey, + pub fee_growth_global_b: u128, + pub reward_last_updated_timestamp: u64, + pub reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS], +} + +// Whirlpool::LEN = 8 (discriminator) + 261 + 384 = 653 +// 261 = 32 + 1 + 2 + 2 + 2 + 2 + 16 + 16 + 4 + 8 + 8 + 32 + 32 + 16 + 32 + 32 + 16 + 8 +// 384 = 3 * 128 (each WhirlpoolRewardInfo is 128 bytes) +// 总数据大小(不包括 discriminator)= 261 + 384 = 645 +pub const WHIRLPOOL_SIZE: usize = 261 + 384; // 645 bytes (不包括 discriminator) +pub const WHIRLPOOL_REWARD_INFO_SIZE: usize = 128; + +pub fn whirlpool_decode(data: &[u8]) -> Option { + if data.len() < WHIRLPOOL_SIZE { + return None; + } + + let mut offset = 0; + + // whirlpools_config: Pubkey (32 bytes) + let whirlpools_config = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // whirlpool_bump: [u8; 1] (1 byte) + let whirlpool_bump = [data[offset]]; + offset += 1; + + // tick_spacing: u16 (2 bytes) + let tick_spacing = u16::from_le_bytes([data[offset], data[offset + 1]]); + offset += 2; + + // fee_tier_index_seed: [u8; 2] (2 bytes) + let fee_tier_index_seed = [data[offset], data[offset + 1]]; + offset += 2; + + // fee_rate: u16 (2 bytes) + let fee_rate = u16::from_le_bytes([data[offset], data[offset + 1]]); + offset += 2; + + // protocol_fee_rate: u16 (2 bytes) + let protocol_fee_rate = u16::from_le_bytes([data[offset], data[offset + 1]]); + offset += 2; + + // liquidity: u128 (16 bytes) + let mut liquidity_bytes = [0u8; 16]; + liquidity_bytes.copy_from_slice(&data[offset..offset + 16]); + let liquidity = u128::from_le_bytes(liquidity_bytes); + offset += 16; + + // sqrt_price: u128 (16 bytes) + let mut sqrt_price_bytes = [0u8; 16]; + sqrt_price_bytes.copy_from_slice(&data[offset..offset + 16]); + let sqrt_price = u128::from_le_bytes(sqrt_price_bytes); + offset += 16; + + // tick_current_index: i32 (4 bytes) + let tick_current_index = i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + offset += 4; + + // protocol_fee_owed_a: u64 (8 bytes) + let protocol_fee_owed_a = u64::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]); + offset += 8; + + // protocol_fee_owed_b: u64 (8 bytes) + let protocol_fee_owed_b = u64::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]); + offset += 8; + + // token_mint_a: Pubkey (32 bytes) + let token_mint_a = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // token_vault_a: Pubkey (32 bytes) + let token_vault_a = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // fee_growth_global_a: u128 (16 bytes) + let mut fee_growth_global_a_bytes = [0u8; 16]; + fee_growth_global_a_bytes.copy_from_slice(&data[offset..offset + 16]); + let fee_growth_global_a = u128::from_le_bytes(fee_growth_global_a_bytes); + offset += 16; + + // token_mint_b: Pubkey (32 bytes) + let token_mint_b = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // token_vault_b: Pubkey (32 bytes) + let token_vault_b = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // fee_growth_global_b: u128 (16 bytes) + let mut fee_growth_global_b_bytes = [0u8; 16]; + fee_growth_global_b_bytes.copy_from_slice(&data[offset..offset + 16]); + let fee_growth_global_b = u128::from_le_bytes(fee_growth_global_b_bytes); + offset += 16; + + // reward_last_updated_timestamp: u64 (8 bytes) + let reward_last_updated_timestamp = u64::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]); + offset += 8; + + // reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS] (384 bytes) + // 检查是否有足够的数据来解析所有奖励信息 + if data.len() < offset + (NUM_REWARDS * WHIRLPOOL_REWARD_INFO_SIZE) { + log::warn!( + "Whirlpool 奖励信息数据不足: 需要 {} 字节,实际 {} 字节", + offset + (NUM_REWARDS * WHIRLPOOL_REWARD_INFO_SIZE), + data.len() + ); + return None; + } + + let mut reward_infos = [WhirlpoolRewardInfo::default(); NUM_REWARDS]; + for i in 0..NUM_REWARDS { + + // mint: Pubkey (32 bytes) + let mint = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // vault: Pubkey (32 bytes) + let vault = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // authority: Pubkey (32 bytes) + let authority = Pubkey::try_from(&data[offset..offset + 32]).ok()?; + offset += 32; + + // emissions_per_second_x64: u128 (16 bytes) + let mut emissions_bytes = [0u8; 16]; + emissions_bytes.copy_from_slice(&data[offset..offset + 16]); + let emissions_per_second_x64 = u128::from_le_bytes(emissions_bytes); + offset += 16; + + // growth_global_x64: u128 (16 bytes) + let mut growth_bytes = [0u8; 16]; + growth_bytes.copy_from_slice(&data[offset..offset + 16]); + let growth_global_x64 = u128::from_le_bytes(growth_bytes); + offset += 16; + + reward_infos[i] = WhirlpoolRewardInfo { + mint, + vault, + authority, + emissions_per_second_x64, + growth_global_x64, + }; + } + + Some(Whirlpool { + whirlpools_config, + whirlpool_bump, + tick_spacing, + fee_tier_index_seed, + fee_rate, + protocol_fee_rate, + liquidity, + sqrt_price, + tick_current_index, + protocol_fee_owed_a, + protocol_fee_owed_b, + token_mint_a, + token_vault_a, + fee_growth_global_a, + token_mint_b, + token_vault_b, + fee_growth_global_b, + reward_last_updated_timestamp, + reward_infos, + }) +} + +pub fn whirlpool_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option { + metadata.event_type = EventType::AccountWhirlpool; + + // 账户总大小应该是 8 (discriminator) + 645 (数据) = 653 字节 + let expected_size = 8 + WHIRLPOOL_SIZE; + if account.data.len() < expected_size { + log::warn!( + "Whirlpool 账户数据长度不足: 需要至少 {} 字节,实际 {} 字节", + expected_size, + account.data.len() + ); + return None; + } + + log::debug!( + "开始解析 Whirlpool 账户: pubkey={}, 数据长度={}, 期望长度={}", + account.pubkey, + account.data.len(), + expected_size + ); + + // 跳过前 8 字节的 discriminator,解析接下来的 645 字节 + if let Some(whirlpool) = whirlpool_decode(&account.data[8..8 + WHIRLPOOL_SIZE]) { + Some(DexEvent::WhirlpoolAccountEvent(WhirlpoolAccountEvent { + metadata, + pubkey: account.pubkey, + executable: account.executable, + lamports: account.lamports, + owner: account.owner, + rent_epoch: account.rent_epoch, + whirlpool, + })) + } else { + log::warn!( + "Whirlpool 账户数据解析失败: pubkey={}, 数据长度={}", + account.pubkey, + account.data.len() + ); + None + } +}