feat: bridge streamer to sol-parser-sdk

This commit is contained in:
0xfnzero
2026-05-15 05:09:29 +08:00
parent e29a2e4f5a
commit ab84249f4a
85 changed files with 6350 additions and 1362 deletions
+16 -6
View File
@@ -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 }
+6
View File
@@ -2,6 +2,12 @@
与 gRPC 订阅相比,shredstream 路径存在以下限制和解析差异,使用时请注意。
## gRPCYellowstone)说明
当订阅返回的交易带有完整 `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 |
+19 -17
View File
@@ -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<InnerInstructions> = 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,
+9 -13
View File
@@ -61,8 +61,9 @@ async fn main() -> Result<()> {
let mut inner_instructions_vec: Vec<InnerInstructions> = 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,
+19 -22
View File
@@ -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(
+6 -3
View File
@@ -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<dyn std::error::Error>> {
@@ -14,7 +16,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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(),
)?;
@@ -73,7 +73,8 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
};
// 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...");
+5 -2
View File
@@ -41,7 +41,9 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
TransactionFilter { account_include, account_exclude, account_required };
let nonce_account = std::env::var("NONCE_ACCOUNT").unwrap_or_else(|_| {
eprintln!("Usage: NONCE_ACCOUNT=<pubkey> cargo run --example nonce_listen_example --release");
eprintln!(
"Usage: NONCE_ACCOUNT=<pubkey> 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<dyn std::error::Error>> {
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...");
+13 -17
View File
@@ -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<InnerInstructions> = 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,
+15 -18
View File
@@ -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<InnerInstructions> = 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,
+15 -18
View File
@@ -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<InnerInstructions> = 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,
+159 -113
View File
@@ -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<InnerInstructions> = 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<Pubkey> = 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::<Pubkey>().ok()).chain(
loaded_addresses.readonly.iter().filter_map(|s| s.parse::<Pubkey>().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<InnerInstructions> = 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<Pubkey> = 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::<Pubkey>().ok())
.chain(
loaded_addresses
.readonly
.iter()
.filter_map(|s| s.parse::<Pubkey>().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);
+8 -5
View File
@@ -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<dyn std::error::Error>> {
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,
)?;
+17 -28
View File
@@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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
);
}
+6 -3
View File
@@ -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<dyn std::error::Error>> {
@@ -17,7 +19,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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,
)?;
@@ -75,7 +75,8 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
};
// 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...");
+6 -3
View File
@@ -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<dyn std::error::Error>> {
@@ -17,7 +19,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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,
)?;
+1 -1
View File
@@ -1,5 +1,5 @@
use solana_streamer_sdk::streaming::{
event_parser::{Protocol, DexEvent},
event_parser::{DexEvent, Protocol},
shred::StreamClientConfig,
ShredStreamGrpc,
};
+2 -1
View File
@@ -47,7 +47,8 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
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...");
+2 -1
View File
@@ -50,7 +50,8 @@ async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
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...");
+10
View File
@@ -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,
};
+54 -111
View File
@@ -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<T> {
inner: tonic::client::Grpc<T>,
@@ -94,9 +94,8 @@ pub mod shredstream_client {
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
<T as tonic::codegen::Service<http::Request<tonic::body::Body>>>::Error:
Into<StdError> + 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<super::Heartbeat>,
) -> std::result::Result<
tonic::Response<super::HeartbeatResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
) -> std::result::Result<tonic::Response<super::HeartbeatResponse>, 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<super::Heartbeat>,
) -> std::result::Result<
tonic::Response<super::HeartbeatResponse>,
tonic::Status,
>;
) -> std::result::Result<tonic::Response<super::HeartbeatResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct ShredstreamServer<T> {
@@ -201,10 +188,7 @@ pub mod shredstream_server {
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
@@ -259,13 +243,9 @@ pub mod shredstream_server {
"/shredstream.Shredstream/SendHeartbeat" => {
#[allow(non_camel_case_types)]
struct SendHeartbeatSvc<T: Shredstream>(pub Arc<T>);
impl<T: Shredstream> tonic::server::UnaryService<super::Heartbeat>
for SendHeartbeatSvc<T> {
impl<T: Shredstream> tonic::server::UnaryService<super::Heartbeat> for SendHeartbeatSvc<T> {
type Response = super::HeartbeatResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::Heartbeat>,
@@ -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<T> {
inner: tonic::client::Grpc<T>,
@@ -393,9 +364,8 @@ pub mod shredstream_proxy_client {
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
<T as tonic::codegen::Service<http::Request<tonic::body::Body>>>::Error:
Into<StdError> + std::marker::Send + std::marker::Sync,
{
ShredstreamProxyClient::new(InterceptedService::new(inner, interceptor))
}
@@ -437,23 +407,16 @@ pub mod shredstream_proxy_client {
tonic::Response<tonic::codec::Streaming<super::Entry>>,
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<super::Entry, tonic::Status>,
>
+ std::marker::Send
> + std::marker::Send
+ 'static;
async fn subscribe_entries(
&self,
request: tonic::Request<super::SubscribeEntriesRequest>,
) -> std::result::Result<
tonic::Response<Self::SubscribeEntriesStream>,
tonic::Status,
>;
) -> std::result::Result<tonic::Response<Self::SubscribeEntriesStream>, tonic::Status>;
}
#[derive(Debug)]
pub struct ShredstreamProxyServer<T> {
@@ -506,10 +465,7 @@ pub mod shredstream_proxy_server {
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
@@ -564,25 +520,21 @@ pub mod shredstream_proxy_server {
"/shredstream.ShredstreamProxy/SubscribeEntries" => {
#[allow(non_camel_case_types)]
struct SubscribeEntriesSvc<T: ShredstreamProxy>(pub Arc<T>);
impl<
T: ShredstreamProxy,
> tonic::server::ServerStreamingService<
super::SubscribeEntriesRequest,
> for SubscribeEntriesSvc<T> {
impl<T: ShredstreamProxy>
tonic::server::ServerStreamingService<super::SubscribeEntriesRequest>
for SubscribeEntriesSvc<T>
{
type Response = super::Entry;
type ResponseStream = T::SubscribeEntriesStream;
type Future = BoxFuture<
tonic::Response<Self::ResponseStream>,
tonic::Status,
>;
type Future =
BoxFuture<tonic::Response<Self::ResponseStream>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::SubscribeEntriesRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as ShredstreamProxy>::subscribe_entries(&inner, request)
.await
<T as ShredstreamProxy>::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)
}),
}
}
}
+30 -8
View File
@@ -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<dyn Fn(DexEvent) + Send + Sync>,
@@ -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,
);
}
+4 -4
View File
@@ -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::*;
+393 -5
View File
@@ -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<EventType>,
pub exclude: Vec<EventType>,
}
impl EventTypeFilter {
#[inline]
pub fn all() -> Self {
Self::default()
}
#[inline]
pub fn include_only(include: impl Into<Vec<EventType>>) -> Self {
Self { include: include.into(), exclude: Vec::new() }
}
#[inline]
pub fn exclude_only(exclude: impl Into<Vec<EventType>>) -> Self {
Self { include: Vec::new(), exclude: exclude.into() }
}
#[inline]
pub fn include_exclude(
include: impl Into<Vec<EventType>>,
exclude: impl Into<Vec<EventType>>,
) -> 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<SdkGrpcEventTypeFilter> {
let f = filter?;
if !f.exclude.is_empty() && f.include.is_empty() {
let mut raw: Vec<SdkGrpcEventType> = 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<SdkGrpcEventType> = 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<SdkGrpcEventType>) {
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<SdkGrpcEventType> {
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());
}
}
@@ -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<HighPerformanceClock> =
std::sync::OnceLock::new();
static HIGH_PERF_CLOCK: std::sync::OnceLock<HighPerformanceClock> = std::sync::OnceLock::new();
/// 获取全局高性能时钟实例(最简单的实现)
#[inline(always)]
+69 -12
View File
@@ -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<Pubkey> =
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<Pubkey> = 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<Pubkey>, Option<Pubkey>,
Option<Pubkey>, Option<Pubkey>,
Option<Pubkey>, Option<Pubkey>,
Option<Pubkey>,
Option<Pubkey>,
Option<Pubkey>,
Option<Pubkey>,
Option<Pubkey>,
Option<Pubkey>,
) {
let mut swap_data = SwapData::default();
let mut from_mint: Option<Pubkey> = None;
@@ -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::<Mint2022>::unpack(&account.data) {
let mut metadata = metadata.clone();
metadata.event_type = EventType::TokenInfo;
let mut event = TokenInfoEvent {
metadata,
pubkey,
+52 -10
View File
@@ -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)
}
@@ -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<u64>,
block_time: Option<Timestamp>,
recv_us: i64,
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> 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<u8>> = 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<u8>> =
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<Pubkey> = 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<u64>,
block_time: Option<Timestamp>,
recv_us: i64,
accounts: &[Pubkey],
inner_instructions: &[InnerInstructions],
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> 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<Pubkey> = 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<u64>,
block_time: Option<Timestamp>,
recv_us: i64,
accounts: &[Pubkey],
inner_instructions: &[yellowstone_grpc_proto::prelude::InnerInstructions],
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
recent_blockhash: Option<String>,
callback: Arc<dyn for<'a> 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<Timestamp>,
recv_us: i64,
outer_index: i64,
inner_index: Option<i64>,
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
recent_blockhash: Option<&str>,
inner_instructions: Option<&yellowstone_grpc_proto::prelude::InnerInstructions>,
callback: Arc<dyn for<'a> 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<Pubkey> = 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<DexEvent> = 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<Timestamp>,
recv_us: i64,
outer_index: i64,
inner_index: Option<i64>,
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
recent_blockhash: Option<&str>,
inner_instructions: Option<&InnerInstructions>,
callback: Arc<dyn for<'a> 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<Pubkey> = 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<DexEvent> = 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<Pubkey>) -> 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,
}
}
}
@@ -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<Timestamp>,
recv_us: i64,
outer_index: i64,
inner_index: Option<i64>,
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
recent_blockhash: Option<&str>,
inner_instructions: Option<&InnerInstructions>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> 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<Pubkey> = 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<DexEvent> = 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(())
}
@@ -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<u64>,
block_time: Option<Timestamp>,
recv_us: i64,
accounts: &[Pubkey],
inner_instructions: &[InnerInstructions],
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> anyhow::Result<()> {
let compiled_instructions = transaction.message.instructions();
let recent_blockhash = Some(transaction.message.recent_blockhash().to_string());
let mut accounts: Vec<Pubkey> = 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(())
}
@@ -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;
@@ -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<Timestamp>,
recv_us: i64,
outer_index: i64,
inner_index: Option<i64>,
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
recent_blockhash: Option<&str>,
inner_instructions: Option<&yellowstone_grpc_proto::prelude::InnerInstructions>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> 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<Pubkey> = 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<DexEvent> = 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(())
}
@@ -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,
}
@@ -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<u64>,
block_time: Option<Timestamp>,
recv_us: i64,
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> 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<Pubkey> {
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<Pubkey>, 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<u64>,
block_time: Option<Timestamp>,
recv_us: i64,
accounts: &[Pubkey],
inner_instructions: &[yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions],
bot_wallet: Option<Pubkey>,
tx_index: Option<u64>,
recent_blockhash: Option<String>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> 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(())
}
@@ -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;
@@ -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<Pubkey>) -> 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,
}
}
@@ -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<u64>,
block_time: Option<prost_types::Timestamp>,
recv_us: i64,
bot_wallet: Option<solana_sdk::pubkey::Pubkey>,
tx_index: Option<u64>,
callback: std::sync::Arc<dyn Fn(crate::streaming::event_parser::DexEvent) + Send + Sync>,
) -> 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<u64>,
block_time: Option<prost_types::Timestamp>,
recv_us: i64,
accounts: &[solana_sdk::pubkey::Pubkey],
inner_instructions: &[solana_transaction_status::InnerInstructions],
bot_wallet: Option<solana_sdk::pubkey::Pubkey>,
tx_index: Option<u64>,
callback: std::sync::Arc<dyn Fn(crate::streaming::event_parser::DexEvent) + Send + Sync>,
) -> 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
}
}
+29 -18
View File
@@ -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<Signature> = self.signature_data.iter()
.take(CLEANUP_BATCH_SIZE)
.map(|entry| *entry.key())
.collect();
let signatures_to_remove: Vec<Signature> =
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<Pubkey> {
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<Pubkey> {
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<GlobalState> =
std::sync::LazyLock::new(GlobalState::new);
static GLOBAL_STATE: std::sync::LazyLock<GlobalState> = std::sync::LazyLock::new(GlobalState::new);
/// Get global state instance
pub fn get_global_state() -> &'static GlobalState {
@@ -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;
}
_ => {}
},
_ => {}
}
}
+2 -2
View File
@@ -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;
pub mod merger_event;
@@ -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<HashMap<CacheKey, Arc<Vec<Pubkey>>>>,
> = LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
static GLOBAL_PROGRAM_IDS_CACHE: LazyLock<std::sync::RwLock<HashMap<CacheKey, Arc<Vec<Pubkey>>>>> =
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,
}
+85 -1
View File
@@ -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,
);
+9 -1
View File
@@ -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;
pub use protocols::types::Protocol;
@@ -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,
@@ -1 +1 @@
pub mod block_meta_event;
pub mod block_meta_event;
@@ -25,24 +25,14 @@ pub fn parse_bonk_instruction_data(
metadata: EventMetadata,
) -> Option<DexEvent> {
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<DexEvent> {
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<crate::streaming::event_parser::DexEvent> {
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<DexEvent> {
fn parse_pool_create_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// 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) {
@@ -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<PlatformConfig> {
if data.len() < PLATFORM_CONFIG_SIZE {
@@ -374,6 +374,68 @@ pub struct MeteoraDammV2InitializePoolWithDynamicConfigEvent {
pub config: Pubkey,
}
/// DAMM v2 Add Liquidityparser-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
@@ -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;
@@ -323,7 +323,8 @@ pub fn pumpfun_trade_event_log_decode(data: &[u8]) -> Option<PumpFunTradeEvent>
if data.len() < PUMPFUN_TRADE_EVENT_LOG_SIZE {
return None;
}
let mut event = borsh::from_slice::<PumpFunTradeEvent>(&data[..PUMPFUN_TRADE_EVENT_LOG_SIZE]).ok()?;
let mut event =
borsh::from_slice::<PumpFunTradeEvent>(&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<PumpFunTradeEvent>
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<Pubkey>,
pub sharing_config: Pubkey,
pub admin: Pubkey,
pub initial_shareholders: Vec<PumpFeesShareholder>,
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<PumpFeesShareholder>,
pub new_admin: Pubkey,
pub new_shareholders: Vec<PumpFeesShareholder>,
}
#[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<PumpFeesFeeTier>,
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<PumpFeesShareholder>,
}
#[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<PumpFeesFeeTier>,
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<PumpFunMigrateEvent> {
@@ -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<DexEvent> {
metadata.event_type = EventType::PumpFunBuy;
metadata.event_type = EventType::PumpFunBuyExactSolIn;
if data.len() < 16 || accounts.len() < 16 {
return None;
@@ -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<Global> {
if data.len() < GLOBAL_SIZE {
@@ -93,7 +93,8 @@ pub fn pump_swap_buy_event_log_decode(data: &[u8]) -> Option<PumpSwapBuyEvent> {
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<PumpSwapSellEvent>
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)
@@ -25,11 +25,11 @@ pub fn parse_pumpswap_instruction_data(
) -> Option<DexEvent> {
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<crate::streaming::event_parser::DexEvent> {
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<DexEvent> {
fn parse_create_pool_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// 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 }))
@@ -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;
@@ -22,18 +22,14 @@ pub fn parse_raydium_amm_v4_instruction_data(
metadata: EventMetadata,
) -> Option<DexEvent> {
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<crate::streaming::event_parser::DexEvent> {
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],
@@ -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<Pubkey>,
}
/// 交易v2
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RaydiumClmmSwapV2Event {
@@ -90,6 +87,16 @@ pub struct RaydiumClmmDecreaseLiquidityV2Event {
pub remaining_accounts: Vec<Pubkey>,
}
/// 收取流动性费用(与 `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 {
@@ -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<crate::streaming::event_parser::DexEvent> {
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,
}
@@ -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 {
@@ -23,9 +23,7 @@ pub fn parse_raydium_cpmm_instruction_data(
metadata: EventMetadata,
) -> Option<DexEvent> {
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<crate::streaming::event_parser::DexEvent> {
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],
@@ -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,
}
@@ -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");
@@ -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<i64> {
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<DexEvent> {
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<DexEvent> {
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))
}
+58 -7
View File
@@ -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);
}
}
+5 -8
View File
@@ -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<String>) -> Self {
Self {
endpoint,
x_token,
}
Self { endpoint, x_token }
}
pub async fn create_connection(&self) -> AnyResult<GeyserGrpcClient<impl Interceptor>> {
-1
View File
@@ -103,4 +103,3 @@ impl Default for TransactionPretty {
}
}
}
+9
View File
@@ -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};
+133
View File
@@ -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<DexEvent> {
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<SdkGrpcEventType> {
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<SdkGrpcEventType>) {
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;
}
}
}
+54
View File
@@ -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(),
)
}
@@ -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),
}
}
+917
View File
@@ -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<DexEvent> {
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<PbDexEvent>,
bt: Option<&Timestamp>,
recv_wall_us: i64,
protocols: &[Protocol],
event_type_filter: Option<&crate::streaming::event_parser::common::filter::EventTypeFilter>,
) -> Vec<DexEvent> {
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<DexEvent> {
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)
}
+116
View File
@@ -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,
}
}
@@ -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,
}
}
+299
View File
@@ -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"),
}
}
}
@@ -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
}
@@ -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<DexEvent> {
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()
}))
}
}
@@ -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,
}
}
+98
View File
@@ -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<Vec<DexEvent>, 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<Vec<DexEvent>, 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<Vec<DexEvent>, 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)
}
}
+64
View File
@@ -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<SdkEventTypeFilter> {
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<DexEvent> {
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<SdkDexEvent>,
block_time: Option<&Timestamp>,
recv_wall_us: i64,
protocols: &[Protocol],
event_type_filter: Option<&EventTypeFilter>,
) -> Vec<DexEvent> {
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<DexEvent> {
crate::streaming::parser_sdk_bridge::parse_sdk_account_event(
account,
protocols,
event_type_filter,
)
}
+13
View File
@@ -12,6 +12,7 @@ use crate::streaming::common::{
#[derive(Clone)]
pub struct ShredStreamGrpc {
pub shredstream_client: Arc<ShredstreamProxyClient<Channel>>,
pub sdk_shredstream_client: Arc<sol_parser_sdk::shredstream::ShredStreamClient>,
pub config: StreamClientConfig,
pub subscription_handle: Arc<Mutex<Option<SubscriptionHandle>>>,
}
@@ -25,9 +26,20 @@ impl ShredStreamGrpc {
/// 创建客户端,使用自定义配置
pub async fn new_with_config(endpoint: String, config: StreamClientConfig) -> AnyResult<Self> {
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();
+6 -7
View File
@@ -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<Mutex<VecDeque<Box<TransactionWithSlot>>>>,
@@ -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,
}
}
}
+45 -47
View File
@@ -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::<Vec<SolanaEntry>>(&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,
);
}
}
});
+2 -2
View File
@@ -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::{