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
@@ -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,
);