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/src/streaming/event_parser/common/types.rs b/src/streaming/event_parser/common/types.rs index 7bcaa96..fb0f81c 100755 --- a/src/streaming/event_parser/common/types.rs +++ b/src/streaming/event_parser/common/types.rs @@ -52,6 +52,7 @@ pub enum ProtocolType { RaydiumClmm, RaydiumAmmV4, MeteoraDammV2, + MeteoraDlmm, Common, } @@ -133,6 +134,7 @@ pub enum EventType { AccountRaydiumClmmTickArrayState, AccountRaydiumCpmmAmmConfig, AccountRaydiumCpmmPoolState, + AccountMeteoraDlmmLbPair, NonceAccount, TokenAccount, @@ -159,6 +161,7 @@ pub const ACCOUNT_EVENT_TYPES: &[EventType] = &[ EventType::AccountRaydiumClmmTickArrayState, EventType::AccountRaydiumCpmmAmmConfig, EventType::AccountRaydiumCpmmPoolState, + EventType::AccountMeteoraDlmmLbPair, EventType::TokenAccount, EventType::NonceAccount, ]; @@ -232,6 +235,7 @@ impl fmt::Display for EventType { } EventType::AccountRaydiumCpmmAmmConfig => write!(f, "AccountRaydiumCpmmAmmConfig"), EventType::AccountRaydiumCpmmPoolState => write!(f, "AccountRaydiumCpmmPoolState"), + EventType::AccountMeteoraDlmmLbPair => write!(f, "AccountMeteoraDlmmLbPair"), 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..b735383 100644 --- a/src/streaming/event_parser/core/dispatcher.rs +++ b/src/streaming/event_parser/core/dispatcher.rs @@ -11,7 +11,8 @@ 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, }, @@ -54,6 +55,7 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, }; match protocol { @@ -99,6 +101,10 @@ impl EventDispatcher { accounts, metadata, ), + Protocol::MeteoraDlmm => { + // Meteora DLMM 目前不需要解析指令数据,返回 None + None + } } } @@ -129,6 +135,7 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, }; match protocol { @@ -167,6 +174,10 @@ impl EventDispatcher { inner_instruction_data, metadata, ), + Protocol::MeteoraDlmm => { + // Meteora DLMM 目前不需要解析 inner instruction 数据,返回 None + None + } } } @@ -187,6 +198,8 @@ 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 { None } @@ -225,6 +238,7 @@ 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, } } @@ -261,6 +275,7 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, }; match protocol { @@ -284,6 +299,9 @@ impl EventDispatcher { // Meteora DAMM 目前不需要解析账户数据,返回 None None } + Protocol::MeteoraDlmm => { + meteora_dlmm::parse_meteora_dlmm_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..7014373 100755 --- a/src/streaming/event_parser/core/traits.rs +++ b/src/streaming/event_parser/core/traits.rs @@ -8,6 +8,7 @@ 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::*; @@ -81,6 +82,9 @@ pub enum DexEvent { MeteoraDammV2InitializeCustomizablePoolEvent(MeteoraDammV2InitializeCustomizablePoolEvent), MeteoraDammV2InitializePoolWithDynamicConfigEvent(MeteoraDammV2InitializePoolWithDynamicConfigEvent), + // Meteora DLMM events + MeteoraDlmmLbPairAccountEvent(MeteoraDlmmLbPairAccountEvent), + // Common events TokenAccountEvent(TokenAccountEvent), NonceAccountEvent(NonceAccountEvent), @@ -141,6 +145,7 @@ impl DexEvent { DexEvent::MeteoraDammV2InitializePoolEvent(e) => &e.metadata, DexEvent::MeteoraDammV2InitializeCustomizablePoolEvent(e) => &e.metadata, DexEvent::MeteoraDammV2InitializePoolWithDynamicConfigEvent(e) => &e.metadata, + DexEvent::MeteoraDlmmLbPairAccountEvent(e) => &e.metadata, DexEvent::TokenAccountEvent(e) => &e.metadata, DexEvent::NonceAccountEvent(e) => &e.metadata, DexEvent::TokenInfoEvent(e) => &e.metadata, @@ -200,6 +205,7 @@ 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::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..fa76bc4 100755 --- a/src/streaming/event_parser/protocols/mod.rs +++ b/src/streaming/event_parser/protocols/mod.rs @@ -1,6 +1,7 @@ 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; diff --git a/src/streaming/event_parser/protocols/types.rs b/src/streaming/event_parser/protocols/types.rs index 3d864dc..c91be73 100755 --- a/src/streaming/event_parser/protocols/types.rs +++ b/src/streaming/event_parser/protocols/types.rs @@ -1,8 +1,8 @@ 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, }; use anyhow::{anyhow, Result}; use solana_sdk::pubkey::Pubkey; @@ -17,6 +17,7 @@ pub enum Protocol { RaydiumClmm, RaydiumAmmV4, MeteoraDammV2, + MeteoraDlmm, } impl Protocol { @@ -29,6 +30,7 @@ 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], } } } @@ -43,6 +45,7 @@ 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"), } } } @@ -59,6 +62,7 @@ impl std::str::FromStr for Protocol { "raydiumclmm" => Ok(Protocol::RaydiumClmm), "raydiumammv4" => Ok(Protocol::RaydiumAmmV4), "meteoradamm_v2" => Ok(Protocol::MeteoraDammV2), + "meteoradlmm" => Ok(Protocol::MeteoraDlmm), _ => Err(anyhow!("Unsupported protocol: {}", s)), } }