diff --git a/Cargo.toml b/Cargo.toml index b9d3e21..642470b 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,20 +12,30 @@ readme = "README.md" [lib] crate-type = ["cdylib", "rlib"] +[features] +default = ["sdk-parse-borsh"] +sdk-parse-borsh = ["sol-parser-sdk/parse-borsh"] +# If both parser backend features are enabled, sol-parser-sdk 0.4.4+ uses zero-copy. +sdk-parse-zero-copy = ["sol-parser-sdk/parse-zero-copy"] +sdk-perf-stats = ["sol-parser-sdk/perf-stats"] +sdk-ultra-perf = ["sol-parser-sdk/ultra-perf"] + [dependencies] +# Keep the streamer on the crates.io SDK while allowing local workspace patches by semver. +sol-parser-sdk = { version = "0.4.4", default-features = false } solana-sdk = "3.0.0" -solana-client = "3.1.11" -solana-transaction-status = "3.1.11" -solana-account-decoder = "3.1.11" -solana-entry = { version = "3.1.11", features = ["agave-unstable-api"] } +solana-client = "3.1.12" +solana-transaction-status = "3.1.12" +solana-account-decoder = "3.1.12" +solana-entry = "3.0.0" borsh = { version = "1.6.0", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } serde-big-array = "0.5.1" futures = "0.3.32" bincode = "1.3" anyhow = "1.0.102" -yellowstone-grpc-client = { version = "10.2.0" } -yellowstone-grpc-proto = { version = "10.1.1" } +yellowstone-grpc-client = "12.1.0" +yellowstone-grpc-proto = "12.1.0" tokio = { version = "1.50.0", features = ["full", "rt-multi-thread"]} tonic = { version = "0.14.5", features = ["transport"] } rustls = { version = "0.23.37", features = ["ring"], default-features = false } diff --git a/docs/SHREDSTREAM_LIMITATIONS.md b/docs/SHREDSTREAM_LIMITATIONS.md index 9c11ee8..35bfef0 100644 --- a/docs/SHREDSTREAM_LIMITATIONS.md +++ b/docs/SHREDSTREAM_LIMITATIONS.md @@ -2,6 +2,12 @@ 与 gRPC 订阅相比,shredstream 路径存在以下限制和解析差异,使用时请注意。 +## gRPC(Yellowstone)说明 + +当订阅返回的交易带有完整 `meta`(日志、loaded addresses、inner instructions)时,DEX 相关事件在内部由 **sol-parser-sdk** 解析(与 upstream 相同的 logs + instructions 与 log/ix 去重;streamer 使用 **顺序**解析路径以降低单笔延迟),再映射为本 crate 的 `DexEvent`(对外 API 不变);Compute Budget 仍单独走原有指令路径,且在 DEX 事件派发 **之后** 运行,以便 Swap 等事件更早送达回调。 + +ShredStream 路径仍为下面的原始交易解析限制,不使用上述日志管线。 + ## 1. 数据源差异 | 数据 | gRPC | Shredstream | diff --git a/examples/debug_pump_tx.rs b/examples/debug_pump_tx.rs index 7877044..bfca9d4 100644 --- a/examples/debug_pump_tx.rs +++ b/examples/debug_pump_tx.rs @@ -12,7 +12,8 @@ use std::sync::Arc; #[tokio::main] async fn main() -> Result<()> { - let default_sig = "5curEt85cQhAK6R9pntSJ4fmYCiPEG22NjZyGrnGSbNwAkHJMN25T9Efp1n9Tf9vGXhnDXMQYrCNpoRHQTMcZ1s9"; + let default_sig = + "5curEt85cQhAK6R9pntSJ4fmYCiPEG22NjZyGrnGSbNwAkHJMN25T9Efp1n9Tf9vGXhnDXMQYrCNpoRHQTMcZ1s9"; let tx_sig = std::env::var("TX_SIGNATURE").unwrap_or_else(|_| default_sig.to_string()); let rpc_url = std::env::var("SOLANA_RPC_URL") .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); @@ -48,13 +49,18 @@ async fn main() -> Result<()> { if let Some(ref meta) = transaction.transaction.meta { println!("\n=== Transaction Meta ==="); println!("Fee: {}", meta.fee); - if let solana_transaction_status::option_serializer::OptionSerializer::Some(units) = &meta.compute_units_consumed { + if let solana_transaction_status::option_serializer::OptionSerializer::Some(units) = + &meta.compute_units_consumed + { println!("Compute units: {}", units); } - if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = &meta.log_messages { + if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = + &meta.log_messages + { println!("\n=== Logs ({} lines) ===", logs.len()); for (i, log) in logs.iter().enumerate() { - if log.contains("Program 6EF8") || log.contains("invoke") || log.contains("success") { + if log.contains("Program 6EF8") || log.contains("invoke") || log.contains("success") + { println!(" {}: {}", i, log); } } @@ -76,8 +82,9 @@ async fn main() -> Result<()> { let mut inner_instructions_vec: Vec = Vec::new(); if let Some(meta) = &transaction.transaction.meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some(ui_inner_insts) = - &meta.inner_instructions + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + ui_inner_insts, + ) = &meta.inner_instructions { for ui_inner in ui_inner_insts { let mut converted = Vec::new(); @@ -95,10 +102,8 @@ async fn main() -> Result<()> { } } } - inner_instructions_vec.push(InnerInstructions { - index: ui_inner.index, - instructions: converted, - }); + inner_instructions_vec + .push(InnerInstructions { index: ui_inner.index, instructions: converted }); } } } @@ -120,13 +125,10 @@ async fn main() -> Result<()> { accounts.extend(address_table_lookups); let slot = transaction.slot; - let block_time = transaction - .block_time - .map(|t| Timestamp { seconds: t as i64, nanos: 0 }); - let recv_us = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as i64; + let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); + let recv_us = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() + as i64; let protocols = vec![ Protocol::PumpFun, diff --git a/examples/debug_pumpswap_tx.rs b/examples/debug_pumpswap_tx.rs index dc85a3c..9116439 100644 --- a/examples/debug_pumpswap_tx.rs +++ b/examples/debug_pumpswap_tx.rs @@ -61,8 +61,9 @@ async fn main() -> Result<()> { let mut inner_instructions_vec: Vec = Vec::new(); if let Some(meta) = &transaction.transaction.meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some(ui_inner_insts) = - &meta.inner_instructions + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + ui_inner_insts, + ) = &meta.inner_instructions { for ui_inner in ui_inner_insts { let mut converted = Vec::new(); @@ -80,10 +81,8 @@ async fn main() -> Result<()> { } } } - inner_instructions_vec.push(InnerInstructions { - index: ui_inner.index, - instructions: converted, - }); + inner_instructions_vec + .push(InnerInstructions { index: ui_inner.index, instructions: converted }); } } } @@ -105,13 +104,10 @@ async fn main() -> Result<()> { accounts.extend(address_table_lookups); let slot = transaction.slot; - let block_time = transaction - .block_time - .map(|t| Timestamp { seconds: t as i64, nanos: 0 }); - let recv_us = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as i64; + let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); + let recv_us = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() + as i64; let protocols = vec![ Protocol::PumpFun, diff --git a/examples/dynamic_subscription.rs b/examples/dynamic_subscription.rs index b20754f..9fe2b6a 100644 --- a/examples/dynamic_subscription.rs +++ b/examples/dynamic_subscription.rs @@ -28,21 +28,20 @@ async fn main() -> Result<()> { let event_counter = Arc::new(AtomicU64::new(0)); let counter = event_counter.clone(); - let callback = - move |event: solana_streamer_sdk::streaming::event_parser::DexEvent| { - let count = counter.fetch_add(1, Ordering::Relaxed); + let callback = move |event: solana_streamer_sdk::streaming::event_parser::DexEvent| { + let count = counter.fetch_add(1, Ordering::Relaxed); - let protocol = match event.metadata().event_type { - EventType::PumpFunBuy | EventType::PumpFunSell => "PumpFun", - EventType::RaydiumCpmmSwapBaseInput | EventType::RaydiumCpmmSwapBaseOutput => { - "RaydiumCpmm" - } - _ => "Unknown", - }; - - println!("Event #{}: {:11} - {:.8}...", count + 1, protocol, event.metadata().signature); + let protocol = match event.metadata().event_type { + EventType::PumpFunBuy | EventType::PumpFunSell => "PumpFun", + EventType::RaydiumCpmmSwapBaseInput | EventType::RaydiumCpmmSwapBaseOutput => { + "RaydiumCpmm" + } + _ => "Unknown", }; + println!("Event #{}: {:11} - {:.8}...", count + 1, protocol, event.metadata().signature); + }; + println!("\n=== Phase 1: PumpFun only ==="); let pumpfun_filter = TransactionFilter { account_include: vec![PUMPFUN_PROGRAM_ID.to_string()], @@ -58,6 +57,7 @@ async fn main() -> Result<()> { EventType::RaydiumCpmmSwapBaseInput, EventType::RaydiumCpmmSwapBaseOutput, ], + ..Default::default() }; if let Err(e) = client @@ -324,8 +324,7 @@ async fn main() -> Result<()> { println!("\n=== Subscription enforcement ==="); - let test_callback = - |_event: solana_streamer_sdk::streaming::event_parser::DexEvent| {}; + let test_callback = |_event: solana_streamer_sdk::streaming::event_parser::DexEvent| {}; match client .subscribe_events_immediate( @@ -355,10 +354,9 @@ async fn main() -> Result<()> { let client2_counter = Arc::new(AtomicU64::new(0)); let counter2 = client2_counter.clone(); - let client2_callback = - move |_event: solana_streamer_sdk::streaming::event_parser::DexEvent| { - counter2.fetch_add(1, Ordering::Relaxed); - }; + let client2_callback = move |_event: solana_streamer_sdk::streaming::event_parser::DexEvent| { + counter2.fetch_add(1, Ordering::Relaxed); + }; match client2 .subscribe_events_immediate( @@ -444,10 +442,9 @@ async fn main() -> Result<()> { let client4_counter = Arc::new(AtomicU64::new(0)); let counter4 = client4_counter.clone(); - let client4_callback = - move |_event: solana_streamer_sdk::streaming::event_parser::DexEvent| { - counter4.fetch_add(1, Ordering::Relaxed); - }; + let client4_callback = move |_event: solana_streamer_sdk::streaming::event_parser::DexEvent| { + counter4.fetch_add(1, Ordering::Relaxed); + }; match client4 .subscribe_events_immediate( diff --git a/examples/meteora_damm_grpc.rs b/examples/meteora_damm_grpc.rs index 5cd6eaa..1cb67fe 100644 --- a/examples/meteora_damm_grpc.rs +++ b/examples/meteora_damm_grpc.rs @@ -2,10 +2,12 @@ //! //! Usage: cargo run --example meteora_damm_grpc --release -use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol}; use solana_streamer_sdk::streaming::event_parser::protocols::meteora_damm_v2::parser::METEORA_DAMM_V2_PROGRAM_ID; +use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol}; use solana_streamer_sdk::streaming::grpc::ClientConfig; -use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter, YellowstoneGrpc}; +use solana_streamer_sdk::streaming::yellowstone_grpc::{ + AccountFilter, TransactionFilter, YellowstoneGrpc, +}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -14,7 +16,8 @@ async fn main() -> Result<(), Box> { println!("Meteora DAMM v2 gRPC (solana-streamer)\n"); let grpc = YellowstoneGrpc::new_with_config( - std::env::var("GRPC_ENDPOINT").unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), + std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), std::env::var("GRPC_AUTH_TOKEN").ok(), ClientConfig::default(), )?; diff --git a/examples/mint_all_ata_account_listen_example.rs b/examples/mint_all_ata_account_listen_example.rs index 86368ea..cec7850 100644 --- a/examples/mint_all_ata_account_listen_example.rs +++ b/examples/mint_all_ata_account_listen_example.rs @@ -73,7 +73,8 @@ async fn test_grpc() -> Result<(), Box> { }; // Event filtering - let event_type_filter = Some(EventTypeFilter { include: vec![EventType::TokenAccount] }); + let event_type_filter = + Some(EventTypeFilter { include: vec![EventType::TokenAccount], ..Default::default() }); println!("Starting to listen for events, press Ctrl+C to stop..."); println!("Starting subscription..."); diff --git a/examples/nonce_listen_example.rs b/examples/nonce_listen_example.rs index 1933f5b..96a9010 100644 --- a/examples/nonce_listen_example.rs +++ b/examples/nonce_listen_example.rs @@ -41,7 +41,9 @@ async fn test_grpc() -> Result<(), Box> { TransactionFilter { account_include, account_exclude, account_required }; let nonce_account = std::env::var("NONCE_ACCOUNT").unwrap_or_else(|_| { - eprintln!("Usage: NONCE_ACCOUNT= cargo run --example nonce_listen_example --release"); + eprintln!( + "Usage: NONCE_ACCOUNT= cargo run --example nonce_listen_example --release" + ); std::process::exit(1); }); // Listen to account data belonging to owner programs -> account event monitoring @@ -49,7 +51,8 @@ async fn test_grpc() -> Result<(), Box> { AccountFilter { account: vec![nonce_account.clone()], owner: vec![], filters: vec![] }; // Event filtering - let event_type_filter = Some(EventTypeFilter { include: vec![EventType::NonceAccount] }); + let event_type_filter = + Some(EventTypeFilter { include: vec![EventType::NonceAccount], ..Default::default() }); println!("Starting to listen for events, press Ctrl+C to stop..."); println!("Starting subscription..."); diff --git a/examples/parse_meteora_damm_tx.rs b/examples/parse_meteora_damm_tx.rs index c3bb9f0..6af8f2f 100644 --- a/examples/parse_meteora_damm_tx.rs +++ b/examples/parse_meteora_damm_tx.rs @@ -31,11 +31,11 @@ async fn main() -> Result<()> { async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { use prost_types::Timestamp; use solana_sdk::{ - message::compiled_instruction::CompiledInstruction, - pubkey::Pubkey, - signature::Signature, + message::compiled_instruction::CompiledInstruction, pubkey::Pubkey, signature::Signature, + }; + use solana_transaction_status::{ + InnerInstruction, InnerInstructions, UiInstruction, UiTransactionEncoding, }; - use solana_transaction_status::{InnerInstruction, InnerInstructions, UiInstruction, UiTransactionEncoding}; let signature = Signature::from_str(signature_str)?; let client = solana_client::nonblocking::rpc_client::RpcClient::new(rpc_url.to_string()); @@ -62,8 +62,9 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { let mut inner_instructions_vec: Vec = Vec::new(); if let Some(meta) = &transaction.transaction.meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some(ui_inner_insts) = - &meta.inner_instructions + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + ui_inner_insts, + ) = &meta.inner_instructions { for ui_inner in ui_inner_insts { let mut converted = Vec::new(); @@ -81,10 +82,8 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { } } } - inner_instructions_vec.push(InnerInstructions { - index: ui_inner.index, - instructions: converted, - }); + inner_instructions_vec + .push(InnerInstructions { index: ui_inner.index, instructions: converted }); } } } @@ -106,13 +105,10 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { accounts.extend(address_table_lookups); let slot = transaction.slot; - let block_time = transaction - .block_time - .map(|t| Timestamp { seconds: t as i64, nanos: 0 }); - let recv_us = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as i64; + let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); + let recv_us = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() + as i64; let protocols = vec![ Protocol::PumpFun, diff --git a/examples/parse_pump_tx.rs b/examples/parse_pump_tx.rs index 4c99b0a..4ec20a2 100644 --- a/examples/parse_pump_tx.rs +++ b/examples/parse_pump_tx.rs @@ -16,7 +16,8 @@ use std::sync::Arc; #[tokio::main] async fn main() -> Result<()> { - let default_sig = "64srGF8CnTz9zPbdayWYmzs5aVRFBcfjDcidFVvBgAD25VMh52wr88vma7ytSbAZT3C5Giu5BPyGfNfLexLSrKhP"; + let default_sig = + "64srGF8CnTz9zPbdayWYmzs5aVRFBcfjDcidFVvBgAD25VMh52wr88vma7ytSbAZT3C5Giu5BPyGfNfLexLSrKhP"; let tx_sig = std::env::var("TX_SIGNATURE") .ok() .or_else(|| std::env::args().nth(1)) @@ -36,11 +37,11 @@ async fn main() -> Result<()> { async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { use prost_types::Timestamp; use solana_sdk::{ - message::compiled_instruction::CompiledInstruction, - pubkey::Pubkey, - signature::Signature, + message::compiled_instruction::CompiledInstruction, pubkey::Pubkey, signature::Signature, + }; + use solana_transaction_status::{ + InnerInstruction, InnerInstructions, UiInstruction, UiTransactionEncoding, }; - use solana_transaction_status::{InnerInstruction, InnerInstructions, UiInstruction, UiTransactionEncoding}; let signature = Signature::from_str(signature_str)?; let client = solana_client::nonblocking::rpc_client::RpcClient::new(rpc_url.to_string()); @@ -77,8 +78,9 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { let mut inner_instructions_vec: Vec = Vec::new(); if let Some(meta) = &transaction.transaction.meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some(ui_inner_insts) = - &meta.inner_instructions + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + ui_inner_insts, + ) = &meta.inner_instructions { for ui_inner in ui_inner_insts { let mut converted = Vec::new(); @@ -96,10 +98,8 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { } } } - inner_instructions_vec.push(InnerInstructions { - index: ui_inner.index, - instructions: converted, - }); + inner_instructions_vec + .push(InnerInstructions { index: ui_inner.index, instructions: converted }); } } } @@ -123,13 +123,10 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { accounts.extend(address_table_lookups); let slot = transaction.slot; - let block_time = transaction - .block_time - .map(|t| Timestamp { seconds: t as i64, nanos: 0 }); - let recv_us = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as i64; + let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); + let recv_us = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() + as i64; let protocols = vec![ Protocol::PumpFun, diff --git a/examples/parse_pumpswap_tx.rs b/examples/parse_pumpswap_tx.rs index 9e39766..f44326b 100644 --- a/examples/parse_pumpswap_tx.rs +++ b/examples/parse_pumpswap_tx.rs @@ -13,7 +13,8 @@ use std::sync::Arc; #[tokio::main] async fn main() -> Result<()> { - let default_sig = "3zsihbygW7hoKGtduAyDDFzp4E1eis8gaBzEzzNKr8ma39baffpFcphok9wHFgR3EauDe9vYYsVf4Puh5pZ6UJiS"; + let default_sig = + "3zsihbygW7hoKGtduAyDDFzp4E1eis8gaBzEzzNKr8ma39baffpFcphok9wHFgR3EauDe9vYYsVf4Puh5pZ6UJiS"; let tx_sig = std::env::var("TX_SIGNATURE").unwrap_or_else(|_| default_sig.to_string()); let rpc_url = std::env::var("SOLANA_RPC_URL") .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); @@ -30,11 +31,11 @@ async fn main() -> Result<()> { async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { use prost_types::Timestamp; use solana_sdk::{ - message::compiled_instruction::CompiledInstruction, - pubkey::Pubkey, - signature::Signature, + message::compiled_instruction::CompiledInstruction, pubkey::Pubkey, signature::Signature, + }; + use solana_transaction_status::{ + InnerInstruction, InnerInstructions, UiInstruction, UiTransactionEncoding, }; - use solana_transaction_status::{InnerInstruction, InnerInstructions, UiInstruction, UiTransactionEncoding}; let signature = Signature::from_str(signature_str)?; let client = solana_client::nonblocking::rpc_client::RpcClient::new(rpc_url.to_string()); @@ -61,8 +62,9 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { let mut inner_instructions_vec: Vec = Vec::new(); if let Some(meta) = &transaction.transaction.meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some(ui_inner_insts) = - &meta.inner_instructions + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + ui_inner_insts, + ) = &meta.inner_instructions { for ui_inner in ui_inner_insts { let mut converted = Vec::new(); @@ -80,10 +82,8 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { } } } - inner_instructions_vec.push(InnerInstructions { - index: ui_inner.index, - instructions: converted, - }); + inner_instructions_vec + .push(InnerInstructions { index: ui_inner.index, instructions: converted }); } } } @@ -105,13 +105,10 @@ async fn parse_one_tx(signature_str: &str, rpc_url: &str) -> Result<()> { accounts.extend(address_table_lookups); let slot = transaction.slot; - let block_time = transaction - .block_time - .map(|t| Timestamp { seconds: t as i64, nanos: 0 }); - let recv_us = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as i64; + let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); + let recv_us = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() + as i64; let protocols = vec![ Protocol::PumpFun, diff --git a/examples/parse_tx_events.rs b/examples/parse_tx_events.rs index 10506a7..fe22e83 100644 --- a/examples/parse_tx_events.rs +++ b/examples/parse_tx_events.rs @@ -1,11 +1,32 @@ +//! RPC 单笔解析示例: +//! - **默认**:本地 ix 路径(便于打印完整日志等)。 +//! - **对齐 sol-parser-sdk**:`SOL_PARSER_SDK_RPC=1` 时对同一笔 RPC 响应使用 `parse_encoded_rpc_transaction_as_streamer_events`。 +//! +//! 亦可直接使用 crate 根的 `fetch_rpc_transaction_as_streamer_events_async`(单独 RPC 拉取 + 对齐解析)。 +//! +//! 环境变量:`SOLANA_RPC_URL`(可选,默认 mainnet 公共 RPC)。 + use anyhow::Result; use solana_commitment_config::CommitmentConfig; +use solana_streamer_sdk::parse_encoded_rpc_transaction_as_streamer_events; use solana_streamer_sdk::streaming::event_parser::core::event_parser::EventParser; -use solana_streamer_sdk::streaming::event_parser::Protocol; use solana_streamer_sdk::streaming::event_parser::DexEvent; +use solana_streamer_sdk::streaming::event_parser::Protocol; use std::str::FromStr; use std::sync::Arc; -/// Get transaction data based on transaction signature + +fn rpc_url_from_env() -> String { + std::env::var("SOLANA_RPC_URL") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()) +} + +fn use_sol_parser_sdk_rpc_path() -> bool { + matches!( + std::env::var("SOL_PARSER_SDK_RPC").as_deref(), + Ok("1") | Ok("true") | Ok("yes") | Ok("TRUE") | Ok("YES") + ) +} + #[tokio::main] async fn main() -> Result<()> { let signatures = vec![ @@ -34,19 +55,127 @@ async fn main() -> Result<()> { Ok(()) } +/// 本地版本化消息 + inner ix 路径(与订阅管线中的「无 sdk」解析相近)。 +async fn parse_local_ix_path( + transaction: &solana_transaction_status::EncodedConfirmedTransactionWithStatusMeta, + signature: solana_sdk::signature::Signature, + recv_us: i64, + protocols: &[Protocol], +) -> Result<()> { + use prost_types::Timestamp; + use solana_sdk::{message::compiled_instruction::CompiledInstruction, pubkey::Pubkey}; + use solana_transaction_status::{InnerInstruction, InnerInstructions, UiInstruction}; + + println!("\n--- Parsed events (本地 ix 路径) ---\n"); + + let versioned_tx = match transaction.transaction.transaction.decode() { + Some(tx) => tx, + None => { + println!("Failed to decode transaction"); + return Ok(()); + } + }; + + let mut inner_instructions_vec: Vec = Vec::new(); + if let Some(meta) = &transaction.transaction.meta { + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + ui_inner_insts, + ) = &meta.inner_instructions + { + for ui_inner in ui_inner_insts { + let mut converted_instructions = Vec::new(); + + for ui_instruction in &ui_inner.instructions { + if let UiInstruction::Compiled(ui_compiled) = ui_instruction { + if let Ok(data) = solana_sdk::bs58::decode(&ui_compiled.data).into_vec() { + let compiled_instruction = CompiledInstruction { + program_id_index: ui_compiled.program_id_index, + accounts: ui_compiled.accounts.to_vec(), + data, + }; + + let inner_instruction = InnerInstruction { + instruction: compiled_instruction, + stack_height: ui_compiled.stack_height, + }; + + converted_instructions.push(inner_instruction); + } + } + } + + let inner_instructions = InnerInstructions { + index: ui_inner.index, + instructions: converted_instructions, + }; + + inner_instructions_vec.push(inner_instructions); + } + } + } + + let meta = transaction.transaction.meta.clone(); + let mut address_table_lookups: Vec = vec![]; + if let Some(meta) = meta { + if let solana_transaction_status::option_serializer::OptionSerializer::Some( + loaded_addresses, + ) = &meta.loaded_addresses + { + address_table_lookups + .reserve(loaded_addresses.writable.len() + loaded_addresses.readonly.len()); + address_table_lookups.extend( + loaded_addresses.writable.iter().filter_map(|s| s.parse::().ok()).chain( + loaded_addresses.readonly.iter().filter_map(|s| s.parse::().ok()), + ), + ); + } + } + + let mut accounts = Vec::with_capacity( + versioned_tx.message.static_account_keys().len() + address_table_lookups.len(), + ); + accounts.extend_from_slice(versioned_tx.message.static_account_keys()); + accounts.extend(address_table_lookups); + + let slot = transaction.slot; + let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); + let bot_wallet = None; + let tx_index = None; + + let callback = Arc::new(move |event: DexEvent| { + println!("{:?}\n", event); + }); + + EventParser::parse_instruction_events_from_versioned_transaction( + protocols, + None, + &versioned_tx, + signature, + Some(slot), + block_time, + recv_us, + &accounts, + &inner_instructions_vec, + bot_wallet, + tx_index, + callback, + ) + .await?; + + Ok(()) +} + /// Get details of a single transaction async fn get_single_transaction_details(signature_str: &str) -> Result<()> { - use solana_sdk::{signature::Signature, pubkey::Pubkey, message::compiled_instruction::CompiledInstruction}; - use solana_transaction_status::{UiTransactionEncoding, InnerInstruction, InnerInstructions, UiInstruction}; - use prost_types::Timestamp; + use solana_sdk::signature::Signature; + use solana_transaction_status::UiTransactionEncoding; let signature = Signature::from_str(signature_str)?; - // Create Solana RPC client - let rpc_url = "https://api.mainnet-beta.solana.com"; + let rpc_url = rpc_url_from_env(); println!("Connecting to Solana RPC: {}", rpc_url); - let client = solana_client::nonblocking::rpc_client::RpcClient::new(rpc_url.to_string()); + let client = solana_client::nonblocking::rpc_client::RpcClient::new(rpc_url); match client .get_transaction_with_config( @@ -90,94 +219,10 @@ async fn get_single_transaction_details(signature_str: &str) -> Result<()> { } } - // Parse the transaction and extract necessary data - let versioned_tx = match transaction.transaction.transaction.decode() { - Some(tx) => tx, - None => { - println!("Failed to decode transaction"); - return Ok(()); - } - }; - - // Convert inner instructions from meta - let mut inner_instructions_vec: Vec = Vec::new(); - if let Some(meta) = &transaction.transaction.meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some( - ui_inner_insts, - ) = &meta.inner_instructions - { - for ui_inner in ui_inner_insts { - let mut converted_instructions = Vec::new(); - - for ui_instruction in &ui_inner.instructions { - if let UiInstruction::Compiled(ui_compiled) = ui_instruction { - if let Ok(data) = solana_sdk::bs58::decode(&ui_compiled.data).into_vec() { - let compiled_instruction = CompiledInstruction { - program_id_index: ui_compiled.program_id_index, - accounts: ui_compiled.accounts.to_vec(), - data, - }; - - let inner_instruction = InnerInstruction { - instruction: compiled_instruction, - stack_height: ui_compiled.stack_height, - }; - - converted_instructions.push(inner_instruction); - } - } - } - - let inner_instructions = InnerInstructions { - index: ui_inner.index, - instructions: converted_instructions, - }; - - inner_instructions_vec.push(inner_instructions); - } - } - } - - // Extract address table lookups - let meta = transaction.transaction.meta; - let mut address_table_lookups: Vec = vec![]; - if let Some(meta) = meta { - if let solana_transaction_status::option_serializer::OptionSerializer::Some( - loaded_addresses, - ) = &meta.loaded_addresses - { - address_table_lookups - .reserve(loaded_addresses.writable.len() + loaded_addresses.readonly.len()); - address_table_lookups.extend( - loaded_addresses - .writable - .iter() - .filter_map(|s| s.parse::().ok()) - .chain( - loaded_addresses - .readonly - .iter() - .filter_map(|s| s.parse::().ok()), - ), - ); - } - } - - // Build complete accounts list - let mut accounts = Vec::with_capacity( - versioned_tx.message.static_account_keys().len() + address_table_lookups.len(), - ); - accounts.extend_from_slice(versioned_tx.message.static_account_keys()); - accounts.extend(address_table_lookups); - - let slot = transaction.slot; - let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 }); let recv_us = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_micros() as i64; - let bot_wallet = None; - let tx_index = None; let protocols = vec![ Protocol::Bonk, @@ -187,29 +232,30 @@ async fn get_single_transaction_details(signature_str: &str) -> Result<()> { Protocol::RaydiumCpmm, Protocol::RaydiumAmmV4, Protocol::MeteoraDammV2, + Protocol::OrcaWhirlpool, + Protocol::MeteoraPools, + Protocol::MeteoraDlmm, ]; - // Create callback - let callback = Arc::new(move |event: DexEvent| { - println!("{:?}\n", event); - }); - - // Call parse_instruction_events_from_versioned_transaction - EventParser::parse_instruction_events_from_versioned_transaction( - &protocols, - None, - &versioned_tx, - signature, - Some(slot), - block_time, - recv_us, - &accounts, - &inner_instructions_vec, - bot_wallet, - tx_index, - callback, - ) - .await?; + if use_sol_parser_sdk_rpc_path() { + println!("\n--- Parsed events (SOL_PARSER_SDK_RPC=1, sol-parser-sdk 对齐) ---\n"); + match parse_encoded_rpc_transaction_as_streamer_events( + &transaction, + recv_us, + &protocols, + None, + ) { + Ok(events) => { + println!("Total {} streamer DexEvent(s):\n", events.len()); + for ev in events { + println!("{:?}\n", ev); + } + } + Err(e) => println!("SDK-aligned parse error: {}", e), + } + } else { + parse_local_ix_path(&transaction, signature, recv_us, &protocols).await?; + } } Err(e) => { println!("Failed to get transaction: {}", e); diff --git a/examples/pumpfun_quick_test.rs b/examples/pumpfun_quick_test.rs index fdcbfb4..7759289 100644 --- a/examples/pumpfun_quick_test.rs +++ b/examples/pumpfun_quick_test.rs @@ -3,11 +3,13 @@ //! Usage: cargo run --example pumpfun_quick_test --release use solana_streamer_sdk::streaming::event_parser::common::types::EventType; -use solana_streamer_sdk::streaming::event_parser::DexEvent; -use solana_streamer_sdk::streaming::grpc::ClientConfig; -use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter, YellowstoneGrpc}; -use solana_streamer_sdk::streaming::event_parser::Protocol; use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID; +use solana_streamer_sdk::streaming::event_parser::DexEvent; +use solana_streamer_sdk::streaming::event_parser::Protocol; +use solana_streamer_sdk::streaming::grpc::ClientConfig; +use solana_streamer_sdk::streaming::yellowstone_grpc::{ + AccountFilter, TransactionFilter, YellowstoneGrpc, +}; use std::sync::atomic::{AtomicU64, Ordering}; #[tokio::main] @@ -20,7 +22,8 @@ async fn main() -> Result<(), Box> { config.enable_metrics = true; let grpc = YellowstoneGrpc::new_with_config( - std::env::var("GRPC_ENDPOINT").unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), + std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), std::env::var("GRPC_AUTH_TOKEN").ok(), config, )?; diff --git a/examples/pumpfun_trade_filter.rs b/examples/pumpfun_trade_filter.rs index 8620597..5091cd7 100644 --- a/examples/pumpfun_trade_filter.rs +++ b/examples/pumpfun_trade_filter.rs @@ -2,20 +2,21 @@ //! //! Usage: cargo run --example pumpfun_trade_filter --release -use solana_streamer_sdk::streaming::event_parser::common::types::EventType; -use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::events::{PumpFunCreateTokenEvent, PumpFunCreateV2TokenEvent, PumpFunTradeEvent}; -use solana_streamer_sdk::streaming::event_parser::DexEvent; -use solana_streamer_sdk::streaming::grpc::ClientConfig; -use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter, YellowstoneGrpc}; -use solana_streamer_sdk::streaming::event_parser::Protocol; use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter; +use solana_streamer_sdk::streaming::event_parser::common::types::EventType; +use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::events::{ + PumpFunCreateTokenEvent, PumpFunCreateV2TokenEvent, PumpFunTradeEvent, +}; use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID; +use solana_streamer_sdk::streaming::event_parser::DexEvent; +use solana_streamer_sdk::streaming::event_parser::Protocol; +use solana_streamer_sdk::streaming::grpc::ClientConfig; +use solana_streamer_sdk::streaming::yellowstone_grpc::{ + AccountFilter, TransactionFilter, YellowstoneGrpc, +}; fn now_micros() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as i64 + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros() as i64 } #[tokio::main] @@ -28,7 +29,8 @@ async fn main() -> Result<(), Box> { config.enable_metrics = true; let grpc = YellowstoneGrpc::new_with_config( - std::env::var("GRPC_ENDPOINT").unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), + std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), std::env::var("GRPC_AUTH_TOKEN").ok(), config, )?; @@ -50,6 +52,7 @@ async fn main() -> Result<(), Box> { EventType::PumpFunCreateToken, EventType::PumpFunCreateV2Token, ], + ..Default::default() }); let callback = |event: DexEvent| { @@ -84,13 +87,7 @@ fn print_trade(e: &PumpFunTradeEvent, now_us: i64) { let kind = if e.is_buy { "BUY" } else { "SELL" }; println!( "│ {} | sig={:.8}.. | mint={:.8}.. | sol={} tok={} | user={:.8}.. | latency={} μs", - kind, - e.metadata.signature, - e.mint, - e.sol_amount, - e.token_amount, - e.user, - latency_us + kind, e.metadata.signature, e.mint, e.sol_amount, e.token_amount, e.user, latency_us ); } @@ -98,11 +95,7 @@ fn print_create_legacy(e: &PumpFunCreateTokenEvent, now_us: i64) { let latency_us = now_us - e.metadata.recv_us; println!( "│ CREATE | sig={:.8}.. | name={} symbol={} | mint={:.8}.. | latency={} μs", - e.metadata.signature, - e.name, - e.symbol, - e.mint, - latency_us + e.metadata.signature, e.name, e.symbol, e.mint, latency_us ); } @@ -110,10 +103,6 @@ fn print_create_v2(e: &PumpFunCreateV2TokenEvent, now_us: i64) { let latency_us = now_us - e.metadata.recv_us; println!( "│ CREATE_V2 | sig={:.8}.. | name={} symbol={} | mint={:.8}.. | latency={} μs", - e.metadata.signature, - e.name, - e.symbol, - e.mint, - latency_us + e.metadata.signature, e.name, e.symbol, e.mint, latency_us ); } diff --git a/examples/pumpfun_with_metrics.rs b/examples/pumpfun_with_metrics.rs index dcdc580..6a3fc31 100644 --- a/examples/pumpfun_with_metrics.rs +++ b/examples/pumpfun_with_metrics.rs @@ -2,10 +2,12 @@ //! //! Usage: cargo run --example pumpfun_with_metrics --release -use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol}; use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID; +use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol}; use solana_streamer_sdk::streaming::grpc::ClientConfig; -use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter, YellowstoneGrpc}; +use solana_streamer_sdk::streaming::yellowstone_grpc::{ + AccountFilter, TransactionFilter, YellowstoneGrpc, +}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -17,7 +19,8 @@ async fn main() -> Result<(), Box> { config.enable_metrics = true; let grpc = YellowstoneGrpc::new_with_config( - std::env::var("GRPC_ENDPOINT").unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), + std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), std::env::var("GRPC_AUTH_TOKEN").ok(), config, )?; diff --git a/examples/pumpswap_pool_account_listen_example.rs b/examples/pumpswap_pool_account_listen_example.rs index 9bab93e..5c3d4e8 100644 --- a/examples/pumpswap_pool_account_listen_example.rs +++ b/examples/pumpswap_pool_account_listen_example.rs @@ -75,7 +75,8 @@ async fn test_grpc() -> Result<(), Box> { }; // Event filtering - let event_type_filter = Some(EventTypeFilter { include: vec![EventType::TokenAccount] }); + let event_type_filter = + Some(EventTypeFilter { include: vec![EventType::TokenAccount], ..Default::default() }); println!("Starting to listen for events, press Ctrl+C to stop..."); println!("Starting subscription..."); diff --git a/examples/pumpswap_with_metrics.rs b/examples/pumpswap_with_metrics.rs index 6234cee..5920205 100644 --- a/examples/pumpswap_with_metrics.rs +++ b/examples/pumpswap_with_metrics.rs @@ -2,10 +2,12 @@ //! //! Usage: cargo run --example pumpswap_with_metrics --release -use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol}; use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID; +use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol}; use solana_streamer_sdk::streaming::grpc::ClientConfig; -use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter, YellowstoneGrpc}; +use solana_streamer_sdk::streaming::yellowstone_grpc::{ + AccountFilter, TransactionFilter, YellowstoneGrpc, +}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -17,7 +19,8 @@ async fn main() -> Result<(), Box> { config.enable_metrics = true; let grpc = YellowstoneGrpc::new_with_config( - std::env::var("GRPC_ENDPOINT").unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), + std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()), std::env::var("GRPC_AUTH_TOKEN").ok(), config, )?; diff --git a/examples/shred_example.rs b/examples/shred_example.rs index 74666f2..bf88644 100644 --- a/examples/shred_example.rs +++ b/examples/shred_example.rs @@ -1,5 +1,5 @@ use solana_streamer_sdk::streaming::{ - event_parser::{Protocol, DexEvent}, + event_parser::{DexEvent, Protocol}, shred::StreamClientConfig, ShredStreamGrpc, }; diff --git a/examples/token_balance_listen_example.rs b/examples/token_balance_listen_example.rs index 2e5dbbc..2fa071f 100644 --- a/examples/token_balance_listen_example.rs +++ b/examples/token_balance_listen_example.rs @@ -47,7 +47,8 @@ async fn test_grpc() -> Result<(), Box> { AccountFilter { account: vec![account_to_listen], owner: vec![], filters: vec![] }; // Event filtering - let event_type_filter = Some(EventTypeFilter { include: vec![EventType::TokenAccount] }); + let event_type_filter = + Some(EventTypeFilter { include: vec![EventType::TokenAccount], ..Default::default() }); println!("Starting to listen for events, press Ctrl+C to stop..."); println!("Starting subscription..."); diff --git a/examples/token_decimals_listen_example.rs b/examples/token_decimals_listen_example.rs index dbd276f..10ae281 100644 --- a/examples/token_decimals_listen_example.rs +++ b/examples/token_decimals_listen_example.rs @@ -50,7 +50,8 @@ async fn test_grpc() -> Result<(), Box> { AccountFilter { account: vec![account_to_listen], owner: vec![], filters: vec![] }; // Event filtering - let event_type_filter = Some(EventTypeFilter { include: vec![EventType::TokenAccount] }); + let event_type_filter = + Some(EventTypeFilter { include: vec![EventType::TokenAccount], ..Default::default() }); println!("Starting to listen for events, press Ctrl+C to stop..."); println!("Starting subscription..."); diff --git a/src/lib.rs b/src/lib.rs index 2e79b9d..0508943 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,13 @@ pub mod common; pub mod protos; pub mod streaming; + +#[cfg(all(not(feature = "sdk-parse-borsh"), not(feature = "sdk-parse-zero-copy")))] +compile_error!("Enable one SDK parser backend: sdk-parse-borsh or sdk-parse-zero-copy."); + +pub use sol_parser_sdk as parser_sdk; +pub use streaming::sdk_bridge; +pub use streaming::{ + fetch_rpc_transaction_as_streamer_events, fetch_rpc_transaction_as_streamer_events_async, + parse_encoded_rpc_transaction_as_streamer_events, RpcParseError, +}; diff --git a/src/protos/shredstream.rs b/src/protos/shredstream.rs index 876f635..78ae7b3 100644 --- a/src/protos/shredstream.rs +++ b/src/protos/shredstream.rs @@ -47,10 +47,10 @@ pub mod shredstream_client { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] - use tonic::codegen::*; use tonic::codegen::http::Uri; + use tonic::codegen::*; #[derive(Debug, Clone)] pub struct ShredstreamClient { inner: tonic::client::Grpc, @@ -94,9 +94,8 @@ pub mod shredstream_client { >::ResponseBody, >, >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, + >>::Error: + Into + std::marker::Send + std::marker::Sync, { ShredstreamClient::new(InterceptedService::new(inner, interceptor)) } @@ -135,22 +134,13 @@ pub mod shredstream_client { pub async fn send_heartbeat( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic_prost::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/shredstream.Shredstream/SendHeartbeat", - ); + let path = + http::uri::PathAndQuery::from_static("/shredstream.Shredstream/SendHeartbeat"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("shredstream.Shredstream", "SendHeartbeat")); @@ -165,7 +155,7 @@ pub mod shredstream_server { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with ShredstreamServer. @@ -175,10 +165,7 @@ pub mod shredstream_server { async fn send_heartbeat( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; } #[derive(Debug)] pub struct ShredstreamServer { @@ -201,10 +188,7 @@ pub mod shredstream_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { @@ -259,13 +243,9 @@ pub mod shredstream_server { "/shredstream.Shredstream/SendHeartbeat" => { #[allow(non_camel_case_types)] struct SendHeartbeatSvc(pub Arc); - impl tonic::server::UnaryService - for SendHeartbeatSvc { + impl tonic::server::UnaryService for SendHeartbeatSvc { type Response = super::HeartbeatResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, @@ -299,25 +279,16 @@ pub mod shredstream_server { }; Box::pin(fut) } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tonic::body::Body::default(), - ); - let headers = response.headers_mut(); - headers - .insert( - tonic::Status::GRPC_STATUS, - (tonic::Code::Unimplemented as i32).into(), - ); - headers - .insert( - http::header::CONTENT_TYPE, - tonic::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) - } + _ => Box::pin(async move { + let mut response = http::Response::new(tonic::body::Body::default()); + let headers = response.headers_mut(); + headers.insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers.insert(http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE); + Ok(response) + }), } } } @@ -346,10 +317,10 @@ pub mod shredstream_proxy_client { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] - use tonic::codegen::*; use tonic::codegen::http::Uri; + use tonic::codegen::*; #[derive(Debug, Clone)] pub struct ShredstreamProxyClient { inner: tonic::client::Grpc, @@ -393,9 +364,8 @@ pub mod shredstream_proxy_client { >::ResponseBody, >, >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, + >>::Error: + Into + std::marker::Send + std::marker::Sync, { ShredstreamProxyClient::new(InterceptedService::new(inner, interceptor)) } @@ -437,23 +407,16 @@ pub mod shredstream_proxy_client { tonic::Response>, tonic::Status, > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/shredstream.ShredstreamProxy/SubscribeEntries", ); let mut req = request.into_request(); req.extensions_mut() - .insert( - GrpcMethod::new("shredstream.ShredstreamProxy", "SubscribeEntries"), - ); + .insert(GrpcMethod::new("shredstream.ShredstreamProxy", "SubscribeEntries")); self.inner.server_streaming(req, path, codec).await } } @@ -465,7 +428,7 @@ pub mod shredstream_proxy_server { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with ShredstreamProxyServer. @@ -474,16 +437,12 @@ pub mod shredstream_proxy_server { /// Server streaming response type for the SubscribeEntries method. type SubscribeEntriesStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result, - > - + std::marker::Send + > + std::marker::Send + 'static; async fn subscribe_entries( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; } #[derive(Debug)] pub struct ShredstreamProxyServer { @@ -506,10 +465,7 @@ pub mod shredstream_proxy_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { @@ -564,25 +520,21 @@ pub mod shredstream_proxy_server { "/shredstream.ShredstreamProxy/SubscribeEntries" => { #[allow(non_camel_case_types)] struct SubscribeEntriesSvc(pub Arc); - impl< - T: ShredstreamProxy, - > tonic::server::ServerStreamingService< - super::SubscribeEntriesRequest, - > for SubscribeEntriesSvc { + impl + tonic::server::ServerStreamingService + for SubscribeEntriesSvc + { type Response = super::Entry; type ResponseStream = T::SubscribeEntriesStream; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = + BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::subscribe_entries(&inner, request) - .await + ::subscribe_entries(&inner, request).await }; Box::pin(fut) } @@ -609,25 +561,16 @@ pub mod shredstream_proxy_server { }; Box::pin(fut) } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tonic::body::Body::default(), - ); - let headers = response.headers_mut(); - headers - .insert( - tonic::Status::GRPC_STATUS, - (tonic::Code::Unimplemented as i32).into(), - ); - headers - .insert( - http::header::CONTENT_TYPE, - tonic::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) - } + _ => Box::pin(async move { + let mut response = http::Response::new(tonic::body::Body::default()); + let headers = response.headers_mut(); + headers.insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers.insert(http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE); + Ok(response) + }), } } } diff --git a/src/streaming/common/event_processor.rs b/src/streaming/common/event_processor.rs index 133f02b..0a42059 100644 --- a/src/streaming/common/event_processor.rs +++ b/src/streaming/common/event_processor.rs @@ -1,18 +1,18 @@ use crate::common::AnyResult; use crate::streaming::common::MetricsEventType; -use crate::streaming::event_parser::common::filter::EventTypeFilter; +use crate::streaming::event_parser::common::filter::{passes_event_type_filter, EventTypeFilter}; +use crate::streaming::event_parser::common::high_performance_clock::elapsed_micros_since; use crate::streaming::event_parser::core::account_event_parser::AccountEventParser; use crate::streaming::event_parser::core::common_event_parser::CommonEventParser; use crate::streaming::event_parser::core::event_parser::EventParser; use crate::streaming::event_parser::{core::traits::DexEvent, Protocol}; use crate::streaming::grpc::{EventPretty, MetricsManager}; +use crate::streaming::parser_sdk_bridge::{parse_account_event_for_streamer, AccountParseResult}; use crate::streaming::shred::TransactionWithSlot; use solana_sdk::pubkey::Pubkey; use std::sync::Arc; -/// 创建带 metrics 统计的 callback 包装器 -/// -/// 用于 Transaction 事件处理,在调用原始 callback 的同时更新 metrics +/// Wrap the user callback and update transaction metrics after delivery. #[inline] fn create_metrics_callback( callback: Arc, @@ -47,6 +47,18 @@ pub async fn process_grpc_transaction( EventPretty::Account(account_pretty) => { MetricsManager::global().add_account_process_count(); + match parse_account_event_for_streamer(&account_pretty, protocols, event_type_filter) { + AccountParseResult::Event(mut event) => { + event.metadata_mut().handle_us = elapsed_micros_since(account_pretty.recv_us); + let processing_time_us = event.metadata().handle_us as f64; + callback(event); + update_metrics(MetricsEventType::Account, 1, processing_time_us); + return Ok(()); + } + AccountParseResult::Filtered => return Ok(()), + AccountParseResult::Unsupported => {} + } + let account_event = AccountEventParser::parse_account_event( protocols, account_pretty, @@ -105,6 +117,10 @@ pub async fn process_grpc_transaction( block_meta_pretty.recv_us, ); + if !passes_event_type_filter(event_type_filter, &block_meta_event) { + return Ok(()); + } + let processing_time_us = block_meta_event.metadata().handle_us as f64; callback(block_meta_event); update_metrics(MetricsEventType::BlockMeta, 1, processing_time_us); @@ -136,8 +152,8 @@ pub async fn process_shred_transaction( let recv_us = transaction_with_slot.recv_us; let adapter_callback = create_metrics_callback(callback); - // Shred 路径仅能拿到 static_account_keys,且无 inner_instructions,解析限制见 docs/SHREDSTREAM_LIMITATIONS.md - // 若交易使用 ALT,账户可能为 default/错误;无 CPI 合并,timestamp/reserves 等多为 0。 + // Shred only exposes static account keys and no inner instructions; see + // docs/SHREDSTREAM_LIMITATIONS.md for the expected parser limits. let accounts = tx.message.static_account_keys(); EventParser::parse_instruction_events_from_versioned_transaction( @@ -146,7 +162,7 @@ pub async fn process_shred_transaction( &tx, signature, Some(slot), - None, // shred 无 block_time + None, // shred has no block_time recv_us, accounts, &[], @@ -174,5 +190,11 @@ fn update_metrics_with_latency( recv_us: i64, block_time_ms: i64, ) { - MetricsManager::global().update_metrics_with_latency(ty, count, time_us, recv_us, block_time_ms); + MetricsManager::global().update_metrics_with_latency( + ty, + count, + time_us, + recv_us, + block_time_ms, + ); } diff --git a/src/streaming/common/mod.rs b/src/streaming/common/mod.rs index bdf43c3..7f5d7d5 100644 --- a/src/streaming/common/mod.rs +++ b/src/streaming/common/mod.rs @@ -1,13 +1,13 @@ // 公用模块 - 包含流处理相关的通用功能 pub mod config; -pub mod metrics; pub mod constants; -pub mod subscription; pub mod event_processor; +pub mod metrics; +pub mod subscription; // 重新导出主要类型 pub use config::*; -pub use metrics::*; pub use constants::*; +pub use event_processor::*; +pub use metrics::*; pub use subscription::*; -pub use event_processor::*; \ No newline at end of file diff --git a/src/streaming/event_parser/common/filter.rs b/src/streaming/event_parser/common/filter.rs index 3c22fe5..3a99d52 100644 --- a/src/streaming/event_parser/common/filter.rs +++ b/src/streaming/event_parser/common/filter.rs @@ -1,24 +1,412 @@ use crate::streaming::event_parser::common::{ types::EventType, ACCOUNT_EVENT_TYPES, BLOCK_EVENT_TYPES, }; +use crate::streaming::event_parser::DexEvent; +use sol_parser_sdk::grpc::types::EventType as SdkGrpcEventType; +use sol_parser_sdk::grpc::types::EventTypeFilter as SdkGrpcEventTypeFilter; #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] pub struct EventTypeFilter { pub include: Vec, + pub exclude: Vec, } impl EventTypeFilter { + #[inline] + pub fn all() -> Self { + Self::default() + } + + #[inline] + pub fn include_only(include: impl Into>) -> Self { + Self { include: include.into(), exclude: Vec::new() } + } + + #[inline] + pub fn exclude_only(exclude: impl Into>) -> Self { + Self { include: Vec::new(), exclude: exclude.into() } + } + + #[inline] + pub fn include_exclude( + include: impl Into>, + exclude: impl Into>, + ) -> Self { + Self { include: include.into(), exclude: exclude.into() } + } + pub fn include_transaction_event(&self) -> bool { - self.include - .iter() - .any(|event| !ACCOUNT_EVENT_TYPES.contains(event) && !BLOCK_EVENT_TYPES.contains(event)) + if self.include.is_empty() && self.exclude.is_empty() { + return true; + } + if !self.include.is_empty() { + return self.include.iter().any(|event| { + !ACCOUNT_EVENT_TYPES.contains(event) && !BLOCK_EVENT_TYPES.contains(event) + }); + } + // With exclude-only filters, keep the stream open and drop matching events locally. + !self.exclude.is_empty() } pub fn include_account_event(&self) -> bool { - self.include.iter().any(|event| ACCOUNT_EVENT_TYPES.contains(event)) + if self.include.is_empty() && self.exclude.is_empty() { + return true; + } + if !self.include.is_empty() { + return self.include.iter().any(|event| ACCOUNT_EVENT_TYPES.contains(event)); + } + !self.exclude.is_empty() } pub fn include_block_event(&self) -> bool { - self.include.iter().any(|event| BLOCK_EVENT_TYPES.contains(event)) + if self.include.is_empty() && self.exclude.is_empty() { + return true; + } + if !self.include.is_empty() { + return self.include.iter().any(|event| BLOCK_EVENT_TYPES.contains(event)); + } + !self.exclude.is_empty() + } + + /// Apply `exclude` first, then `include`. Empty `include` means "allow all non-excluded types". + #[inline] + pub fn passes_event_type(&self, et: &EventType) -> bool { + if self.exclude.iter().any(|excluded| event_type_matches(excluded, et)) { + return false; + } + if self.include.is_empty() { + return true; + } + self.include.iter().any(|included| event_type_matches(included, et)) + } + + #[inline] + pub fn passes_for_event(&self, ev: &DexEvent) -> bool { + self.passes_event_type(&ev.metadata().event_type) + } +} + +/// `None` means no filtering; `Some(f)` applies include/exclude event-type semantics. +#[inline] +pub(crate) fn passes_event_type_filter(filter: Option<&EventTypeFilter>, ev: &DexEvent) -> bool { + match filter { + None => true, + Some(f) => f.passes_for_event(ev), + } +} + +/// Whether the local pass should parse ComputeBudget instructions. +#[inline] +pub(crate) fn filter_includes_compute_budget_types(filter: Option<&EventTypeFilter>) -> bool { + match filter { + None => true, + Some(f) => { + let limit_excluded = f.exclude.contains(&EventType::SetComputeUnitLimit); + let price_excluded = f.exclude.contains(&EventType::SetComputeUnitPrice); + if limit_excluded && price_excluded { + return false; + } + if f.include.is_empty() { + return true; + } + f.include.iter().any(|t| { + matches!(t, EventType::SetComputeUnitLimit | EventType::SetComputeUnitPrice) + }) + } + } +} + +/// Map the streamer filter to the SDK gRPC event-type filter used by +/// [`sol_parser_sdk::grpc::parse_subscribe_update_transaction_low_latency`]. +/// +/// - Empty include/exclude maps to `None`. +/// - Exclude-only maps to SDK `exclude_types` when at least one SDK type is known. +/// - Non-empty include maps to SDK `include_only`; streamer still applies exclude locally. +/// - If any included type cannot map to an SDK type, return `None` to avoid dropping it upstream. +pub(crate) fn build_sdk_parse_event_filter( + filter: Option<&EventTypeFilter>, +) -> Option { + let f = filter?; + + if !f.exclude.is_empty() && f.include.is_empty() { + let mut raw: Vec = Vec::new(); + for et in &f.exclude { + raw.extend(streamer_event_to_sdk_grpc_types(et)); + } + dedup_sdk_grpc_event_types(&mut raw); + return (!raw.is_empty()).then(|| SdkGrpcEventTypeFilter::exclude_types(raw)); + } + + if f.include.is_empty() { + return None; + } + let mut raw: Vec = Vec::new(); + for et in &f.include { + let mapped = streamer_event_to_sdk_grpc_types(et); + if mapped.is_empty() { + return None; + } + raw.extend(mapped); + } + dedup_sdk_grpc_event_types(&mut raw); + Some(SdkGrpcEventTypeFilter::include_only(raw)) +} + +fn dedup_sdk_grpc_event_types(v: &mut Vec) { + let mut i = 0; + while i < v.len() { + if v[..i].contains(&v[i]) { + v.remove(i); + } else { + i += 1; + } + } +} + +fn streamer_event_to_sdk_grpc_types(t: &EventType) -> Vec { + use EventType as St; + use SdkGrpcEventType as Sdk; + match t { + St::BlockMeta => vec![Sdk::BlockMeta], + St::PumpFunCreateToken => vec![Sdk::PumpFunCreate], + St::PumpFunCreateV2Token => vec![Sdk::PumpFunCreateV2], + St::PumpFunBuy => vec![Sdk::PumpFunBuy, Sdk::PumpFunBuyExactSolIn], + St::PumpFunBuyExactSolIn => vec![Sdk::PumpFunBuyExactSolIn], + St::PumpFunSell => vec![Sdk::PumpFunSell], + St::PumpFunMigrate => vec![Sdk::PumpFunMigrate], + St::PumpFeesCreateFeeSharingConfig => vec![Sdk::PumpFeesCreateFeeSharingConfig], + St::PumpFeesInitializeFeeConfig => vec![Sdk::PumpFeesInitializeFeeConfig], + St::PumpFeesResetFeeSharingConfig => vec![Sdk::PumpFeesResetFeeSharingConfig], + St::PumpFeesRevokeFeeSharingAuthority => vec![Sdk::PumpFeesRevokeFeeSharingAuthority], + St::PumpFeesTransferFeeSharingAuthority => vec![Sdk::PumpFeesTransferFeeSharingAuthority], + St::PumpFeesUpdateAdmin => vec![Sdk::PumpFeesUpdateAdmin], + St::PumpFeesUpdateFeeConfig => vec![Sdk::PumpFeesUpdateFeeConfig], + St::PumpFeesUpdateFeeShares => vec![Sdk::PumpFeesUpdateFeeShares], + St::PumpFeesUpsertFeeTiers => vec![Sdk::PumpFeesUpsertFeeTiers], + St::PumpFunMigrateBondingCurveCreator => vec![Sdk::PumpFunMigrateBondingCurveCreator], + St::PumpSwapBuy => vec![Sdk::PumpSwapBuy], + St::PumpSwapSell => vec![Sdk::PumpSwapSell], + St::PumpSwapCreatePool => vec![Sdk::PumpSwapCreatePool], + St::PumpSwapDeposit => vec![Sdk::PumpSwapLiquidityAdded], + St::PumpSwapWithdraw => vec![Sdk::PumpSwapLiquidityRemoved], + St::BonkBuyExactIn | St::BonkBuyExactOut | St::BonkSellExactIn | St::BonkSellExactOut => { + vec![Sdk::BonkTrade] + } + St::BonkInitialize | St::BonkInitializeV2 | St::BonkInitializeWithToken2022 => { + vec![Sdk::BonkPoolCreate] + } + St::BonkMigrateToAmm => vec![Sdk::BonkMigrateAmm], + St::MeteoraDammV2Swap | St::MeteoraDammV2Swap2 => vec![Sdk::MeteoraDammV2Swap], + St::MeteoraDammV2AddLiquidity => vec![Sdk::MeteoraDammV2AddLiquidity], + St::MeteoraDammV2RemoveLiquidity => vec![Sdk::MeteoraDammV2RemoveLiquidity], + St::MeteoraDammV2CreatePosition => vec![Sdk::MeteoraDammV2CreatePosition], + St::MeteoraDammV2ClosePosition => vec![Sdk::MeteoraDammV2ClosePosition], + St::TokenAccount => vec![Sdk::TokenAccount], + St::TokenInfo => vec![Sdk::TokenAccount], + St::NonceAccount => vec![Sdk::NonceAccount], + St::AccountPumpFunGlobal => vec![Sdk::AccountPumpFunGlobal], + St::AccountPumpSwapGlobalConfig => vec![Sdk::AccountPumpSwapGlobalConfig], + St::AccountPumpSwapPool => vec![Sdk::AccountPumpSwapPool], + _ => vec![], + } +} + +#[inline] +fn event_type_matches(filter_type: &EventType, event_type: &EventType) -> bool { + filter_type == event_type + || matches!( + (filter_type, event_type), + (EventType::PumpFunBuy, EventType::PumpFunBuyExactSolIn) + | (EventType::TokenAccount, EventType::TokenInfo) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::streaming::event_parser::common::types::ProtocolType; + use crate::streaming::event_parser::common::EventMetadata; + use crate::streaming::event_parser::protocols::block::block_meta_event::BlockMetaEvent; + use crate::streaming::event_parser::protocols::sol_parser_forward::events::ParserSdkErrorEvent; + + fn mk_meta(et: EventType) -> EventMetadata { + EventMetadata::new( + Default::default(), + 0, + 0, + 0, + ProtocolType::PumpFun, + et, + Default::default(), + 0, + None, + 0, + None, + None, + ) + } + + #[test] + fn passes_for_event_empty_include_is_all_true() { + let f = EventTypeFilter { include: vec![], ..Default::default() }; + let ev = DexEvent::ParserSdkErrorEvent(ParserSdkErrorEvent { + metadata: mk_meta(EventType::ParserSdkError), + message: "x".into(), + }); + assert!(f.passes_for_event(&ev)); + } + + #[test] + fn constructors_set_expected_filter_sides() { + assert_eq!(EventTypeFilter::all(), EventTypeFilter::default()); + assert_eq!( + EventTypeFilter::include_only([EventType::PumpFunBuy]).include, + vec![EventType::PumpFunBuy] + ); + assert_eq!( + EventTypeFilter::exclude_only([EventType::PumpFunSell]).exclude, + vec![EventType::PumpFunSell] + ); + let both = + EventTypeFilter::include_exclude([EventType::PumpFunBuy], [EventType::PumpFunSell]); + assert_eq!(both.include, vec![EventType::PumpFunBuy]); + assert_eq!(both.exclude, vec![EventType::PumpFunSell]); + } + + #[test] + fn empty_filter_includes_all_subscription_kinds() { + let f = EventTypeFilter::default(); + assert!(f.include_transaction_event()); + assert!(f.include_account_event()); + assert!(f.include_block_event()); + } + + #[test] + fn exclude_blocks_even_when_include_empty() { + let f = EventTypeFilter { + include: vec![], + exclude: vec![EventType::PumpFunSell], + ..Default::default() + }; + let ev = DexEvent::ParserSdkErrorEvent(ParserSdkErrorEvent { + metadata: mk_meta(EventType::PumpFunSell), + message: "x".into(), + }); + assert!(!f.passes_for_event(&ev)); + } + + #[test] + fn exclude_blocks_block_meta_when_stream_kept_open() { + let f = EventTypeFilter::exclude_only([EventType::BlockMeta]); + let ev = DexEvent::BlockMetaEvent(BlockMetaEvent { + metadata: mk_meta(EventType::BlockMeta), + slot: 0, + block_hash: String::new(), + }); + assert!(f.include_block_event()); + assert!(!f.passes_for_event(&ev)); + } + + #[test] + fn exclude_applies_after_include_allow() { + let f = EventTypeFilter { + include: vec![EventType::PumpFunBuy, EventType::PumpFunSell], + exclude: vec![EventType::PumpFunSell], + ..Default::default() + }; + let buy = DexEvent::ParserSdkErrorEvent(ParserSdkErrorEvent { + metadata: mk_meta(EventType::PumpFunBuy), + message: "x".into(), + }); + let sell = DexEvent::ParserSdkErrorEvent(ParserSdkErrorEvent { + metadata: mk_meta(EventType::PumpFunSell), + message: "x".into(), + }); + assert!(f.passes_for_event(&buy)); + assert!(!f.passes_for_event(&sell)); + } + + #[test] + fn build_sdk_filter_exclude_only_pumpfun_sell() { + let f = EventTypeFilter { + include: vec![], + exclude: vec![EventType::PumpFunSell], + ..Default::default() + }; + let sdk_f = build_sdk_parse_event_filter(Some(&f)).expect("mapped"); + assert!(sdk_f.should_include(SdkGrpcEventType::PumpFunBuy)); + assert!(!sdk_f.should_include(SdkGrpcEventType::PumpFunSell)); + } + + #[test] + fn build_sdk_filter_pumpfun_buy_only() { + let f = EventTypeFilter { include: vec![EventType::PumpFunBuy], ..Default::default() }; + let sdk_f = build_sdk_parse_event_filter(Some(&f)).expect("mapped"); + assert!(sdk_f.should_include(SdkGrpcEventType::PumpFunBuy)); + assert!(sdk_f.should_include(SdkGrpcEventType::PumpFunBuyExactSolIn)); + assert!(!sdk_f.should_include(SdkGrpcEventType::PumpFunSell)); + } + + #[test] + fn pumpfun_buy_filter_matches_exact_sol_in_for_backward_compat() { + let f = EventTypeFilter { include: vec![EventType::PumpFunBuy], ..Default::default() }; + assert!(f.passes_event_type(&EventType::PumpFunBuy)); + assert!(f.passes_event_type(&EventType::PumpFunBuyExactSolIn)); + + let f = EventTypeFilter { + include: vec![EventType::PumpFunBuyExactSolIn], + ..Default::default() + }; + assert!(!f.passes_event_type(&EventType::PumpFunBuy)); + assert!(f.passes_event_type(&EventType::PumpFunBuyExactSolIn)); + } + + #[test] + fn token_account_filter_matches_token_info_for_backward_compat() { + let f = EventTypeFilter { include: vec![EventType::TokenAccount], ..Default::default() }; + assert!(f.passes_event_type(&EventType::TokenAccount)); + assert!(f.passes_event_type(&EventType::TokenInfo)); + + let f = EventTypeFilter { include: vec![EventType::TokenInfo], ..Default::default() }; + assert!(!f.passes_event_type(&EventType::TokenAccount)); + assert!(f.passes_event_type(&EventType::TokenInfo)); + } + + #[test] + fn build_sdk_filter_pumpfun_exact_sol_in_only() { + let f = EventTypeFilter { + include: vec![EventType::PumpFunBuyExactSolIn], + ..Default::default() + }; + let sdk_f = build_sdk_parse_event_filter(Some(&f)).expect("mapped"); + assert!(!sdk_f.should_include(SdkGrpcEventType::PumpFunBuy)); + assert!(sdk_f.should_include(SdkGrpcEventType::PumpFunBuyExactSolIn)); + assert!(!sdk_f.should_include(SdkGrpcEventType::PumpFunSell)); + } + + #[test] + fn build_sdk_filter_token_info_maps_to_sdk_token_account() { + let f = EventTypeFilter { include: vec![EventType::TokenInfo], ..Default::default() }; + let sdk_f = build_sdk_parse_event_filter(Some(&f)).expect("mapped"); + assert!(sdk_f.should_include(SdkGrpcEventType::TokenAccount)); + } + + #[test] + fn build_sdk_filter_none_when_orca_in_mix() { + let f = EventTypeFilter { + include: vec![EventType::PumpFunBuy, EventType::OrcaWhirlpoolSwap], + ..Default::default() + }; + assert!(build_sdk_parse_event_filter(Some(&f)).is_none()); + } + + #[test] + fn build_sdk_filter_exclude_only_none_when_only_unmapped_types() { + let f = EventTypeFilter { + include: vec![], + exclude: vec![EventType::OrcaWhirlpoolSwap], + ..Default::default() + }; + assert!(build_sdk_parse_event_filter(Some(&f)).is_none()); } } diff --git a/src/streaming/event_parser/common/high_performance_clock.rs b/src/streaming/event_parser/common/high_performance_clock.rs index 6be9e4b..dc2ab17 100644 --- a/src/streaming/event_parser/common/high_performance_clock.rs +++ b/src/streaming/event_parser/common/high_performance_clock.rs @@ -25,12 +25,14 @@ impl HighPerformanceClock { // 通过多次采样来减少初始化误差 let mut best_offset = i64::MAX; let mut best_instant = Instant::now(); - let mut best_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as i64; + let mut best_timestamp = + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as i64; // 进行3次采样,选择延迟最小的 for _ in 0..3 { let instant_before = Instant::now(); - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as i64; + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as i64; let instant_after = Instant::now(); let sample_latency = instant_after.duration_since(instant_before).as_nanos() as i64; @@ -113,8 +115,7 @@ impl Default for HighPerformanceClock { } /// 全局高性能时钟实例 -static HIGH_PERF_CLOCK: std::sync::OnceLock = - std::sync::OnceLock::new(); +static HIGH_PERF_CLOCK: std::sync::OnceLock = std::sync::OnceLock::new(); /// 获取全局高性能时钟实例(最简单的实现) #[inline(always)] diff --git a/src/streaming/event_parser/common/types.rs b/src/streaming/event_parser/common/types.rs index f8a6a4f..04f78a5 100755 --- a/src/streaming/event_parser/common/types.rs +++ b/src/streaming/event_parser/common/types.rs @@ -51,12 +51,24 @@ pub enum ProtocolType { RaydiumClmm, RaydiumAmmV4, MeteoraDammV2, + OrcaWhirlpool, + MeteoraPools, + MeteoraDlmm, Common, } /// Event type enumeration #[derive( - Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Debug, + Clone, + Default, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, )] pub enum EventType { // PumpSwap events @@ -71,8 +83,19 @@ pub enum EventType { PumpFunCreateToken, PumpFunCreateV2Token, PumpFunBuy, + PumpFunBuyExactSolIn, PumpFunSell, PumpFunMigrate, + PumpFeesCreateFeeSharingConfig, + PumpFeesInitializeFeeConfig, + PumpFeesResetFeeSharingConfig, + PumpFeesRevokeFeeSharingAuthority, + PumpFeesTransferFeeSharingAuthority, + PumpFeesUpdateAdmin, + PumpFeesUpdateFeeConfig, + PumpFeesUpdateFeeShares, + PumpFeesUpsertFeeTiers, + PumpFunMigrateBondingCurveCreator, // Bonk events BonkBuyExactIn, @@ -101,6 +124,7 @@ pub enum EventType { RaydiumClmmCreatePool, RaydiumClmmOpenPositionWithToken22Nft, RaydiumClmmOpenPositionV2, + RaydiumClmmCollectFee, // Raydium AMM V4 events RaydiumAmmV4SwapBaseIn, @@ -116,6 +140,34 @@ pub enum EventType { MeteoraDammV2InitializePool, MeteoraDammV2InitializeCustomizablePool, MeteoraDammV2InitializePoolWithDynamicConfig, + MeteoraDammV2CreatePosition, + MeteoraDammV2ClosePosition, + MeteoraDammV2AddLiquidity, + MeteoraDammV2RemoveLiquidity, + + // Orca Whirlpool + OrcaWhirlpoolSwap, + OrcaWhirlpoolLiquidityIncreased, + OrcaWhirlpoolLiquidityDecreased, + OrcaWhirlpoolPoolInitialized, + + // Meteora Pools + MeteoraPoolsSwap, + MeteoraPoolsAddLiquidity, + MeteoraPoolsRemoveLiquidity, + MeteoraPoolsBootstrapLiquidity, + MeteoraPoolsPoolCreated, + MeteoraPoolsSetPoolFees, + + // Meteora DLMM + MeteoraDlmmSwap, + MeteoraDlmmAddLiquidity, + MeteoraDlmmRemoveLiquidity, + MeteoraDlmmInitializePool, + MeteoraDlmmInitializeBinArray, + MeteoraDlmmCreatePosition, + MeteoraDlmmClosePosition, + MeteoraDlmmClaimFee, // Account events AccountRaydiumAmmV4AmmInfo, @@ -135,11 +187,13 @@ pub enum EventType { NonceAccount, TokenAccount, + TokenInfo, // Common events BlockMeta, SetComputeUnitLimit, SetComputeUnitPrice, + ParserSdkError, Unknown, } @@ -159,6 +213,7 @@ pub const ACCOUNT_EVENT_TYPES: &[EventType] = &[ EventType::AccountRaydiumCpmmAmmConfig, EventType::AccountRaydiumCpmmPoolState, EventType::TokenAccount, + EventType::TokenInfo, EventType::NonceAccount, ]; pub const BLOCK_EVENT_TYPES: &[EventType] = &[EventType::BlockMeta]; @@ -240,13 +295,10 @@ impl EventMetadata { } } -static SOL_MINT: std::sync::LazyLock = - std::sync::LazyLock::new(spl_token::native_mint::id); -static SYSTEM_PROGRAMS: std::sync::LazyLock<[Pubkey; 3]> = std::sync::LazyLock::new(|| [ - spl_token::id(), - spl_token_2022::id(), - solana_sdk::pubkey!("11111111111111111111111111111111"), -]); +static SOL_MINT: std::sync::LazyLock = std::sync::LazyLock::new(spl_token::native_mint::id); +static SYSTEM_PROGRAMS: std::sync::LazyLock<[Pubkey; 3]> = std::sync::LazyLock::new(|| { + [spl_token::id(), spl_token_2022::id(), solana_sdk::pubkey!("11111111111111111111111111111111")] +}); /// Trait abstracting over different inner-instruction types for swap data extraction pub trait InnerInstructionLike { @@ -282,11 +334,16 @@ impl InnerInstructionLike for yellowstone_grpc_proto::prelude::InnerInstruction } /// Extract event context (mint/token account/vault info) from a DexEvent -fn extract_swap_context(event: &DexEvent) -> ( +fn extract_swap_context( + event: &DexEvent, +) -> ( SwapData, - Option, Option, - Option, Option, - Option, Option, + Option, + Option, + Option, + Option, + Option, + Option, ) { let mut swap_data = SwapData::default(); let mut from_mint: Option = None; diff --git a/src/streaming/event_parser/core/account_event_parser.rs b/src/streaming/event_parser/core/account_event_parser.rs index 5711d04..a78dc43 100644 --- a/src/streaming/event_parser/core/account_event_parser.rs +++ b/src/streaming/event_parser/core/account_event_parser.rs @@ -92,7 +92,7 @@ impl AccountEventParser { ) { // 应用事件类型过滤 if let Some(filter) = event_type_filter { - if filter.include.contains(&event.metadata().event_type) { + if filter.passes_event_type(&event.metadata().event_type) { return Some(event); } // 不匹配过滤器,继续尝试其他解析方式 @@ -120,7 +120,7 @@ impl AccountEventParser { // 尝试解析 Nonce 账户 if let Some(event) = Self::parse_nonce_account_event(&account, metadata.clone()) { if let Some(filter) = event_type_filter { - if filter.include.contains(&event.metadata().event_type) { + if filter.passes_event_type(&event.metadata().event_type) { return Some(event); } } else { @@ -131,7 +131,7 @@ impl AccountEventParser { // 尝试解析 Token 账户 if let Some(event) = Self::parse_token_account_event(&account, metadata) { if let Some(filter) = event_type_filter { - if filter.include.contains(&event.metadata().event_type) { + if filter.passes_event_type(&event.metadata().event_type) { return Some(event); } } else { @@ -156,6 +156,8 @@ impl AccountEventParser { // Spl Token Mint if account.data.len() >= Mint::LEN { if let Ok(mint) = Mint::unpack_from_slice(&account.data) { + let mut metadata = metadata.clone(); + metadata.event_type = EventType::TokenInfo; let mut event = TokenInfoEvent { metadata, pubkey, @@ -174,6 +176,8 @@ impl AccountEventParser { // Spl Token2022 Mint if account.data.len() >= Account2022::LEN { if let Ok(mint) = StateWithExtensions::::unpack(&account.data) { + let mut metadata = metadata.clone(); + metadata.event_type = EventType::TokenInfo; let mut event = TokenInfoEvent { metadata, pubkey, diff --git a/src/streaming/event_parser/core/dispatcher.rs b/src/streaming/event_parser/core/dispatcher.rs index 77691ce..1df8f37 100644 --- a/src/streaming/event_parser/core/dispatcher.rs +++ b/src/streaming/event_parser/core/dispatcher.rs @@ -1,6 +1,9 @@ -//! 中心事件解析调度器 +//! 事件路由入口(类比 sol-parser-sdk 的 `instr`,区分「原生字节解析」与「sdk 事件对齐」)。 //! -//! 根据协议类型路由到对应的解析函数,替代原有的静态 CONFIGS 数组架构 +//! ## 代码去哪找 +//! - **`protocols/<协议>/parser.rs`** — Yellowstone / shred 路径下的顶层与 inner 指令解析(手写)。 +//! - **`streaming/parser_sdk_bridge/`** — `sol-parser-sdk::DexEvent` → streamer `DexEvent` 字段映射。 +//! - **`protocols/sol_parser_forward/native.rs`** — Orca / Meteora Pools & DLMM:调用 sdk `instr` 后再走 bridge。 //! //! ## 设计原则 //! - **单一职责**: 每个函数只负责一件事(路由、解析、合并分离) @@ -11,9 +14,10 @@ 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, - pumpswap::parser as pumpswap, raydium_amm_v4::parser as raydium_amm_v4, - raydium_clmm::parser as raydium_clmm, raydium_cpmm::parser as raydium_cpmm, + bonk::parser as bonk, meteora_damm_v2::parser as meteora_damm_v2, + 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, sol_parser_forward, }, DexEvent, Protocol, }; @@ -54,9 +58,21 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::OrcaWhirlpool => ProtocolType::OrcaWhirlpool, + Protocol::MeteoraPools => ProtocolType::MeteoraPools, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, }; match protocol { + Protocol::OrcaWhirlpool | Protocol::MeteoraPools | Protocol::MeteoraDlmm => { + sol_parser_forward::native::dispatch_instruction( + protocol.clone(), + instruction_discriminator, + instruction_data, + accounts, + &metadata, + ) + } Protocol::PumpFun => pumpfun::parse_pumpfun_instruction_data( instruction_discriminator, instruction_data, @@ -129,9 +145,20 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::OrcaWhirlpool => ProtocolType::OrcaWhirlpool, + Protocol::MeteoraPools => ProtocolType::MeteoraPools, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, }; match protocol { + Protocol::OrcaWhirlpool | Protocol::MeteoraPools | Protocol::MeteoraDlmm => { + sol_parser_forward::native::dispatch_inner_instruction( + protocol.clone(), + inner_instruction_discriminator, + inner_instruction_data, + &metadata, + ) + } Protocol::PumpFun => pumpfun::parse_pumpfun_inner_instruction_data( inner_instruction_discriminator, inner_instruction_data, @@ -162,11 +189,13 @@ impl EventDispatcher { inner_instruction_data, metadata, ), - Protocol::MeteoraDammV2 => meteora_damm_v2::parse_meteora_damm_v2_inner_instruction_data( - inner_instruction_discriminator, - inner_instruction_data, - metadata, - ), + Protocol::MeteoraDammV2 => { + meteora_damm_v2::parse_meteora_damm_v2_inner_instruction_data( + inner_instruction_discriminator, + inner_instruction_data, + metadata, + ) + } } } @@ -187,6 +216,12 @@ impl EventDispatcher { Some(Protocol::RaydiumAmmV4) } else if program_id == &meteora_damm_v2::METEORA_DAMM_V2_PROGRAM_ID { Some(Protocol::MeteoraDammV2) + } else if program_id == &sol_parser_forward::ORCA_WHIRLPOOL_PROGRAM_ID { + Some(Protocol::OrcaWhirlpool) + } else if program_id == &sol_parser_forward::METEORA_POOLS_PROGRAM_ID { + Some(Protocol::MeteoraPools) + } else if program_id == &sol_parser_forward::METEORA_DLMM_PROGRAM_ID { + Some(Protocol::MeteoraDlmm) } else { None } @@ -225,6 +260,9 @@ 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::OrcaWhirlpool => sol_parser_forward::ORCA_WHIRLPOOL_PROGRAM_ID, + Protocol::MeteoraPools => sol_parser_forward::METEORA_POOLS_PROGRAM_ID, + Protocol::MeteoraDlmm => sol_parser_forward::METEORA_DLMM_PROGRAM_ID, } } @@ -261,9 +299,13 @@ impl EventDispatcher { Protocol::RaydiumClmm => ProtocolType::RaydiumClmm, Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4, Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2, + Protocol::OrcaWhirlpool => ProtocolType::OrcaWhirlpool, + Protocol::MeteoraPools => ProtocolType::MeteoraPools, + Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm, }; match protocol { + Protocol::OrcaWhirlpool | Protocol::MeteoraPools | Protocol::MeteoraDlmm => None, Protocol::PumpFun => { pumpfun::parse_pumpfun_account_data(discriminator, account, metadata) } diff --git a/src/streaming/event_parser/core/event_parser.rs b/src/streaming/event_parser/core/event_parser.rs deleted file mode 100644 index 5ac8601..0000000 --- a/src/streaming/event_parser/core/event_parser.rs +++ /dev/null @@ -1,740 +0,0 @@ -use crate::streaming::event_parser::{ - DexEvent, Protocol, common::{ - EventMetadata, filter::EventTypeFilter, high_performance_clock::elapsed_micros_since, parse_swap_data_from_next_grpc_instructions, parse_swap_data_from_next_instructions - }, core::{ - dispatcher::EventDispatcher, - global_state::{ - add_bonk_dev_address, add_dev_address, is_bonk_dev_address_in_signature, - is_dev_address_in_signature, - }, - merger_event::merge, - }, protocols::raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID -}; -use prost_types::Timestamp; -use solana_sdk::{ - message::compiled_instruction::CompiledInstruction, pubkey::Pubkey, signature::Signature, - transaction::VersionedTransaction, -}; -use solana_transaction_status::InnerInstructions; -use std::sync::Arc; -use yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo; - -pub struct EventParser {} - -impl EventParser { - // ================================================================================================ - // Public API - Entry Points - // ================================================================================================ - - /// Parse transaction from gRPC stream - /// - /// This is the main entry point for parsing transactions received from gRPC streams. - /// It extracts account keys, inner instructions, and delegates to instruction parsing. - pub async fn parse_grpc_transaction( - protocols: &[Protocol], - event_type_filter: Option<&EventTypeFilter>, - grpc_tx: SubscribeUpdateTransactionInfo, - signature: Signature, - slot: Option, - block_time: Option, - recv_us: i64, - bot_wallet: Option, - tx_index: Option, - callback: Arc, - ) -> anyhow::Result<()> { - // 创建适配器回调,将所有权回调转换为引用回调 - let adapter_callback = Arc::new(move |event: &DexEvent| { - callback(event.clone()); - }); - if let Some(transition) = grpc_tx.transaction { - if let Some(message) = &transition.message { - let mut address_table_lookups: Vec> = vec![]; - let mut inner_instructions: Vec< - yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions, - > = vec![]; - - if let Some(meta) = grpc_tx.meta { - inner_instructions = meta.inner_instructions; - address_table_lookups.reserve( - meta.loaded_writable_addresses.len() + meta.loaded_readonly_addresses.len(), - ); - let loaded_writable_addresses = meta.loaded_writable_addresses; - let loaded_readonly_addresses = meta.loaded_readonly_addresses; - address_table_lookups.extend( - loaded_writable_addresses.into_iter().chain(loaded_readonly_addresses), - ); - } - - let mut accounts_bytes: Vec> = - Vec::with_capacity(message.account_keys.len() + address_table_lookups.len()); - accounts_bytes.extend_from_slice(&message.account_keys); - accounts_bytes.extend(address_table_lookups); - // 转换为 Pubkey - let accounts: Vec = accounts_bytes - .iter() - .filter_map(|account| { - if account.len() == 32 { - Some(Pubkey::try_from(account.as_slice()).unwrap_or_default()) - } else { - None - } - }) - .collect(); - // 解析指令事件 - let instructions = &message.instructions; - let recent_blockhash = if message.recent_blockhash.len() != 32 { - None - } else { - Some(solana_sdk::bs58::encode(&message.recent_blockhash).into_string()) - }; - Self::parse_instruction_events_from_grpc_transaction( - protocols, - event_type_filter, - &instructions, - signature, - slot, - block_time, - recv_us, - &accounts, - &inner_instructions, - bot_wallet, - tx_index, - recent_blockhash, - adapter_callback, - ) - .await?; - } - } - - Ok(()) - } - - /// Parse transaction from VersionedTransaction - /// - /// This is the entry point for parsing VersionedTransaction objects. - /// It's used when working with RPC responses or historical data. - #[allow(clippy::too_many_arguments)] - pub async fn parse_instruction_events_from_versioned_transaction( - protocols: &[Protocol], - event_type_filter: Option<&EventTypeFilter>, - transaction: &VersionedTransaction, - signature: Signature, - slot: Option, - block_time: Option, - recv_us: i64, - accounts: &[Pubkey], - inner_instructions: &[InnerInstructions], - bot_wallet: Option, - tx_index: Option, - callback: Arc, - ) -> anyhow::Result<()> { - // 创建适配器回调,将所有权回调转换为引用回调 - let adapter_callback = Arc::new(move |event: &DexEvent| { - callback(event.clone()); - }); - // 获取交易的指令和账户 - let compiled_instructions = transaction.message.instructions(); - let recent_blockhash = Some(transaction.message.recent_blockhash().to_string()); - let mut accounts: Vec = accounts.to_vec(); - // 检查交易中是否包含程序 - let has_program = accounts - .iter() - .any(|account| Self::should_handle(protocols, event_type_filter, account)); - if has_program { - // 解析每个指令 - for (index, instruction) in compiled_instructions.iter().enumerate() { - if let Some(program_id) = accounts.get(instruction.program_id_index as usize) { - let program_id = *program_id; // 克隆程序ID,避免借用冲突 - let inner_instructions = inner_instructions - .iter() - .find(|inner_instruction| inner_instruction.index == index as u8); - if Self::should_handle(protocols, event_type_filter, &program_id) { - let max_idx = instruction.accounts.iter().max().unwrap_or(&0); - // 补齐accounts(使用Pubkey::default()) - if *max_idx as usize >= accounts.len() { - accounts.resize(*max_idx as usize + 1, Pubkey::default()); - } - Self::parse_events_from_instruction( - protocols, - event_type_filter, - instruction, - &accounts, - signature, - slot.unwrap_or(0), - block_time, - recv_us, - index as i64, - None, - bot_wallet, - tx_index, - recent_blockhash.as_deref(), - inner_instructions, - adapter_callback.clone(), - )?; - } - // Immediately process inner instructions for correct ordering - if let Some(inner_instructions) = inner_instructions { - for (inner_index, inner_instruction) in - inner_instructions.instructions.iter().enumerate() - { - Self::parse_events_from_instruction( - protocols, - event_type_filter, - &inner_instruction.instruction, - &accounts, - signature, - slot.unwrap_or(0), - block_time, - recv_us, - index as i64, - Some(inner_index as i64), - bot_wallet, - tx_index, - recent_blockhash.as_deref(), - Some(&inner_instructions), - adapter_callback.clone(), - )?; - } - } - } - } - } - Ok(()) - } - - // ================================================================================================ - // gRPC Transaction Processing - // ================================================================================================ - - /// Parse instruction events from gRPC transaction format - /// - /// Iterates through all instructions in a gRPC transaction, checks if they should be handled, - /// and delegates to instruction-level parsing for both outer and inner instructions. - #[allow(clippy::too_many_arguments)] - async fn parse_instruction_events_from_grpc_transaction( - protocols: &[Protocol], - event_type_filter: Option<&EventTypeFilter>, - compiled_instructions: &[yellowstone_grpc_proto::prelude::CompiledInstruction], - signature: Signature, - slot: Option, - block_time: Option, - recv_us: i64, - accounts: &[Pubkey], - inner_instructions: &[yellowstone_grpc_proto::prelude::InnerInstructions], - bot_wallet: Option, - tx_index: Option, - recent_blockhash: Option, - callback: Arc Fn(&'a DexEvent) + Send + Sync>, - ) -> anyhow::Result<()> { - // 获取交易的指令和账户 - let mut accounts = accounts.to_vec(); - // 检查交易中是否包含程序 - let has_program = accounts - .iter() - .any(|account| Self::should_handle(protocols, event_type_filter, account)); - if has_program { - // 解析每个指令 - for (index, instruction) in compiled_instructions.iter().enumerate() { - if let Some(program_id) = accounts.get(instruction.program_id_index as usize) { - let program_id = *program_id; // 克隆程序ID,避免借用冲突 - let inner_instructions = inner_instructions - .iter() - .find(|inner_instruction| inner_instruction.index == index as u32); - let max_idx = instruction.accounts.iter().max().unwrap_or(&0); - // 补齐accounts(使用Pubkey::default()) - if *max_idx as usize >= accounts.len() { - accounts.resize(*max_idx as usize + 1, Pubkey::default()); - } - if Self::should_handle(protocols, event_type_filter, &program_id) { - Self::parse_events_from_grpc_instruction( - protocols, - event_type_filter, - instruction, - &accounts, - signature, - slot.unwrap_or(0), - block_time, - recv_us, - index as i64, - None, - bot_wallet, - tx_index, - recent_blockhash.as_deref(), - inner_instructions, - callback.clone(), - )?; - } - // Immediately process inner instructions for correct ordering - if let Some(inner_instructions) = inner_instructions { - for (inner_index, inner_instruction) in - inner_instructions.instructions.iter().enumerate() - { - let inner_accounts = &inner_instruction.accounts; - let data = &inner_instruction.data; - let instruction = - yellowstone_grpc_proto::prelude::CompiledInstruction { - program_id_index: inner_instruction.program_id_index, - accounts: inner_accounts.to_vec(), - data: data.to_vec(), - }; - Self::parse_events_from_grpc_instruction( - protocols, - event_type_filter, - &instruction, - &accounts, - signature, - slot.unwrap_or(0), - block_time, - recv_us, - inner_instructions.index as i64, - Some(inner_index as i64), - bot_wallet, - tx_index, - recent_blockhash.as_deref(), - Some(&inner_instructions), - callback.clone(), - )?; - } - } - } - } - } - Ok(()) - } - - /// Parse events from gRPC instruction - /// - /// Core parsing logic for a single gRPC instruction. Extracts discriminator, dispatches - /// to protocol-specific parsers, handles inner instructions, and processes swap data. - #[allow(clippy::too_many_arguments)] - fn parse_events_from_grpc_instruction( - protocols: &[Protocol], - event_type_filter: Option<&EventTypeFilter>, - instruction: &yellowstone_grpc_proto::prelude::CompiledInstruction, - accounts: &[Pubkey], - signature: Signature, - slot: u64, - block_time: Option, - recv_us: i64, - outer_index: i64, - inner_index: Option, - bot_wallet: Option, - tx_index: Option, - recent_blockhash: Option<&str>, - inner_instructions: Option<&yellowstone_grpc_proto::prelude::InnerInstructions>, - callback: Arc Fn(&'a DexEvent) + Send + Sync>, - ) -> anyhow::Result<()> { - // 添加边界检查以防止越界访问 - let program_id_index = instruction.program_id_index as usize; - if program_id_index >= accounts.len() { - return Ok(()); - } - let program_id = accounts[program_id_index]; - if !Self::should_handle(protocols, event_type_filter, &program_id) { - return Ok(()); - } - - let is_cu_program = EventDispatcher::is_compute_budget_program(&program_id); - - let disc_len = match program_id { - RAYDIUM_AMM_V4_PROGRAM_ID => 1, - _ => 8, - }; - - // 检查指令数据长度(至少需要 disc_len 字节的 discriminator) - if !is_cu_program && instruction.data.len() < disc_len { - return Ok(()); - } - // 创建元数据 - let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 }); - let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000; - let metadata = EventMetadata::new( - signature, - slot, - timestamp.seconds, - block_time_ms, - Default::default(), // protocol will be set by dispatcher - Default::default(), // event_type will be set by dispatcher - program_id, - outer_index, - inner_index, - recv_us, - tx_index, - recent_blockhash.map(|s| s.to_string()), - ); - - if is_cu_program { - if let Some(event) = EventDispatcher::dispatch_compute_budget_instruction( - &instruction.data, - metadata.clone(), - ) { - callback(&event); - } - return Ok(()); - } - - // 使用 EventDispatcher 匹配协议 - let protocol = match EventDispatcher::match_protocol_by_program_id(&program_id) { - Some(p) => p, - None => return Ok(()), - }; - - // 提取 discriminator 和数据 - let instruction_discriminator = &instruction.data[..disc_len]; - let instruction_data = &instruction.data[disc_len..]; - - // 构建账户公钥列表 - let account_pubkeys: Vec = instruction - .accounts - .iter() - .filter_map(|&idx| accounts.get(idx as usize).copied()) - .collect(); - - // 使用 EventDispatcher 解析 instruction 事件 - let mut event = match EventDispatcher::dispatch_instruction( - protocol.clone(), - instruction_discriminator, - instruction_data, - &account_pubkeys, - metadata.clone(), - ) { - Some(e) => e, - None => return Ok(()), - }; - - // 处理 inner instructions - 查找对应的 CPI log 进行 merge - // 当 inner_index 有值时,只查找索引大于当前 inner_index 的 CPI log - // 超低延迟:顺序执行,避免 thread::scope 的 spawn/join 开销 - let mut inner_instruction_event: Option = None; - if let Some(inner_instructions_ref) = inner_instructions { - let raw = inner_index.unwrap_or(-1); - let current_inner_idx = raw.clamp(i32::MIN as i64, i32::MAX as i64) as i32; - - for (idx, inner_instruction) in inner_instructions_ref.instructions.iter().enumerate() { - if (idx as i32) <= current_inner_idx { - continue; - } - let inner_data = &inner_instruction.data; - if inner_data.len() < 16 { - continue; - } - let inner_discriminator = &inner_data[..16]; - let inner_instruction_data = &inner_data[16..]; - if let Some(inner_event) = EventDispatcher::dispatch_inner_instruction( - protocol.clone(), - inner_discriminator, - inner_instruction_data, - metadata.clone(), - ) { - inner_instruction_event = Some(inner_event); - break; - } - } - - if event.metadata().swap_data.is_none() { - if let Some(swap_data) = parse_swap_data_from_next_grpc_instructions( - &event, - inner_instructions_ref, - current_inner_idx, - accounts, - ) { - event.metadata_mut().set_swap_data(swap_data); - } - } - } - - // PumpFun MIGRATE: 有 CPI 时合并 log;无 CPI 时仍发出仅含指令数据的事件。 - - // 合并事件 - if let Some(inner_instruction_event) = inner_instruction_event { - merge(&mut event, inner_instruction_event); - } - - // 设置处理时间(使用高性能时钟) - event.metadata_mut().handle_us = elapsed_micros_since(recv_us); - event = Self::process_event(event, bot_wallet); - callback(&event); - - Ok(()) - } - - // ================================================================================================ - // Standard Instruction Processing - // ================================================================================================ - - /// Parse events from standard Solana instruction - /// - /// Similar to gRPC instruction parsing but works with standard CompiledInstruction format. - /// Used when parsing VersionedTransaction or RPC data. - #[allow(clippy::too_many_arguments)] - fn parse_events_from_instruction( - protocols: &[Protocol], - event_type_filter: Option<&EventTypeFilter>, - instruction: &CompiledInstruction, - accounts: &[Pubkey], - signature: Signature, - slot: u64, - block_time: Option, - recv_us: i64, - outer_index: i64, - inner_index: Option, - bot_wallet: Option, - tx_index: Option, - recent_blockhash: Option<&str>, - inner_instructions: Option<&InnerInstructions>, - callback: Arc Fn(&'a DexEvent) + Send + Sync>, - ) -> anyhow::Result<()> { - // 添加边界检查以防止越界访问 - let program_id_index = instruction.program_id_index as usize; - if program_id_index >= accounts.len() { - return Ok(()); - } - let program_id = accounts[program_id_index]; - if !Self::should_handle(protocols, event_type_filter, &program_id) { - return Ok(()); - } - - let is_cu_program = EventDispatcher::is_compute_budget_program(&program_id); - - let disc_len = match program_id { - RAYDIUM_AMM_V4_PROGRAM_ID => 1, - _ => 8, - }; - - // 检查指令数据长度(至少需要 8 字节的 discriminator) - if !is_cu_program && instruction.data.len() < disc_len { - return Ok(()); - } - - // 创建元数据 - let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 }); - let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000; - let metadata = EventMetadata::new( - signature, - slot, - timestamp.seconds, - block_time_ms, - Default::default(), // protocol will be set by dispatcher - Default::default(), // event_type will be set by dispatcher - program_id, - outer_index, - inner_index, - recv_us, - tx_index, - recent_blockhash.map(|s| s.to_string()), - ); - - if is_cu_program { - if let Some(event) = EventDispatcher::dispatch_compute_budget_instruction( - &instruction.data, - metadata.clone(), - ) { - callback(&event); - } - return Ok(()); - } - - // 使用 EventDispatcher 匹配协议 - let protocol = match EventDispatcher::match_protocol_by_program_id(&program_id) { - Some(p) => p, - None => return Ok(()), - }; - - // 提取 discriminator 和数据 - let instruction_discriminator = &instruction.data[..disc_len]; - let instruction_data = &instruction.data[disc_len..]; - - // 构建账户公钥列表 - let account_pubkeys: Vec = instruction - .accounts - .iter() - .filter_map(|&idx| accounts.get(idx as usize).copied()) - .collect(); - - // 使用 EventDispatcher 解析 instruction 事件 - let mut event = match EventDispatcher::dispatch_instruction( - protocol.clone(), - instruction_discriminator, - instruction_data, - &account_pubkeys, - metadata.clone(), - ) { - Some(e) => e, - None => return Ok(()), - }; - - // 处理 inner instructions - 查找对应的 CPI log 进行 merge - // 当 inner_index 有值时,只查找索引大于当前 inner_index 的 CPI log - let mut inner_instruction_event: Option = None; - if let Some(inner_instructions_ref) = inner_instructions { - let raw = inner_index.unwrap_or(-1); - let current_inner_idx = raw.clamp(i32::MIN as i64, i32::MAX as i64) as i32; - - // 并行执行两个任务: 解析 inner event 和提取 swap_data - let (inner_event_result, swap_data_result) = std::thread::scope(|s| { - let inner_event_handle = s.spawn(|| { - for (idx, inner_instruction) in inner_instructions_ref.instructions.iter().enumerate() { - // 只查找索引大于当前 inner_index 的 CPI log - if (idx as i32) <= current_inner_idx { - continue; - } - - let inner_data = &inner_instruction.instruction.data; - // 检查长度(需要 16 字节的 discriminator) - if inner_data.len() < 16 { - continue; - } - let inner_discriminator = &inner_data[..16]; - let inner_instruction_data = &inner_data[16..]; - - if let Some(inner_event) = EventDispatcher::dispatch_inner_instruction( - protocol.clone(), - inner_discriminator, - inner_instruction_data, - metadata.clone(), - ) { - return Some(inner_event); - } - } - None - }); - - let swap_data_handle = s.spawn(|| { - if event.metadata().swap_data.is_none() { - parse_swap_data_from_next_instructions( - &event, - inner_instructions_ref, - current_inner_idx, - accounts, - ) - } else { - None - } - }); - - // 等待两个任务完成 - (inner_event_handle.join().unwrap(), swap_data_handle.join().unwrap()) - }); - - inner_instruction_event = inner_event_result; - if let Some(swap_data) = swap_data_result { - event.metadata_mut().set_swap_data(swap_data); - } - } - - // PumpFun MIGRATE: 有 CPI 时合并 log;无 CPI(如 shred)仍发出仅含指令数据的事件。 - - // 合并事件 - if let Some(inner_instruction_event) = inner_instruction_event { - merge(&mut event, inner_instruction_event); - } - - // 设置处理时间(使用高性能时钟) - event.metadata_mut().handle_us = elapsed_micros_since(recv_us); - event = Self::process_event(event, bot_wallet); - callback(&event); - - Ok(()) - } - - // ================================================================================================ - // Helper Functions - // ================================================================================================ - - /// Check if instruction should be processed based on protocol filter - /// - /// Determines whether a program_id matches any of the protocols we're interested in. - fn should_handle( - protocols: &[Protocol], - _event_type_filter: Option<&EventTypeFilter>, - program_id: &Pubkey, - ) -> bool { - // 使用 EventDispatcher 来匹配协议 - if let Some(protocol) = EventDispatcher::match_protocol_by_program_id(program_id) { - protocols.contains(&protocol) - } else if EventDispatcher::is_compute_budget_program(program_id) { - return true; - } else { - false - } - } - - // ================================================================================================ - // Event Post-Processing - // ================================================================================================ - - /// Process and enrich parsed event with additional context - /// - /// Handles protocol-specific post-processing: - /// - PumpFun: Tracks dev addresses and marks dev trades - /// - PumpSwap: Fills swap data amounts - /// - Bonk: Tracks pool creators and marks dev trades - /// - General: Marks bot wallet trades - fn process_event(event: DexEvent, bot_wallet: Option) -> DexEvent { - let signature = event.metadata().signature; // Copy the signature to avoid borrowing issues - match event { - DexEvent::PumpFunCreateTokenEvent(token_info) => { - add_dev_address(&signature, token_info.user); - if token_info.creator != Pubkey::default() && token_info.creator != token_info.user - { - add_dev_address(&signature, token_info.creator); - } - DexEvent::PumpFunCreateTokenEvent(token_info) - } - DexEvent::PumpFunCreateV2TokenEvent(token_info) => { - add_dev_address(&signature, token_info.user); - if token_info.creator != Pubkey::default() && token_info.creator != token_info.user - { - add_dev_address(&signature, token_info.creator); - } - DexEvent::PumpFunCreateV2TokenEvent(token_info) - } - DexEvent::PumpFunTradeEvent(mut trade_info) => { - trade_info.is_dev_create_token_trade = - is_dev_address_in_signature(&signature, &trade_info.user) - || is_dev_address_in_signature(&signature, &trade_info.creator); - trade_info.is_bot = Some(trade_info.user) == bot_wallet; - - if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() { - swap_data.from_amount = if trade_info.is_buy { - trade_info.sol_amount - } else { - trade_info.token_amount - }; - swap_data.to_amount = if trade_info.is_buy { - trade_info.token_amount - } else { - trade_info.sol_amount - }; - } - DexEvent::PumpFunTradeEvent(trade_info) - } - DexEvent::PumpSwapBuyEvent(mut trade_info) => { - if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() { - swap_data.from_amount = trade_info.user_quote_amount_in; - swap_data.to_amount = trade_info.base_amount_out; - } - DexEvent::PumpSwapBuyEvent(trade_info) - } - DexEvent::PumpSwapSellEvent(mut trade_info) => { - if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() { - swap_data.from_amount = trade_info.base_amount_in; - swap_data.to_amount = trade_info.user_quote_amount_out; - } - DexEvent::PumpSwapSellEvent(trade_info) - } - DexEvent::BonkPoolCreateEvent(pool_info) => { - add_bonk_dev_address(&signature, pool_info.creator); - DexEvent::BonkPoolCreateEvent(pool_info) - } - DexEvent::BonkTradeEvent(mut trade_info) => { - trade_info.is_dev_create_token_trade = - is_bonk_dev_address_in_signature(&signature, &trade_info.payer); - trade_info.is_bot = Some(trade_info.payer) == bot_wallet; - DexEvent::BonkTradeEvent(trade_info) - } - _ => event, - } - } -} - diff --git a/src/streaming/event_parser/core/event_parser/compiled_path/compiled_instruction.rs b/src/streaming/event_parser/core/event_parser/compiled_path/compiled_instruction.rs new file mode 100644 index 0000000..9f728aa --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/compiled_path/compiled_instruction.rs @@ -0,0 +1,195 @@ +//! Single Solana [`CompiledInstruction`] parsing with local inner merge and swap enrichment. +use crate::streaming::event_parser::{ + common::{ + filter::{passes_event_type_filter, EventTypeFilter}, + high_performance_clock::elapsed_micros_since, + parse_swap_data_from_next_instructions, EventMetadata, + }, + core::{dispatcher::EventDispatcher, merger_event::merge}, + protocols::{ + raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID, + sol_parser_forward::METEORA_DLMM_PROGRAM_ID, + }, + DexEvent, Protocol, +}; +use prost_types::Timestamp; +use solana_sdk::{ + message::compiled_instruction::CompiledInstruction, pubkey::Pubkey, signature::Signature, +}; +use solana_transaction_status::InnerInstructions; +use std::sync::Arc; + +pub(super) fn parse_events_from_instruction( + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: Signature, + slot: u64, + block_time: Option, + recv_us: i64, + outer_index: i64, + inner_index: Option, + bot_wallet: Option, + tx_index: Option, + recent_blockhash: Option<&str>, + inner_instructions: Option<&InnerInstructions>, + callback: Arc, +) -> anyhow::Result<()> { + // Bounds check before reading the program id index. + let program_id_index = instruction.program_id_index as usize; + if program_id_index >= accounts.len() { + return Ok(()); + } + let program_id = accounts[program_id_index]; + if !super::super::helpers::should_handle(protocols, event_type_filter, &program_id) { + return Ok(()); + } + + let is_cu_program = EventDispatcher::is_compute_budget_program(&program_id); + + let disc_len = match program_id { + RAYDIUM_AMM_V4_PROGRAM_ID | METEORA_DLMM_PROGRAM_ID => 1, + _ => 8, + }; + + // Non-ComputeBudget instructions need at least a discriminator. + if !is_cu_program && instruction.data.len() < disc_len { + return Ok(()); + } + + // Build streamer metadata. + let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 }); + let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000; + let metadata = EventMetadata::new( + signature, + slot, + timestamp.seconds, + block_time_ms, + Default::default(), // protocol will be set by dispatcher + Default::default(), // event_type will be set by dispatcher + program_id, + outer_index, + inner_index, + recv_us, + tx_index, + recent_blockhash.map(|s| s.to_string()), + ); + + if is_cu_program { + if let Some(event) = EventDispatcher::dispatch_compute_budget_instruction( + &instruction.data, + metadata.clone(), + ) { + if passes_event_type_filter(event_type_filter, &event) { + callback(event); + } + } + return Ok(()); + } + + // Match the parser protocol. + let protocol = match EventDispatcher::match_protocol_by_program_id(&program_id) { + Some(p) => p, + None => return Ok(()), + }; + + // Split discriminator and instruction payload. + let instruction_discriminator = &instruction.data[..disc_len]; + let instruction_data = &instruction.data[disc_len..]; + + // Build the account pubkey list for this instruction. + let account_pubkeys: Vec = instruction + .accounts + .iter() + .filter_map(|&idx| accounts.get(idx as usize).copied()) + .collect(); + + // Parse the instruction event. + let mut event = match EventDispatcher::dispatch_instruction( + protocol.clone(), + instruction_discriminator, + instruction_data, + &account_pubkeys, + metadata.clone(), + ) { + Some(e) => e, + None => return Ok(()), + }; + + // Find the next CPI log for merge. + let mut inner_instruction_event: Option = None; + if let Some(inner_instructions_ref) = inner_instructions { + let raw = inner_index.unwrap_or(-1); + let current_inner_idx = raw.clamp(i32::MIN as i64, i32::MAX as i64) as i32; + + // Parse the inner event and swap data in parallel on the compiled local path. + let (inner_event_result, swap_data_result) = std::thread::scope(|s| { + let inner_event_handle = s.spawn(|| { + for (idx, inner_instruction) in + inner_instructions_ref.instructions.iter().enumerate() + { + // Only inspect CPI logs after the current inner instruction. + if (idx as i32) <= current_inner_idx { + continue; + } + + let inner_data = &inner_instruction.instruction.data; + // Inner CPI logs use a 16-byte discriminator. + if inner_data.len() < 16 { + continue; + } + let inner_discriminator = &inner_data[..16]; + let inner_instruction_data = &inner_data[16..]; + + if let Some(inner_event) = EventDispatcher::dispatch_inner_instruction( + protocol.clone(), + inner_discriminator, + inner_instruction_data, + metadata.clone(), + ) { + return Some(inner_event); + } + } + None + }); + + let swap_data_handle = s.spawn(|| { + if event.metadata().swap_data.is_none() { + parse_swap_data_from_next_instructions( + &event, + inner_instructions_ref, + current_inner_idx, + accounts, + ) + } else { + None + } + }); + + // Wait for both local tasks. + (inner_event_handle.join().unwrap(), swap_data_handle.join().unwrap()) + }); + + inner_instruction_event = inner_event_result; + if let Some(swap_data) = swap_data_result { + event.metadata_mut().set_swap_data(swap_data); + } + } + + // PumpFun MIGRATE emits instruction-only data when no CPI log exists. + + // Merge CPI details into the outer event. + if let Some(inner_instruction_event) = inner_instruction_event { + merge(&mut event, inner_instruction_event); + } + + // Stamp handling latency using the high-performance clock. + event.metadata_mut().handle_us = elapsed_micros_since(recv_us); + event = super::super::helpers::process_event(event, bot_wallet); + if passes_event_type_filter(event_type_filter, &event) { + callback(event); + } + + Ok(()) +} diff --git a/src/streaming/event_parser/core/event_parser/compiled_path/compiled_transaction.rs b/src/streaming/event_parser/core/event_parser/compiled_path/compiled_transaction.rs new file mode 100644 index 0000000..ec39f7a --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/compiled_path/compiled_transaction.rs @@ -0,0 +1,87 @@ +//! Sequential top-level and inner ix traversal for [`VersionedTransaction`]. +use crate::streaming::event_parser::{common::filter::EventTypeFilter, DexEvent, Protocol}; +use prost_types::Timestamp; +use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::VersionedTransaction}; +use solana_transaction_status::InnerInstructions; +use std::sync::Arc; + +pub(crate) async fn parse_instruction_events_from_versioned_transaction( + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, + transaction: &VersionedTransaction, + signature: Signature, + slot: Option, + block_time: Option, + recv_us: i64, + accounts: &[Pubkey], + inner_instructions: &[InnerInstructions], + bot_wallet: Option, + tx_index: Option, + callback: Arc, +) -> anyhow::Result<()> { + let compiled_instructions = transaction.message.instructions(); + let recent_blockhash = Some(transaction.message.recent_blockhash().to_string()); + let mut accounts: Vec = accounts.to_vec(); + let has_program = accounts + .iter() + .any(|account| super::super::helpers::should_handle(protocols, event_type_filter, account)); + if has_program { + // Parse each instruction in order. + for (index, instruction) in compiled_instructions.iter().enumerate() { + if let Some(program_id) = accounts.get(instruction.program_id_index as usize) { + let program_id = *program_id; + let inner_instructions = inner_instructions + .iter() + .find(|inner_instruction| inner_instruction.index == index as u8); + if super::super::helpers::should_handle(protocols, event_type_filter, &program_id) { + let max_idx = instruction.accounts.iter().max().unwrap_or(&0); + if *max_idx as usize >= accounts.len() { + accounts.resize(*max_idx as usize + 1, Pubkey::default()); + } + super::compiled_instruction::parse_events_from_instruction( + protocols, + event_type_filter, + instruction, + &accounts, + signature, + slot.unwrap_or(0), + block_time, + recv_us, + index as i64, + None, + bot_wallet, + tx_index, + recent_blockhash.as_deref(), + inner_instructions, + callback.clone(), + )?; + } + // Immediately process inner instructions for correct ordering + if let Some(inner_instructions) = inner_instructions { + for (inner_index, inner_instruction) in + inner_instructions.instructions.iter().enumerate() + { + super::compiled_instruction::parse_events_from_instruction( + protocols, + event_type_filter, + &inner_instruction.instruction, + &accounts, + signature, + slot.unwrap_or(0), + block_time, + recv_us, + index as i64, + Some(inner_index as i64), + bot_wallet, + tx_index, + recent_blockhash.as_deref(), + Some(&inner_instructions), + callback.clone(), + )?; + } + } + } + } + } + Ok(()) +} diff --git a/src/streaming/event_parser/core/event_parser/compiled_path/mod.rs b/src/streaming/event_parser/core/event_parser/compiled_path/mod.rs new file mode 100644 index 0000000..afd58e1 --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/compiled_path/mod.rs @@ -0,0 +1,11 @@ +//! Standard [`VersionedTransaction`] / [`CompiledInstruction`] path for RPC and replay. +//! +//! | Module | Responsibility | +//! |--------|------| +//! | [`compiled_transaction`] | top-level ix loop | +//! | [`compiled_instruction`] | single Solana `CompiledInstruction` | + +mod compiled_instruction; +mod compiled_transaction; + +pub(super) use compiled_transaction::parse_instruction_events_from_versioned_transaction; diff --git a/src/streaming/event_parser/core/event_parser/grpc_path/grpc_instruction.rs b/src/streaming/event_parser/core/event_parser/grpc_path/grpc_instruction.rs new file mode 100644 index 0000000..95dc191 --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/grpc_path/grpc_instruction.rs @@ -0,0 +1,171 @@ +//! Single Yellowstone [`CompiledInstruction`] parsing: dispatch, inner merge, swap enrichment. +use crate::streaming::event_parser::{ + common::{ + filter::{passes_event_type_filter, EventTypeFilter}, + high_performance_clock::elapsed_micros_since, + parse_swap_data_from_next_grpc_instructions, EventMetadata, + }, + core::{dispatcher::EventDispatcher, merger_event::merge}, + protocols::{ + raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID, + sol_parser_forward::METEORA_DLMM_PROGRAM_ID, + }, + DexEvent, Protocol, +}; +use prost_types::Timestamp; +use solana_sdk::{pubkey::Pubkey, signature::Signature}; +use std::sync::Arc; + +pub(super) fn parse_events_from_grpc_instruction( + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, + instruction: &yellowstone_grpc_proto::prelude::CompiledInstruction, + accounts: &[Pubkey], + signature: Signature, + slot: u64, + block_time: Option, + recv_us: i64, + outer_index: i64, + inner_index: Option, + bot_wallet: Option, + tx_index: Option, + recent_blockhash: Option<&str>, + inner_instructions: Option<&yellowstone_grpc_proto::prelude::InnerInstructions>, + callback: Arc, +) -> anyhow::Result<()> { + // Bounds check before reading the program id index. + let program_id_index = instruction.program_id_index as usize; + if program_id_index >= accounts.len() { + return Ok(()); + } + let program_id = accounts[program_id_index]; + if !super::super::helpers::should_handle(protocols, event_type_filter, &program_id) { + return Ok(()); + } + + let is_cu_program = EventDispatcher::is_compute_budget_program(&program_id); + + let disc_len = match program_id { + RAYDIUM_AMM_V4_PROGRAM_ID | METEORA_DLMM_PROGRAM_ID => 1, + _ => 8, + }; + + // Non-ComputeBudget instructions need at least a discriminator. + if !is_cu_program && instruction.data.len() < disc_len { + return Ok(()); + } + // Build streamer metadata. + let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 }); + let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000; + let metadata = EventMetadata::new( + signature, + slot, + timestamp.seconds, + block_time_ms, + Default::default(), // protocol will be set by dispatcher + Default::default(), // event_type will be set by dispatcher + program_id, + outer_index, + inner_index, + recv_us, + tx_index, + recent_blockhash.map(|s| s.to_string()), + ); + + if is_cu_program { + if let Some(event) = EventDispatcher::dispatch_compute_budget_instruction( + &instruction.data, + metadata.clone(), + ) { + if passes_event_type_filter(event_type_filter, &event) { + callback(event); + } + } + return Ok(()); + } + + // Match the parser protocol. + let protocol = match EventDispatcher::match_protocol_by_program_id(&program_id) { + Some(p) => p, + None => return Ok(()), + }; + + // Split discriminator and instruction payload. + let instruction_discriminator = &instruction.data[..disc_len]; + let instruction_data = &instruction.data[disc_len..]; + + // Build the account pubkey list for this instruction. + let account_pubkeys: Vec = instruction + .accounts + .iter() + .filter_map(|&idx| accounts.get(idx as usize).copied()) + .collect(); + + // Parse the instruction event. + let mut event = match EventDispatcher::dispatch_instruction( + protocol.clone(), + instruction_discriminator, + instruction_data, + &account_pubkeys, + metadata.clone(), + ) { + Some(e) => e, + None => return Ok(()), + }; + + // Find the next CPI log for merge. The gRPC hot path stays sequential to avoid + // thread::scope spawn/join overhead. + let mut inner_instruction_event: Option = None; + if let Some(inner_instructions_ref) = inner_instructions { + let raw = inner_index.unwrap_or(-1); + let current_inner_idx = raw.clamp(i32::MIN as i64, i32::MAX as i64) as i32; + + for (idx, inner_instruction) in inner_instructions_ref.instructions.iter().enumerate() { + if (idx as i32) <= current_inner_idx { + continue; + } + let inner_data = &inner_instruction.data; + if inner_data.len() < 16 { + continue; + } + let inner_discriminator = &inner_data[..16]; + let inner_instruction_data = &inner_data[16..]; + if let Some(inner_event) = EventDispatcher::dispatch_inner_instruction( + protocol.clone(), + inner_discriminator, + inner_instruction_data, + metadata.clone(), + ) { + inner_instruction_event = Some(inner_event); + break; + } + } + + if event.metadata().swap_data.is_none() { + if let Some(swap_data) = parse_swap_data_from_next_grpc_instructions( + &event, + inner_instructions_ref, + current_inner_idx, + accounts, + ) { + event.metadata_mut().set_swap_data(swap_data); + } + } + } + + // PumpFun MIGRATE emits instruction-only data when no CPI log exists. + + // Merge CPI details into the outer event. + if let Some(inner_instruction_event) = inner_instruction_event { + merge(&mut event, inner_instruction_event); + } + + // Stamp handling latency using the high-performance clock. + event.metadata_mut().handle_us = elapsed_micros_since(recv_us); + event = super::super::helpers::process_event(event, bot_wallet); + if passes_event_type_filter(event_type_filter, &event) { + callback(event); + } + + Ok(()) +} diff --git a/src/streaming/event_parser/core/event_parser/grpc_path/grpc_ix_mode.rs b/src/streaming/event_parser/core/event_parser/grpc_path/grpc_ix_mode.rs new file mode 100644 index 0000000..41c8400 --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/grpc_path/grpc_ix_mode.rs @@ -0,0 +1,9 @@ +//! Top-level ix parsing strategy for the gRPC path. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum GrpcIxParseMode { + /// Parse all subscribed instructions, used when transaction meta is missing. + Full, + /// Parse only ComputeBudget locally; DEX events come from sol-parser-sdk. + ComputeBudgetOnly, +} diff --git a/src/streaming/event_parser/core/event_parser/grpc_path/grpc_transaction.rs b/src/streaming/event_parser/core/event_parser/grpc_path/grpc_transaction.rs new file mode 100644 index 0000000..abf3a75 --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/grpc_path/grpc_transaction.rs @@ -0,0 +1,265 @@ +//! Yellowstone transaction parsing: SDK-first DEX parsing plus optional local ix fallback. +//! +//! When `transaction.meta` is missing, the SDK low-latency parser cannot see logs / complete inner +//! instruction context and may return no events. In that case streamer uses the local full ix path. +//! +//! When meta exists, DEX events come from `sol-parser-sdk`; the local second pass is limited to +//! ComputeBudget events when the user asked for them. +use crate::streaming::event_parser::{ + common::{ + filter::{ + build_sdk_parse_event_filter, filter_includes_compute_budget_types, EventTypeFilter, + }, + high_performance_clock::elapsed_micros_since, + }, + core::dispatcher::EventDispatcher, + DexEvent, Protocol, +}; +use prost_types::Timestamp; +use sol_parser_sdk::grpc::parse_subscribe_update_transaction_low_latency; +use solana_sdk::{pubkey::Pubkey, signature::Signature}; +use std::sync::Arc; +use yellowstone_grpc_proto::geyser::{SubscribeUpdateTransaction, SubscribeUpdateTransactionInfo}; + +use super::grpc_ix_mode::GrpcIxParseMode; + +pub(crate) async fn parse_grpc_transaction( + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, + mut grpc_tx: SubscribeUpdateTransactionInfo, + signature: Signature, + slot: Option, + block_time: Option, + recv_us: i64, + bot_wallet: Option, + tx_index: Option, + callback: Arc, +) -> anyhow::Result<()> { + let slot_u = slot.unwrap_or(0); + let block_us_micro = block_time.map(|t| t.seconds * 1_000_000 + t.nanos as i64 / 1_000); + + if grpc_tx.transaction.as_ref().and_then(|tx| tx.message.as_ref()).is_none() { + return Ok(()); + } + + let use_sol_parser_sdk = grpc_tx.meta.is_some(); + let skip_ix_pass = + use_sol_parser_sdk && !filter_includes_compute_budget_types(event_type_filter); + + if use_sol_parser_sdk { + let mut update = SubscribeUpdateTransaction { + slot: slot_u, + transaction: Some(grpc_tx), + ..Default::default() + }; + let sdk_parse_filter = build_sdk_parse_event_filter(event_type_filter); + let pb_events = parse_subscribe_update_transaction_low_latency( + &update, + recv_us, + block_us_micro, + sdk_parse_filter.as_ref(), + ); + let adapted = crate::streaming::parser_sdk_bridge::adapt_parser_events_list( + pb_events, + block_time.as_ref(), + recv_us, + protocols, + event_type_filter, + ); + for mut ev in adapted { + ev.metadata_mut().handle_us = elapsed_micros_since(recv_us); + ev = super::super::helpers::process_event(ev, bot_wallet); + callback(ev); + } + + if skip_ix_pass { + return Ok(()); + } + + let Some(tx) = update.transaction.take() else { + return Ok(()); + }; + grpc_tx = tx; + } + + let Some(transition) = grpc_tx.transaction.as_ref() else { + return Ok(()); + }; + let Some(message) = transition.message.as_ref() else { + return Ok(()); + }; + + let ix_mode = + if use_sol_parser_sdk { GrpcIxParseMode::ComputeBudgetOnly } else { GrpcIxParseMode::Full }; + + let accounts = build_account_keys(message, grpc_tx.meta.as_ref()); + let inner_instructions = + grpc_tx.meta.as_ref().map(|meta| meta.inner_instructions.as_slice()).unwrap_or_default(); + let recent_blockhash = if message.recent_blockhash.len() == 32 { + Some(solana_sdk::bs58::encode(&message.recent_blockhash).into_string()) + } else { + None + }; + + parse_instruction_events_from_grpc_transaction( + protocols, + event_type_filter, + ix_mode, + &message.instructions, + signature, + slot, + block_time, + recv_us, + &accounts, + inner_instructions, + bot_wallet, + tx_index, + recent_blockhash, + callback, + ) + .await?; + + Ok(()) +} + +fn build_account_keys( + message: &yellowstone_grpc_proto::prelude::Message, + meta: Option<&yellowstone_grpc_proto::prelude::TransactionStatusMeta>, +) -> Vec { + let loaded_len = meta + .map(|m| m.loaded_writable_addresses.len() + m.loaded_readonly_addresses.len()) + .unwrap_or(0); + let mut accounts = Vec::with_capacity(message.account_keys.len() + loaded_len); + + for account in &message.account_keys { + push_account_key(&mut accounts, account); + } + + if let Some(meta) = meta { + for account in + meta.loaded_writable_addresses.iter().chain(meta.loaded_readonly_addresses.iter()) + { + push_account_key(&mut accounts, account); + } + } + + accounts +} + +#[inline] +fn push_account_key(accounts: &mut Vec, account: &[u8]) { + let pubkey = if account.len() == 32 { + Pubkey::try_from(account).unwrap_or_default() + } else { + Pubkey::default() + }; + accounts.push(pubkey); +} + +pub(super) async fn parse_instruction_events_from_grpc_transaction( + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, + ix_mode: GrpcIxParseMode, + compiled_instructions: &[yellowstone_grpc_proto::prelude::CompiledInstruction], + signature: Signature, + slot: Option, + block_time: Option, + recv_us: i64, + accounts: &[Pubkey], + inner_instructions: &[yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions], + bot_wallet: Option, + tx_index: Option, + recent_blockhash: Option, + callback: Arc, +) -> anyhow::Result<()> { + let mut accounts = accounts.to_vec(); + let has_program = match ix_mode { + GrpcIxParseMode::Full => accounts.iter().any(|account| { + super::super::helpers::should_handle(protocols, event_type_filter, account) + }), + GrpcIxParseMode::ComputeBudgetOnly => compiled_instructions.iter().any(|ix| { + accounts + .get(ix.program_id_index as usize) + .map(EventDispatcher::is_compute_budget_program) + .unwrap_or(false) + }), + }; + if has_program { + // Parse each instruction in order. + for (index, instruction) in compiled_instructions.iter().enumerate() { + if let Some(program_id) = accounts.get(instruction.program_id_index as usize) { + let program_id = *program_id; + let inner_instructions_ref = inner_instructions + .iter() + .find(|inner_instruction| inner_instruction.index == index as u32); + let max_idx = instruction.accounts.iter().max().unwrap_or(&0); + if *max_idx as usize >= accounts.len() { + accounts.resize(*max_idx as usize + 1, Pubkey::default()); + } + let handle_outer = match ix_mode { + GrpcIxParseMode::Full => super::super::helpers::should_handle( + protocols, + event_type_filter, + &program_id, + ), + GrpcIxParseMode::ComputeBudgetOnly => { + EventDispatcher::is_compute_budget_program(&program_id) + } + }; + if handle_outer { + super::grpc_instruction::parse_events_from_grpc_instruction( + protocols, + event_type_filter, + instruction, + &accounts, + signature, + slot.unwrap_or(0), + block_time, + recv_us, + index as i64, + None, + bot_wallet, + tx_index, + recent_blockhash.as_deref(), + inner_instructions_ref, + callback.clone(), + )?; + } + if ix_mode == GrpcIxParseMode::Full { + if let Some(inner_instructions) = inner_instructions_ref { + for (inner_index, inner_instruction) in + inner_instructions.instructions.iter().enumerate() + { + let inner_accounts = &inner_instruction.accounts; + let data = &inner_instruction.data; + let instruction = + yellowstone_grpc_proto::prelude::CompiledInstruction { + program_id_index: inner_instruction.program_id_index, + accounts: inner_accounts.to_vec(), + data: data.to_vec(), + }; + super::grpc_instruction::parse_events_from_grpc_instruction( + protocols, + event_type_filter, + &instruction, + &accounts, + signature, + slot.unwrap_or(0), + block_time, + recv_us, + inner_instructions.index as i64, + Some(inner_index as i64), + bot_wallet, + tx_index, + recent_blockhash.as_deref(), + Some(inner_instructions), + callback.clone(), + )?; + } + } + } + } + } + } + Ok(()) +} diff --git a/src/streaming/event_parser/core/event_parser/grpc_path/mod.rs b/src/streaming/event_parser/core/event_parser/grpc_path/mod.rs new file mode 100644 index 0000000..ffa8d0d --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/grpc_path/mod.rs @@ -0,0 +1,13 @@ +//! Yellowstone gRPC path for `SubscribeUpdateTransactionInfo` and SDK aggregate parsing. +//! +//! | Module | Responsibility | +//! |--------|------| +//! | [`grpc_ix_mode`] | `GrpcIxParseMode` | +//! | [`grpc_transaction`] | whole subscription message and top-level ix loop | +//! | [`grpc_instruction`] | single Yellowstone `CompiledInstruction` | + +mod grpc_instruction; +mod grpc_ix_mode; +mod grpc_transaction; + +pub(super) use grpc_transaction::parse_grpc_transaction; diff --git a/src/streaming/event_parser/core/event_parser/helpers.rs b/src/streaming/event_parser/core/event_parser/helpers.rs new file mode 100644 index 0000000..8c85964 --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/helpers.rs @@ -0,0 +1,96 @@ +//! Protocol filtering and event enrichment for PumpFun / PumpSwap / Bonk / bot flags. +use crate::streaming::event_parser::{ + common::filter::{filter_includes_compute_budget_types, EventTypeFilter}, + core::dispatcher::EventDispatcher, + core::global_state::{ + add_bonk_dev_address, add_dev_address, is_bonk_dev_address_in_signature, + is_dev_address_in_signature, + }, + DexEvent, Protocol, +}; +use solana_sdk::pubkey::Pubkey; + +pub(super) fn should_handle( + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, + program_id: &Pubkey, +) -> bool { + if EventDispatcher::is_compute_budget_program(program_id) { + return filter_includes_compute_budget_types(event_type_filter); + } + if let Some(protocol) = EventDispatcher::match_protocol_by_program_id(program_id) { + protocols.contains(&protocol) + } else { + false + } +} + +// ================================================================================================ +// Event Post-Processing +// ================================================================================================ + +/// Process and enrich parsed event with additional context +/// +/// Handles protocol-specific post-processing: +/// - PumpFun: Tracks dev addresses and marks dev trades +/// - PumpSwap: Fills swap data amounts +/// - Bonk: Tracks pool creators and marks dev trades +/// - General: Marks bot wallet trades +pub(crate) fn process_event(event: DexEvent, bot_wallet: Option) -> DexEvent { + let signature = event.metadata().signature; // Copy the signature to avoid borrowing issues + match event { + DexEvent::PumpFunCreateTokenEvent(token_info) => { + add_dev_address(&signature, token_info.user); + if token_info.creator != Pubkey::default() && token_info.creator != token_info.user { + add_dev_address(&signature, token_info.creator); + } + DexEvent::PumpFunCreateTokenEvent(token_info) + } + DexEvent::PumpFunCreateV2TokenEvent(token_info) => { + add_dev_address(&signature, token_info.user); + if token_info.creator != Pubkey::default() && token_info.creator != token_info.user { + add_dev_address(&signature, token_info.creator); + } + DexEvent::PumpFunCreateV2TokenEvent(token_info) + } + DexEvent::PumpFunTradeEvent(mut trade_info) => { + trade_info.is_dev_create_token_trade = + is_dev_address_in_signature(&signature, &trade_info.user) + || is_dev_address_in_signature(&signature, &trade_info.creator); + trade_info.is_bot = Some(trade_info.user) == bot_wallet; + + if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() { + swap_data.from_amount = + if trade_info.is_buy { trade_info.sol_amount } else { trade_info.token_amount }; + swap_data.to_amount = + if trade_info.is_buy { trade_info.token_amount } else { trade_info.sol_amount }; + } + DexEvent::PumpFunTradeEvent(trade_info) + } + DexEvent::PumpSwapBuyEvent(mut trade_info) => { + if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() { + swap_data.from_amount = trade_info.user_quote_amount_in; + swap_data.to_amount = trade_info.base_amount_out; + } + DexEvent::PumpSwapBuyEvent(trade_info) + } + DexEvent::PumpSwapSellEvent(mut trade_info) => { + if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() { + swap_data.from_amount = trade_info.base_amount_in; + swap_data.to_amount = trade_info.user_quote_amount_out; + } + DexEvent::PumpSwapSellEvent(trade_info) + } + DexEvent::BonkPoolCreateEvent(pool_info) => { + add_bonk_dev_address(&signature, pool_info.creator); + DexEvent::BonkPoolCreateEvent(pool_info) + } + DexEvent::BonkTradeEvent(mut trade_info) => { + trade_info.is_dev_create_token_trade = + is_bonk_dev_address_in_signature(&signature, &trade_info.payer); + trade_info.is_bot = Some(trade_info.payer) == bot_wallet; + DexEvent::BonkTradeEvent(trade_info) + } + _ => event, + } +} diff --git a/src/streaming/event_parser/core/event_parser/mod.rs b/src/streaming/event_parser/core/event_parser/mod.rs new file mode 100644 index 0000000..23fcb1f --- /dev/null +++ b/src/streaming/event_parser/core/event_parser/mod.rs @@ -0,0 +1,74 @@ +//! Transaction parser entry point with separate gRPC and standard ix paths. +//! +//! | Module | Path | +//! |--------|------| +//! | [`grpc_path`] | Yellowstone gRPC | +//! | [`compiled_path`] | standard transaction / RPC replay | +//! | [`helpers`] | `should_handle`、`process_event` | + +mod compiled_path; +mod grpc_path; +pub(crate) mod helpers; + +pub struct EventParser; + +impl EventParser { + pub async fn parse_grpc_transaction( + protocols: &[crate::streaming::event_parser::Protocol], + event_type_filter: Option<&crate::streaming::event_parser::common::filter::EventTypeFilter>, + grpc_tx: yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo, + signature: solana_sdk::signature::Signature, + slot: Option, + block_time: Option, + recv_us: i64, + bot_wallet: Option, + tx_index: Option, + callback: std::sync::Arc, + ) -> anyhow::Result<()> { + grpc_path::parse_grpc_transaction( + protocols, + event_type_filter, + grpc_tx, + signature, + slot, + block_time, + recv_us, + bot_wallet, + tx_index, + callback, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + pub async fn parse_instruction_events_from_versioned_transaction( + protocols: &[crate::streaming::event_parser::Protocol], + event_type_filter: Option<&crate::streaming::event_parser::common::filter::EventTypeFilter>, + transaction: &solana_sdk::transaction::VersionedTransaction, + signature: solana_sdk::signature::Signature, + slot: Option, + block_time: Option, + recv_us: i64, + accounts: &[solana_sdk::pubkey::Pubkey], + inner_instructions: &[solana_transaction_status::InnerInstructions], + bot_wallet: Option, + tx_index: Option, + callback: std::sync::Arc, + ) -> anyhow::Result<()> { + compiled_path::parse_instruction_events_from_versioned_transaction( + protocols, + event_type_filter, + transaction, + signature, + slot, + block_time, + recv_us, + accounts, + inner_instructions, + bot_wallet, + tx_index, + callback, + ) + .await + } +} diff --git a/src/streaming/event_parser/core/global_state.rs b/src/streaming/event_parser/core/global_state.rs index 2c64538..41154bd 100644 --- a/src/streaming/event_parser/core/global_state.rs +++ b/src/streaming/event_parser/core/global_state.rs @@ -1,8 +1,8 @@ +use dashmap::DashMap; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Signature; -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; -use dashmap::DashMap; use std::collections::BTreeSet; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; const MAX_SIGNATURES: usize = 1000; const CLEANUP_BATCH_SIZE: usize = 100; @@ -45,15 +45,17 @@ impl GlobalState { // Use CAS to ensure only one thread performs cleanup let gen = self.generation.load(Ordering::Relaxed); - if self.generation.compare_exchange_weak(gen, gen + 1, Ordering::Acquire, Ordering::Relaxed).is_err() { + if self + .generation + .compare_exchange_weak(gen, gen + 1, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { return; // Another thread is cleaning up } // Collect only the batch we need to remove (avoid allocating full list) - let signatures_to_remove: Vec = self.signature_data.iter() - .take(CLEANUP_BATCH_SIZE) - .map(|entry| *entry.key()) - .collect(); + let signatures_to_remove: Vec = + self.signature_data.iter().take(CLEANUP_BATCH_SIZE).map(|entry| *entry.key()).collect(); // Remove old signatures atomically; only decrement count when entry was present for signature in signatures_to_remove { @@ -66,8 +68,9 @@ impl GlobalState { /// Add developer address for a specific signature (lock-free) pub fn add_dev_address(&self, signature: &Signature, address: Pubkey) { self.maybe_cleanup(); - - self.signature_data.entry(*signature) + + self.signature_data + .entry(*signature) .and_modify(|addresses| { addresses.dev_addresses.insert(address); }) @@ -82,8 +85,9 @@ impl GlobalState { /// Add Bonk developer address for a specific signature (lock-free) pub fn add_bonk_dev_address(&self, signature: &Signature, address: Pubkey) { self.maybe_cleanup(); - - self.signature_data.entry(*signature) + + self.signature_data + .entry(*signature) .and_modify(|addresses| { addresses.bonk_dev_addresses.insert(address); }) @@ -97,14 +101,20 @@ impl GlobalState { /// High-performance: Check if address is a developer address in specific signature (O(log m)) pub fn is_dev_address_in_signature(&self, signature: &Signature, address: &Pubkey) -> bool { - self.signature_data.get(signature) + self.signature_data + .get(signature) .map(|entry| entry.dev_addresses.contains(address)) .unwrap_or(false) } /// High-performance: Check if address is a Bonk developer address in specific signature (O(log m)) - pub fn is_bonk_dev_address_in_signature(&self, signature: &Signature, address: &Pubkey) -> bool { - self.signature_data.get(signature) + pub fn is_bonk_dev_address_in_signature( + &self, + signature: &Signature, + address: &Pubkey, + ) -> bool { + self.signature_data + .get(signature) .map(|entry| entry.bonk_dev_addresses.contains(address)) .unwrap_or(false) } @@ -143,14 +153,16 @@ impl GlobalState { /// Get developer addresses for a specific signature pub fn get_dev_addresses_for_signature(&self, signature: &Signature) -> Vec { - self.signature_data.get(signature) + self.signature_data + .get(signature) .map(|entry| entry.dev_addresses.iter().copied().collect()) .unwrap_or_default() } /// Get Bonk developer addresses for a specific signature pub fn get_bonk_dev_addresses_for_signature(&self, signature: &Signature) -> Vec { - self.signature_data.get(signature) + self.signature_data + .get(signature) .map(|entry| entry.bonk_dev_addresses.iter().copied().collect()) .unwrap_or_default() } @@ -175,8 +187,7 @@ impl Default for GlobalState { } /// Global state instance -static GLOBAL_STATE: std::sync::LazyLock = - std::sync::LazyLock::new(GlobalState::new); +static GLOBAL_STATE: std::sync::LazyLock = std::sync::LazyLock::new(GlobalState::new); /// Get global state instance pub fn get_global_state() -> &'static GlobalState { diff --git a/src/streaming/event_parser/core/merger_event.rs b/src/streaming/event_parser/core/merger_event.rs index 7b69f7e..25c85bb 100644 --- a/src/streaming/event_parser/core/merger_event.rs +++ b/src/streaming/event_parser/core/merger_event.rs @@ -1,4 +1,5 @@ use crate::streaming::event_parser::DexEvent; +use solana_sdk::pubkey::Pubkey; pub fn merge(instruction_event: &mut DexEvent, cpi_log_event: DexEvent) { match instruction_event { @@ -437,6 +438,249 @@ pub fn merge(instruction_event: &mut DexEvent, cpi_log_event: DexEvent) { _ => {} }, + // Orca Whirlpool:外层指令粗字段 + CPI 日志精修 + DexEvent::OrcaWhirlpoolSwapEvent(e) => match cpi_log_event { + DexEvent::OrcaWhirlpoolSwapEvent(cpie) => { + if cpie.whirlpool != Pubkey::default() { + e.whirlpool = cpie.whirlpool; + } + if cpie.input_amount != 0 { + e.input_amount = cpie.input_amount; + } + if cpie.output_amount != 0 { + e.output_amount = cpie.output_amount; + } + e.a_to_b = cpie.a_to_b; + if cpie.pre_sqrt_price != 0 { + e.pre_sqrt_price = cpie.pre_sqrt_price; + } + if cpie.post_sqrt_price != 0 { + e.post_sqrt_price = cpie.post_sqrt_price; + } + if cpie.input_transfer_fee != 0 { + e.input_transfer_fee = cpie.input_transfer_fee; + } + if cpie.output_transfer_fee != 0 { + e.output_transfer_fee = cpie.output_transfer_fee; + } + if cpie.lp_fee != 0 { + e.lp_fee = cpie.lp_fee; + } + if cpie.protocol_fee != 0 { + e.protocol_fee = cpie.protocol_fee; + } + } + _ => {} + }, + DexEvent::OrcaWhirlpoolLiquidityIncreasedEvent(e) => match cpi_log_event { + DexEvent::OrcaWhirlpoolLiquidityIncreasedEvent(cpie) => { + if cpie.position != Pubkey::default() { + e.position = cpie.position; + } + if cpie.tick_lower_index != 0 || cpie.tick_upper_index != 0 { + e.tick_lower_index = cpie.tick_lower_index; + e.tick_upper_index = cpie.tick_upper_index; + } + if cpie.token_a_amount != 0 { + e.token_a_amount = cpie.token_a_amount; + } + if cpie.token_b_amount != 0 { + e.token_b_amount = cpie.token_b_amount; + } + if cpie.liquidity != 0 { + e.liquidity = cpie.liquidity; + } + if cpie.token_a_transfer_fee != 0 { + e.token_a_transfer_fee = cpie.token_a_transfer_fee; + } + if cpie.token_b_transfer_fee != 0 { + e.token_b_transfer_fee = cpie.token_b_transfer_fee; + } + } + _ => {} + }, + DexEvent::OrcaWhirlpoolLiquidityDecreasedEvent(e) => match cpi_log_event { + DexEvent::OrcaWhirlpoolLiquidityDecreasedEvent(cpie) => { + if cpie.position != Pubkey::default() { + e.position = cpie.position; + } + if cpie.tick_lower_index != 0 || cpie.tick_upper_index != 0 { + e.tick_lower_index = cpie.tick_lower_index; + e.tick_upper_index = cpie.tick_upper_index; + } + if cpie.token_a_amount != 0 { + e.token_a_amount = cpie.token_a_amount; + } + if cpie.token_b_amount != 0 { + e.token_b_amount = cpie.token_b_amount; + } + if cpie.liquidity != 0 { + e.liquidity = cpie.liquidity; + } + if cpie.token_a_transfer_fee != 0 { + e.token_a_transfer_fee = cpie.token_a_transfer_fee; + } + if cpie.token_b_transfer_fee != 0 { + e.token_b_transfer_fee = cpie.token_b_transfer_fee; + } + } + _ => {} + }, + + // Meteora Pools swap:外层 min_out 等与 CPI 实际结算合并 + DexEvent::MeteoraPoolsSwapEvent(e) => match cpi_log_event { + DexEvent::MeteoraPoolsSwapEvent(cpie) => { + if cpie.in_amount != 0 { + e.in_amount = cpie.in_amount; + } + if cpie.out_amount != 0 { + e.out_amount = cpie.out_amount; + } + if cpie.trade_fee != 0 { + e.trade_fee = cpie.trade_fee; + } + if cpie.admin_fee != 0 { + e.admin_fee = cpie.admin_fee; + } + if cpie.host_fee != 0 { + e.host_fee = cpie.host_fee; + } + } + _ => {} + }, + DexEvent::MeteoraPoolsAddLiquidityEvent(e) => match cpi_log_event { + DexEvent::MeteoraPoolsAddLiquidityEvent(cpie) => { + if cpie.lp_mint_amount != 0 { + e.lp_mint_amount = cpie.lp_mint_amount; + } + if cpie.token_a_amount != 0 { + e.token_a_amount = cpie.token_a_amount; + } + if cpie.token_b_amount != 0 { + e.token_b_amount = cpie.token_b_amount; + } + } + _ => {} + }, + DexEvent::MeteoraPoolsRemoveLiquidityEvent(e) => match cpi_log_event { + DexEvent::MeteoraPoolsRemoveLiquidityEvent(cpie) => { + if cpie.lp_unmint_amount != 0 { + e.lp_unmint_amount = cpie.lp_unmint_amount; + } + if cpie.token_a_out_amount != 0 { + e.token_a_out_amount = cpie.token_a_out_amount; + } + if cpie.token_b_out_amount != 0 { + e.token_b_out_amount = cpie.token_b_out_amount; + } + } + _ => {} + }, + + // Meteora DLMM + DexEvent::MeteoraDlmmSwapEvent(e) => match cpi_log_event { + DexEvent::MeteoraDlmmSwapEvent(cpie) => { + if cpie.pool != Pubkey::default() { + e.pool = cpie.pool; + } + if cpie.from != Pubkey::default() { + e.from = cpie.from; + } + if cpie.start_bin_id != 0 || cpie.end_bin_id != 0 { + e.start_bin_id = cpie.start_bin_id; + e.end_bin_id = cpie.end_bin_id; + } + if cpie.amount_out != 0 { + e.amount_out = cpie.amount_out; + } + if cpie.amount_in != 0 { + e.amount_in = cpie.amount_in; + } + e.swap_for_y = cpie.swap_for_y; + if cpie.fee != 0 { + e.fee = cpie.fee; + } + if cpie.protocol_fee != 0 { + e.protocol_fee = cpie.protocol_fee; + } + if cpie.fee_bps != 0 { + e.fee_bps = cpie.fee_bps; + } + if cpie.host_fee != 0 { + e.host_fee = cpie.host_fee; + } + } + _ => {} + }, + DexEvent::MeteoraDlmmAddLiquidityEvent(e) => match cpi_log_event { + DexEvent::MeteoraDlmmAddLiquidityEvent(cpie) => { + if cpie.active_bin_id != 0 { + e.active_bin_id = cpie.active_bin_id; + } + e.amounts = cpie.amounts; + } + _ => {} + }, + DexEvent::MeteoraDlmmRemoveLiquidityEvent(e) => match cpi_log_event { + DexEvent::MeteoraDlmmRemoveLiquidityEvent(cpie) => { + if cpie.active_bin_id != 0 { + e.active_bin_id = cpie.active_bin_id; + } + e.amounts = cpie.amounts; + } + _ => {} + }, + + DexEvent::MeteoraPoolsBootstrapLiquidityEvent(e) => match cpi_log_event { + DexEvent::MeteoraPoolsBootstrapLiquidityEvent(cpie) => { + if cpie.pool != Pubkey::default() { + e.pool = cpie.pool; + } + if cpie.lp_mint_amount != 0 { + e.lp_mint_amount = cpie.lp_mint_amount; + } + if cpie.token_a_amount != 0 { + e.token_a_amount = cpie.token_a_amount; + } + if cpie.token_b_amount != 0 { + e.token_b_amount = cpie.token_b_amount; + } + } + _ => {} + }, + DexEvent::MeteoraPoolsPoolCreatedEvent(e) => match cpi_log_event { + DexEvent::MeteoraPoolsPoolCreatedEvent(cpie) => { + if cpie.pool != Pubkey::default() { + e.pool = cpie.pool; + } + if cpie.lp_mint != Pubkey::default() { + e.lp_mint = cpie.lp_mint; + } + if cpie.token_a_mint != Pubkey::default() { + e.token_a_mint = cpie.token_a_mint; + } + if cpie.token_b_mint != Pubkey::default() { + e.token_b_mint = cpie.token_b_mint; + } + if cpie.pool_type != 0 { + e.pool_type = cpie.pool_type; + } + } + _ => {} + }, + DexEvent::MeteoraPoolsSetPoolFeesEvent(e) => match cpi_log_event { + DexEvent::MeteoraPoolsSetPoolFeesEvent(cpie) => { + if cpie.pool != Pubkey::default() { + e.pool = cpie.pool; + } + e.trade_fee_numerator = cpie.trade_fee_numerator; + e.trade_fee_denominator = cpie.trade_fee_denominator; + e.owner_trade_fee_numerator = cpie.owner_trade_fee_numerator; + e.owner_trade_fee_denominator = cpie.owner_trade_fee_denominator; + } + _ => {} + }, + _ => {} } } diff --git a/src/streaming/event_parser/core/mod.rs b/src/streaming/event_parser/core/mod.rs index 5f2b3e5..9dba272 100755 --- a/src/streaming/event_parser/core/mod.rs +++ b/src/streaming/event_parser/core/mod.rs @@ -5,8 +5,8 @@ pub mod global_state; pub mod parser_cache; pub mod traits; -pub use traits::DexEvent; pub use dispatcher::EventDispatcher; +pub use traits::DexEvent; pub mod event_parser; -pub mod merger_event; \ No newline at end of file +pub mod merger_event; diff --git a/src/streaming/event_parser/core/parser_cache.rs b/src/streaming/event_parser/core/parser_cache.rs index 17d76c6..9082e15 100644 --- a/src/streaming/event_parser/core/parser_cache.rs +++ b/src/streaming/event_parser/core/parser_cache.rs @@ -13,7 +13,7 @@ use crate::streaming::{ event_parser::{ common::{filter::EventTypeFilter, EventMetadata, EventType, ProtocolType}, core::dispatcher::EventDispatcher, - Protocol, DexEvent, + DexEvent, Protocol, }, grpc::AccountPretty, }; @@ -53,9 +53,8 @@ impl CacheKey { } /// 全局程序ID缓存(使用读写锁保护) -static GLOBAL_PROGRAM_IDS_CACHE: LazyLock< - std::sync::RwLock>>>, -> = LazyLock::new(|| std::sync::RwLock::new(HashMap::new())); +static GLOBAL_PROGRAM_IDS_CACHE: LazyLock>>>> = + LazyLock::new(|| std::sync::RwLock::new(HashMap::new())); /// 获取指定协议的程序ID列表 /// @@ -101,9 +100,7 @@ impl AccountPubkeyCache { /// /// 预分配32个位置,覆盖大多数交易场景 pub fn new() -> Self { - Self { - cache: Vec::with_capacity(32), - } + Self { cache: Vec::with_capacity(32) } } /// 从指令账户索引构建账户公钥向量 @@ -201,4 +198,3 @@ pub struct AccountEventParseConfig { /// 账户解析器函数 pub account_parser: AccountEventParserFn, } - diff --git a/src/streaming/event_parser/core/traits.rs b/src/streaming/event_parser/core/traits.rs index 0a27eb7..c015bec 100755 --- a/src/streaming/event_parser/core/traits.rs +++ b/src/streaming/event_parser/core/traits.rs @@ -13,6 +13,16 @@ 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::sol_parser_forward::events::{ + MeteoraDlmmAddLiquidityEvent, MeteoraDlmmClaimFeeEvent, MeteoraDlmmClosePositionEvent, + MeteoraDlmmCreatePositionEvent, MeteoraDlmmInitializeBinArrayEvent, + MeteoraDlmmInitializePoolEvent, MeteoraDlmmRemoveLiquidityEvent, MeteoraDlmmSwapEvent, + MeteoraPoolsAddLiquidityEvent, MeteoraPoolsBootstrapLiquidityEvent, + MeteoraPoolsPoolCreatedEvent, MeteoraPoolsRemoveLiquidityEvent, MeteoraPoolsSetPoolFeesEvent, + MeteoraPoolsSwapEvent, OrcaWhirlpoolLiquidityDecreasedEvent, + OrcaWhirlpoolLiquidityIncreasedEvent, OrcaWhirlpoolPoolInitializedEvent, + OrcaWhirlpoolSwapEvent, ParserSdkErrorEvent, +}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -33,6 +43,16 @@ pub enum DexEvent { PumpFunCreateV2TokenEvent(PumpFunCreateV2TokenEvent), PumpFunTradeEvent(PumpFunTradeEvent), PumpFunMigrateEvent(PumpFunMigrateEvent), + PumpFeesCreateFeeSharingConfigEvent(PumpFeesCreateFeeSharingConfigEvent), + PumpFeesInitializeFeeConfigEvent(PumpFeesInitializeFeeConfigEvent), + PumpFeesResetFeeSharingConfigEvent(PumpFeesResetFeeSharingConfigEvent), + PumpFeesRevokeFeeSharingAuthorityEvent(PumpFeesRevokeFeeSharingAuthorityEvent), + PumpFeesTransferFeeSharingAuthorityEvent(PumpFeesTransferFeeSharingAuthorityEvent), + PumpFeesUpdateAdminEvent(PumpFeesUpdateAdminEvent), + PumpFeesUpdateFeeConfigEvent(PumpFeesUpdateFeeConfigEvent), + PumpFeesUpdateFeeSharesEvent(PumpFeesUpdateFeeSharesEvent), + PumpFeesUpsertFeeTiersEvent(PumpFeesUpsertFeeTiersEvent), + PumpFunMigrateBondingCurveCreatorEvent(PumpFunMigrateBondingCurveCreatorEvent), PumpFunBondingCurveAccountEvent(PumpFunBondingCurveAccountEvent), PumpFunGlobalAccountEvent(PumpFunGlobalAccountEvent), @@ -59,6 +79,7 @@ pub enum DexEvent { RaydiumClmmClosePositionEvent(RaydiumClmmClosePositionEvent), RaydiumClmmIncreaseLiquidityV2Event(RaydiumClmmIncreaseLiquidityV2Event), RaydiumClmmDecreaseLiquidityV2Event(RaydiumClmmDecreaseLiquidityV2Event), + RaydiumClmmCollectFeeEvent(RaydiumClmmCollectFeeEvent), RaydiumClmmCreatePoolEvent(RaydiumClmmCreatePoolEvent), RaydiumClmmOpenPositionWithToken22NftEvent(RaydiumClmmOpenPositionWithToken22NftEvent), RaydiumClmmOpenPositionV2Event(RaydiumClmmOpenPositionV2Event), @@ -79,7 +100,35 @@ pub enum DexEvent { MeteoraDammV2Swap2Event(MeteoraDammV2Swap2Event), MeteoraDammV2InitializePoolEvent(MeteoraDammV2InitializePoolEvent), MeteoraDammV2InitializeCustomizablePoolEvent(MeteoraDammV2InitializeCustomizablePoolEvent), - MeteoraDammV2InitializePoolWithDynamicConfigEvent(MeteoraDammV2InitializePoolWithDynamicConfigEvent), + MeteoraDammV2InitializePoolWithDynamicConfigEvent( + MeteoraDammV2InitializePoolWithDynamicConfigEvent, + ), + + MeteoraDammV2AddLiquidityEvent(MeteoraDammV2AddLiquidityEvent), + MeteoraDammV2RemoveLiquidityEvent(MeteoraDammV2RemoveLiquidityEvent), + MeteoraDammV2CreatePositionEvent(MeteoraDammV2CreatePositionEvent), + MeteoraDammV2ClosePositionEvent(MeteoraDammV2ClosePositionEvent), + + OrcaWhirlpoolSwapEvent(OrcaWhirlpoolSwapEvent), + OrcaWhirlpoolLiquidityIncreasedEvent(OrcaWhirlpoolLiquidityIncreasedEvent), + OrcaWhirlpoolLiquidityDecreasedEvent(OrcaWhirlpoolLiquidityDecreasedEvent), + OrcaWhirlpoolPoolInitializedEvent(OrcaWhirlpoolPoolInitializedEvent), + + MeteoraPoolsSwapEvent(MeteoraPoolsSwapEvent), + MeteoraPoolsAddLiquidityEvent(MeteoraPoolsAddLiquidityEvent), + MeteoraPoolsRemoveLiquidityEvent(MeteoraPoolsRemoveLiquidityEvent), + MeteoraPoolsBootstrapLiquidityEvent(MeteoraPoolsBootstrapLiquidityEvent), + MeteoraPoolsPoolCreatedEvent(MeteoraPoolsPoolCreatedEvent), + MeteoraPoolsSetPoolFeesEvent(MeteoraPoolsSetPoolFeesEvent), + + MeteoraDlmmSwapEvent(MeteoraDlmmSwapEvent), + MeteoraDlmmAddLiquidityEvent(MeteoraDlmmAddLiquidityEvent), + MeteoraDlmmRemoveLiquidityEvent(MeteoraDlmmRemoveLiquidityEvent), + MeteoraDlmmInitializePoolEvent(MeteoraDlmmInitializePoolEvent), + MeteoraDlmmInitializeBinArrayEvent(MeteoraDlmmInitializeBinArrayEvent), + MeteoraDlmmCreatePositionEvent(MeteoraDlmmCreatePositionEvent), + MeteoraDlmmClosePositionEvent(MeteoraDlmmClosePositionEvent), + MeteoraDlmmClaimFeeEvent(MeteoraDlmmClaimFeeEvent), // Common events TokenAccountEvent(TokenAccountEvent), @@ -88,6 +137,7 @@ pub enum DexEvent { BlockMetaEvent(BlockMetaEvent), SetComputeUnitLimitEvent(SetComputeUnitLimitEvent), SetComputeUnitPriceEvent(SetComputeUnitPriceEvent), + ParserSdkErrorEvent(ParserSdkErrorEvent), } /// Macro to generate metadata accessors for all DexEvent variants @@ -123,6 +173,16 @@ impl_dex_event_metadata!( PumpFunCreateV2TokenEvent, PumpFunTradeEvent, PumpFunMigrateEvent, + PumpFeesCreateFeeSharingConfigEvent, + PumpFeesInitializeFeeConfigEvent, + PumpFeesResetFeeSharingConfigEvent, + PumpFeesRevokeFeeSharingAuthorityEvent, + PumpFeesTransferFeeSharingAuthorityEvent, + PumpFeesUpdateAdminEvent, + PumpFeesUpdateFeeConfigEvent, + PumpFeesUpdateFeeSharesEvent, + PumpFeesUpsertFeeTiersEvent, + PumpFunMigrateBondingCurveCreatorEvent, PumpFunBondingCurveAccountEvent, PumpFunGlobalAccountEvent, // PumpSwap events @@ -146,6 +206,7 @@ impl_dex_event_metadata!( RaydiumClmmClosePositionEvent, RaydiumClmmIncreaseLiquidityV2Event, RaydiumClmmDecreaseLiquidityV2Event, + RaydiumClmmCollectFeeEvent, RaydiumClmmCreatePoolEvent, RaydiumClmmOpenPositionWithToken22NftEvent, RaydiumClmmOpenPositionV2Event, @@ -165,6 +226,28 @@ impl_dex_event_metadata!( MeteoraDammV2InitializePoolEvent, MeteoraDammV2InitializeCustomizablePoolEvent, MeteoraDammV2InitializePoolWithDynamicConfigEvent, + MeteoraDammV2AddLiquidityEvent, + MeteoraDammV2RemoveLiquidityEvent, + MeteoraDammV2CreatePositionEvent, + MeteoraDammV2ClosePositionEvent, + OrcaWhirlpoolSwapEvent, + OrcaWhirlpoolLiquidityIncreasedEvent, + OrcaWhirlpoolLiquidityDecreasedEvent, + OrcaWhirlpoolPoolInitializedEvent, + MeteoraPoolsSwapEvent, + MeteoraPoolsAddLiquidityEvent, + MeteoraPoolsRemoveLiquidityEvent, + MeteoraPoolsBootstrapLiquidityEvent, + MeteoraPoolsPoolCreatedEvent, + MeteoraPoolsSetPoolFeesEvent, + MeteoraDlmmSwapEvent, + MeteoraDlmmAddLiquidityEvent, + MeteoraDlmmRemoveLiquidityEvent, + MeteoraDlmmInitializePoolEvent, + MeteoraDlmmInitializeBinArrayEvent, + MeteoraDlmmCreatePositionEvent, + MeteoraDlmmClosePositionEvent, + MeteoraDlmmClaimFeeEvent, // Common events TokenAccountEvent, NonceAccountEvent, @@ -172,4 +255,5 @@ impl_dex_event_metadata!( BlockMetaEvent, SetComputeUnitLimitEvent, SetComputeUnitPriceEvent, + ParserSdkErrorEvent, ); diff --git a/src/streaming/event_parser/mod.rs b/src/streaming/event_parser/mod.rs index b981df0..b5e7ceb 100755 --- a/src/streaming/event_parser/mod.rs +++ b/src/streaming/event_parser/mod.rs @@ -1,6 +1,14 @@ +//! Solana DEX 交易解析:**对外类型** [`DexEvent`]、[`Protocol`]; +//! **`common`**(元数据/过滤器)、**`core`**(调度、合并、gRPC 解析入口)、**`protocols`**(按协议的 parser/events)。 +//! +//! 与 **sol-parser-sdk** 对照:`protocols/*/parser` ≈ sdk `instr/*`,`parser_sdk_bridge` ≈ sdk 事件枚举与各实现的胶水层。 +//! +//! [`DexEvent`]: crate::streaming::event_parser::DexEvent +//! [`Protocol`]: crate::streaming::event_parser::Protocol + pub mod common; pub mod core; pub mod protocols; pub use core::traits::DexEvent; -pub use protocols::types::Protocol; \ No newline at end of file +pub use protocols::types::Protocol; diff --git a/src/streaming/event_parser/protocols/block/block_meta_event.rs b/src/streaming/event_parser/protocols/block/block_meta_event.rs index fb42319..7613e8f 100644 --- a/src/streaming/event_parser/protocols/block/block_meta_event.rs +++ b/src/streaming/event_parser/protocols/block/block_meta_event.rs @@ -13,12 +13,7 @@ pub struct BlockMetaEvent { } impl BlockMetaEvent { - pub fn new( - slot: u64, - block_hash: String, - block_time_ms: i64, - recv_us: i64, - ) -> Self { + pub fn new(slot: u64, block_hash: String, block_time_ms: i64, recv_us: i64) -> Self { let metadata = EventMetadata::new( Signature::default(), slot, diff --git a/src/streaming/event_parser/protocols/block/mod.rs b/src/streaming/event_parser/protocols/block/mod.rs index a8d6d32..fde3b30 100644 --- a/src/streaming/event_parser/protocols/block/mod.rs +++ b/src/streaming/event_parser/protocols/block/mod.rs @@ -1 +1 @@ -pub mod block_meta_event; \ No newline at end of file +pub mod block_meta_event; diff --git a/src/streaming/event_parser/protocols/bonk/parser.rs b/src/streaming/event_parser/protocols/bonk/parser.rs index bc7bcfd..9686f61 100755 --- a/src/streaming/event_parser/protocols/bonk/parser.rs +++ b/src/streaming/event_parser/protocols/bonk/parser.rs @@ -25,24 +25,14 @@ pub fn parse_bonk_instruction_data( metadata: EventMetadata, ) -> Option { match discriminator { - discriminators::BUY_EXACT_IN => { - parse_buy_exact_in_instruction(data, accounts, metadata) - } - discriminators::BUY_EXACT_OUT => { - parse_buy_exact_out_instruction(data, accounts, metadata) - } - discriminators::SELL_EXACT_IN => { - parse_sell_exact_in_instruction(data, accounts, metadata) - } + discriminators::BUY_EXACT_IN => parse_buy_exact_in_instruction(data, accounts, metadata), + discriminators::BUY_EXACT_OUT => parse_buy_exact_out_instruction(data, accounts, metadata), + discriminators::SELL_EXACT_IN => parse_sell_exact_in_instruction(data, accounts, metadata), discriminators::SELL_EXACT_OUT => { parse_sell_exact_out_instruction(data, accounts, metadata) } - discriminators::INITIALIZE => { - parse_initialize_instruction(data, accounts, metadata) - } - discriminators::INITIALIZE_V2 => { - parse_initialize_v2_instruction(data, accounts, metadata) - } + discriminators::INITIALIZE => parse_initialize_instruction(data, accounts, metadata), + discriminators::INITIALIZE_V2 => parse_initialize_v2_instruction(data, accounts, metadata), discriminators::INITIALIZE_WITH_TOKEN_2022 => { parse_initialize_with_token_2022_instruction(data, accounts, metadata) } @@ -65,12 +55,8 @@ pub fn parse_bonk_inner_instruction_data( metadata: EventMetadata, ) -> Option { match discriminator { - discriminators::TRADE_EVENT => { - parse_trade_inner_instruction(data, metadata) - } - discriminators::POOL_CREATE_EVENT => { - parse_pool_create_inner_instruction(data, metadata) - } + discriminators::TRADE_EVENT => parse_trade_inner_instruction(data, metadata), + discriminators::POOL_CREATE_EVENT => parse_pool_create_inner_instruction(data, metadata), _ => None, } } @@ -85,23 +71,26 @@ pub fn parse_bonk_account_data( ) -> Option { match discriminator { discriminators::POOL_STATE_ACCOUNT => { - crate::streaming::event_parser::protocols::bonk::types::pool_state_parser(account, metadata) + crate::streaming::event_parser::protocols::bonk::types::pool_state_parser( + account, metadata, + ) } discriminators::GLOBAL_CONFIG_ACCOUNT => { - crate::streaming::event_parser::protocols::bonk::types::global_config_parser(account, metadata) + crate::streaming::event_parser::protocols::bonk::types::global_config_parser( + account, metadata, + ) } discriminators::PLATFORM_CONFIG_ACCOUNT => { - crate::streaming::event_parser::protocols::bonk::types::platform_config_parser(account, metadata) + crate::streaming::event_parser::protocols::bonk::types::platform_config_parser( + account, metadata, + ) } _ => None, } } /// Parse pool creation event -fn parse_pool_create_inner_instruction( - data: &[u8], - metadata: EventMetadata, -) -> Option { +fn parse_pool_create_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option { // Note: event_type will be set by the instruction parser, not here // Because different initialize instructions have different event types if let Some(event) = bonk_pool_create_event_log_decode(data) { diff --git a/src/streaming/event_parser/protocols/bonk/types.rs b/src/streaming/event_parser/protocols/bonk/types.rs index 74bc8f0..586314a 100755 --- a/src/streaming/event_parser/protocols/bonk/types.rs +++ b/src/streaming/event_parser/protocols/bonk/types.rs @@ -349,7 +349,8 @@ impl Default for PlatformConfig { } } -pub const PLATFORM_CONFIG_SIZE: usize = 8 + 32 * 2 + 8 * 4 + 64 + 256 + 256 + 32 + 8 + 32 + 32 + 8 + 32 + 108; +pub const PLATFORM_CONFIG_SIZE: usize = + 8 + 32 * 2 + 8 * 4 + 64 + 256 + 256 + 32 + 8 + 32 + 32 + 8 + 32 + 108; pub fn platform_config_decode(data: &[u8]) -> Option { if data.len() < PLATFORM_CONFIG_SIZE { diff --git a/src/streaming/event_parser/protocols/meteora_damm_v2/events.rs b/src/streaming/event_parser/protocols/meteora_damm_v2/events.rs index 06933b3..ac20fe8 100644 --- a/src/streaming/event_parser/protocols/meteora_damm_v2/events.rs +++ b/src/streaming/event_parser/protocols/meteora_damm_v2/events.rs @@ -374,6 +374,68 @@ pub struct MeteoraDammV2InitializePoolWithDynamicConfigEvent { pub config: Pubkey, } +/// DAMM v2 Add Liquidity(parser-sdk / CPI 日志字段对齐) +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct MeteoraDammV2AddLiquidityEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub pool: Pubkey, + pub position: Pubkey, + pub owner: Pubkey, + pub token_a_amount: u64, + pub token_b_amount: u64, + #[borsh(skip)] + pub liquidity_delta: u128, + #[borsh(skip)] + pub token_a_amount_threshold: u64, + #[borsh(skip)] + pub token_b_amount_threshold: u64, + #[borsh(skip)] + pub total_amount_a: u64, + #[borsh(skip)] + pub total_amount_b: u64, +} + +/// DAMM v2 Remove Liquidity +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct MeteoraDammV2RemoveLiquidityEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub pool: Pubkey, + pub position: Pubkey, + pub owner: Pubkey, + pub token_a_amount: u64, + pub token_b_amount: u64, + #[borsh(skip)] + pub liquidity_delta: u128, + #[borsh(skip)] + pub token_a_amount_threshold: u64, + #[borsh(skip)] + pub token_b_amount_threshold: u64, +} + +/// DAMM v2 Create Position +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct MeteoraDammV2CreatePositionEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub pool: Pubkey, + pub owner: Pubkey, + pub position: Pubkey, + pub position_nft_mint: Pubkey, +} + +/// DAMM v2 Close Position +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] +pub struct MeteoraDammV2ClosePositionEvent { + #[borsh(skip)] + pub metadata: EventMetadata, + pub pool: Pubkey, + pub owner: Pubkey, + pub position: Pubkey, + pub position_nft_mint: Pubkey, +} + /// Event discriminators pub mod discriminators { // Instruction discriminators diff --git a/src/streaming/event_parser/protocols/mod.rs b/src/streaming/event_parser/protocols/mod.rs index 0d44048..61e9426 100755 --- a/src/streaming/event_parser/protocols/mod.rs +++ b/src/streaming/event_parser/protocols/mod.rs @@ -6,6 +6,7 @@ pub mod pumpswap; pub mod raydium_amm_v4; pub mod raydium_clmm; pub mod raydium_cpmm; +pub mod sol_parser_forward; pub mod types; pub use block::block_meta_event::BlockMetaEvent; pub use types::Protocol; diff --git a/src/streaming/event_parser/protocols/pumpfun/events.rs b/src/streaming/event_parser/protocols/pumpfun/events.rs index a7d88d6..6bc7f78 100755 --- a/src/streaming/event_parser/protocols/pumpfun/events.rs +++ b/src/streaming/event_parser/protocols/pumpfun/events.rs @@ -323,7 +323,8 @@ pub fn pumpfun_trade_event_log_decode(data: &[u8]) -> Option if data.len() < PUMPFUN_TRADE_EVENT_LOG_SIZE { return None; } - let mut event = borsh::from_slice::(&data[..PUMPFUN_TRADE_EVENT_LOG_SIZE]).ok()?; + let mut event = + borsh::from_slice::(&data[..PUMPFUN_TRADE_EVENT_LOG_SIZE]).ok()?; let mut offset = PUMPFUN_TRADE_EVENT_LOG_SIZE; if offset < data.len() { let (ix_name, inc) = read_borsh_string(data, offset).unwrap_or((String::new(), 0)); @@ -335,7 +336,8 @@ pub fn pumpfun_trade_event_log_decode(data: &[u8]) -> Option offset += 1; } if offset + 8 <= data.len() { - event.cashback_fee_basis_points = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?); + event.cashback_fee_basis_points = + u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?); offset += 8; } if offset + 8 <= data.len() { @@ -415,6 +417,138 @@ pub struct PumpFunMigrateEvent { pub program: Pubkey, } +// ---------- pump-fees IDL: `idls/pump_fees.json` (Program `pfeeUx...`) ---------- + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesShareholder { + pub address: Pubkey, + pub share_bps: u16, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PumpFeesConfigStatus { + Paused, + Active, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesFees { + pub lp_fee_bps: u64, + pub protocol_fee_bps: u64, + pub creator_fee_bps: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesFeeTier { + pub market_cap_lamports_threshold: u128, + pub fees: PumpFeesFees, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesCreateFeeSharingConfigEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub mint: Pubkey, + pub bonding_curve: Pubkey, + pub pool: Option, + pub sharing_config: Pubkey, + pub admin: Pubkey, + pub initial_shareholders: Vec, + pub status: PumpFeesConfigStatus, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesInitializeFeeConfigEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub admin: Pubkey, + pub fee_config: Pubkey, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesResetFeeSharingConfigEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub mint: Pubkey, + pub sharing_config: Pubkey, + pub old_admin: Pubkey, + pub old_shareholders: Vec, + pub new_admin: Pubkey, + pub new_shareholders: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesRevokeFeeSharingAuthorityEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub mint: Pubkey, + pub sharing_config: Pubkey, + pub admin: Pubkey, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesTransferFeeSharingAuthorityEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub mint: Pubkey, + pub sharing_config: Pubkey, + pub old_admin: Pubkey, + pub new_admin: Pubkey, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesUpdateAdminEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub old_admin: Pubkey, + pub new_admin: Pubkey, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesUpdateFeeConfigEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub admin: Pubkey, + pub fee_config: Pubkey, + pub fee_tiers: Vec, + pub flat_fees: PumpFeesFees, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesUpdateFeeSharesEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub mint: Pubkey, + pub sharing_config: Pubkey, + pub admin: Pubkey, + #[serde(default)] + pub bonding_curve: Pubkey, + #[serde(default)] + pub pump_creator_vault: Pubkey, + pub new_shareholders: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFeesUpsertFeeTiersEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub admin: Pubkey, + pub fee_config: Pubkey, + pub fee_tiers: Vec, + pub offset: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PumpFunMigrateBondingCurveCreatorEvent { + pub metadata: EventMetadata, + pub timestamp: i64, + pub mint: Pubkey, + pub bonding_curve: Pubkey, + pub sharing_config: Pubkey, + pub old_creator: Pubkey, + pub new_creator: Pubkey, +} + pub const PUMPFUN_MIGRATE_EVENT_LOG_SIZE: usize = 160; pub fn pumpfun_migrate_event_log_decode(data: &[u8]) -> Option { diff --git a/src/streaming/event_parser/protocols/pumpfun/parser.rs b/src/streaming/event_parser/protocols/pumpfun/parser.rs index 2d529f6..959d3f0 100755 --- a/src/streaming/event_parser/protocols/pumpfun/parser.rs +++ b/src/streaming/event_parser/protocols/pumpfun/parser.rs @@ -28,7 +28,9 @@ pub fn parse_pumpfun_instruction_data( parse_create_v2_token_instruction(data, accounts, metadata) } discriminators::BUY_IX => parse_buy_instruction(data, accounts, metadata), - discriminators::BUY_EXACT_SOL_IN_IX => parse_buy_exact_sol_in_instruction(data, accounts, metadata), + discriminators::BUY_EXACT_SOL_IN_IX => { + parse_buy_exact_sol_in_instruction(data, accounts, metadata) + } discriminators::SELL_IX => parse_sell_instruction(data, accounts, metadata), discriminators::MIGRATE_IX => parse_migrate_instruction(data, accounts, metadata), _ => None, @@ -320,10 +322,11 @@ fn parse_buy_instruction( /// Same account layout as buy: 16 fixed + optional 17th (index 16). /// Args: spendable_sol_in (SOL), min_tokens_out (token). fn parse_buy_exact_sol_in_instruction( - data: &[u8], accounts: &[Pubkey], + data: &[u8], + accounts: &[Pubkey], mut metadata: EventMetadata, ) -> Option { - metadata.event_type = EventType::PumpFunBuy; + metadata.event_type = EventType::PumpFunBuyExactSolIn; if data.len() < 16 || accounts.len() < 16 { return None; diff --git a/src/streaming/event_parser/protocols/pumpfun/types.rs b/src/streaming/event_parser/protocols/pumpfun/types.rs index 5118866..f4118b4 100644 --- a/src/streaming/event_parser/protocols/pumpfun/types.rs +++ b/src/streaming/event_parser/protocols/pumpfun/types.rs @@ -82,7 +82,8 @@ pub struct Global { pub is_cashback_enabled: bool, } -pub const GLOBAL_SIZE: usize = 1 + 32 * 2 + 8 * 5 + 32 + 1 + 8 * 2 + 32 * 7 + 32 * 2 + 1 + 32 * 2 + 1 + 32 * 7 + 1; +pub const GLOBAL_SIZE: usize = + 1 + 32 * 2 + 8 * 5 + 32 + 1 + 8 * 2 + 32 * 7 + 32 * 2 + 1 + 32 * 2 + 1 + 32 * 7 + 1; pub fn global_decode(data: &[u8]) -> Option { if data.len() < GLOBAL_SIZE { diff --git a/src/streaming/event_parser/protocols/pumpswap/events.rs b/src/streaming/event_parser/protocols/pumpswap/events.rs index 2694eca..04119fc 100755 --- a/src/streaming/event_parser/protocols/pumpswap/events.rs +++ b/src/streaming/event_parser/protocols/pumpswap/events.rs @@ -93,7 +93,8 @@ pub fn pump_swap_buy_event_log_decode(data: &[u8]) -> Option { let user_base_token_account = Pubkey::new_from_array(data.get(176..208)?.try_into().ok()?); let user_quote_token_account = Pubkey::new_from_array(data.get(208..240)?.try_into().ok()?); let protocol_fee_recipient = Pubkey::new_from_array(data.get(240..272)?.try_into().ok()?); - let protocol_fee_recipient_token_account = Pubkey::new_from_array(data.get(272..304)?.try_into().ok()?); + let protocol_fee_recipient_token_account = + Pubkey::new_from_array(data.get(272..304)?.try_into().ok()?); let coin_creator = Pubkey::new_from_array(data.get(304..336)?.try_into().ok()?); let coin_creator_fee_basis_points = read_u64_le(data, 336)?; let coin_creator_fee = read_u64_le(data, 344)?; @@ -243,11 +244,13 @@ pub fn pump_swap_sell_event_log_decode(data: &[u8]) -> Option let user_base_token_account = Pubkey::new_from_array(data.get(176..208)?.try_into().ok()?); let user_quote_token_account = Pubkey::new_from_array(data.get(208..240)?.try_into().ok()?); let protocol_fee_recipient = Pubkey::new_from_array(data.get(240..272)?.try_into().ok()?); - let protocol_fee_recipient_token_account = Pubkey::new_from_array(data.get(272..304)?.try_into().ok()?); + let protocol_fee_recipient_token_account = + Pubkey::new_from_array(data.get(272..304)?.try_into().ok()?); let coin_creator = Pubkey::new_from_array(data.get(304..336)?.try_into().ok()?); let coin_creator_fee_basis_points = read_u64_le(data, 336)?; let coin_creator_fee = read_u64_le(data, 344)?; - let (cashback_fee_basis_points, cashback) = if data.len() >= PUMP_SWAP_SELL_EVENT_WITH_CASHBACK { + let (cashback_fee_basis_points, cashback) = if data.len() >= PUMP_SWAP_SELL_EVENT_WITH_CASHBACK + { (read_u64_le(data, 352)?, read_u64_le(data, 360)?) } else { (0, 0) diff --git a/src/streaming/event_parser/protocols/pumpswap/parser.rs b/src/streaming/event_parser/protocols/pumpswap/parser.rs index 8b7e2f3..2a76205 100755 --- a/src/streaming/event_parser/protocols/pumpswap/parser.rs +++ b/src/streaming/event_parser/protocols/pumpswap/parser.rs @@ -25,11 +25,11 @@ pub fn parse_pumpswap_instruction_data( ) -> Option { match discriminator { discriminators::BUY_IX => parse_buy_instruction(data, accounts, metadata), - discriminators::BUY_EXACT_QUOTE_IN_IX => parse_buy_exact_quote_in_instruction(data, accounts, metadata), - discriminators::SELL_IX => parse_sell_instruction(data, accounts, metadata), - discriminators::CREATE_POOL_IX => { - parse_create_pool_instruction(data, accounts, metadata) + discriminators::BUY_EXACT_QUOTE_IN_IX => { + parse_buy_exact_quote_in_instruction(data, accounts, metadata) } + discriminators::SELL_IX => parse_sell_instruction(data, accounts, metadata), + discriminators::CREATE_POOL_IX => parse_create_pool_instruction(data, accounts, metadata), discriminators::DEPOSIT_IX => parse_deposit_instruction(data, accounts, metadata), discriminators::WITHDRAW_IX => parse_withdraw_instruction(data, accounts, metadata), _ => None, @@ -47,16 +47,13 @@ pub fn parse_pumpswap_inner_instruction_data( match discriminator { discriminators::BUY_EVENT => parse_buy_inner_instruction(data, metadata), discriminators::SELL_EVENT => parse_sell_inner_instruction(data, metadata), - discriminators::CREATE_POOL_EVENT => { - parse_create_pool_inner_instruction(data, metadata) - } + discriminators::CREATE_POOL_EVENT => parse_create_pool_inner_instruction(data, metadata), discriminators::DEPOSIT_EVENT => parse_deposit_inner_instruction(data, metadata), discriminators::WITHDRAW_EVENT => parse_withdraw_inner_instruction(data, metadata), _ => None, } } - /// 解析 PumpSwap 账户数据 /// /// 根据判别器路由到具体的账户解析函数 @@ -67,10 +64,14 @@ pub fn parse_pumpswap_account_data( ) -> Option { match discriminator { discriminators::GLOBAL_CONFIG_ACCOUNT => { - crate::streaming::event_parser::protocols::pumpswap::types::global_config_parser(account, metadata) + crate::streaming::event_parser::protocols::pumpswap::types::global_config_parser( + account, metadata, + ) } discriminators::POOL_ACCOUNT => { - crate::streaming::event_parser::protocols::pumpswap::types::pool_parser(account, metadata) + crate::streaming::event_parser::protocols::pumpswap::types::pool_parser( + account, metadata, + ) } _ => None, } @@ -97,10 +98,7 @@ fn parse_sell_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option< } /// 解析创建池子日志事件 -fn parse_create_pool_inner_instruction( - data: &[u8], - metadata: EventMetadata, -) -> Option { +fn parse_create_pool_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option { // Note: event_type will be set by instruction parser if let Some(event) = pump_swap_create_pool_event_log_decode(data) { Some(DexEvent::PumpSwapCreatePoolEvent(PumpSwapCreatePoolEvent { metadata, ..event })) diff --git a/src/streaming/event_parser/protocols/raydium_amm_v4/events.rs b/src/streaming/event_parser/protocols/raydium_amm_v4/events.rs index e28e1f1..fac8f40 100755 --- a/src/streaming/event_parser/protocols/raydium_amm_v4/events.rs +++ b/src/streaming/event_parser/protocols/raydium_amm_v4/events.rs @@ -1,7 +1,5 @@ use crate::streaming::event_parser::common::EventMetadata; -use crate::{ - streaming::event_parser::protocols::raydium_amm_v4::types::AmmInfo, -}; +use crate::streaming::event_parser::protocols::raydium_amm_v4::types::AmmInfo; use borsh::BorshDeserialize; use serde::{Deserialize, Serialize}; use solana_sdk::pubkey::Pubkey; diff --git a/src/streaming/event_parser/protocols/raydium_amm_v4/parser.rs b/src/streaming/event_parser/protocols/raydium_amm_v4/parser.rs index 76831a4..090366f 100755 --- a/src/streaming/event_parser/protocols/raydium_amm_v4/parser.rs +++ b/src/streaming/event_parser/protocols/raydium_amm_v4/parser.rs @@ -22,18 +22,14 @@ pub fn parse_raydium_amm_v4_instruction_data( metadata: EventMetadata, ) -> Option { match discriminator { - discriminators::SWAP_BASE_IN => { - parse_swap_base_input_instruction(data, accounts, metadata) - } + discriminators::SWAP_BASE_IN => parse_swap_base_input_instruction(data, accounts, metadata), discriminators::SWAP_BASE_OUT => { parse_swap_base_output_instruction(data, accounts, metadata) } discriminators::DEPOSIT => parse_deposit_instruction(data, accounts, metadata), discriminators::INITIALIZE2 => parse_initialize2_instruction(data, accounts, metadata), discriminators::WITHDRAW => parse_withdraw_instruction(data, accounts, metadata), - discriminators::WITHDRAW_PNL => { - parse_withdraw_pnl_instruction(data, accounts, metadata) - } + discriminators::WITHDRAW_PNL => parse_withdraw_pnl_instruction(data, accounts, metadata), _ => None, } } @@ -49,7 +45,6 @@ pub fn parse_raydium_amm_v4_inner_instruction_data( None } - /// 解析 Raydium AMM V4 账户数据 /// /// 根据判别器路由到具体的账户解析函数 @@ -60,13 +55,14 @@ pub fn parse_raydium_amm_v4_account_data( ) -> Option { match discriminator { discriminators::AMM_INFO => { - crate::streaming::event_parser::protocols::raydium_amm_v4::types::amm_info_parser(account, metadata) + crate::streaming::event_parser::protocols::raydium_amm_v4::types::amm_info_parser( + account, metadata, + ) } _ => None, } } - /// 解析提现指令事件 fn parse_withdraw_pnl_instruction( _data: &[u8], diff --git a/src/streaming/event_parser/protocols/raydium_clmm/events.rs b/src/streaming/event_parser/protocols/raydium_clmm/events.rs index 492fa57..60c8769 100755 --- a/src/streaming/event_parser/protocols/raydium_clmm/events.rs +++ b/src/streaming/event_parser/protocols/raydium_clmm/events.rs @@ -1,8 +1,6 @@ use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::raydium_clmm::types::AmmConfig; use crate::streaming::event_parser::protocols::raydium_clmm::types::{PoolState, TickArrayState}; -use crate::{ - streaming::event_parser::protocols::raydium_clmm::types::AmmConfig, -}; use serde::{Deserialize, Serialize}; use solana_sdk::pubkey::Pubkey; @@ -27,7 +25,6 @@ pub struct RaydiumClmmSwapEvent { pub remaining_accounts: Vec, } - /// 交易v2 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RaydiumClmmSwapV2Event { @@ -90,6 +87,16 @@ pub struct RaydiumClmmDecreaseLiquidityV2Event { pub remaining_accounts: Vec, } +/// 收取流动性费用(与 `sol-parser-sdk` 日志事件字段对齐的精简版) +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct RaydiumClmmCollectFeeEvent { + pub metadata: EventMetadata, + pub pool_state: Pubkey, + pub position_nft_mint: Pubkey, + pub amount_0: u64, + pub amount_1: u64, +} + /// 创建池 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RaydiumClmmCreatePoolEvent { diff --git a/src/streaming/event_parser/protocols/raydium_clmm/parser.rs b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs index b127644..41e9bc0 100755 --- a/src/streaming/event_parser/protocols/raydium_clmm/parser.rs +++ b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs @@ -60,7 +60,6 @@ pub fn parse_raydium_clmm_inner_instruction_data( None } - /// 解析 Raydium CLMM 账户数据 /// /// 根据判别器路由到具体的账户解析函数 @@ -71,13 +70,19 @@ pub fn parse_raydium_clmm_account_data( ) -> Option { match discriminator { discriminators::AMM_CONFIG => { - crate::streaming::event_parser::protocols::raydium_clmm::types::amm_config_parser(account, metadata) + crate::streaming::event_parser::protocols::raydium_clmm::types::amm_config_parser( + account, metadata, + ) } discriminators::POOL_STATE => { - crate::streaming::event_parser::protocols::raydium_clmm::types::pool_state_parser(account, metadata) + crate::streaming::event_parser::protocols::raydium_clmm::types::pool_state_parser( + account, metadata, + ) } discriminators::TICK_ARRAY_STATE => { - crate::streaming::event_parser::protocols::raydium_clmm::types::tick_array_state_parser(account, metadata) + crate::streaming::event_parser::protocols::raydium_clmm::types::tick_array_state_parser( + account, metadata, + ) } _ => None, } diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/events.rs b/src/streaming/event_parser/protocols/raydium_cpmm/events.rs index 8aef79e..f209dc8 100755 --- a/src/streaming/event_parser/protocols/raydium_cpmm/events.rs +++ b/src/streaming/event_parser/protocols/raydium_cpmm/events.rs @@ -1,8 +1,6 @@ use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::raydium_cpmm::types::AmmConfig; use crate::streaming::event_parser::protocols::raydium_cpmm::types::PoolState; -use crate::{ - streaming::event_parser::protocols::raydium_cpmm::types::AmmConfig, -}; use borsh::BorshDeserialize; use serde::{Deserialize, Serialize}; use solana_sdk::pubkey::Pubkey; @@ -31,7 +29,6 @@ pub struct RaydiumCpmmSwapEvent { pub observation_state: Pubkey, } - /// 存款 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)] pub struct RaydiumCpmmDepositEvent { diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs b/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs index 1c20d13..633011f 100755 --- a/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs +++ b/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs @@ -23,9 +23,7 @@ pub fn parse_raydium_cpmm_instruction_data( metadata: EventMetadata, ) -> Option { match discriminator { - discriminators::SWAP_BASE_IN => { - parse_swap_base_input_instruction(data, accounts, metadata) - } + discriminators::SWAP_BASE_IN => parse_swap_base_input_instruction(data, accounts, metadata), discriminators::SWAP_BASE_OUT => { parse_swap_base_output_instruction(data, accounts, metadata) } @@ -47,7 +45,6 @@ pub fn parse_raydium_cpmm_inner_instruction_data( None } - /// 解析 Raydium CPMM 账户数据 /// /// 根据判别器路由到具体的账户解析函数 @@ -58,16 +55,19 @@ pub fn parse_raydium_cpmm_account_data( ) -> Option { match discriminator { discriminators::AMM_CONFIG => { - crate::streaming::event_parser::protocols::raydium_cpmm::types::amm_config_parser(account, metadata) + crate::streaming::event_parser::protocols::raydium_cpmm::types::amm_config_parser( + account, metadata, + ) } discriminators::POOL_STATE => { - crate::streaming::event_parser::protocols::raydium_cpmm::types::pool_state_parser(account, metadata) + crate::streaming::event_parser::protocols::raydium_cpmm::types::pool_state_parser( + account, metadata, + ) } _ => None, } } - /// 解析提款指令事件 fn parse_withdraw_instruction( data: &[u8], diff --git a/src/streaming/event_parser/protocols/sol_parser_forward/events.rs b/src/streaming/event_parser/protocols/sol_parser_forward/events.rs new file mode 100644 index 0000000..0d09171 --- /dev/null +++ b/src/streaming/event_parser/protocols/sol_parser_forward/events.rs @@ -0,0 +1,210 @@ +use crate::streaming::event_parser::common::EventMetadata; +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +/// `sol-parser-sdk` 错误占位(无有效 EventMetadata 字段) +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ParserSdkErrorEvent { + pub metadata: EventMetadata, + pub message: String, +} + +// --- Orca Whirlpool --- + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrcaWhirlpoolSwapEvent { + pub metadata: EventMetadata, + pub whirlpool: Pubkey, + pub input_amount: u64, + pub output_amount: u64, + pub a_to_b: bool, + pub pre_sqrt_price: u128, + pub post_sqrt_price: u128, + pub input_transfer_fee: u64, + pub output_transfer_fee: u64, + pub lp_fee: u64, + pub protocol_fee: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrcaWhirlpoolLiquidityIncreasedEvent { + pub metadata: EventMetadata, + pub whirlpool: Pubkey, + pub liquidity: u128, + pub token_a_amount: u64, + pub token_b_amount: u64, + pub position: Pubkey, + pub tick_lower_index: i32, + pub tick_upper_index: i32, + pub token_a_transfer_fee: u64, + pub token_b_transfer_fee: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrcaWhirlpoolLiquidityDecreasedEvent { + pub metadata: EventMetadata, + pub whirlpool: Pubkey, + pub liquidity: u128, + pub token_a_amount: u64, + pub token_b_amount: u64, + pub position: Pubkey, + pub tick_lower_index: i32, + pub tick_upper_index: i32, + pub token_a_transfer_fee: u64, + pub token_b_transfer_fee: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct OrcaWhirlpoolPoolInitializedEvent { + pub metadata: EventMetadata, + pub whirlpool: Pubkey, + pub whirlpools_config: Pubkey, + pub token_mint_a: Pubkey, + pub token_mint_b: Pubkey, + pub tick_spacing: u16, + pub token_program_a: Pubkey, + pub token_program_b: Pubkey, + pub decimals_a: u8, + pub decimals_b: u8, + pub initial_sqrt_price: u128, +} + +// --- Meteora Pools --- + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraPoolsSwapEvent { + pub metadata: EventMetadata, + pub in_amount: u64, + pub out_amount: u64, + pub trade_fee: u64, + pub admin_fee: u64, + pub host_fee: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraPoolsAddLiquidityEvent { + pub metadata: EventMetadata, + pub lp_mint_amount: u64, + pub token_a_amount: u64, + pub token_b_amount: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraPoolsRemoveLiquidityEvent { + pub metadata: EventMetadata, + pub lp_unmint_amount: u64, + pub token_a_out_amount: u64, + pub token_b_out_amount: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraPoolsBootstrapLiquidityEvent { + pub metadata: EventMetadata, + pub lp_mint_amount: u64, + pub token_a_amount: u64, + pub token_b_amount: u64, + pub pool: Pubkey, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraPoolsPoolCreatedEvent { + pub metadata: EventMetadata, + pub lp_mint: Pubkey, + pub token_a_mint: Pubkey, + pub token_b_mint: Pubkey, + pub pool_type: u8, + pub pool: Pubkey, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraPoolsSetPoolFeesEvent { + pub metadata: EventMetadata, + pub trade_fee_numerator: u64, + pub trade_fee_denominator: u64, + pub owner_trade_fee_numerator: u64, + pub owner_trade_fee_denominator: u64, + pub pool: Pubkey, +} + +// --- Meteora DLMM --- + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmSwapEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub from: Pubkey, + pub start_bin_id: i32, + pub end_bin_id: i32, + pub amount_in: u64, + pub amount_out: u64, + pub swap_for_y: bool, + pub fee: u64, + pub protocol_fee: u64, + pub fee_bps: u128, + pub host_fee: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmAddLiquidityEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub from: Pubkey, + pub position: Pubkey, + pub amounts: [u64; 2], + pub active_bin_id: i32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmRemoveLiquidityEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub from: Pubkey, + pub position: Pubkey, + pub amounts: [u64; 2], + pub active_bin_id: i32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmInitializePoolEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub creator: Pubkey, + pub active_bin_id: i32, + pub bin_step: u16, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmInitializeBinArrayEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub bin_array: Pubkey, + pub index: i64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmCreatePositionEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub position: Pubkey, + pub owner: Pubkey, + pub lower_bin_id: i32, + pub width: u32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmClosePositionEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub position: Pubkey, + pub owner: Pubkey, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeteoraDlmmClaimFeeEvent { + pub metadata: EventMetadata, + pub pool: Pubkey, + pub position: Pubkey, + pub owner: Pubkey, + pub fee_x: u64, + pub fee_y: u64, +} diff --git a/src/streaming/event_parser/protocols/sol_parser_forward/mod.rs b/src/streaming/event_parser/protocols/sol_parser_forward/mod.rs new file mode 100644 index 0000000..efa57b9 --- /dev/null +++ b/src/streaming/event_parser/protocols/sol_parser_forward/mod.rs @@ -0,0 +1,12 @@ +//! 由 `sol-parser-sdk` 产出、经 `parser_sdk_bridge` 映射的协议事件类型;`native` 将 sdk 指令解析接到 Yellowstone/shred 路径。 +pub mod events; +pub mod native; + +use solana_sdk::pubkey::Pubkey; + +pub const ORCA_WHIRLPOOL_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"); +pub const METEORA_POOLS_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB"); +pub const METEORA_DLMM_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"); diff --git a/src/streaming/event_parser/protocols/sol_parser_forward/native.rs b/src/streaming/event_parser/protocols/sol_parser_forward/native.rs new file mode 100644 index 0000000..79da726 --- /dev/null +++ b/src/streaming/event_parser/protocols/sol_parser_forward/native.rs @@ -0,0 +1,98 @@ +//! 将 `sol-parser-sdk` 的 Orca Whirlpool / Meteora Pools / DLMM 顶层与 inner 指令解析接到 streamer `DexEvent`。 + +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::{DexEvent, Protocol}; +use crate::streaming::parser_sdk_bridge::{ + block_timestamp_from_stream_meta, convert_parser_event, fuse_streamer_ix_ctx, +}; +use sol_parser_sdk::core::events::EventMetadata as PbEventMetadata; +use sol_parser_sdk::instr::all_inner::{ + meteora_amm as pools_inner, meteora_dlmm as dlmm_inner, orca as orca_inner, +}; +use sol_parser_sdk::instr::{meteora_amm, meteora_dlmm, orca_whirlpool}; +use solana_sdk::pubkey::Pubkey; + +#[inline] +fn block_time_us_for_sdk(sm: &EventMetadata) -> Option { + Some(sm.block_time_ms.saturating_mul(1000)) +} + +#[inline] +fn pb_meta_from_streamer(sm: &EventMetadata) -> PbEventMetadata { + PbEventMetadata { + signature: sm.signature, + slot: sm.slot, + tx_index: sm.tx_index.unwrap_or(0), + block_time_us: sm.block_time_ms.saturating_mul(1000), + grpc_recv_us: sm.recv_us, + recent_blockhash: sm.recent_blockhash.clone(), + } +} + +pub fn dispatch_instruction( + protocol: Protocol, + instruction_discriminator: &[u8], + instruction_data: &[u8], + accounts: &[Pubkey], + stream_meta: &EventMetadata, +) -> Option { + let mut full = Vec::with_capacity(instruction_discriminator.len() + instruction_data.len()); + full.extend_from_slice(instruction_discriminator); + full.extend_from_slice(instruction_data); + + let tx_index = stream_meta.tx_index.unwrap_or(0); + let bt_us = block_time_us_for_sdk(stream_meta); + + let pb = match protocol { + Protocol::OrcaWhirlpool => orca_whirlpool::parse_instruction( + &full, + accounts, + stream_meta.signature, + stream_meta.slot, + tx_index, + bt_us, + )?, + Protocol::MeteoraPools => meteora_amm::parse_instruction( + &full, + accounts, + stream_meta.signature, + stream_meta.slot, + tx_index, + bt_us, + )?, + Protocol::MeteoraDlmm => meteora_dlmm::parse_instruction( + &full, + accounts, + stream_meta.signature, + stream_meta.slot, + tx_index, + bt_us, + )?, + _ => return None, + }; + + let ts = block_timestamp_from_stream_meta(stream_meta); + let ev = convert_parser_event(pb, Some(&ts), stream_meta.recv_us)?; + Some(fuse_streamer_ix_ctx(ev, stream_meta)) +} + +pub fn dispatch_inner_instruction( + protocol: Protocol, + inner_instruction_discriminator: &[u8], + inner_instruction_data: &[u8], + stream_meta: &EventMetadata, +) -> Option { + let disc: [u8; 16] = inner_instruction_discriminator.try_into().ok()?; + let pm = pb_meta_from_streamer(stream_meta); + + let pb = match protocol { + Protocol::OrcaWhirlpool => orca_inner::parse(&disc, inner_instruction_data, pm)?, + Protocol::MeteoraPools => pools_inner::parse(&disc, inner_instruction_data, pm)?, + Protocol::MeteoraDlmm => dlmm_inner::parse(&disc, inner_instruction_data, pm)?, + _ => return None, + }; + + let ts = block_timestamp_from_stream_meta(stream_meta); + let ev = convert_parser_event(pb, Some(&ts), stream_meta.recv_us)?; + Some(fuse_streamer_ix_ctx(ev, stream_meta)) +} diff --git a/src/streaming/event_parser/protocols/types.rs b/src/streaming/event_parser/protocols/types.rs index 3d864dc..34dbde2 100755 --- a/src/streaming/event_parser/protocols/types.rs +++ b/src/streaming/event_parser/protocols/types.rs @@ -1,8 +1,14 @@ 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, + 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, + sol_parser_forward::{ + METEORA_DLMM_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID, ORCA_WHIRLPOOL_PROGRAM_ID, + }, }; use anyhow::{anyhow, Result}; use solana_sdk::pubkey::Pubkey; @@ -17,6 +23,9 @@ pub enum Protocol { RaydiumClmm, RaydiumAmmV4, MeteoraDammV2, + OrcaWhirlpool, + MeteoraPools, + MeteoraDlmm, } impl Protocol { @@ -29,6 +38,9 @@ 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::OrcaWhirlpool => vec![ORCA_WHIRLPOOL_PROGRAM_ID], + Protocol::MeteoraPools => vec![METEORA_POOLS_PROGRAM_ID], + Protocol::MeteoraDlmm => vec![METEORA_DLMM_PROGRAM_ID], } } } @@ -43,6 +55,9 @@ impl std::fmt::Display for Protocol { Protocol::RaydiumClmm => write!(f, "RaydiumClmm"), Protocol::RaydiumAmmV4 => write!(f, "RaydiumAmmV4"), Protocol::MeteoraDammV2 => write!(f, "MeteoraDammV2"), + Protocol::OrcaWhirlpool => write!(f, "OrcaWhirlpool"), + Protocol::MeteoraPools => write!(f, "MeteoraPools"), + Protocol::MeteoraDlmm => write!(f, "MeteoraDlmm"), } } } @@ -55,11 +70,47 @@ impl std::str::FromStr for Protocol { "pumpswap" => Ok(Protocol::PumpSwap), "pumpfun" => Ok(Protocol::PumpFun), "bonk" => Ok(Protocol::Bonk), - "raydiumcpmm" => Ok(Protocol::RaydiumCpmm), - "raydiumclmm" => Ok(Protocol::RaydiumClmm), - "raydiumammv4" => Ok(Protocol::RaydiumAmmV4), - "meteoradamm_v2" => Ok(Protocol::MeteoraDammV2), + "raydiumcpmm" | "raydium_cpmm" => Ok(Protocol::RaydiumCpmm), + "raydiumclmm" | "raydium_clmm" => Ok(Protocol::RaydiumClmm), + "raydiumammv4" | "raydium_amm_v4" => Ok(Protocol::RaydiumAmmV4), + "meteoradammv2" | "meteoradamm_v2" | "meteora_damm_v2" => Ok(Protocol::MeteoraDammV2), + "orcawhirlpool" | "orca_whirlpool" | "orca" => Ok(Protocol::OrcaWhirlpool), + "meteorapools" | "meteora_pools" => Ok(Protocol::MeteoraPools), + "meteoradlmm" | "meteora_dlmm" => Ok(Protocol::MeteoraDlmm), _ => Err(anyhow!("Unsupported protocol: {}", s)), } } } + +#[cfg(test)] +mod tests { + use super::Protocol; + use std::str::FromStr; + + #[test] + fn parses_display_style_protocol_names() { + for protocol in [ + Protocol::RaydiumCpmm, + Protocol::RaydiumClmm, + Protocol::RaydiumAmmV4, + Protocol::MeteoraDammV2, + Protocol::OrcaWhirlpool, + Protocol::MeteoraPools, + Protocol::MeteoraDlmm, + ] { + let parsed = Protocol::from_str(&protocol.to_string()).unwrap(); + assert_eq!(parsed, protocol); + } + } + + #[test] + fn parses_snake_case_protocol_aliases() { + assert_eq!(Protocol::from_str("raydium_cpmm").unwrap(), Protocol::RaydiumCpmm); + assert_eq!(Protocol::from_str("raydium_clmm").unwrap(), Protocol::RaydiumClmm); + assert_eq!(Protocol::from_str("raydium_amm_v4").unwrap(), Protocol::RaydiumAmmV4); + assert_eq!(Protocol::from_str("meteora_damm_v2").unwrap(), Protocol::MeteoraDammV2); + assert_eq!(Protocol::from_str("orca_whirlpool").unwrap(), Protocol::OrcaWhirlpool); + assert_eq!(Protocol::from_str("meteora_pools").unwrap(), Protocol::MeteoraPools); + assert_eq!(Protocol::from_str("meteora_dlmm").unwrap(), Protocol::MeteoraDlmm); + } +} diff --git a/src/streaming/grpc/connection.rs b/src/streaming/grpc/connection.rs index bfbc8f5..d7360a9 100644 --- a/src/streaming/grpc/connection.rs +++ b/src/streaming/grpc/connection.rs @@ -1,10 +1,10 @@ +use crate::common::AnyResult; +use crate::streaming::common::constants::{ + DEFAULT_CONNECT_TIMEOUT, DEFAULT_MAX_DECODING_MESSAGE_SIZE, DEFAULT_REQUEST_TIMEOUT, +}; use std::time::Duration; use tonic::transport::channel::ClientTlsConfig; use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor}; -use crate::common::AnyResult; -use crate::streaming::common::constants::{ - DEFAULT_CONNECT_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, DEFAULT_MAX_DECODING_MESSAGE_SIZE -}; /// gRPC连接池 - 简化版本 pub struct GrpcConnectionPool { @@ -14,10 +14,7 @@ pub struct GrpcConnectionPool { impl GrpcConnectionPool { pub fn new(endpoint: String, x_token: Option) -> Self { - Self { - endpoint, - x_token, - } + Self { endpoint, x_token } } pub async fn create_connection(&self) -> AnyResult> { diff --git a/src/streaming/grpc/types.rs b/src/streaming/grpc/types.rs index b335290..2d5f9db 100644 --- a/src/streaming/grpc/types.rs +++ b/src/streaming/grpc/types.rs @@ -103,4 +103,3 @@ impl Default for TransactionPretty { } } } - diff --git a/src/streaming/mod.rs b/src/streaming/mod.rs index 8747e2a..a286bd3 100755 --- a/src/streaming/mod.rs +++ b/src/streaming/mod.rs @@ -1,11 +1,20 @@ pub mod common; pub mod event_parser; pub mod grpc; +pub mod rpc_parse; +pub mod sdk_bridge; pub mod shred; pub mod shred_stream; pub mod yellowstone_grpc; pub mod yellowstone_sub_system; +/// Internal `sol-parser-sdk::DexEvent` to streamer event adapter. +pub(crate) mod parser_sdk_bridge; + +pub use rpc_parse::{ + fetch_rpc_transaction_as_streamer_events, fetch_rpc_transaction_as_streamer_events_async, + parse_encoded_rpc_transaction_as_streamer_events, ParseError as RpcParseError, +}; pub use shred::ShredStreamGrpc; pub use yellowstone_grpc::YellowstoneGrpc; pub use yellowstone_sub_system::{SystemEvent, TransferInfo}; diff --git a/src/streaming/parser_sdk_bridge/accounts.rs b/src/streaming/parser_sdk_bridge/accounts.rs new file mode 100644 index 0000000..389a7a6 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/accounts.rs @@ -0,0 +1,133 @@ +//! Account parser bridge: keep SDK account parsing details out of streamer core paths. +use crate::streaming::event_parser::common::filter::{passes_event_type_filter, EventTypeFilter}; +use crate::streaming::event_parser::common::EventType; +use crate::streaming::event_parser::{DexEvent, Protocol}; +use crate::streaming::grpc::AccountPretty; +use sol_parser_sdk::grpc::types::{ + EventType as SdkGrpcEventType, EventTypeFilter as SdkGrpcEventTypeFilter, +}; + +use super::convert_parser_event; +use super::filter::event_matches_protocol; + +pub(crate) enum AccountParseResult { + Event(DexEvent), + Filtered, + Unsupported, +} + +// SDK account event omits the streamer wrapper fields and this byte; recover them from raw data. +const PUMPFUN_GLOBAL_CASHBACK_OFFSET: usize = 8 + 764; + +pub(crate) fn parse_account_event( + account: &AccountPretty, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Option { + match parse_account_event_for_streamer(account, protocols, event_type_filter) { + AccountParseResult::Event(event) => Some(event), + AccountParseResult::Filtered | AccountParseResult::Unsupported => None, + } +} + +pub(crate) fn parse_account_event_for_streamer( + account: &AccountPretty, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> AccountParseResult { + let sdk_account = sol_parser_sdk::accounts::AccountData { + pubkey: account.pubkey, + executable: account.executable, + lamports: account.lamports, + owner: account.owner, + rent_epoch: account.rent_epoch, + data: account.data.clone(), + }; + let sdk_metadata = sol_parser_sdk::core::events::EventMetadata { + signature: account.signature, + slot: account.slot, + tx_index: 0, + block_time_us: 0, + grpc_recv_us: account.recv_us, + recent_blockhash: None, + }; + let sdk_parse_filter = build_sdk_account_event_filter(event_type_filter); + let sdk_event = sol_parser_sdk::accounts::parse_account_unified( + &sdk_account, + sdk_metadata, + Some(&sdk_parse_filter), + ); + + let Some(sdk_event) = sdk_event else { + return AccountParseResult::Unsupported; + }; + + let Some(mut event) = convert_parser_event(sdk_event, None, account.recv_us) else { + return AccountParseResult::Unsupported; + }; + if !event_matches_protocol(protocols, &event) + || !passes_event_type_filter(event_type_filter, &event) + { + return AccountParseResult::Filtered; + } + normalize_account_event(&mut event, account); + AccountParseResult::Event(event) +} + +fn build_sdk_account_event_filter(filter: Option<&EventTypeFilter>) -> SdkGrpcEventTypeFilter { + let Some(f) = filter else { + return SdkGrpcEventTypeFilter::exclude_types(Vec::new()); + }; + + if f.include.is_empty() { + let mut raw = Vec::new(); + for et in &f.exclude { + raw.extend(streamer_account_event_to_sdk_types(et)); + } + dedup_sdk_grpc_event_types(&mut raw); + return SdkGrpcEventTypeFilter::exclude_types(raw); + } + + let mut raw = Vec::new(); + for et in &f.include { + raw.extend(streamer_account_event_to_sdk_types(et)); + } + dedup_sdk_grpc_event_types(&mut raw); + SdkGrpcEventTypeFilter::include_only(raw) +} + +fn streamer_account_event_to_sdk_types(t: &EventType) -> Vec { + match t { + EventType::TokenAccount | EventType::TokenInfo => vec![SdkGrpcEventType::TokenAccount], + EventType::NonceAccount => vec![SdkGrpcEventType::NonceAccount], + EventType::AccountPumpFunGlobal => vec![SdkGrpcEventType::AccountPumpFunGlobal], + EventType::AccountPumpSwapGlobalConfig => { + vec![SdkGrpcEventType::AccountPumpSwapGlobalConfig] + } + EventType::AccountPumpSwapPool => vec![SdkGrpcEventType::AccountPumpSwapPool], + _ => Vec::new(), + } +} + +fn dedup_sdk_grpc_event_types(v: &mut Vec) { + let mut i = 0; + while i < v.len() { + if v[..i].contains(&v[i]) { + v.remove(i); + } else { + i += 1; + } + } +} + +fn normalize_account_event(event: &mut DexEvent, account: &AccountPretty) { + if let DexEvent::PumpFunGlobalAccountEvent(e) = event { + e.executable = account.executable; + e.lamports = account.lamports; + e.owner = account.owner; + e.rent_epoch = account.rent_epoch; + if let Some(flag) = account.data.get(PUMPFUN_GLOBAL_CASHBACK_OFFSET) { + e.global.is_cashback_enabled = *flag != 0; + } + } +} diff --git a/src/streaming/parser_sdk_bridge/adapt.rs b/src/streaming/parser_sdk_bridge/adapt.rs new file mode 100644 index 0000000..a076d41 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/adapt.rs @@ -0,0 +1,54 @@ +//! Block time and `recv_us` alignment with streamer [`EventMetadata`]. +use crate::streaming::event_parser::common::types::{EventType, ProtocolType}; +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::DexEvent; +use prost_types::Timestamp; +use solana_sdk::pubkey::Pubkey; + +/// Build a prost `Timestamp` from streamer `EventMetadata`. +pub(crate) fn block_timestamp_from_stream_meta(meta: &EventMetadata) -> Timestamp { + let sec = meta.block_time; + let rem_ms = meta.block_time_ms.saturating_sub(sec.saturating_mul(1000)); + let nanos = rem_ms.saturating_mul(1_000_000).min(999_999_999) as i32; + Timestamp { seconds: sec, nanos } +} + +/// Preserve outer parser instruction indexes and optional blockhash when the SDK metadata has no +/// equivalent context. +pub(crate) fn fuse_streamer_ix_ctx(mut ev: DexEvent, sm: &EventMetadata) -> DexEvent { + let m = ev.metadata_mut(); + m.outer_index = sm.outer_index; + m.inner_index = sm.inner_index; + if sm.recent_blockhash.is_some() { + m.recent_blockhash = sm.recent_blockhash.clone(); + } + ev +} + +pub(crate) fn adapt_pm( + pm: sol_parser_sdk::core::events::EventMetadata, + bt: Option<&Timestamp>, + recv_wall_us: i64, + proto: ProtocolType, + et: EventType, + program_id: Pubkey, +) -> EventMetadata { + let block_time_sec = bt.map(|t| t.seconds).unwrap_or_else(|| pm.block_time_us / 1_000_000); + let block_time_ms = bt + .map(|t| t.seconds * 1000 + t.nanos as i64 / 1_000_000) + .unwrap_or(pm.block_time_us / 1000); + EventMetadata::new( + pm.signature, + pm.slot, + block_time_sec, + block_time_ms, + proto, + et, + program_id, + 0, + None, + recv_wall_us, + Some(pm.tx_index), + pm.recent_blockhash.clone(), + ) +} diff --git a/src/streaming/parser_sdk_bridge/bonk_accounts.rs b/src/streaming/parser_sdk_bridge/bonk_accounts.rs new file mode 100644 index 0000000..53b69c5 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/bonk_accounts.rs @@ -0,0 +1,220 @@ +//! Bonk plus Token / Nonce / PumpSwap account event mapping. +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::core::account_event_parser::{ + NonceAccountEvent, TokenAccountEvent, TokenInfoEvent, +}; +use crate::streaming::event_parser::protocols::bonk::events::{ + BonkMigrateToAmmEvent, BonkPoolCreateEvent, BonkTradeEvent, +}; +use crate::streaming::event_parser::protocols::bonk::types::{ + CurveParams, MintParams, PoolStatus, TradeDirection as BonkTradeDirection, VestingParams, +}; +use crate::streaming::event_parser::protocols::pumpswap::events::{ + PumpSwapGlobalConfigAccountEvent, PumpSwapPoolAccountEvent, +}; +use crate::streaming::event_parser::protocols::pumpswap::types::{GlobalConfig, Pool}; +use sol_parser_sdk::core::events::{ + BonkTradeEvent as PbBonkTrade, TradeDirection as PbBonkTradeDirection, +}; +use solana_sdk::pubkey::Pubkey; + +#[inline] +pub(crate) fn pb_bonk_trade_direction(d: PbBonkTradeDirection) -> BonkTradeDirection { + match d { + PbBonkTradeDirection::Buy => BonkTradeDirection::Buy, + PbBonkTradeDirection::Sell => BonkTradeDirection::Sell, + } +} + +/// Align SDK Bonk trades with the four native Bonk trade instruction variants. +#[inline] +pub(crate) fn sdk_bonk_trade_event_type( + b: &PbBonkTrade, +) -> crate::streaming::event_parser::common::types::EventType { + use crate::streaming::event_parser::common::types::EventType; + match (&b.trade_direction, b.exact_in) { + (&PbBonkTradeDirection::Buy, true) => EventType::BonkBuyExactIn, + (&PbBonkTradeDirection::Buy, false) => EventType::BonkBuyExactOut, + (&PbBonkTradeDirection::Sell, true) => EventType::BonkSellExactIn, + (&PbBonkTradeDirection::Sell, false) => EventType::BonkSellExactOut, + } +} + +/// SDK Bonk trade events do not expose reserves or fee rates; keep those fields at streamer +/// defaults. +pub(crate) fn bonk_trade_from_parser( + b: sol_parser_sdk::core::events::BonkTradeEvent, + meta: EventMetadata, +) -> BonkTradeEvent { + BonkTradeEvent { + metadata: meta, + pool_state: b.pool_state, + total_base_sell: 0, + virtual_base: 0, + virtual_quote: 0, + real_base_before: 0, + real_quote_before: 0, + real_base_after: 0, + real_quote_after: 0, + amount_in: b.amount_in, + amount_out: b.amount_out, + protocol_fee: 0, + platform_fee: 0, + creator_fee: 0, + share_fee: 0, + trade_direction: pb_bonk_trade_direction(b.trade_direction), + pool_status: PoolStatus::Trade, + exact_in: b.exact_in, + payer: b.user, + ..Default::default() + } +} + +pub(crate) fn bonk_pool_create_from_parser( + p: sol_parser_sdk::core::events::BonkPoolCreateEvent, + meta: EventMetadata, +) -> BonkPoolCreateEvent { + BonkPoolCreateEvent { + metadata: meta, + pool_state: p.pool_state, + creator: p.creator, + config: Pubkey::default(), + base_mint_param: MintParams { + decimals: p.base_mint_param.decimals, + name: p.base_mint_param.name.clone(), + symbol: p.base_mint_param.symbol.clone(), + uri: p.base_mint_param.uri.clone(), + }, + curve_param: CurveParams::default(), + vesting_param: VestingParams::default(), + amm_fee_on: None, + ..Default::default() + } +} + +pub(crate) fn bonk_migrate_to_amm_from_parser( + m: sol_parser_sdk::core::events::BonkMigrateAmmEvent, + meta: EventMetadata, +) -> BonkMigrateToAmmEvent { + BonkMigrateToAmmEvent { + metadata: meta, + payer: m.user, + pool_state: m.old_pool, + amm_pool: m.new_pool, + ..Default::default() + } +} + +pub(crate) fn token_account_from_parser( + e: sol_parser_sdk::core::events::TokenAccountEvent, + meta: EventMetadata, +) -> TokenAccountEvent { + TokenAccountEvent { + metadata: meta, + pubkey: e.pubkey, + executable: e.executable, + lamports: e.lamports, + owner: e.owner, + rent_epoch: e.rent_epoch, + amount: e.amount, + token_owner: e.token_owner, + } +} + +pub(crate) fn token_info_from_parser( + e: sol_parser_sdk::core::events::TokenInfoEvent, + meta: EventMetadata, +) -> TokenInfoEvent { + TokenInfoEvent { + metadata: meta, + pubkey: e.pubkey, + executable: e.executable, + lamports: e.lamports, + owner: e.owner, + rent_epoch: e.rent_epoch, + supply: e.supply, + decimals: e.decimals, + } +} + +pub(crate) fn nonce_account_from_parser( + e: sol_parser_sdk::core::events::NonceAccountEvent, + meta: EventMetadata, +) -> NonceAccountEvent { + NonceAccountEvent { + metadata: meta, + pubkey: e.pubkey, + executable: e.executable, + lamports: e.lamports, + owner: e.owner, + rent_epoch: e.rent_epoch, + nonce: e.nonce, + authority: e.authority, + } +} + +pub(crate) fn pumpswap_global_config_from_pb( + g: sol_parser_sdk::core::events::PumpSwapGlobalConfig, +) -> GlobalConfig { + GlobalConfig { + admin: g.admin, + lp_fee_basis_points: g.lp_fee_basis_points, + protocol_fee_basis_points: g.protocol_fee_basis_points, + disable_flags: g.disable_flags, + protocol_fee_recipients: g.protocol_fee_recipients, + coin_creator_fee_basis_points: g.coin_creator_fee_basis_points, + admin_set_coin_creator_authority: g.admin_set_coin_creator_authority, + whitelist_pda: g.whitelist_pda, + reserved_fee_recipient: g.reserved_fee_recipient, + mayhem_mode_enabled: g.mayhem_mode_enabled, + reserved_fee_recipients: g.reserved_fee_recipients, + } +} + +pub(crate) fn pumpswap_pool_from_pb(p: sol_parser_sdk::core::events::PumpSwapPool) -> Pool { + Pool { + pool_bump: p.pool_bump, + index: p.index, + creator: p.creator, + base_mint: p.base_mint, + quote_mint: p.quote_mint, + lp_mint: p.lp_mint, + pool_base_token_account: p.pool_base_token_account, + pool_quote_token_account: p.pool_quote_token_account, + lp_supply: p.lp_supply, + coin_creator: p.coin_creator, + is_mayhem_mode: p.is_mayhem_mode, + is_cashback_coin: p.is_cashback_coin, + reserved: [0u8; 7], + } +} + +pub(crate) fn pumpswap_global_config_account_from_parser( + e: sol_parser_sdk::core::events::PumpSwapGlobalConfigAccountEvent, + meta: EventMetadata, +) -> PumpSwapGlobalConfigAccountEvent { + PumpSwapGlobalConfigAccountEvent { + metadata: meta, + pubkey: e.pubkey, + executable: e.executable, + lamports: e.lamports, + owner: e.owner, + rent_epoch: e.rent_epoch, + global_config: pumpswap_global_config_from_pb(e.global_config), + } +} + +pub(crate) fn pumpswap_pool_account_from_parser( + e: sol_parser_sdk::core::events::PumpSwapPoolAccountEvent, + meta: EventMetadata, +) -> PumpSwapPoolAccountEvent { + PumpSwapPoolAccountEvent { + metadata: meta, + pubkey: e.pubkey, + executable: e.executable, + lamports: e.lamports, + owner: e.owner, + rent_epoch: e.rent_epoch, + pool: pumpswap_pool_from_pb(e.pool), + } +} diff --git a/src/streaming/parser_sdk_bridge/convert.rs b/src/streaming/parser_sdk_bridge/convert.rs new file mode 100644 index 0000000..88bbcf6 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/convert.rs @@ -0,0 +1,917 @@ +//! Main dispatch from [`sol_parser_sdk::DexEvent`] to streamer [`DexEvent`]. +use crate::streaming::event_parser::common::filter::passes_event_type_filter; +use crate::streaming::event_parser::common::types::{EventType, ProtocolType}; +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::block::block_meta_event::BlockMetaEvent; +use crate::streaming::event_parser::protocols::sol_parser_forward::events::ParserSdkErrorEvent; +use crate::streaming::event_parser::{DexEvent, Protocol}; +use prost_types::Timestamp; +use sol_parser_sdk::DexEvent as PbDexEvent; +use solana_sdk::pubkey::Pubkey; + +use super::adapt::adapt_pm; +use super::bonk_accounts::*; +use super::filter::event_matches_protocol; +use super::forward_pb::*; +use super::program_ids::*; +use super::pump_pumpswap::*; +use super::raydium_and_damm::*; + +pub(crate) fn convert_parser_event( + ev: PbDexEvent, + bt: Option<&Timestamp>, + recv_wall_us: i64, +) -> Option { + match ev { + PbDexEvent::PumpFunTrade(t) => Some(pumpfun_trade_from_parser(t, bt, recv_wall_us)), + PbDexEvent::PumpFunBuy(t) => Some(pumpfun_trade_from_parser_with_event_type( + t, + bt, + recv_wall_us, + EventType::PumpFunBuy, + )), + PbDexEvent::PumpFunSell(t) => Some(pumpfun_trade_from_parser_with_event_type( + t, + bt, + recv_wall_us, + EventType::PumpFunSell, + )), + PbDexEvent::PumpFunBuyExactSolIn(t) => Some(pumpfun_trade_from_parser_with_event_type( + t, + bt, + recv_wall_us, + EventType::PumpFunBuyExactSolIn, + )), + + PbDexEvent::PumpFunCreate(c) => { + let meta = adapt_pm( + c.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFunCreateToken, + pump_program(), + ); + Some(DexEvent::PumpFunCreateTokenEvent(pumpfun_create_token_from_parser(c, meta))) + } + PbDexEvent::PumpFunCreateV2(c) => { + let meta = adapt_pm( + c.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFunCreateV2Token, + pump_program(), + ); + Some(DexEvent::PumpFunCreateV2TokenEvent(pumpfun_create_v2_from_parser(c, meta))) + } + PbDexEvent::PumpFunMigrate(m) => { + let meta = adapt_pm( + m.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFunMigrate, + pump_program(), + ); + Some(DexEvent::PumpFunMigrateEvent(pumpfun_migrate_from_parser(m, meta))) + } + PbDexEvent::PumpFeesCreateFeeSharingConfig(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesCreateFeeSharingConfig, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesCreateFeeSharingConfigEvent( + pump_fees_create_sharing_config_from_parser(e, meta), + )) + } + PbDexEvent::PumpFeesInitializeFeeConfig(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesInitializeFeeConfig, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesInitializeFeeConfigEvent( + pump_fees_initialize_fee_config_from_parser(e, meta), + )) + } + PbDexEvent::PumpFeesResetFeeSharingConfig(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesResetFeeSharingConfig, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesResetFeeSharingConfigEvent( + pump_fees_reset_sharing_config_from_parser(e, meta), + )) + } + PbDexEvent::PumpFeesRevokeFeeSharingAuthority(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesRevokeFeeSharingAuthority, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesRevokeFeeSharingAuthorityEvent( + pump_fees_revoke_authority_from_parser(e, meta), + )) + } + PbDexEvent::PumpFeesTransferFeeSharingAuthority(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesTransferFeeSharingAuthority, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesTransferFeeSharingAuthorityEvent( + pump_fees_transfer_authority_from_parser(e, meta), + )) + } + PbDexEvent::PumpFeesUpdateAdmin(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesUpdateAdmin, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesUpdateAdminEvent(pump_fees_update_admin_from_parser(e, meta))) + } + PbDexEvent::PumpFeesUpdateFeeConfig(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesUpdateFeeConfig, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesUpdateFeeConfigEvent(pump_fees_update_fee_config_from_parser( + e, meta, + ))) + } + PbDexEvent::PumpFeesUpdateFeeShares(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesUpdateFeeShares, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesUpdateFeeSharesEvent(pump_fees_update_fee_shares_from_parser( + e, meta, + ))) + } + PbDexEvent::PumpFeesUpsertFeeTiers(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFeesUpsertFeeTiers, + pump_fees_program(), + ); + Some(DexEvent::PumpFeesUpsertFeeTiersEvent(pump_fees_upsert_fee_tiers_from_parser( + e, meta, + ))) + } + PbDexEvent::PumpFunMigrateBondingCurveCreator(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::PumpFunMigrateBondingCurveCreator, + pump_program(), + ); + Some(DexEvent::PumpFunMigrateBondingCurveCreatorEvent( + pumpfun_migrate_bonding_creator_from_parser(e, meta), + )) + } + PbDexEvent::PumpFunGlobalAccount(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpFun, + EventType::AccountPumpFunGlobal, + pump_program(), + ); + Some(DexEvent::PumpFunGlobalAccountEvent(pumpfun_global_account_from_parser(e, meta))) + } + + PbDexEvent::PumpSwapTrade(t) => pumpswap_trade_from_parser(t, bt, recv_wall_us), + PbDexEvent::PumpSwapBuy(b) => { + let meta = adapt_pm( + b.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::PumpSwapBuy, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapBuyEvent(pumpswap_buy_full_from_parser(b, meta))) + } + PbDexEvent::PumpSwapSell(s) => { + let meta = adapt_pm( + s.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::PumpSwapSell, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapSellEvent(pumpswap_sell_full_from_parser(s, meta))) + } + PbDexEvent::PumpSwapCreatePool(c) => { + let meta = adapt_pm( + c.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::PumpSwapCreatePool, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapCreatePoolEvent(pumpswap_create_pool_from_parser(c, meta))) + } + PbDexEvent::PumpSwapLiquidityAdded(a) => { + let meta = adapt_pm( + a.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::PumpSwapDeposit, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapDepositEvent(pumpswap_liquidity_added_to_deposit(a, meta))) + } + PbDexEvent::PumpSwapLiquidityRemoved(r) => { + let meta = adapt_pm( + r.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::PumpSwapWithdraw, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapWithdrawEvent(pumpswap_liquidity_removed_to_withdraw(r, meta))) + } + + PbDexEvent::BonkTrade(b) => { + let et = sdk_bonk_trade_event_type(&b); + let meta = adapt_pm( + b.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Bonk, + et, + bonk_program(), + ); + Some(DexEvent::BonkTradeEvent(bonk_trade_from_parser(b, meta))) + } + PbDexEvent::BonkPoolCreate(p) => { + let meta = adapt_pm( + p.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Bonk, + EventType::BonkInitialize, + bonk_program(), + ); + Some(DexEvent::BonkPoolCreateEvent(bonk_pool_create_from_parser(p, meta))) + } + PbDexEvent::BonkMigrateAmm(m) => { + let meta = adapt_pm( + m.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Bonk, + EventType::BonkMigrateToAmm, + bonk_program(), + ); + Some(DexEvent::BonkMigrateToAmmEvent(bonk_migrate_to_amm_from_parser(m, meta))) + } + + PbDexEvent::RaydiumCpmmSwap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumCpmm, + EventType::RaydiumCpmmSwapBaseInput, + raydium_cpmm_program(), + ); + Some(DexEvent::RaydiumCpmmSwapEvent(raydium_cpmm_swap_from_parser(e, meta))) + } + PbDexEvent::RaydiumCpmmDeposit(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumCpmm, + EventType::RaydiumCpmmDeposit, + raydium_cpmm_program(), + ); + Some(DexEvent::RaydiumCpmmDepositEvent(raydium_cpmm_deposit_from_parser(e, meta))) + } + PbDexEvent::RaydiumCpmmWithdraw(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumCpmm, + EventType::RaydiumCpmmWithdraw, + raydium_cpmm_program(), + ); + Some(DexEvent::RaydiumCpmmWithdrawEvent(raydium_cpmm_withdraw_from_parser(e, meta))) + } + PbDexEvent::RaydiumCpmmInitialize(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumCpmm, + EventType::RaydiumCpmmInitialize, + raydium_cpmm_program(), + ); + Some(DexEvent::RaydiumCpmmInitializeEvent(raydium_cpmm_initialize_from_parser(e, meta))) + } + + PbDexEvent::RaydiumClmmSwap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmSwap, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmSwapEvent(raydium_clmm_swap_from_parser(e, meta))) + } + PbDexEvent::RaydiumClmmCreatePool(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmCreatePool, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmCreatePoolEvent(raydium_clmm_create_pool_from_parser( + e, meta, + ))) + } + PbDexEvent::RaydiumClmmOpenPosition(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmOpenPositionV2, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmOpenPositionV2Event( + raydium_clmm_open_position_v2_from_parser(e, meta), + )) + } + PbDexEvent::RaydiumClmmOpenPositionWithTokenExtNft(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmOpenPositionWithToken22Nft, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmOpenPositionWithToken22NftEvent( + raydium_clmm_open_position_token22_from_parser(e, meta), + )) + } + PbDexEvent::RaydiumClmmClosePosition(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmClosePosition, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmClosePositionEvent(raydium_clmm_close_position_from_parser( + e, meta, + ))) + } + PbDexEvent::RaydiumClmmIncreaseLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmIncreaseLiquidityV2, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmIncreaseLiquidityV2Event( + raydium_clmm_increase_liquidity_v2_from_parser(e, meta), + )) + } + PbDexEvent::RaydiumClmmDecreaseLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmDecreaseLiquidityV2, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmDecreaseLiquidityV2Event( + raydium_clmm_decrease_liquidity_v2_from_parser(e, meta), + )) + } + PbDexEvent::RaydiumClmmCollectFee(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumClmm, + EventType::RaydiumClmmCollectFee, + raydium_clmm_program(), + ); + Some(DexEvent::RaydiumClmmCollectFeeEvent(raydium_clmm_collect_fee_from_parser( + e, meta, + ))) + } + + PbDexEvent::RaydiumAmmV4Swap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumAmmV4, + EventType::RaydiumAmmV4SwapBaseIn, + raydium_amm_v4_program(), + ); + Some(DexEvent::RaydiumAmmV4SwapEvent(raydium_amm_v4_swap_from_parser(e, meta))) + } + PbDexEvent::RaydiumAmmV4Deposit(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumAmmV4, + EventType::RaydiumAmmV4Deposit, + raydium_amm_v4_program(), + ); + Some(DexEvent::RaydiumAmmV4DepositEvent(raydium_amm_v4_deposit_from_parser(e, meta))) + } + PbDexEvent::RaydiumAmmV4Withdraw(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumAmmV4, + EventType::RaydiumAmmV4Withdraw, + raydium_amm_v4_program(), + ); + Some(DexEvent::RaydiumAmmV4WithdrawEvent(raydium_amm_v4_withdraw_from_parser(e, meta))) + } + PbDexEvent::RaydiumAmmV4WithdrawPnl(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumAmmV4, + EventType::RaydiumAmmV4WithdrawPnl, + raydium_amm_v4_program(), + ); + Some(DexEvent::RaydiumAmmV4WithdrawPnlEvent(raydium_amm_v4_withdraw_pnl_from_parser( + e, meta, + ))) + } + PbDexEvent::RaydiumAmmV4Initialize2(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::RaydiumAmmV4, + EventType::RaydiumAmmV4Initialize2, + raydium_amm_v4_program(), + ); + Some(DexEvent::RaydiumAmmV4Initialize2Event(raydium_amm_v4_initialize2_from_parser( + e, meta, + ))) + } + + PbDexEvent::MeteoraDammV2Swap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDammV2, + EventType::MeteoraDammV2Swap, + meteora_damm_program(), + ); + Some(DexEvent::MeteoraDammV2SwapEvent(meteora_damm_v2_swap_from_parser(e, meta))) + } + PbDexEvent::MeteoraDammV2AddLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDammV2, + EventType::MeteoraDammV2AddLiquidity, + meteora_damm_program(), + ); + Some(DexEvent::MeteoraDammV2AddLiquidityEvent(meteora_damm_v2_add_liquidity_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraDammV2RemoveLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDammV2, + EventType::MeteoraDammV2RemoveLiquidity, + meteora_damm_program(), + ); + Some(DexEvent::MeteoraDammV2RemoveLiquidityEvent( + meteora_damm_v2_remove_liquidity_from_pb(e, meta), + )) + } + PbDexEvent::MeteoraDammV2CreatePosition(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDammV2, + EventType::MeteoraDammV2CreatePosition, + meteora_damm_program(), + ); + Some(DexEvent::MeteoraDammV2CreatePositionEvent( + meteora_damm_v2_create_position_from_pb(e, meta), + )) + } + PbDexEvent::MeteoraDammV2ClosePosition(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDammV2, + EventType::MeteoraDammV2ClosePosition, + meteora_damm_program(), + ); + Some(DexEvent::MeteoraDammV2ClosePositionEvent(meteora_damm_v2_close_position_from_pb( + e, meta, + ))) + } + + PbDexEvent::TokenAccount(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Common, + EventType::TokenAccount, + Pubkey::default(), + ); + Some(DexEvent::TokenAccountEvent(token_account_from_parser(e, meta))) + } + PbDexEvent::TokenInfo(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Common, + EventType::TokenInfo, + Pubkey::default(), + ); + Some(DexEvent::TokenInfoEvent(token_info_from_parser(e, meta))) + } + PbDexEvent::NonceAccount(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Common, + EventType::NonceAccount, + Pubkey::default(), + ); + Some(DexEvent::NonceAccountEvent(nonce_account_from_parser(e, meta))) + } + PbDexEvent::PumpSwapGlobalConfigAccount(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::AccountPumpSwapGlobalConfig, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapGlobalConfigAccountEvent( + pumpswap_global_config_account_from_parser(e, meta), + )) + } + PbDexEvent::PumpSwapPoolAccount(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::PumpSwap, + EventType::AccountPumpSwapPool, + pumpswap_program(), + ); + Some(DexEvent::PumpSwapPoolAccountEvent(pumpswap_pool_account_from_parser(e, meta))) + } + PbDexEvent::BlockMeta(m) => { + let meta = adapt_pm( + m.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::Common, + EventType::BlockMeta, + Pubkey::default(), + ); + Some(DexEvent::BlockMetaEvent(BlockMetaEvent { + metadata: meta, + slot: m.metadata.slot, + block_hash: m.metadata.recent_blockhash.clone().unwrap_or_default(), + })) + } + + PbDexEvent::Error(msg) => Some(DexEvent::ParserSdkErrorEvent(ParserSdkErrorEvent { + metadata: EventMetadata { + recv_us: recv_wall_us, + protocol: ProtocolType::Common, + event_type: EventType::ParserSdkError, + ..Default::default() + }, + message: msg, + })), + PbDexEvent::OrcaWhirlpoolSwap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::OrcaWhirlpool, + EventType::OrcaWhirlpoolSwap, + orca_whirlpool_program(), + ); + Some(DexEvent::OrcaWhirlpoolSwapEvent(orca_swap_from_pb(e, meta))) + } + PbDexEvent::OrcaWhirlpoolLiquidityIncreased(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::OrcaWhirlpool, + EventType::OrcaWhirlpoolLiquidityIncreased, + orca_whirlpool_program(), + ); + Some(DexEvent::OrcaWhirlpoolLiquidityIncreasedEvent(orca_liquidity_increased_from_pb( + e, meta, + ))) + } + PbDexEvent::OrcaWhirlpoolLiquidityDecreased(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::OrcaWhirlpool, + EventType::OrcaWhirlpoolLiquidityDecreased, + orca_whirlpool_program(), + ); + Some(DexEvent::OrcaWhirlpoolLiquidityDecreasedEvent(orca_liquidity_decreased_from_pb( + e, meta, + ))) + } + PbDexEvent::OrcaWhirlpoolPoolInitialized(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::OrcaWhirlpool, + EventType::OrcaWhirlpoolPoolInitialized, + orca_whirlpool_program(), + ); + Some(DexEvent::OrcaWhirlpoolPoolInitializedEvent(orca_pool_initialized_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraPoolsSwap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraPools, + EventType::MeteoraPoolsSwap, + meteora_pools_program(), + ); + Some(DexEvent::MeteoraPoolsSwapEvent(meteora_pools_swap_from_pb(e, meta))) + } + PbDexEvent::MeteoraPoolsAddLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraPools, + EventType::MeteoraPoolsAddLiquidity, + meteora_pools_program(), + ); + Some(DexEvent::MeteoraPoolsAddLiquidityEvent(meteora_pools_add_liquidity_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraPoolsRemoveLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraPools, + EventType::MeteoraPoolsRemoveLiquidity, + meteora_pools_program(), + ); + Some(DexEvent::MeteoraPoolsRemoveLiquidityEvent( + meteora_pools_remove_liquidity_from_pb(e, meta), + )) + } + PbDexEvent::MeteoraPoolsBootstrapLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraPools, + EventType::MeteoraPoolsBootstrapLiquidity, + meteora_pools_program(), + ); + Some(DexEvent::MeteoraPoolsBootstrapLiquidityEvent(meteora_pools_bootstrap_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraPoolsPoolCreated(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraPools, + EventType::MeteoraPoolsPoolCreated, + meteora_pools_program(), + ); + Some(DexEvent::MeteoraPoolsPoolCreatedEvent(meteora_pools_pool_created_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraPoolsSetPoolFees(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraPools, + EventType::MeteoraPoolsSetPoolFees, + meteora_pools_program(), + ); + Some(DexEvent::MeteoraPoolsSetPoolFeesEvent(meteora_pools_set_fees_from_pb(e, meta))) + } + PbDexEvent::MeteoraDlmmSwap(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmSwap, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmSwapEvent(meteora_dlmm_swap_from_pb(e, meta))) + } + PbDexEvent::MeteoraDlmmAddLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmAddLiquidity, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmAddLiquidityEvent(meteora_dlmm_add_liquidity_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraDlmmRemoveLiquidity(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmRemoveLiquidity, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmRemoveLiquidityEvent(meteora_dlmm_remove_liquidity_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraDlmmInitializePool(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmInitializePool, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmInitializePoolEvent(meteora_dlmm_init_pool_from_pb(e, meta))) + } + PbDexEvent::MeteoraDlmmInitializeBinArray(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmInitializeBinArray, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmInitializeBinArrayEvent(meteora_dlmm_init_bin_array_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraDlmmCreatePosition(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmCreatePosition, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmCreatePositionEvent(meteora_dlmm_create_position_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraDlmmClosePosition(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmClosePosition, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmClosePositionEvent(meteora_dlmm_close_position_from_pb( + e, meta, + ))) + } + PbDexEvent::MeteoraDlmmClaimFee(e) => { + let meta = adapt_pm( + e.metadata.clone(), + bt, + recv_wall_us, + ProtocolType::MeteoraDlmm, + EventType::MeteoraDlmmClaimFee, + meteora_dlmm_program(), + ); + Some(DexEvent::MeteoraDlmmClaimFeeEvent(meteora_dlmm_claim_fee_from_pb(e, meta))) + } + } +} + +pub(crate) fn adapt_parser_events_list( + pb: Vec, + bt: Option<&Timestamp>, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&crate::streaming::event_parser::common::filter::EventTypeFilter>, +) -> Vec { + pb.into_iter() + .filter_map(|e| adapt_parser_event(e, bt, recv_wall_us, protocols, event_type_filter)) + .collect() +} + +pub(crate) fn adapt_parser_event( + pb: PbDexEvent, + bt: Option<&Timestamp>, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&crate::streaming::event_parser::common::filter::EventTypeFilter>, +) -> Option { + let ev = convert_parser_event(pb, bt, recv_wall_us)?; + if !event_matches_protocol(protocols, &ev) { + return None; + } + if !passes_event_type_filter(event_type_filter, &ev) { + return None; + } + Some(ev) +} diff --git a/src/streaming/parser_sdk_bridge/filter.rs b/src/streaming/parser_sdk_bridge/filter.rs new file mode 100644 index 0000000..b229288 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/filter.rs @@ -0,0 +1,116 @@ +//! Matching between subscribed [`Protocol`] values and streamer [`DexEvent`] variants. +use crate::streaming::event_parser::{DexEvent, Protocol}; + +pub(crate) fn event_matches_protocol(protocols: &[Protocol], ev: &DexEvent) -> bool { + if is_protocol_independent_event(ev) { + return true; + } + if protocols.is_empty() { + return true; + } + protocols.iter().any(|p| protocol_matches_event(p, ev)) +} + +#[inline] +fn is_protocol_independent_event(ev: &DexEvent) -> bool { + matches!( + ev, + DexEvent::TokenAccountEvent(_) + | DexEvent::TokenInfoEvent(_) + | DexEvent::NonceAccountEvent(_) + | DexEvent::BlockMetaEvent(_) + | DexEvent::SetComputeUnitLimitEvent(_) + | DexEvent::SetComputeUnitPriceEvent(_) + | DexEvent::ParserSdkErrorEvent(_) + ) +} + +fn protocol_matches_event(p: &Protocol, ev: &DexEvent) -> bool { + match (p, ev) { + (Protocol::PumpFun, DexEvent::PumpFunCreateTokenEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFunCreateV2TokenEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFunTradeEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFunMigrateEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesCreateFeeSharingConfigEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesInitializeFeeConfigEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesResetFeeSharingConfigEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesRevokeFeeSharingAuthorityEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesTransferFeeSharingAuthorityEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesUpdateAdminEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesUpdateFeeConfigEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesUpdateFeeSharesEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFeesUpsertFeeTiersEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFunMigrateBondingCurveCreatorEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFunBondingCurveAccountEvent(_)) + | (Protocol::PumpFun, DexEvent::PumpFunGlobalAccountEvent(_)) => true, + (Protocol::PumpSwap, DexEvent::PumpSwapBuyEvent(_)) + | (Protocol::PumpSwap, DexEvent::PumpSwapSellEvent(_)) + | (Protocol::PumpSwap, DexEvent::PumpSwapCreatePoolEvent(_)) + | (Protocol::PumpSwap, DexEvent::PumpSwapDepositEvent(_)) + | (Protocol::PumpSwap, DexEvent::PumpSwapWithdrawEvent(_)) + | (Protocol::PumpSwap, DexEvent::PumpSwapGlobalConfigAccountEvent(_)) + | (Protocol::PumpSwap, DexEvent::PumpSwapPoolAccountEvent(_)) => true, + (Protocol::Bonk, DexEvent::BonkTradeEvent(_)) + | (Protocol::Bonk, DexEvent::BonkPoolCreateEvent(_)) + | (Protocol::Bonk, DexEvent::BonkMigrateToAmmEvent(_)) + | (Protocol::Bonk, DexEvent::BonkMigrateToCpswapEvent(_)) + | (Protocol::Bonk, DexEvent::BonkPoolStateAccountEvent(_)) + | (Protocol::Bonk, DexEvent::BonkGlobalConfigAccountEvent(_)) + | (Protocol::Bonk, DexEvent::BonkPlatformConfigAccountEvent(_)) => true, + (Protocol::RaydiumCpmm, DexEvent::RaydiumCpmmSwapEvent(_)) + | (Protocol::RaydiumCpmm, DexEvent::RaydiumCpmmDepositEvent(_)) + | (Protocol::RaydiumCpmm, DexEvent::RaydiumCpmmWithdrawEvent(_)) + | (Protocol::RaydiumCpmm, DexEvent::RaydiumCpmmInitializeEvent(_)) + | (Protocol::RaydiumCpmm, DexEvent::RaydiumCpmmAmmConfigAccountEvent(_)) + | (Protocol::RaydiumCpmm, DexEvent::RaydiumCpmmPoolStateAccountEvent(_)) => true, + (Protocol::RaydiumClmm, DexEvent::RaydiumClmmSwapEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmSwapV2Event(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmClosePositionEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmIncreaseLiquidityV2Event(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmDecreaseLiquidityV2Event(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmCollectFeeEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmCreatePoolEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmOpenPositionWithToken22NftEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmOpenPositionV2Event(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmAmmConfigAccountEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmPoolStateAccountEvent(_)) + | (Protocol::RaydiumClmm, DexEvent::RaydiumClmmTickArrayStateAccountEvent(_)) => true, + (Protocol::RaydiumAmmV4, DexEvent::RaydiumAmmV4SwapEvent(_)) + | (Protocol::RaydiumAmmV4, DexEvent::RaydiumAmmV4DepositEvent(_)) + | (Protocol::RaydiumAmmV4, DexEvent::RaydiumAmmV4WithdrawEvent(_)) + | (Protocol::RaydiumAmmV4, DexEvent::RaydiumAmmV4WithdrawPnlEvent(_)) + | (Protocol::RaydiumAmmV4, DexEvent::RaydiumAmmV4Initialize2Event(_)) + | (Protocol::RaydiumAmmV4, DexEvent::RaydiumAmmV4AmmInfoAccountEvent(_)) => true, + (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2SwapEvent(_)) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2Swap2Event(_)) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2InitializePoolEvent(_)) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2InitializeCustomizablePoolEvent(_)) + | ( + Protocol::MeteoraDammV2, + DexEvent::MeteoraDammV2InitializePoolWithDynamicConfigEvent(_), + ) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2AddLiquidityEvent(_)) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2RemoveLiquidityEvent(_)) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2CreatePositionEvent(_)) + | (Protocol::MeteoraDammV2, DexEvent::MeteoraDammV2ClosePositionEvent(_)) => true, + (Protocol::OrcaWhirlpool, DexEvent::OrcaWhirlpoolSwapEvent(_)) + | (Protocol::OrcaWhirlpool, DexEvent::OrcaWhirlpoolLiquidityIncreasedEvent(_)) + | (Protocol::OrcaWhirlpool, DexEvent::OrcaWhirlpoolLiquidityDecreasedEvent(_)) + | (Protocol::OrcaWhirlpool, DexEvent::OrcaWhirlpoolPoolInitializedEvent(_)) => true, + (Protocol::MeteoraPools, DexEvent::MeteoraPoolsSwapEvent(_)) + | (Protocol::MeteoraPools, DexEvent::MeteoraPoolsAddLiquidityEvent(_)) + | (Protocol::MeteoraPools, DexEvent::MeteoraPoolsRemoveLiquidityEvent(_)) + | (Protocol::MeteoraPools, DexEvent::MeteoraPoolsBootstrapLiquidityEvent(_)) + | (Protocol::MeteoraPools, DexEvent::MeteoraPoolsPoolCreatedEvent(_)) + | (Protocol::MeteoraPools, DexEvent::MeteoraPoolsSetPoolFeesEvent(_)) => true, + (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmSwapEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmAddLiquidityEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmRemoveLiquidityEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmInitializePoolEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmInitializeBinArrayEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmCreatePositionEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmClosePositionEvent(_)) + | (Protocol::MeteoraDlmm, DexEvent::MeteoraDlmmClaimFeeEvent(_)) => true, + _ => false, + } +} diff --git a/src/streaming/parser_sdk_bridge/forward_pb.rs b/src/streaming/parser_sdk_bridge/forward_pb.rs new file mode 100644 index 0000000..0006388 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/forward_pb.rs @@ -0,0 +1,278 @@ +//! Orca Whirlpool and Meteora Pools / DLMM events with SDK-shaped payloads. +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::sol_parser_forward::events::{ + MeteoraDlmmAddLiquidityEvent, MeteoraDlmmClaimFeeEvent, MeteoraDlmmClosePositionEvent, + MeteoraDlmmCreatePositionEvent, MeteoraDlmmInitializeBinArrayEvent, + MeteoraDlmmInitializePoolEvent, MeteoraDlmmRemoveLiquidityEvent, MeteoraDlmmSwapEvent, + MeteoraPoolsAddLiquidityEvent, MeteoraPoolsBootstrapLiquidityEvent, + MeteoraPoolsPoolCreatedEvent, MeteoraPoolsRemoveLiquidityEvent, MeteoraPoolsSetPoolFeesEvent, + MeteoraPoolsSwapEvent, OrcaWhirlpoolLiquidityDecreasedEvent, + OrcaWhirlpoolLiquidityIncreasedEvent, OrcaWhirlpoolPoolInitializedEvent, + OrcaWhirlpoolSwapEvent, +}; + +pub(crate) fn orca_swap_from_pb( + e: sol_parser_sdk::core::events::OrcaWhirlpoolSwapEvent, + meta: EventMetadata, +) -> OrcaWhirlpoolSwapEvent { + OrcaWhirlpoolSwapEvent { + metadata: meta, + whirlpool: e.whirlpool, + input_amount: e.input_amount, + output_amount: e.output_amount, + a_to_b: e.a_to_b, + pre_sqrt_price: e.pre_sqrt_price, + post_sqrt_price: e.post_sqrt_price, + input_transfer_fee: e.input_transfer_fee, + output_transfer_fee: e.output_transfer_fee, + lp_fee: e.lp_fee, + protocol_fee: e.protocol_fee, + } +} + +pub(crate) fn orca_liquidity_increased_from_pb( + e: sol_parser_sdk::core::events::OrcaWhirlpoolLiquidityIncreasedEvent, + meta: EventMetadata, +) -> OrcaWhirlpoolLiquidityIncreasedEvent { + OrcaWhirlpoolLiquidityIncreasedEvent { + metadata: meta, + whirlpool: e.whirlpool, + liquidity: e.liquidity, + token_a_amount: e.token_a_amount, + token_b_amount: e.token_b_amount, + position: e.position, + tick_lower_index: e.tick_lower_index, + tick_upper_index: e.tick_upper_index, + token_a_transfer_fee: e.token_a_transfer_fee, + token_b_transfer_fee: e.token_b_transfer_fee, + } +} + +pub(crate) fn orca_liquidity_decreased_from_pb( + e: sol_parser_sdk::core::events::OrcaWhirlpoolLiquidityDecreasedEvent, + meta: EventMetadata, +) -> OrcaWhirlpoolLiquidityDecreasedEvent { + OrcaWhirlpoolLiquidityDecreasedEvent { + metadata: meta, + whirlpool: e.whirlpool, + liquidity: e.liquidity, + token_a_amount: e.token_a_amount, + token_b_amount: e.token_b_amount, + position: e.position, + tick_lower_index: e.tick_lower_index, + tick_upper_index: e.tick_upper_index, + token_a_transfer_fee: e.token_a_transfer_fee, + token_b_transfer_fee: e.token_b_transfer_fee, + } +} + +pub(crate) fn orca_pool_initialized_from_pb( + e: sol_parser_sdk::core::events::OrcaWhirlpoolPoolInitializedEvent, + meta: EventMetadata, +) -> OrcaWhirlpoolPoolInitializedEvent { + OrcaWhirlpoolPoolInitializedEvent { + metadata: meta, + whirlpool: e.whirlpool, + whirlpools_config: e.whirlpools_config, + token_mint_a: e.token_mint_a, + token_mint_b: e.token_mint_b, + tick_spacing: e.tick_spacing, + token_program_a: e.token_program_a, + token_program_b: e.token_program_b, + decimals_a: e.decimals_a, + decimals_b: e.decimals_b, + initial_sqrt_price: e.initial_sqrt_price, + } +} + +pub(crate) fn meteora_pools_swap_from_pb( + e: sol_parser_sdk::core::events::MeteoraPoolsSwapEvent, + meta: EventMetadata, +) -> MeteoraPoolsSwapEvent { + MeteoraPoolsSwapEvent { + metadata: meta, + in_amount: e.in_amount, + out_amount: e.out_amount, + trade_fee: e.trade_fee, + admin_fee: e.admin_fee, + host_fee: e.host_fee, + } +} + +pub(crate) fn meteora_pools_add_liquidity_from_pb( + e: sol_parser_sdk::core::events::MeteoraPoolsAddLiquidityEvent, + meta: EventMetadata, +) -> MeteoraPoolsAddLiquidityEvent { + MeteoraPoolsAddLiquidityEvent { + metadata: meta, + lp_mint_amount: e.lp_mint_amount, + token_a_amount: e.token_a_amount, + token_b_amount: e.token_b_amount, + } +} + +pub(crate) fn meteora_pools_remove_liquidity_from_pb( + e: sol_parser_sdk::core::events::MeteoraPoolsRemoveLiquidityEvent, + meta: EventMetadata, +) -> MeteoraPoolsRemoveLiquidityEvent { + MeteoraPoolsRemoveLiquidityEvent { + metadata: meta, + lp_unmint_amount: e.lp_unmint_amount, + token_a_out_amount: e.token_a_out_amount, + token_b_out_amount: e.token_b_out_amount, + } +} + +pub(crate) fn meteora_pools_bootstrap_from_pb( + e: sol_parser_sdk::core::events::MeteoraPoolsBootstrapLiquidityEvent, + meta: EventMetadata, +) -> MeteoraPoolsBootstrapLiquidityEvent { + MeteoraPoolsBootstrapLiquidityEvent { + metadata: meta, + lp_mint_amount: e.lp_mint_amount, + token_a_amount: e.token_a_amount, + token_b_amount: e.token_b_amount, + pool: e.pool, + } +} + +pub(crate) fn meteora_pools_pool_created_from_pb( + e: sol_parser_sdk::core::events::MeteoraPoolsPoolCreatedEvent, + meta: EventMetadata, +) -> MeteoraPoolsPoolCreatedEvent { + MeteoraPoolsPoolCreatedEvent { + metadata: meta, + lp_mint: e.lp_mint, + token_a_mint: e.token_a_mint, + token_b_mint: e.token_b_mint, + pool_type: e.pool_type, + pool: e.pool, + } +} + +pub(crate) fn meteora_pools_set_fees_from_pb( + e: sol_parser_sdk::core::events::MeteoraPoolsSetPoolFeesEvent, + meta: EventMetadata, +) -> MeteoraPoolsSetPoolFeesEvent { + MeteoraPoolsSetPoolFeesEvent { + metadata: meta, + trade_fee_numerator: e.trade_fee_numerator, + trade_fee_denominator: e.trade_fee_denominator, + owner_trade_fee_numerator: e.owner_trade_fee_numerator, + owner_trade_fee_denominator: e.owner_trade_fee_denominator, + pool: e.pool, + } +} + +pub(crate) fn meteora_dlmm_swap_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmSwapEvent, + meta: EventMetadata, +) -> MeteoraDlmmSwapEvent { + MeteoraDlmmSwapEvent { + metadata: meta, + pool: e.pool, + from: e.from, + start_bin_id: e.start_bin_id, + end_bin_id: e.end_bin_id, + amount_in: e.amount_in, + amount_out: e.amount_out, + swap_for_y: e.swap_for_y, + fee: e.fee, + protocol_fee: e.protocol_fee, + fee_bps: e.fee_bps, + host_fee: e.host_fee, + } +} + +pub(crate) fn meteora_dlmm_add_liquidity_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmAddLiquidityEvent, + meta: EventMetadata, +) -> MeteoraDlmmAddLiquidityEvent { + MeteoraDlmmAddLiquidityEvent { + metadata: meta, + pool: e.pool, + from: e.from, + position: e.position, + amounts: e.amounts, + active_bin_id: e.active_bin_id, + } +} + +pub(crate) fn meteora_dlmm_remove_liquidity_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmRemoveLiquidityEvent, + meta: EventMetadata, +) -> MeteoraDlmmRemoveLiquidityEvent { + MeteoraDlmmRemoveLiquidityEvent { + metadata: meta, + pool: e.pool, + from: e.from, + position: e.position, + amounts: e.amounts, + active_bin_id: e.active_bin_id, + } +} + +pub(crate) fn meteora_dlmm_init_pool_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmInitializePoolEvent, + meta: EventMetadata, +) -> MeteoraDlmmInitializePoolEvent { + MeteoraDlmmInitializePoolEvent { + metadata: meta, + pool: e.pool, + creator: e.creator, + active_bin_id: e.active_bin_id, + bin_step: e.bin_step, + } +} + +pub(crate) fn meteora_dlmm_init_bin_array_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmInitializeBinArrayEvent, + meta: EventMetadata, +) -> MeteoraDlmmInitializeBinArrayEvent { + MeteoraDlmmInitializeBinArrayEvent { + metadata: meta, + pool: e.pool, + bin_array: e.bin_array, + index: e.index, + } +} + +pub(crate) fn meteora_dlmm_create_position_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmCreatePositionEvent, + meta: EventMetadata, +) -> MeteoraDlmmCreatePositionEvent { + MeteoraDlmmCreatePositionEvent { + metadata: meta, + pool: e.pool, + position: e.position, + owner: e.owner, + lower_bin_id: e.lower_bin_id, + width: e.width, + } +} + +pub(crate) fn meteora_dlmm_close_position_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmClosePositionEvent, + meta: EventMetadata, +) -> MeteoraDlmmClosePositionEvent { + MeteoraDlmmClosePositionEvent { + metadata: meta, + pool: e.pool, + position: e.position, + owner: e.owner, + } +} + +pub(crate) fn meteora_dlmm_claim_fee_from_pb( + e: sol_parser_sdk::core::events::MeteoraDlmmClaimFeeEvent, + meta: EventMetadata, +) -> MeteoraDlmmClaimFeeEvent { + MeteoraDlmmClaimFeeEvent { + metadata: meta, + pool: e.pool, + position: e.position, + owner: e.owner, + fee_x: e.fee_x, + fee_y: e.fee_y, + } +} diff --git a/src/streaming/parser_sdk_bridge/mod.rs b/src/streaming/parser_sdk_bridge/mod.rs new file mode 100644 index 0000000..29d273e --- /dev/null +++ b/src/streaming/parser_sdk_bridge/mod.rs @@ -0,0 +1,299 @@ +//! `sol-parser-sdk` to streamer [`DexEvent`](crate::streaming::event_parser::DexEvent) mapping. +//! +//! The bridge is split by responsibility so the conversion layer stays reviewable. +//! +//! | Module | Responsibility | +//! |------|------| +//! | [`adapt`] | block_time / recv_us alignment with [`EventMetadata`] | +//! | [`program_ids`] | protocol program pubkeys | +//! | [`filter`] | subscribed [`Protocol`](crate::streaming::event_parser::Protocol) checks | +//! | [`pump_pumpswap`] | PumpFun and PumpSwap field mapping | +//! | [`bonk_accounts`] | Bonk plus Token / Nonce / PumpSwap account events | +//! | [`raydium_and_damm`] | Raydium lines and Meteora DAMM v2 | +//! | [`forward_pb`] | Orca / Meteora Pools / Meteora DLMM SDK-shaped events | +//! | [`convert`] | `PbDexEvent` dispatch and batch adaptation | +//! | [`accounts`] | SDK account parser compatibility | + +mod accounts; +mod adapt; +mod bonk_accounts; +mod convert; +mod filter; +mod forward_pb; +mod program_ids; +mod pump_pumpswap; +mod raydium_and_damm; + +pub(crate) use accounts::{ + parse_account_event as parse_sdk_account_event, parse_account_event_for_streamer, + AccountParseResult, +}; +pub(crate) use adapt::{block_timestamp_from_stream_meta, fuse_streamer_ix_ctx}; +pub(crate) use convert::{adapt_parser_event, adapt_parser_events_list, convert_parser_event}; + +#[cfg(test)] +mod tests { + use super::filter::event_matches_protocol; + use super::{adapt_parser_event, convert_parser_event}; + use crate::streaming::event_parser::common::filter::EventTypeFilter; + use crate::streaming::event_parser::common::types::{EventType, ProtocolType}; + use crate::streaming::event_parser::core::account_event_parser::TokenInfoEvent; + use crate::streaming::event_parser::{DexEvent, Protocol}; + use sol_parser_sdk::core::events::{ + BonkTradeEvent as PbBonkTrade, EventMetadata, MeteoraDlmmSwapEvent as PbDlmmSwap, + OrcaWhirlpoolSwapEvent as PbOrcaSwap, PumpFunTradeEvent as PbPumpTrade, + TokenInfoEvent as PbTokenInfo, TradeDirection as PbBonkDir, + }; + use sol_parser_sdk::DexEvent as PbDexEvent; + use solana_sdk::{pubkey::Pubkey, signature::Signature}; + + #[test] + fn converts_pumpfun_trade_preserving_amounts() { + let mut t = PbPumpTrade::default(); + t.metadata = EventMetadata { + signature: Signature::default(), + slot: 42, + tx_index: 7, + block_time_us: 1_000_000, + grpc_recv_us: 88, + recent_blockhash: None, + }; + t.mint = Pubkey::new_unique(); + t.user = Pubkey::new_unique(); + t.sol_amount = 100; + t.token_amount = 200; + t.is_buy = true; + + let ev = convert_parser_event(PbDexEvent::PumpFunTrade(t), None, 999).expect("convert"); + match ev { + DexEvent::PumpFunTradeEvent(st) => { + assert_eq!(st.metadata.slot, 42); + assert_eq!(st.metadata.recv_us, 999); + assert_eq!(st.sol_amount, 100); + assert_eq!(st.token_amount, 200); + assert!(st.is_buy); + } + _ => panic!("expected PumpFunTradeEvent"), + } + } + + #[test] + fn converts_pumpfun_buy_exact_sol_in_preserving_event_type() { + let mut t = PbPumpTrade::default(); + t.metadata = EventMetadata::default(); + t.is_buy = true; + + let ev = + convert_parser_event(PbDexEvent::PumpFunBuyExactSolIn(t), None, 999).expect("convert"); + match ev { + DexEvent::PumpFunTradeEvent(st) => { + assert_eq!(st.metadata.event_type, EventType::PumpFunBuyExactSolIn); + assert!(st.is_buy); + } + _ => panic!("expected PumpFunTradeEvent"), + } + } + + #[test] + fn protocol_filter_keeps_pumpfun_only_when_requested() { + let mut t = PbPumpTrade::default(); + t.metadata = EventMetadata::default(); + let dex = convert_parser_event(PbDexEvent::PumpFunTrade(t), None, 0).expect("convert"); + assert!(event_matches_protocol(&[Protocol::PumpFun], &dex)); + assert!(!event_matches_protocol(&[Protocol::PumpSwap], &dex)); + } + + #[test] + fn converts_parser_sdk_error_preserves_message() { + let ev = convert_parser_event(PbDexEvent::Error("decode failed".into()), None, 404) + .expect("convert"); + match ev { + DexEvent::ParserSdkErrorEvent(e) => { + assert_eq!(e.message, "decode failed"); + assert_eq!(e.metadata.recv_us, 404); + assert_eq!(e.metadata.protocol, ProtocolType::Common); + assert_eq!(e.metadata.event_type, EventType::ParserSdkError); + } + _ => panic!("expected ParserSdkErrorEvent"), + } + } + + #[test] + fn converts_token_info_preserving_event_type() { + let ev = convert_parser_event(PbDexEvent::TokenInfo(PbTokenInfo::default()), None, 404) + .expect("convert"); + match ev { + DexEvent::TokenInfoEvent(e) => { + assert_eq!(e.metadata.event_type, EventType::TokenInfo); + assert_eq!(e.metadata.protocol, ProtocolType::Common); + } + _ => panic!("expected TokenInfoEvent"), + } + } + + #[test] + fn protocol_filter_allows_protocol_independent_events() { + let mut token_info = TokenInfoEvent::default(); + token_info.metadata.event_type = EventType::TokenInfo; + token_info.metadata.protocol = ProtocolType::Common; + let dex = DexEvent::TokenInfoEvent(token_info); + + assert!(event_matches_protocol(&[Protocol::PumpFun], &dex)); + assert!(event_matches_protocol(&[Protocol::RaydiumCpmm], &dex)); + } + + #[test] + fn adapt_token_info_passes_protocol_and_token_account_filter() { + let filter = EventTypeFilter::include_only([EventType::TokenAccount]); + let ev = adapt_parser_event( + PbDexEvent::TokenInfo(PbTokenInfo::default()), + None, + 404, + &[Protocol::PumpFun], + Some(&filter), + ) + .expect("token info should pass common protocol and token-account filter"); + + assert!(matches!(ev, DexEvent::TokenInfoEvent(_))); + assert_eq!(ev.metadata().event_type, EventType::TokenInfo); + } + + #[test] + fn converts_orca_whirlpool_swap_preserving_amounts() { + let whirlpool = Pubkey::new_unique(); + let pb = PbOrcaSwap { + metadata: EventMetadata { + signature: Signature::default(), + slot: 9, + tx_index: 0, + block_time_us: 0, + grpc_recv_us: 0, + recent_blockhash: None, + }, + whirlpool, + input_amount: 10, + output_amount: 20, + a_to_b: false, + pre_sqrt_price: 100, + post_sqrt_price: 200, + input_transfer_fee: 1, + output_transfer_fee: 2, + lp_fee: 3, + protocol_fee: 4, + }; + let ev = + convert_parser_event(PbDexEvent::OrcaWhirlpoolSwap(pb), None, 111).expect("convert"); + match ev { + DexEvent::OrcaWhirlpoolSwapEvent(e) => { + assert_eq!(e.whirlpool, whirlpool); + assert_eq!(e.input_amount, 10); + assert_eq!(e.output_amount, 20); + assert!(!e.a_to_b); + assert_eq!(e.pre_sqrt_price, 100); + assert_eq!(e.post_sqrt_price, 200); + assert_eq!(e.lp_fee, 3); + assert_eq!(e.protocol_fee, 4); + assert_eq!(e.metadata.slot, 9); + assert_eq!(e.metadata.recv_us, 111); + } + _ => panic!("expected OrcaWhirlpoolSwapEvent"), + } + } + + #[test] + fn converts_meteora_dlmm_swap_preserving_amounts() { + let pool = Pubkey::new_unique(); + let from = Pubkey::new_unique(); + let pb = PbDlmmSwap { + metadata: EventMetadata::default(), + pool, + from, + start_bin_id: -5, + end_bin_id: 12, + amount_in: 300, + amount_out: 299, + swap_for_y: true, + fee: 1, + protocol_fee: 2, + fee_bps: 25, + host_fee: 0, + }; + let ev = convert_parser_event(PbDexEvent::MeteoraDlmmSwap(pb), None, 0).expect("convert"); + match ev { + DexEvent::MeteoraDlmmSwapEvent(e) => { + assert_eq!(e.pool, pool); + assert_eq!(e.from, from); + assert_eq!(e.start_bin_id, -5); + assert_eq!(e.end_bin_id, 12); + assert_eq!(e.amount_in, 300); + assert_eq!(e.amount_out, 299); + assert!(e.swap_for_y); + assert_eq!(e.fee_bps, 25); + } + _ => panic!("expected MeteoraDlmmSwapEvent"), + } + } + + #[test] + fn protocol_filter_keeps_orca_only_when_requested() { + let pb = PbOrcaSwap { + metadata: EventMetadata::default(), + whirlpool: Pubkey::new_unique(), + input_amount: 0, + output_amount: 0, + a_to_b: true, + pre_sqrt_price: 0, + post_sqrt_price: 0, + input_transfer_fee: 0, + output_transfer_fee: 0, + lp_fee: 0, + protocol_fee: 0, + }; + let dex = + convert_parser_event(PbDexEvent::OrcaWhirlpoolSwap(pb), None, 0).expect("convert"); + assert!(event_matches_protocol(&[Protocol::OrcaWhirlpool], &dex)); + assert!(!event_matches_protocol(&[Protocol::PumpFun], &dex)); + } + + #[test] + fn converts_bonk_trade_maps_event_type_buy_exact_in() { + let b = PbBonkTrade { + metadata: EventMetadata::default(), + pool_state: Pubkey::default(), + user: Pubkey::default(), + amount_in: 0, + amount_out: 0, + is_buy: true, + trade_direction: PbBonkDir::Buy, + exact_in: true, + }; + let dex = convert_parser_event(PbDexEvent::BonkTrade(b), None, 0).expect("convert"); + match dex { + DexEvent::BonkTradeEvent(e) => { + assert_eq!(e.metadata.event_type, EventType::BonkBuyExactIn) + } + _ => panic!("expected BonkTradeEvent"), + } + } + + #[test] + fn converts_bonk_trade_maps_event_type_sell_exact_out() { + let b = PbBonkTrade { + metadata: EventMetadata::default(), + pool_state: Pubkey::default(), + user: Pubkey::default(), + amount_in: 0, + amount_out: 0, + is_buy: false, + trade_direction: PbBonkDir::Sell, + exact_in: false, + }; + let dex = convert_parser_event(PbDexEvent::BonkTrade(b), None, 0).expect("convert"); + match dex { + DexEvent::BonkTradeEvent(e) => { + assert_eq!(e.metadata.event_type, EventType::BonkSellExactOut) + } + _ => panic!("expected BonkTradeEvent"), + } + } +} diff --git a/src/streaming/parser_sdk_bridge/program_ids.rs b/src/streaming/parser_sdk_bridge/program_ids.rs new file mode 100644 index 0000000..cd23ebd --- /dev/null +++ b/src/streaming/parser_sdk_bridge/program_ids.rs @@ -0,0 +1,41 @@ +//! Protocol program ids used by bridged events. +use crate::streaming::event_parser::protocols::sol_parser_forward; + +use solana_sdk::pubkey::Pubkey; + +pub(crate) fn pumpswap_program() -> Pubkey { + crate::streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID +} +pub(crate) fn pump_program() -> Pubkey { + crate::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID +} +pub(crate) fn pump_fees_program() -> Pubkey { + sol_parser_sdk::instr::program_ids::PUMP_FEES_PROGRAM_ID +} +pub(crate) fn bonk_program() -> Pubkey { + crate::streaming::event_parser::protocols::bonk::parser::BONK_PROGRAM_ID +} +pub(crate) fn raydium_cpmm_program() -> Pubkey { + crate::streaming::event_parser::protocols::raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID +} +pub(crate) fn raydium_clmm_program() -> Pubkey { + crate::streaming::event_parser::protocols::raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID +} +pub(crate) fn raydium_amm_v4_program() -> Pubkey { + crate::streaming::event_parser::protocols::raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID +} +pub(crate) fn meteora_damm_program() -> Pubkey { + crate::streaming::event_parser::protocols::meteora_damm_v2::parser::METEORA_DAMM_V2_PROGRAM_ID +} + +pub(crate) fn orca_whirlpool_program() -> Pubkey { + sol_parser_forward::ORCA_WHIRLPOOL_PROGRAM_ID +} + +pub(crate) fn meteora_pools_program() -> Pubkey { + sol_parser_forward::METEORA_POOLS_PROGRAM_ID +} + +pub(crate) fn meteora_dlmm_program() -> Pubkey { + sol_parser_forward::METEORA_DLMM_PROGRAM_ID +} diff --git a/src/streaming/parser_sdk_bridge/pump_pumpswap.rs b/src/streaming/parser_sdk_bridge/pump_pumpswap.rs new file mode 100644 index 0000000..b656474 --- /dev/null +++ b/src/streaming/parser_sdk_bridge/pump_pumpswap.rs @@ -0,0 +1,650 @@ +//! PumpFun / PumpSwap field mapping and aggregate trade conversion. +use crate::streaming::event_parser::common::types::{EventType, ProtocolType}; +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::pumpfun::events::{ + PumpFeesConfigStatus, PumpFeesCreateFeeSharingConfigEvent, PumpFeesFeeTier, PumpFeesFees, + PumpFeesInitializeFeeConfigEvent, PumpFeesResetFeeSharingConfigEvent, + PumpFeesRevokeFeeSharingAuthorityEvent, PumpFeesShareholder, + PumpFeesTransferFeeSharingAuthorityEvent, PumpFeesUpdateAdminEvent, + PumpFeesUpdateFeeConfigEvent, PumpFeesUpdateFeeSharesEvent, PumpFeesUpsertFeeTiersEvent, + PumpFunCreateTokenEvent, PumpFunCreateV2TokenEvent, PumpFunGlobalAccountEvent, + PumpFunMigrateBondingCurveCreatorEvent, PumpFunMigrateEvent, PumpFunTradeEvent, +}; +use crate::streaming::event_parser::protocols::pumpfun::types::Global; +use crate::streaming::event_parser::protocols::pumpswap::events::{ + PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, PumpSwapSellEvent, + PumpSwapWithdrawEvent, +}; +use crate::streaming::event_parser::DexEvent; +use prost_types::Timestamp; +use solana_sdk::pubkey::Pubkey; + +use super::adapt::adapt_pm; +use super::program_ids::{pump_program, pumpswap_program}; + +pub(crate) fn pumpfun_create_token_from_parser( + c: sol_parser_sdk::core::events::PumpFunCreateTokenEvent, + meta: EventMetadata, +) -> PumpFunCreateTokenEvent { + PumpFunCreateTokenEvent { + metadata: meta, + name: c.name, + symbol: c.symbol, + uri: c.uri, + mint: c.mint, + bonding_curve: c.bonding_curve, + user: c.user, + creator: c.creator, + timestamp: c.timestamp, + virtual_token_reserves: c.virtual_token_reserves, + virtual_sol_reserves: c.virtual_sol_reserves, + real_token_reserves: c.real_token_reserves, + token_total_supply: c.token_total_supply, + token_program: c.token_program, + is_mayhem_mode: c.is_mayhem_mode, + is_cashback_enabled: c.is_cashback_enabled, + ..Default::default() + } +} + +pub(crate) fn pumpfun_create_v2_from_parser( + c: sol_parser_sdk::core::events::PumpFunCreateV2TokenEvent, + meta: EventMetadata, +) -> PumpFunCreateV2TokenEvent { + PumpFunCreateV2TokenEvent { + metadata: meta, + name: c.name, + symbol: c.symbol, + uri: c.uri, + mint: c.mint, + bonding_curve: c.bonding_curve, + user: c.user, + creator: c.creator, + timestamp: c.timestamp, + virtual_token_reserves: c.virtual_token_reserves, + virtual_sol_reserves: c.virtual_sol_reserves, + real_token_reserves: c.real_token_reserves, + token_total_supply: c.token_total_supply, + token_program: c.token_program, + is_mayhem_mode: c.is_mayhem_mode, + is_cashback_enabled: c.is_cashback_enabled, + mint_authority: c.mint_authority, + associated_bonding_curve: c.associated_bonding_curve, + global: c.global, + system_program: c.system_program, + associated_token_program: c.associated_token_program, + mayhem_program_id: c.mayhem_program_id, + global_params: c.global_params, + sol_vault: c.sol_vault, + mayhem_state: c.mayhem_state, + mayhem_token_vault: c.mayhem_token_vault, + event_authority: c.event_authority, + program: c.program, + } +} + +pub(crate) fn pumpfun_migrate_from_parser( + m: sol_parser_sdk::core::events::PumpFunMigrateEvent, + meta: EventMetadata, +) -> PumpFunMigrateEvent { + PumpFunMigrateEvent { + metadata: meta, + user: m.user, + mint: m.mint, + mint_amount: m.mint_amount, + sol_amount: m.sol_amount, + pool_migration_fee: m.pool_migration_fee, + bonding_curve: m.bonding_curve, + timestamp: m.timestamp, + pool: m.pool, + ..Default::default() + } +} + +fn pump_fees_status_from_parser( + s: sol_parser_sdk::core::events::PumpFeesConfigStatus, +) -> PumpFeesConfigStatus { + match s { + sol_parser_sdk::core::events::PumpFeesConfigStatus::Paused => PumpFeesConfigStatus::Paused, + sol_parser_sdk::core::events::PumpFeesConfigStatus::Active => PumpFeesConfigStatus::Active, + } +} + +fn pump_fees_shareholder_from_parser( + s: sol_parser_sdk::core::events::PumpFeesShareholder, +) -> PumpFeesShareholder { + PumpFeesShareholder { address: s.address, share_bps: s.share_bps } +} + +fn pump_fees_fees_from_parser(f: sol_parser_sdk::core::events::PumpFeesFees) -> PumpFeesFees { + PumpFeesFees { + lp_fee_bps: f.lp_fee_bps, + protocol_fee_bps: f.protocol_fee_bps, + creator_fee_bps: f.creator_fee_bps, + } +} + +fn pump_fees_tier_from_parser(t: sol_parser_sdk::core::events::PumpFeesFeeTier) -> PumpFeesFeeTier { + PumpFeesFeeTier { + market_cap_lamports_threshold: t.market_cap_lamports_threshold, + fees: pump_fees_fees_from_parser(t.fees), + } +} + +pub(crate) fn pump_fees_create_sharing_config_from_parser( + e: sol_parser_sdk::core::events::PumpFeesCreateFeeSharingConfigEvent, + meta: EventMetadata, +) -> PumpFeesCreateFeeSharingConfigEvent { + PumpFeesCreateFeeSharingConfigEvent { + metadata: meta, + timestamp: e.timestamp, + mint: e.mint, + bonding_curve: e.bonding_curve, + pool: e.pool, + sharing_config: e.sharing_config, + admin: e.admin, + initial_shareholders: e + .initial_shareholders + .into_iter() + .map(pump_fees_shareholder_from_parser) + .collect(), + status: pump_fees_status_from_parser(e.status), + } +} + +pub(crate) fn pump_fees_initialize_fee_config_from_parser( + e: sol_parser_sdk::core::events::PumpFeesInitializeFeeConfigEvent, + meta: EventMetadata, +) -> PumpFeesInitializeFeeConfigEvent { + PumpFeesInitializeFeeConfigEvent { + metadata: meta, + timestamp: e.timestamp, + admin: e.admin, + fee_config: e.fee_config, + } +} + +pub(crate) fn pump_fees_reset_sharing_config_from_parser( + e: sol_parser_sdk::core::events::PumpFeesResetFeeSharingConfigEvent, + meta: EventMetadata, +) -> PumpFeesResetFeeSharingConfigEvent { + PumpFeesResetFeeSharingConfigEvent { + metadata: meta, + timestamp: e.timestamp, + mint: e.mint, + sharing_config: e.sharing_config, + old_admin: e.old_admin, + old_shareholders: e + .old_shareholders + .into_iter() + .map(pump_fees_shareholder_from_parser) + .collect(), + new_admin: e.new_admin, + new_shareholders: e + .new_shareholders + .into_iter() + .map(pump_fees_shareholder_from_parser) + .collect(), + } +} + +pub(crate) fn pump_fees_revoke_authority_from_parser( + e: sol_parser_sdk::core::events::PumpFeesRevokeFeeSharingAuthorityEvent, + meta: EventMetadata, +) -> PumpFeesRevokeFeeSharingAuthorityEvent { + PumpFeesRevokeFeeSharingAuthorityEvent { + metadata: meta, + timestamp: e.timestamp, + mint: e.mint, + sharing_config: e.sharing_config, + admin: e.admin, + } +} + +pub(crate) fn pump_fees_transfer_authority_from_parser( + e: sol_parser_sdk::core::events::PumpFeesTransferFeeSharingAuthorityEvent, + meta: EventMetadata, +) -> PumpFeesTransferFeeSharingAuthorityEvent { + PumpFeesTransferFeeSharingAuthorityEvent { + metadata: meta, + timestamp: e.timestamp, + mint: e.mint, + sharing_config: e.sharing_config, + old_admin: e.old_admin, + new_admin: e.new_admin, + } +} + +pub(crate) fn pump_fees_update_admin_from_parser( + e: sol_parser_sdk::core::events::PumpFeesUpdateAdminEvent, + meta: EventMetadata, +) -> PumpFeesUpdateAdminEvent { + PumpFeesUpdateAdminEvent { + metadata: meta, + timestamp: e.timestamp, + old_admin: e.old_admin, + new_admin: e.new_admin, + } +} + +pub(crate) fn pump_fees_update_fee_config_from_parser( + e: sol_parser_sdk::core::events::PumpFeesUpdateFeeConfigEvent, + meta: EventMetadata, +) -> PumpFeesUpdateFeeConfigEvent { + PumpFeesUpdateFeeConfigEvent { + metadata: meta, + timestamp: e.timestamp, + admin: e.admin, + fee_config: e.fee_config, + fee_tiers: e.fee_tiers.into_iter().map(pump_fees_tier_from_parser).collect(), + flat_fees: pump_fees_fees_from_parser(e.flat_fees), + } +} + +pub(crate) fn pump_fees_update_fee_shares_from_parser( + e: sol_parser_sdk::core::events::PumpFeesUpdateFeeSharesEvent, + meta: EventMetadata, +) -> PumpFeesUpdateFeeSharesEvent { + PumpFeesUpdateFeeSharesEvent { + metadata: meta, + timestamp: e.timestamp, + mint: e.mint, + sharing_config: e.sharing_config, + admin: e.admin, + bonding_curve: e.bonding_curve, + pump_creator_vault: e.pump_creator_vault, + new_shareholders: e + .new_shareholders + .into_iter() + .map(pump_fees_shareholder_from_parser) + .collect(), + } +} + +pub(crate) fn pump_fees_upsert_fee_tiers_from_parser( + e: sol_parser_sdk::core::events::PumpFeesUpsertFeeTiersEvent, + meta: EventMetadata, +) -> PumpFeesUpsertFeeTiersEvent { + PumpFeesUpsertFeeTiersEvent { + metadata: meta, + timestamp: e.timestamp, + admin: e.admin, + fee_config: e.fee_config, + fee_tiers: e.fee_tiers.into_iter().map(pump_fees_tier_from_parser).collect(), + offset: e.offset, + } +} + +pub(crate) fn pumpfun_migrate_bonding_creator_from_parser( + e: sol_parser_sdk::core::events::PumpFunMigrateBondingCurveCreatorEvent, + meta: EventMetadata, +) -> PumpFunMigrateBondingCurveCreatorEvent { + PumpFunMigrateBondingCurveCreatorEvent { + metadata: meta, + timestamp: e.timestamp, + mint: e.mint, + bonding_curve: e.bonding_curve, + sharing_config: e.sharing_config, + old_creator: e.old_creator, + new_creator: e.new_creator, + } +} + +pub(crate) fn pumpfun_global_account_from_parser( + e: sol_parser_sdk::core::events::PumpFunGlobalAccountEvent, + meta: EventMetadata, +) -> PumpFunGlobalAccountEvent { + let mut fee_recipients = [Pubkey::default(); 7]; + for (dst, src) in fee_recipients.iter_mut().zip(e.global.fee_recipients.iter()) { + *dst = *src; + } + + PumpFunGlobalAccountEvent { + metadata: meta, + pubkey: e.pubkey, + executable: false, + lamports: 0, + owner: pump_program(), + rent_epoch: 0, + global: Global { + initialized: e.global.initialized, + authority: e.global.authority, + fee_recipient: e.global.fee_recipient, + initial_virtual_token_reserves: e.global.initial_virtual_token_reserves, + initial_virtual_sol_reserves: e.global.initial_virtual_sol_reserves, + initial_real_token_reserves: e.global.initial_real_token_reserves, + token_total_supply: e.global.token_total_supply, + fee_basis_points: e.global.fee_basis_points, + withdraw_authority: e.global.withdraw_authority, + enable_migrate: e.global.enable_migrate, + pool_migration_fee: e.global.pool_migration_fee, + creator_fee_basis_points: e.global.creator_fee_basis_points, + fee_recipients, + set_creator_authority: e.global.set_creator_authority, + admin_set_creator_authority: e.global.admin_set_creator_authority, + create_v2_enabled: e.global.create_v2_enabled, + whitelist_pda: e.global.whitelist_pda, + reserved_fee_recipient: e.global.reserved_fee_recipient, + mayhem_mode_enabled: e.global.mayhem_mode_enabled, + reserved_fee_recipients: e.global.reserved_fee_recipients, + is_cashback_enabled: false, + }, + } +} + +pub(crate) fn pumpswap_buy_full_from_parser( + b: sol_parser_sdk::core::events::PumpSwapBuyEvent, + meta: EventMetadata, +) -> PumpSwapBuyEvent { + PumpSwapBuyEvent { + metadata: meta, + timestamp: b.timestamp, + base_amount_out: b.base_amount_out, + max_quote_amount_in: b.max_quote_amount_in, + user_base_token_reserves: b.user_base_token_reserves, + user_quote_token_reserves: b.user_quote_token_reserves, + pool_base_token_reserves: b.pool_base_token_reserves, + pool_quote_token_reserves: b.pool_quote_token_reserves, + quote_amount_in: b.quote_amount_in, + lp_fee_basis_points: b.lp_fee_basis_points, + lp_fee: b.lp_fee, + protocol_fee_basis_points: b.protocol_fee_basis_points, + protocol_fee: b.protocol_fee, + quote_amount_in_with_lp_fee: b.quote_amount_in_with_lp_fee, + user_quote_amount_in: b.user_quote_amount_in, + pool: b.pool, + user: b.user, + user_base_token_account: b.user_base_token_account, + user_quote_token_account: b.user_quote_token_account, + protocol_fee_recipient: b.protocol_fee_recipient, + protocol_fee_recipient_token_account: b.protocol_fee_recipient_token_account, + coin_creator: b.coin_creator, + coin_creator_fee_basis_points: b.coin_creator_fee_basis_points, + coin_creator_fee: b.coin_creator_fee, + track_volume: b.track_volume, + total_unclaimed_tokens: b.total_unclaimed_tokens, + total_claimed_tokens: b.total_claimed_tokens, + current_sol_volume: b.current_sol_volume, + last_update_timestamp: b.last_update_timestamp, + min_base_amount_out: b.min_base_amount_out, + ix_name: b.ix_name, + cashback_fee_basis_points: b.cashback_fee_basis_points, + cashback: b.cashback, + is_pump_pool: b.is_pump_pool, + base_mint: b.base_mint, + quote_mint: b.quote_mint, + pool_base_token_account: b.pool_base_token_account, + pool_quote_token_account: b.pool_quote_token_account, + coin_creator_vault_ata: b.coin_creator_vault_ata, + coin_creator_vault_authority: b.coin_creator_vault_authority, + base_token_program: b.base_token_program, + quote_token_program: b.quote_token_program, + } +} + +pub(crate) fn pumpswap_sell_full_from_parser( + s: sol_parser_sdk::core::events::PumpSwapSellEvent, + meta: EventMetadata, +) -> PumpSwapSellEvent { + PumpSwapSellEvent { + metadata: meta, + timestamp: s.timestamp, + base_amount_in: s.base_amount_in, + min_quote_amount_out: s.min_quote_amount_out, + user_base_token_reserves: s.user_base_token_reserves, + user_quote_token_reserves: s.user_quote_token_reserves, + pool_base_token_reserves: s.pool_base_token_reserves, + pool_quote_token_reserves: s.pool_quote_token_reserves, + quote_amount_out: s.quote_amount_out, + lp_fee_basis_points: s.lp_fee_basis_points, + lp_fee: s.lp_fee, + protocol_fee_basis_points: s.protocol_fee_basis_points, + protocol_fee: s.protocol_fee, + quote_amount_out_without_lp_fee: s.quote_amount_out_without_lp_fee, + user_quote_amount_out: s.user_quote_amount_out, + pool: s.pool, + user: s.user, + user_base_token_account: s.user_base_token_account, + user_quote_token_account: s.user_quote_token_account, + protocol_fee_recipient: s.protocol_fee_recipient, + protocol_fee_recipient_token_account: s.protocol_fee_recipient_token_account, + coin_creator: s.coin_creator, + coin_creator_fee_basis_points: s.coin_creator_fee_basis_points, + coin_creator_fee: s.coin_creator_fee, + cashback_fee_basis_points: s.cashback_fee_basis_points, + cashback: s.cashback, + is_pump_pool: s.is_pump_pool, + base_mint: s.base_mint, + quote_mint: s.quote_mint, + pool_base_token_account: s.pool_base_token_account, + pool_quote_token_account: s.pool_quote_token_account, + coin_creator_vault_ata: s.coin_creator_vault_ata, + coin_creator_vault_authority: s.coin_creator_vault_authority, + base_token_program: s.base_token_program, + quote_token_program: s.quote_token_program, + } +} + +pub(crate) fn pumpswap_create_pool_from_parser( + c: sol_parser_sdk::core::events::PumpSwapCreatePoolEvent, + meta: EventMetadata, +) -> PumpSwapCreatePoolEvent { + PumpSwapCreatePoolEvent { + metadata: meta, + timestamp: c.timestamp, + index: c.index, + creator: c.creator, + base_mint: c.base_mint, + quote_mint: c.quote_mint, + base_mint_decimals: c.base_mint_decimals, + quote_mint_decimals: c.quote_mint_decimals, + base_amount_in: c.base_amount_in, + quote_amount_in: c.quote_amount_in, + pool_base_amount: c.pool_base_amount, + pool_quote_amount: c.pool_quote_amount, + minimum_liquidity: c.minimum_liquidity, + initial_liquidity: c.initial_liquidity, + lp_token_amount_out: c.lp_token_amount_out, + pool_bump: c.pool_bump, + pool: c.pool, + lp_mint: c.lp_mint, + user_base_token_account: c.user_base_token_account, + user_quote_token_account: c.user_quote_token_account, + coin_creator: c.coin_creator, + ..Default::default() + } +} + +pub(crate) fn pumpswap_liquidity_added_to_deposit( + a: sol_parser_sdk::core::events::PumpSwapLiquidityAdded, + meta: EventMetadata, +) -> PumpSwapDepositEvent { + PumpSwapDepositEvent { + metadata: meta, + timestamp: a.timestamp, + lp_token_amount_out: a.lp_token_amount_out, + max_base_amount_in: a.max_base_amount_in, + max_quote_amount_in: a.max_quote_amount_in, + user_base_token_reserves: a.user_base_token_reserves, + user_quote_token_reserves: a.user_quote_token_reserves, + pool_base_token_reserves: a.pool_base_token_reserves, + pool_quote_token_reserves: a.pool_quote_token_reserves, + base_amount_in: a.base_amount_in, + quote_amount_in: a.quote_amount_in, + lp_mint_supply: a.lp_mint_supply, + pool: a.pool, + user: a.user, + user_base_token_account: a.user_base_token_account, + user_quote_token_account: a.user_quote_token_account, + user_pool_token_account: a.user_pool_token_account, + ..Default::default() + } +} + +pub(crate) fn pumpswap_liquidity_removed_to_withdraw( + r: sol_parser_sdk::core::events::PumpSwapLiquidityRemoved, + meta: EventMetadata, +) -> PumpSwapWithdrawEvent { + PumpSwapWithdrawEvent { + metadata: meta, + timestamp: r.timestamp, + lp_token_amount_in: r.lp_token_amount_in, + min_base_amount_out: r.min_base_amount_out, + min_quote_amount_out: r.min_quote_amount_out, + user_base_token_reserves: r.user_base_token_reserves, + user_quote_token_reserves: r.user_quote_token_reserves, + pool_base_token_reserves: r.pool_base_token_reserves, + pool_quote_token_reserves: r.pool_quote_token_reserves, + base_amount_out: r.base_amount_out, + quote_amount_out: r.quote_amount_out, + lp_mint_supply: r.lp_mint_supply, + pool: r.pool, + user: r.user, + user_base_token_account: r.user_base_token_account, + user_quote_token_account: r.user_quote_token_account, + user_pool_token_account: r.user_pool_token_account, + ..Default::default() + } +} +pub(crate) fn pumpfun_trade_from_parser( + t: sol_parser_sdk::core::events::PumpFunTradeEvent, + bt: Option<&Timestamp>, + recv_wall_us: i64, +) -> DexEvent { + let event_type = if t.is_buy { EventType::PumpFunBuy } else { EventType::PumpFunSell }; + pumpfun_trade_from_parser_with_event_type(t, bt, recv_wall_us, event_type) +} + +pub(crate) fn pumpfun_trade_from_parser_with_event_type( + t: sol_parser_sdk::core::events::PumpFunTradeEvent, + bt: Option<&Timestamp>, + recv_wall_us: i64, + event_type: EventType, +) -> DexEvent { + let pm = t.metadata.clone(); + let meta = adapt_pm(pm, bt, recv_wall_us, ProtocolType::PumpFun, event_type, pump_program()); + let st = PumpFunTradeEvent { + metadata: meta, + mint: t.mint, + sol_amount: t.sol_amount, + token_amount: t.token_amount, + is_buy: t.is_buy, + user: t.user, + timestamp: t.timestamp, + virtual_sol_reserves: t.virtual_sol_reserves, + virtual_token_reserves: t.virtual_token_reserves, + real_sol_reserves: t.real_sol_reserves, + real_token_reserves: t.real_token_reserves, + fee_recipient: t.fee_recipient, + fee_basis_points: t.fee_basis_points, + fee: t.fee, + creator: t.creator, + creator_fee_basis_points: t.creator_fee_basis_points, + creator_fee: t.creator_fee, + track_volume: t.track_volume, + total_unclaimed_tokens: t.total_unclaimed_tokens, + total_claimed_tokens: t.total_claimed_tokens, + current_sol_volume: t.current_sol_volume, + last_update_timestamp: t.last_update_timestamp, + bonding_curve: t.bonding_curve, + associated_bonding_curve: t.associated_bonding_curve, + token_program: t.token_program, + creator_vault: t.creator_vault, + account: t.account, + ix_name: t.ix_name, + mayhem_mode: t.mayhem_mode, + cashback_fee_basis_points: t.cashback_fee_basis_points, + cashback: t.cashback, + is_cashback_coin: t.is_cashback_coin, + ..Default::default() + }; + DexEvent::PumpFunTradeEvent(st) +} + +pub(crate) fn pumpswap_trade_from_parser( + t: sol_parser_sdk::core::events::PumpSwapTradeEvent, + bt: Option<&Timestamp>, + recv_wall_us: i64, +) -> Option { + let pm = t.metadata.clone(); + let meta = adapt_pm( + pm, + bt, + recv_wall_us, + ProtocolType::PumpSwap, + if t.is_buy { EventType::PumpSwapBuy } else { EventType::PumpSwapSell }, + pumpswap_program(), + ); + if t.is_buy { + Some(DexEvent::PumpSwapBuyEvent(PumpSwapBuyEvent { + metadata: meta, + timestamp: t.timestamp, + base_amount_out: t.token_amount, + max_quote_amount_in: t.sol_amount, + user_base_token_reserves: t.virtual_token_reserves, + user_quote_token_reserves: t.virtual_sol_reserves, + pool_base_token_reserves: t.real_token_reserves, + pool_quote_token_reserves: t.real_sol_reserves, + quote_amount_in: t.sol_amount, + lp_fee_basis_points: t.fee_basis_points, + lp_fee: t.fee, + protocol_fee_basis_points: 0, + protocol_fee: 0, + quote_amount_in_with_lp_fee: t.sol_amount, + user_quote_amount_in: t.sol_amount, + pool: Pubkey::default(), + user: t.user, + user_base_token_account: Pubkey::default(), + user_quote_token_account: Pubkey::default(), + protocol_fee_recipient: t.fee_recipient, + protocol_fee_recipient_token_account: Pubkey::default(), + coin_creator: t.creator, + coin_creator_fee_basis_points: t.creator_fee_basis_points, + coin_creator_fee: t.creator_fee, + track_volume: t.track_volume, + total_unclaimed_tokens: t.total_unclaimed_tokens, + total_claimed_tokens: t.total_claimed_tokens, + current_sol_volume: t.current_sol_volume, + last_update_timestamp: t.last_update_timestamp, + min_base_amount_out: 0, + ix_name: t.ix_name.clone(), + cashback_fee_basis_points: 0, + cashback: 0, + is_pump_pool: false, + base_mint: t.mint, + ..Default::default() + })) + } else { + Some(DexEvent::PumpSwapSellEvent(PumpSwapSellEvent { + metadata: meta, + timestamp: t.timestamp, + base_amount_in: t.token_amount, + min_quote_amount_out: t.sol_amount, + user_base_token_reserves: t.virtual_token_reserves, + user_quote_token_reserves: t.virtual_sol_reserves, + pool_base_token_reserves: t.real_token_reserves, + pool_quote_token_reserves: t.real_sol_reserves, + quote_amount_out: t.sol_amount, + lp_fee_basis_points: t.fee_basis_points, + lp_fee: t.fee, + protocol_fee_basis_points: 0, + protocol_fee: 0, + quote_amount_out_without_lp_fee: t.sol_amount, + user_quote_amount_out: t.sol_amount, + pool: Pubkey::default(), + user: t.user, + user_base_token_account: Pubkey::default(), + user_quote_token_account: Pubkey::default(), + protocol_fee_recipient: t.fee_recipient, + protocol_fee_recipient_token_account: Pubkey::default(), + coin_creator: t.creator, + coin_creator_fee_basis_points: t.creator_fee_basis_points, + coin_creator_fee: t.creator_fee, + cashback_fee_basis_points: 0, + cashback: 0, + is_pump_pool: false, + base_mint: t.mint, + ..Default::default() + })) + } +} diff --git a/src/streaming/parser_sdk_bridge/raydium_and_damm.rs b/src/streaming/parser_sdk_bridge/raydium_and_damm.rs new file mode 100644 index 0000000..bb546cf --- /dev/null +++ b/src/streaming/parser_sdk_bridge/raydium_and_damm.rs @@ -0,0 +1,496 @@ +//! Raydium CPMM / CLMM / AMM V4 and Meteora DAMM v2 mapping. +use crate::streaming::event_parser::common::EventMetadata; +use crate::streaming::event_parser::protocols::meteora_damm_v2::events::{ + MeteoraDammV2AddLiquidityEvent, MeteoraDammV2ClosePositionEvent, + MeteoraDammV2CreatePositionEvent, MeteoraDammV2RemoveLiquidityEvent, MeteoraDammV2SwapEvent, +}; +use crate::streaming::event_parser::protocols::raydium_amm_v4::events::{ + RaydiumAmmV4DepositEvent, RaydiumAmmV4Initialize2Event, RaydiumAmmV4SwapEvent, + RaydiumAmmV4WithdrawEvent, RaydiumAmmV4WithdrawPnlEvent, +}; +use crate::streaming::event_parser::protocols::raydium_clmm::events::{ + RaydiumClmmClosePositionEvent, RaydiumClmmCollectFeeEvent, RaydiumClmmCreatePoolEvent, + RaydiumClmmDecreaseLiquidityV2Event, RaydiumClmmIncreaseLiquidityV2Event, + RaydiumClmmOpenPositionV2Event, RaydiumClmmOpenPositionWithToken22NftEvent, + RaydiumClmmSwapEvent, +}; +use crate::streaming::event_parser::protocols::raydium_cpmm::events::{ + RaydiumCpmmDepositEvent, RaydiumCpmmInitializeEvent, RaydiumCpmmSwapEvent, + RaydiumCpmmWithdrawEvent, +}; +use solana_sdk::pubkey::Pubkey; +pub(crate) fn meteora_damm_v2_swap_from_parser( + e: sol_parser_sdk::core::events::MeteoraDammV2SwapEvent, + meta: EventMetadata, +) -> MeteoraDammV2SwapEvent { + MeteoraDammV2SwapEvent { + metadata: meta, + pool: e.pool, + trade_direction: e.trade_direction, + collect_fee_mode: 0, + has_referral: e.has_referral, + amount_0: e.amount_in, + amount_1: 0, + swap_mode: 0, + included_fee_input_amount: e.actual_amount_in, + excluded_fee_input_amount: e.amount_in, + amount_left: 0, + output_amount: e.output_amount, + next_sqrt_price: e.next_sqrt_price, + trading_fee: e.lp_fee, + protocol_fee: e.protocol_fee, + partner_fee: e.partner_fee, + referral_fee: e.referral_fee, + included_transfer_fee_amount_in: 0, + included_transfer_fee_amount_out: 0, + excluded_transfer_fee_amount_out: 0, + current_timestamp: e.current_timestamp, + reserve_a_amount: 0, + reserve_b_amount: 0, + pool_authority: Pubkey::default(), + input_token_account: Pubkey::default(), + output_token_account: Pubkey::default(), + token_a_vault: e.token_a_vault, + token_b_vault: e.token_b_vault, + token_a_mint: e.token_a_mint, + token_b_mint: e.token_b_mint, + payer: Pubkey::default(), + token_a_program: e.token_a_program, + token_b_program: e.token_b_program, + referral_token_account: None, + event_authority: Pubkey::default(), + program: Pubkey::default(), + } +} + +pub(crate) fn raydium_cpmm_swap_from_parser( + e: sol_parser_sdk::core::events::RaydiumCpmmSwapEvent, + meta: EventMetadata, +) -> RaydiumCpmmSwapEvent { + RaydiumCpmmSwapEvent { + metadata: meta, + amount_in: e.input_amount, + minimum_amount_out: 0, + max_amount_in: e.input_amount, + amount_out: e.output_amount, + payer: Pubkey::default(), + authority: Pubkey::default(), + amm_config: Pubkey::default(), + pool_state: e.pool_id, + input_token_account: Pubkey::default(), + output_token_account: Pubkey::default(), + input_vault: Pubkey::default(), + output_vault: Pubkey::default(), + input_token_program: Pubkey::default(), + output_token_program: Pubkey::default(), + input_token_mint: Pubkey::default(), + output_token_mint: Pubkey::default(), + observation_state: Pubkey::default(), + } +} + +pub(crate) fn raydium_cpmm_deposit_from_parser( + e: sol_parser_sdk::core::events::RaydiumCpmmDepositEvent, + meta: EventMetadata, +) -> RaydiumCpmmDepositEvent { + RaydiumCpmmDepositEvent { + metadata: meta, + lp_token_amount: e.lp_token_amount, + maximum_token0_amount: e.token0_amount, + maximum_token1_amount: e.token1_amount, + owner: e.user, + authority: Pubkey::default(), + pool_state: e.pool, + owner_lp_token: Pubkey::default(), + token_0_account: Pubkey::default(), + token_1_account: Pubkey::default(), + token_0_vault: Pubkey::default(), + token_1_vault: Pubkey::default(), + token_program: Pubkey::default(), + token_program2022: Pubkey::default(), + vault_0_mint: Pubkey::default(), + vault_1_mint: Pubkey::default(), + lp_mint: Pubkey::default(), + } +} + +pub(crate) fn raydium_cpmm_withdraw_from_parser( + e: sol_parser_sdk::core::events::RaydiumCpmmWithdrawEvent, + meta: EventMetadata, +) -> RaydiumCpmmWithdrawEvent { + RaydiumCpmmWithdrawEvent { + metadata: meta, + lp_token_amount: e.lp_token_amount, + minimum_token0_amount: e.token0_amount, + minimum_token1_amount: e.token1_amount, + owner: e.user, + authority: Pubkey::default(), + pool_state: e.pool, + owner_lp_token: Pubkey::default(), + token_0_account: Pubkey::default(), + token_1_account: Pubkey::default(), + token_0_vault: Pubkey::default(), + token_1_vault: Pubkey::default(), + token_program: Pubkey::default(), + token_program2022: Pubkey::default(), + vault_0_mint: Pubkey::default(), + vault_1_mint: Pubkey::default(), + lp_mint: Pubkey::default(), + memo_program: Pubkey::default(), + } +} + +pub(crate) fn raydium_cpmm_initialize_from_parser( + e: sol_parser_sdk::core::events::RaydiumCpmmInitializeEvent, + meta: EventMetadata, +) -> RaydiumCpmmInitializeEvent { + RaydiumCpmmInitializeEvent { + metadata: meta, + init_amount0: e.init_amount0, + init_amount1: e.init_amount1, + open_time: 0, + creator: e.creator, + pool_state: e.pool, + ..Default::default() + } +} + +pub(crate) fn raydium_amm_v4_swap_from_parser( + e: sol_parser_sdk::core::events::RaydiumAmmV4SwapEvent, + meta: EventMetadata, +) -> RaydiumAmmV4SwapEvent { + RaydiumAmmV4SwapEvent { + metadata: meta, + amount_in: e.amount_in, + minimum_amount_out: e.minimum_amount_out, + max_amount_in: e.max_amount_in, + amount_out: e.amount_out, + token_program: e.token_program, + amm: e.amm, + amm_authority: e.amm_authority, + amm_open_orders: e.amm_open_orders, + amm_target_orders: e.amm_target_orders, + pool_coin_token_account: e.pool_coin_token_account, + pool_pc_token_account: e.pool_pc_token_account, + serum_program: e.serum_program, + serum_market: e.serum_market, + serum_bids: e.serum_bids, + serum_asks: e.serum_asks, + serum_event_queue: e.serum_event_queue, + serum_coin_vault_account: e.serum_coin_vault_account, + serum_pc_vault_account: e.serum_pc_vault_account, + serum_vault_signer: e.serum_vault_signer, + user_source_token_account: e.user_source_token_account, + user_destination_token_account: e.user_destination_token_account, + user_source_owner: e.user_source_owner, + } +} + +pub(crate) fn raydium_amm_v4_deposit_from_parser( + e: sol_parser_sdk::core::events::RaydiumAmmV4DepositEvent, + meta: EventMetadata, +) -> RaydiumAmmV4DepositEvent { + RaydiumAmmV4DepositEvent { + metadata: meta, + max_coin_amount: e.max_coin_amount, + max_pc_amount: e.max_pc_amount, + base_side: e.base_side, + token_program: e.token_program, + amm: e.amm, + amm_authority: e.amm_authority, + amm_open_orders: e.amm_open_orders, + amm_target_orders: e.amm_target_orders, + lp_mint_address: e.lp_mint_address, + pool_coin_token_account: e.pool_coin_token_account, + pool_pc_token_account: e.pool_pc_token_account, + serum_market: e.serum_market, + user_coin_token_account: e.user_coin_token_account, + user_pc_token_account: e.user_pc_token_account, + user_lp_token_account: e.user_lp_token_account, + user_owner: e.user_owner, + serum_event_queue: e.serum_event_queue, + } +} + +pub(crate) fn raydium_amm_v4_withdraw_from_parser( + e: sol_parser_sdk::core::events::RaydiumAmmV4WithdrawEvent, + meta: EventMetadata, +) -> RaydiumAmmV4WithdrawEvent { + RaydiumAmmV4WithdrawEvent { + metadata: meta, + amount: e.amount, + token_program: e.token_program, + amm: e.amm, + amm_authority: e.amm_authority, + amm_open_orders: e.amm_open_orders, + amm_target_orders: e.amm_target_orders, + lp_mint_address: e.lp_mint_address, + pool_coin_token_account: e.pool_coin_token_account, + pool_pc_token_account: e.pool_pc_token_account, + pool_withdraw_queue: e.pool_withdraw_queue, + pool_temp_lp_token_account: e.pool_temp_lp_token_account, + serum_program: e.serum_program, + serum_market: e.serum_market, + serum_coin_vault_account: e.serum_coin_vault_account, + serum_pc_vault_account: e.serum_pc_vault_account, + serum_vault_signer: e.serum_vault_signer, + user_lp_token_account: e.user_lp_token_account, + user_coin_token_account: e.user_coin_token_account, + user_pc_token_account: e.user_pc_token_account, + user_owner: e.user_owner, + serum_event_queue: e.serum_event_queue, + serum_bids: e.serum_bids, + serum_asks: e.serum_asks, + } +} + +pub(crate) fn raydium_amm_v4_withdraw_pnl_from_parser( + e: sol_parser_sdk::core::events::RaydiumAmmV4WithdrawPnlEvent, + meta: EventMetadata, +) -> RaydiumAmmV4WithdrawPnlEvent { + RaydiumAmmV4WithdrawPnlEvent { + metadata: meta, + token_program: e.token_program, + amm: e.amm, + amm_config: e.amm_config, + amm_authority: e.amm_authority, + amm_open_orders: e.amm_open_orders, + pool_coin_token_account: e.pool_coin_token_account, + pool_pc_token_account: e.pool_pc_token_account, + coin_pnl_token_account: e.coin_pnl_token_account, + pc_pnl_token_account: e.pc_pnl_token_account, + pnl_owner_account: e.pnl_owner, + amm_target_orders: e.amm_target_orders, + serum_program: e.serum_program, + serum_market: e.serum_market, + serum_event_queue: e.serum_event_queue, + serum_coin_vault_account: e.serum_coin_vault_account, + serum_pc_vault_account: e.serum_pc_vault_account, + serum_vault_signer: e.serum_vault_signer, + } +} + +pub(crate) fn raydium_amm_v4_initialize2_from_parser( + e: sol_parser_sdk::core::events::RaydiumAmmV4Initialize2Event, + meta: EventMetadata, +) -> RaydiumAmmV4Initialize2Event { + RaydiumAmmV4Initialize2Event { + metadata: meta, + nonce: e.nonce, + open_time: e.open_time, + init_pc_amount: e.init_pc_amount, + init_coin_amount: e.init_coin_amount, + token_program: e.token_program, + spl_associated_token_account: e.spl_associated_token_account, + system_program: e.system_program, + rent: e.rent, + amm: e.amm, + amm_authority: e.amm_authority, + amm_open_orders: e.amm_open_orders, + lp_mint: e.lp_mint, + coin_mint: e.coin_mint, + pc_mint: e.pc_mint, + pool_coin_token_account: e.pool_coin_token_account, + pool_pc_token_account: e.pool_pc_token_account, + pool_withdraw_queue: e.pool_withdraw_queue, + amm_target_orders: e.amm_target_orders, + pool_temp_lp: e.pool_temp_lp, + serum_program: e.serum_program, + serum_market: e.serum_market, + user_wallet: e.user_wallet, + user_token_coin: e.user_token_coin, + user_token_pc: e.user_token_pc, + user_lp_token_account: e.user_lp_token_account, + } +} + +pub(crate) fn raydium_clmm_swap_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmSwapEvent, + meta: EventMetadata, +) -> RaydiumClmmSwapEvent { + let (amount, other_amount_threshold, input_token_account, output_token_account) = + if e.zero_for_one { + (e.amount_0, e.amount_1, e.token_account_0, e.token_account_1) + } else { + (e.amount_1, e.amount_0, e.token_account_1, e.token_account_0) + }; + RaydiumClmmSwapEvent { + metadata: meta, + amount, + other_amount_threshold, + sqrt_price_limit_x64: e.sqrt_price_x64, + is_base_input: e.zero_for_one, + payer: e.sender, + pool_state: e.pool_state, + input_token_account, + output_token_account, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_create_pool_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmCreatePoolEvent, + meta: EventMetadata, +) -> RaydiumClmmCreatePoolEvent { + RaydiumClmmCreatePoolEvent { + metadata: meta, + sqrt_price_x64: e.sqrt_price_x64, + open_time: e.open_time, + pool_creator: e.creator, + pool_state: e.pool, + token_mint0: e.token_0_mint, + token_mint1: e.token_1_mint, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_open_position_v2_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmOpenPositionEvent, + meta: EventMetadata, +) -> RaydiumClmmOpenPositionV2Event { + RaydiumClmmOpenPositionV2Event { + metadata: meta, + tick_lower_index: e.tick_lower_index, + tick_upper_index: e.tick_upper_index, + liquidity: e.liquidity, + payer: e.user, + position_nft_owner: e.user, + position_nft_mint: e.position_nft_mint, + pool_state: e.pool, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_open_position_token22_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmOpenPositionWithTokenExtNftEvent, + meta: EventMetadata, +) -> RaydiumClmmOpenPositionWithToken22NftEvent { + RaydiumClmmOpenPositionWithToken22NftEvent { + metadata: meta, + tick_lower_index: e.tick_lower_index, + tick_upper_index: e.tick_upper_index, + liquidity: e.liquidity, + payer: e.user, + position_nft_owner: e.user, + position_nft_mint: e.position_nft_mint, + pool_state: e.pool, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_close_position_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmClosePositionEvent, + meta: EventMetadata, +) -> RaydiumClmmClosePositionEvent { + RaydiumClmmClosePositionEvent { + metadata: meta, + nft_owner: e.user, + position_nft_mint: e.position_nft_mint, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_increase_liquidity_v2_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmIncreaseLiquidityEvent, + meta: EventMetadata, +) -> RaydiumClmmIncreaseLiquidityV2Event { + RaydiumClmmIncreaseLiquidityV2Event { + metadata: meta, + liquidity: e.liquidity, + amount0_max: e.amount0_max, + amount1_max: e.amount1_max, + nft_owner: e.user, + pool_state: e.pool, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_decrease_liquidity_v2_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmDecreaseLiquidityEvent, + meta: EventMetadata, +) -> RaydiumClmmDecreaseLiquidityV2Event { + RaydiumClmmDecreaseLiquidityV2Event { + metadata: meta, + liquidity: e.liquidity, + amount0_min: e.amount0_min, + amount1_min: e.amount1_min, + nft_owner: e.user, + pool_state: e.pool, + ..Default::default() + } +} + +pub(crate) fn raydium_clmm_collect_fee_from_parser( + e: sol_parser_sdk::core::events::RaydiumClmmCollectFeeEvent, + meta: EventMetadata, +) -> RaydiumClmmCollectFeeEvent { + RaydiumClmmCollectFeeEvent { + metadata: meta, + pool_state: e.pool_state, + position_nft_mint: e.position_nft_mint, + amount_0: e.amount_0, + amount_1: e.amount_1, + } +} + +pub(crate) fn meteora_damm_v2_add_liquidity_from_pb( + e: sol_parser_sdk::core::events::MeteoraDammV2AddLiquidityEvent, + meta: EventMetadata, +) -> MeteoraDammV2AddLiquidityEvent { + MeteoraDammV2AddLiquidityEvent { + metadata: meta, + pool: e.pool, + position: e.position, + owner: e.owner, + token_a_amount: e.token_a_amount, + token_b_amount: e.token_b_amount, + liquidity_delta: e.liquidity_delta, + token_a_amount_threshold: e.token_a_amount_threshold, + token_b_amount_threshold: e.token_b_amount_threshold, + total_amount_a: e.total_amount_a, + total_amount_b: e.total_amount_b, + } +} + +pub(crate) fn meteora_damm_v2_remove_liquidity_from_pb( + e: sol_parser_sdk::core::events::MeteoraDammV2RemoveLiquidityEvent, + meta: EventMetadata, +) -> MeteoraDammV2RemoveLiquidityEvent { + MeteoraDammV2RemoveLiquidityEvent { + metadata: meta, + pool: e.pool, + position: e.position, + owner: e.owner, + token_a_amount: e.token_a_amount, + token_b_amount: e.token_b_amount, + liquidity_delta: e.liquidity_delta, + token_a_amount_threshold: e.token_a_amount_threshold, + token_b_amount_threshold: e.token_b_amount_threshold, + } +} + +pub(crate) fn meteora_damm_v2_create_position_from_pb( + e: sol_parser_sdk::core::events::MeteoraDammV2CreatePositionEvent, + meta: EventMetadata, +) -> MeteoraDammV2CreatePositionEvent { + MeteoraDammV2CreatePositionEvent { + metadata: meta, + pool: e.pool, + owner: e.owner, + position: e.position, + position_nft_mint: e.position_nft_mint, + } +} + +pub(crate) fn meteora_damm_v2_close_position_from_pb( + e: sol_parser_sdk::core::events::MeteoraDammV2ClosePositionEvent, + meta: EventMetadata, +) -> MeteoraDammV2ClosePositionEvent { + MeteoraDammV2ClosePositionEvent { + metadata: meta, + pool: e.pool, + owner: e.owner, + position: e.position, + position_nft_mint: e.position_nft_mint, + } +} diff --git a/src/streaming/rpc_parse.rs b/src/streaming/rpc_parse.rs new file mode 100644 index 0000000..1b846c7 --- /dev/null +++ b/src/streaming/rpc_parse.rs @@ -0,0 +1,98 @@ +//! Single RPC transaction parsing backed by `sol-parser-sdk`, adapted to streamer +//! [`DexEvent`](crate::streaming::event_parser::DexEvent). +//! +//! - Filter mapping uses [`crate::streaming::event_parser::common::filter::build_sdk_parse_event_filter`]. +//! - Works with an existing [`EncodedConfirmedTransactionWithStatusMeta`], async fetch, or blocking +//! [`RpcClient`] fetch. +//! +//! Async callers can also fetch with their own client and call +//! [`parse_encoded_rpc_transaction_as_streamer_events`]. + +use prost_types::Timestamp; +use sol_parser_sdk::{parse_rpc_transaction, parse_transaction_from_rpc}; +use solana_client::rpc_client::RpcClient; +use solana_client::rpc_config::RpcTransactionConfig; +use solana_sdk::signature::Signature; +use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding}; + +use crate::streaming::event_parser::common::filter::{ + build_sdk_parse_event_filter, EventTypeFilter, +}; +use crate::streaming::event_parser::{DexEvent, Protocol}; +use crate::streaming::parser_sdk_bridge::adapt_parser_events_list; +pub use sol_parser_sdk::ParseError; + +/// Parse a transaction payload already returned by RPC. +/// +/// `recv_wall_us` should be the caller's UNIX microsecond receive timestamp. +pub fn parse_encoded_rpc_transaction_as_streamer_events( + rpc_tx: &EncodedConfirmedTransactionWithStatusMeta, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Result, ParseError> { + let sdk_filter = build_sdk_parse_event_filter(event_type_filter); + let pb_events = parse_rpc_transaction(rpc_tx, sdk_filter.as_ref())?; + let block_ts = rpc_tx.block_time.map(|sec| Timestamp { seconds: sec, nanos: 0 }); + Ok(adapt_parser_events_list( + pb_events, + block_ts.as_ref(), + recv_wall_us, + protocols, + event_type_filter, + )) +} + +/// Blocking RPC fetch by signature, then adapt SDK events to streamer events. +pub fn fetch_rpc_transaction_as_streamer_events( + rpc_client: &RpcClient, + signature: &Signature, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Result, ParseError> { + let sdk_filter = build_sdk_parse_event_filter(event_type_filter); + let pb_events = parse_transaction_from_rpc(rpc_client, signature, sdk_filter.as_ref())?; + // The SDK already writes block_time_us into each event; adapter falls back to it when + // no prost Timestamp is available. + Ok(adapt_parser_events_list(pb_events, None, recv_wall_us, protocols, event_type_filter)) +} + +/// Async RPC fetch using the same request config as the SDK blocking helper. +pub async fn fetch_rpc_transaction_as_streamer_events_async( + rpc_client: &solana_client::nonblocking::rpc_client::RpcClient, + signature: &Signature, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Result, ParseError> { + let config = RpcTransactionConfig { + encoding: Some(UiTransactionEncoding::Base64), + commitment: None, + max_supported_transaction_version: Some(0), + }; + let rpc_tx = rpc_client + .get_transaction_with_config(signature, config) + .await + .map_err(|e| map_async_rpc_err(e.to_string()))?; + parse_encoded_rpc_transaction_as_streamer_events( + &rpc_tx, + recv_wall_us, + protocols, + event_type_filter, + ) +} + +#[inline] +fn map_async_rpc_err(msg: String) -> ParseError { + if msg.contains("invalid type: null") + && msg.contains("EncodedConfirmedTransactionWithStatusMeta") + { + ParseError::RpcError(format!( + "Transaction not found (RPC returned null). Common causes: 1) Transaction is too old and pruned (use an archive RPC). 2) Wrong network or invalid signature. Try an archive endpoint or a more recent tx. Original: {}", + msg + )) + } else { + ParseError::RpcError(msg) + } +} diff --git a/src/streaming/sdk_bridge.rs b/src/streaming/sdk_bridge.rs new file mode 100644 index 0000000..1bc080f --- /dev/null +++ b/src/streaming/sdk_bridge.rs @@ -0,0 +1,64 @@ +//! Public extension bridge for advanced `sol-parser-sdk` interop. +//! +//! Existing streamer subscription APIs remain unchanged. Use this module when code needs direct +//! access to raw SDK parsers/events but still wants streamer `DexEvent` compatibility. +use prost_types::Timestamp; + +use crate::streaming::event_parser::common::filter::{ + build_sdk_parse_event_filter, EventTypeFilter, +}; +use crate::streaming::event_parser::{DexEvent, Protocol}; +use crate::streaming::grpc::AccountPretty; + +pub use sol_parser_sdk as raw; + +pub type SdkDexEvent = sol_parser_sdk::DexEvent; +pub type SdkEventTypeFilter = sol_parser_sdk::grpc::types::EventTypeFilter; + +pub fn event_type_filter_to_sdk(filter: Option<&EventTypeFilter>) -> Option { + build_sdk_parse_event_filter(filter) +} + +pub fn adapt_event( + sdk_event: SdkDexEvent, + block_time: Option<&Timestamp>, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Option { + crate::streaming::parser_sdk_bridge::adapt_parser_event( + sdk_event, + block_time, + recv_wall_us, + protocols, + event_type_filter, + ) +} + +pub fn adapt_events( + sdk_events: Vec, + block_time: Option<&Timestamp>, + recv_wall_us: i64, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Vec { + crate::streaming::parser_sdk_bridge::adapt_parser_events_list( + sdk_events, + block_time, + recv_wall_us, + protocols, + event_type_filter, + ) +} + +pub fn parse_account_event( + account: &AccountPretty, + protocols: &[Protocol], + event_type_filter: Option<&EventTypeFilter>, +) -> Option { + crate::streaming::parser_sdk_bridge::parse_sdk_account_event( + account, + protocols, + event_type_filter, + ) +} diff --git a/src/streaming/shred/connection.rs b/src/streaming/shred/connection.rs index 8013930..fc78ecd 100644 --- a/src/streaming/shred/connection.rs +++ b/src/streaming/shred/connection.rs @@ -12,6 +12,7 @@ use crate::streaming::common::{ #[derive(Clone)] pub struct ShredStreamGrpc { pub shredstream_client: Arc>, + pub sdk_shredstream_client: Arc, pub config: StreamClientConfig, pub subscription_handle: Arc>>, } @@ -25,9 +26,20 @@ impl ShredStreamGrpc { /// 创建客户端,使用自定义配置 pub async fn new_with_config(endpoint: String, config: StreamClientConfig) -> AnyResult { let shredstream_client = ShredstreamProxyClient::connect(endpoint.clone()).await?; + let sdk_config = sol_parser_sdk::shredstream::ShredStreamConfig { + connection_timeout_ms: config.connection.connect_timeout.saturating_mul(1000), + request_timeout_ms: config.connection.request_timeout.saturating_mul(1000), + max_decoding_message_size: config.connection.max_decoding_message_size, + ..Default::default() + }; + let sdk_shredstream_client = + sol_parser_sdk::shredstream::ShredStreamClient::new_with_config(endpoint, sdk_config) + .await + .map_err(|e| anyhow::anyhow!(e.to_string()))?; MetricsManager::init(config.enable_metrics); Ok(Self { shredstream_client: Arc::new(shredstream_client), + sdk_shredstream_client: Arc::new(sdk_shredstream_client), config, subscription_handle: Arc::new(Mutex::new(None)), }) @@ -65,6 +77,7 @@ impl ShredStreamGrpc { /// 停止当前订阅 pub async fn stop(&self) { + self.sdk_shredstream_client.stop().await; let mut handle_guard = self.subscription_handle.lock().await; if let Some(handle) = handle_guard.take() { handle.stop(); diff --git a/src/streaming/shred/pool.rs b/src/streaming/shred/pool.rs index b1c127d..81ad6a2 100644 --- a/src/streaming/shred/pool.rs +++ b/src/streaming/shred/pool.rs @@ -1,11 +1,10 @@ -use std::sync::{Arc, Mutex}; +use solana_sdk::transaction::VersionedTransaction; use std::collections::VecDeque; use std::ops::DerefMut; -use solana_sdk::transaction::VersionedTransaction; +use std::sync::{Arc, Mutex}; use super::TransactionWithSlot; - /// TransactionWithSlot 对象池 pub struct TransactionWithSlotPool { pool: Arc>>>, @@ -31,10 +30,10 @@ impl TransactionWithSlotPool { None => Box::new(TransactionWithSlot::default()), }; - PooledTransactionWithSlot { - transaction, - pool: Arc::clone(&self.pool), - max_size: self.max_size + PooledTransactionWithSlot { + transaction, + pool: Arc::clone(&self.pool), + max_size: self.max_size, } } } diff --git a/src/streaming/shred_stream.rs b/src/streaming/shred_stream.rs index bff8f36..40a211e 100755 --- a/src/streaming/shred_stream.rs +++ b/src/streaming/shred_stream.rs @@ -1,18 +1,15 @@ +//! ShredStream 订阅入口:底层订阅与热路径解析直接复用 `sol-parser-sdk::shredstream`, +//! 本模块只负责把 SDK `DexEvent` 适配回 streamer 原有 callback API。 use std::sync::Arc; -use futures::StreamExt; use solana_sdk::pubkey::Pubkey; use crate::common::AnyResult; -use crate::protos::shredstream::SubscribeEntriesRequest; -use crate::streaming::common::{process_shred_transaction, SubscriptionHandle}; +use crate::streaming::common::{MetricsEventType, MetricsManager, SubscriptionHandle}; use crate::streaming::event_parser::common::filter::EventTypeFilter; -use crate::streaming::event_parser::common::high_performance_clock::get_high_perf_clock; -use crate::streaming::event_parser::{Protocol, DexEvent}; -use crate::streaming::grpc::MetricsManager; -use crate::streaming::shred::pool::factory; -use log::error; -use solana_entry::entry::Entry as SolanaEntry; +use crate::streaming::event_parser::common::high_performance_clock::elapsed_micros_since; +use crate::streaming::event_parser::{DexEvent, Protocol}; +use crate::streaming::parser_sdk_bridge::adapt_parser_event; use super::ShredStreamGrpc; @@ -37,49 +34,50 @@ impl ShredStreamGrpc { metrics_handle = MetricsManager::global().start_auto_monitoring().await; } - // 启动流处理 - let mut client = (*self.shredstream_client).clone(); - let request = tonic::Request::new(SubscribeEntriesRequest {}); - let mut stream = client.subscribe_entries(request).await?.into_inner(); + let queue = self + .sdk_shredstream_client + .subscribe() + .await + .map_err(|e| anyhow::anyhow!(e.to_string()))?; // Wrap callback once before the async block let callback = Arc::new(callback); let stream_task = tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Ok(entries) = bincode::deserialize::>(&msg.entries) { - for entry in entries { - for (tx_index, transaction) in entry.transactions.iter().enumerate() { - let transaction_with_slot = - factory::create_transaction_with_slot_pooled( - transaction.clone(), - msg.slot, - get_high_perf_clock(), - Some(tx_index as u64), - ); - // Process transaction - clone Arc and Vec for each call - if let Err(e) = process_shred_transaction( - transaction_with_slot, - &protocols, - event_type_filter.as_ref(), - callback.clone(), - bot_wallet, - ) - .await - { - error!("Error handling message: {e:?}"); - } - } - } - } - continue; - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } + loop { + let Some(sdk_event) = queue.pop() else { + tokio::task::yield_now().await; + continue; + }; + + MetricsManager::global().add_tx_process_count(); + let recv_wall_us = sdk_event.metadata().grpc_recv_us; + if let Some(mut event) = adapt_parser_event( + sdk_event, + None, + recv_wall_us, + &protocols, + event_type_filter.as_ref(), + ) { + event.metadata_mut().handle_us = elapsed_micros_since(event.metadata().recv_us); + event = + crate::streaming::event_parser::core::event_parser::helpers::process_event( + event, bot_wallet, + ); + let metadata = event.metadata(); + let processing_time_us = metadata.handle_us as f64; + let recv_us = metadata.recv_us; + let block_time_ms = metadata.block_time_ms; + + callback(event); + + MetricsManager::global().update_metrics_with_latency( + MetricsEventType::Transaction, + 1, + processing_time_us, + recv_us, + block_time_ms, + ); } } }); diff --git a/src/streaming/yellowstone_grpc.rs b/src/streaming/yellowstone_grpc.rs index 97762d3..b3b5153 100644 --- a/src/streaming/yellowstone_grpc.rs +++ b/src/streaming/yellowstone_grpc.rs @@ -4,17 +4,17 @@ use crate::streaming::common::{ SubscriptionHandle, }; use crate::streaming::event_parser::common::filter::EventTypeFilter; -use crate::streaming::event_parser::{Protocol, DexEvent}; +use crate::streaming::event_parser::{DexEvent, Protocol}; use crate::streaming::grpc::pool::factory; use crate::streaming::grpc::{EventPretty, SubscriptionManager}; use anyhow::anyhow; -use std::time::{SystemTime, UNIX_EPOCH}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use log::error; use solana_sdk::pubkey::Pubkey; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::Mutex; use yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof; use yellowstone_grpc_proto::geyser::{