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
+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;