Release solana-streamer-sdk v1.4.3

Route gRPC, ShredStream, and account parsing through sol-parser-sdk.

Remove local protocol parsing implementations.

Depend on sol-parser-sdk v0.4.8 from crates.io.
This commit is contained in:
0xfnzero
2026-05-16 06:45:35 +08:00
parent 55e6bc6459
commit 92bac16fd4
43 changed files with 837 additions and 5460 deletions
+378 -69
View File
@@ -1,7 +1,7 @@
use crate::streaming::event_parser::common::{
types::EventType, ACCOUNT_EVENT_TYPES, BLOCK_EVENT_TYPES,
};
use crate::streaming::event_parser::DexEvent;
use crate::streaming::event_parser::{DexEvent, Protocol};
use sol_parser_sdk::grpc::types::EventType as SdkGrpcEventType;
use sol_parser_sdk::grpc::types::EventTypeFilter as SdkGrpcEventTypeFilter;
@@ -95,27 +95,6 @@ pub(crate) fn passes_event_type_filter(filter: Option<&EventTypeFilter>, ev: &De
}
}
/// 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`].
///
@@ -129,9 +108,9 @@ pub(crate) fn build_sdk_parse_event_filter(
let f = filter?;
if !f.exclude.is_empty() && f.include.is_empty() {
let mut raw: Vec<SdkGrpcEventType> = Vec::new();
let mut raw: Vec<SdkGrpcEventType> = Vec::with_capacity(f.exclude.len());
for et in &f.exclude {
raw.extend(streamer_event_to_sdk_grpc_types(et));
push_streamer_event_sdk_grpc_types(et, &mut raw);
}
dedup_sdk_grpc_event_types(&mut raw);
return (!raw.is_empty()).then(|| SdkGrpcEventTypeFilter::exclude_types(raw));
@@ -140,18 +119,44 @@ pub(crate) fn build_sdk_parse_event_filter(
if f.include.is_empty() {
return None;
}
let mut raw: Vec<SdkGrpcEventType> = Vec::new();
let mut raw: Vec<SdkGrpcEventType> = Vec::with_capacity(f.include.len());
for et in &f.include {
let mapped = streamer_event_to_sdk_grpc_types(et);
if mapped.is_empty() {
if !push_streamer_event_sdk_grpc_types(et, &mut raw) {
return None;
}
raw.extend(mapped);
}
dedup_sdk_grpc_event_types(&mut raw);
Some(SdkGrpcEventTypeFilter::include_only(raw))
}
/// Build the SDK ShredStream hot-path filter. Unlike Yellowstone, ShredStream
/// subscription itself has no program filter, so protocol narrowing must be
/// pushed into the SDK parser as an event-type include list.
pub(crate) fn build_sdk_shred_parse_event_filter(
protocols: &[Protocol],
filter: Option<&EventTypeFilter>,
) -> Option<SdkGrpcEventTypeFilter> {
if protocols.is_empty() {
return build_sdk_parse_event_filter(filter);
}
if let Some(f) = filter.filter(|f| !f.include.is_empty()) {
if let Some(exact) = build_sdk_parse_event_filter(Some(f)) {
return Some(exact);
}
if !f.include_transaction_event() {
return Some(SdkGrpcEventTypeFilter::include_only(Vec::new()));
}
}
let mut raw = Vec::with_capacity(protocols.len() * 8);
for protocol in protocols {
push_protocol_sdk_grpc_event_types(protocol, &mut raw);
}
dedup_sdk_grpc_event_types(&mut raw);
(!raw.is_empty()).then(|| SdkGrpcEventTypeFilter::include_only(raw))
}
fn dedup_sdk_grpc_event_types(v: &mut Vec<SdkGrpcEventType>) {
let mut i = 0;
while i < v.len() {
@@ -163,51 +168,199 @@ fn dedup_sdk_grpc_event_types(v: &mut Vec<SdkGrpcEventType>) {
}
}
fn streamer_event_to_sdk_grpc_types(t: &EventType) -> Vec<SdkGrpcEventType> {
fn push_streamer_event_sdk_grpc_types(t: &EventType, out: &mut Vec<SdkGrpcEventType>) -> bool {
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::BlockMeta => out.push(Sdk::BlockMeta),
St::PumpFunCreateToken => out.push(Sdk::PumpFunCreate),
St::PumpFunCreateV2Token => out.push(Sdk::PumpFunCreateV2),
St::PumpFunBuy => {
out.push(Sdk::PumpFunBuy);
out.push(Sdk::PumpFunBuyExactSolIn);
}
St::PumpFunBuyExactSolIn => out.push(Sdk::PumpFunBuyExactSolIn),
St::PumpFunSell => out.push(Sdk::PumpFunSell),
St::PumpFunMigrate => out.push(Sdk::PumpFunMigrate),
St::PumpFeesCreateFeeSharingConfig => out.push(Sdk::PumpFeesCreateFeeSharingConfig),
St::PumpFeesInitializeFeeConfig => out.push(Sdk::PumpFeesInitializeFeeConfig),
St::PumpFeesResetFeeSharingConfig => out.push(Sdk::PumpFeesResetFeeSharingConfig),
St::PumpFeesRevokeFeeSharingAuthority => out.push(Sdk::PumpFeesRevokeFeeSharingAuthority),
St::PumpFeesTransferFeeSharingAuthority => {
out.push(Sdk::PumpFeesTransferFeeSharingAuthority)
}
St::PumpFeesUpdateAdmin => out.push(Sdk::PumpFeesUpdateAdmin),
St::PumpFeesUpdateFeeConfig => out.push(Sdk::PumpFeesUpdateFeeConfig),
St::PumpFeesUpdateFeeShares => out.push(Sdk::PumpFeesUpdateFeeShares),
St::PumpFeesUpsertFeeTiers => out.push(Sdk::PumpFeesUpsertFeeTiers),
St::PumpFunMigrateBondingCurveCreator => out.push(Sdk::PumpFunMigrateBondingCurveCreator),
St::PumpSwapBuy => out.push(Sdk::PumpSwapBuy),
St::PumpSwapSell => out.push(Sdk::PumpSwapSell),
St::PumpSwapCreatePool => out.push(Sdk::PumpSwapCreatePool),
St::PumpSwapDeposit => out.push(Sdk::PumpSwapLiquidityAdded),
St::PumpSwapWithdraw => out.push(Sdk::PumpSwapLiquidityRemoved),
St::BonkBuyExactIn | St::BonkBuyExactOut | St::BonkSellExactIn | St::BonkSellExactOut => {
vec![Sdk::BonkTrade]
out.push(Sdk::BonkTrade)
}
St::BonkInitialize | St::BonkInitializeV2 | St::BonkInitializeWithToken2022 => {
vec![Sdk::BonkPoolCreate]
out.push(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![],
St::BonkMigrateToAmm => out.push(Sdk::BonkMigrateAmm),
St::RaydiumCpmmSwapBaseInput | St::RaydiumCpmmSwapBaseOutput => {
out.push(Sdk::RaydiumCpmmSwap)
}
St::RaydiumCpmmDeposit => out.push(Sdk::RaydiumCpmmDeposit),
St::RaydiumCpmmInitialize => out.push(Sdk::RaydiumCpmmInitialize),
St::RaydiumCpmmWithdraw => out.push(Sdk::RaydiumCpmmWithdraw),
St::RaydiumClmmSwap | St::RaydiumClmmSwapV2 => out.push(Sdk::RaydiumClmmSwap),
St::RaydiumClmmClosePosition => out.push(Sdk::RaydiumClmmClosePosition),
St::RaydiumClmmIncreaseLiquidityV2 => out.push(Sdk::RaydiumClmmIncreaseLiquidity),
St::RaydiumClmmDecreaseLiquidityV2 => out.push(Sdk::RaydiumClmmDecreaseLiquidity),
St::RaydiumClmmCreatePool => out.push(Sdk::RaydiumClmmCreatePool),
St::RaydiumClmmOpenPositionWithToken22Nft => {
out.push(Sdk::RaydiumClmmOpenPositionWithTokenExtNft)
}
St::RaydiumClmmOpenPositionV2 => out.push(Sdk::RaydiumClmmOpenPosition),
St::RaydiumClmmCollectFee => out.push(Sdk::RaydiumClmmCollectFee),
St::RaydiumAmmV4SwapBaseIn | St::RaydiumAmmV4SwapBaseOut => out.push(Sdk::RaydiumAmmV4Swap),
St::RaydiumAmmV4Deposit => out.push(Sdk::RaydiumAmmV4Deposit),
St::RaydiumAmmV4Initialize2 => out.push(Sdk::RaydiumAmmV4Initialize2),
St::RaydiumAmmV4Withdraw => out.push(Sdk::RaydiumAmmV4Withdraw),
St::RaydiumAmmV4WithdrawPnl => out.push(Sdk::RaydiumAmmV4WithdrawPnl),
St::OrcaWhirlpoolSwap => out.push(Sdk::OrcaWhirlpoolSwap),
St::OrcaWhirlpoolLiquidityIncreased => out.push(Sdk::OrcaWhirlpoolLiquidityIncreased),
St::OrcaWhirlpoolLiquidityDecreased => out.push(Sdk::OrcaWhirlpoolLiquidityDecreased),
St::OrcaWhirlpoolPoolInitialized => out.push(Sdk::OrcaWhirlpoolPoolInitialized),
St::MeteoraPoolsSwap => out.push(Sdk::MeteoraPoolsSwap),
St::MeteoraPoolsAddLiquidity => out.push(Sdk::MeteoraPoolsAddLiquidity),
St::MeteoraPoolsRemoveLiquidity => out.push(Sdk::MeteoraPoolsRemoveLiquidity),
St::MeteoraPoolsBootstrapLiquidity => out.push(Sdk::MeteoraPoolsBootstrapLiquidity),
St::MeteoraPoolsPoolCreated => out.push(Sdk::MeteoraPoolsPoolCreated),
St::MeteoraPoolsSetPoolFees => out.push(Sdk::MeteoraPoolsSetPoolFees),
St::MeteoraDammV2Swap | St::MeteoraDammV2Swap2 => out.push(Sdk::MeteoraDammV2Swap),
St::MeteoraDammV2AddLiquidity => out.push(Sdk::MeteoraDammV2AddLiquidity),
St::MeteoraDammV2RemoveLiquidity => out.push(Sdk::MeteoraDammV2RemoveLiquidity),
St::MeteoraDammV2CreatePosition => out.push(Sdk::MeteoraDammV2CreatePosition),
St::MeteoraDammV2ClosePosition => out.push(Sdk::MeteoraDammV2ClosePosition),
St::MeteoraDlmmSwap => out.push(Sdk::MeteoraDlmmSwap),
St::MeteoraDlmmAddLiquidity => out.push(Sdk::MeteoraDlmmAddLiquidity),
St::MeteoraDlmmRemoveLiquidity => out.push(Sdk::MeteoraDlmmRemoveLiquidity),
St::MeteoraDlmmInitializePool => out.push(Sdk::MeteoraDlmmInitializePool),
St::MeteoraDlmmInitializeBinArray => out.push(Sdk::MeteoraDlmmInitializeBinArray),
St::MeteoraDlmmCreatePosition => out.push(Sdk::MeteoraDlmmCreatePosition),
St::MeteoraDlmmClosePosition => out.push(Sdk::MeteoraDlmmClosePosition),
St::MeteoraDlmmClaimFee => out.push(Sdk::MeteoraDlmmClaimFee),
St::TokenAccount | St::TokenInfo => out.push(Sdk::TokenAccount),
St::NonceAccount => out.push(Sdk::NonceAccount),
St::AccountPumpFunGlobal => out.push(Sdk::AccountPumpFunGlobal),
St::AccountPumpSwapGlobalConfig => out.push(Sdk::AccountPumpSwapGlobalConfig),
St::AccountPumpSwapPool => out.push(Sdk::AccountPumpSwapPool),
_ => return false,
}
true
}
fn push_protocol_sdk_grpc_event_types(protocol: &Protocol, out: &mut Vec<SdkGrpcEventType>) {
use Protocol as StProtocol;
use SdkGrpcEventType as Sdk;
match protocol {
StProtocol::PumpFun => out.extend_from_slice(&[
Sdk::PumpFunTrade,
Sdk::PumpFunBuy,
Sdk::PumpFunSell,
Sdk::PumpFunBuyExactSolIn,
Sdk::PumpFunCreate,
Sdk::PumpFunCreateV2,
Sdk::PumpFunMigrate,
Sdk::PumpFunMigrateBondingCurveCreator,
// Historical compatibility: streamer PumpFun also included PumpFees.
Sdk::PumpFeesCreateFeeSharingConfig,
Sdk::PumpFeesInitializeFeeConfig,
Sdk::PumpFeesResetFeeSharingConfig,
Sdk::PumpFeesRevokeFeeSharingAuthority,
Sdk::PumpFeesTransferFeeSharingAuthority,
Sdk::PumpFeesUpdateAdmin,
Sdk::PumpFeesUpdateFeeConfig,
Sdk::PumpFeesUpdateFeeShares,
Sdk::PumpFeesUpsertFeeTiers,
]),
StProtocol::PumpFees => out.extend_from_slice(&[
Sdk::PumpFeesCreateFeeSharingConfig,
Sdk::PumpFeesInitializeFeeConfig,
Sdk::PumpFeesResetFeeSharingConfig,
Sdk::PumpFeesRevokeFeeSharingAuthority,
Sdk::PumpFeesTransferFeeSharingAuthority,
Sdk::PumpFeesUpdateAdmin,
Sdk::PumpFeesUpdateFeeConfig,
Sdk::PumpFeesUpdateFeeShares,
Sdk::PumpFeesUpsertFeeTiers,
]),
StProtocol::PumpSwap => out.extend_from_slice(&[
Sdk::PumpSwapTrade,
Sdk::PumpSwapBuy,
Sdk::PumpSwapSell,
Sdk::PumpSwapCreatePool,
Sdk::PumpSwapLiquidityAdded,
Sdk::PumpSwapLiquidityRemoved,
]),
StProtocol::Bonk | StProtocol::RaydiumLaunchpad => {
out.extend_from_slice(&[Sdk::BonkTrade, Sdk::BonkPoolCreate, Sdk::BonkMigrateAmm])
}
StProtocol::RaydiumCpmm => out.extend_from_slice(&[
Sdk::RaydiumCpmmSwap,
Sdk::RaydiumCpmmDeposit,
Sdk::RaydiumCpmmWithdraw,
Sdk::RaydiumCpmmInitialize,
]),
StProtocol::RaydiumClmm => out.extend_from_slice(&[
Sdk::RaydiumClmmSwap,
Sdk::RaydiumClmmCreatePool,
Sdk::RaydiumClmmOpenPosition,
Sdk::RaydiumClmmClosePosition,
Sdk::RaydiumClmmIncreaseLiquidity,
Sdk::RaydiumClmmDecreaseLiquidity,
Sdk::RaydiumClmmOpenPositionWithTokenExtNft,
Sdk::RaydiumClmmCollectFee,
]),
StProtocol::RaydiumAmmV4 => out.extend_from_slice(&[
Sdk::RaydiumAmmV4Swap,
Sdk::RaydiumAmmV4Deposit,
Sdk::RaydiumAmmV4Withdraw,
Sdk::RaydiumAmmV4Initialize2,
Sdk::RaydiumAmmV4WithdrawPnl,
]),
StProtocol::MeteoraDammV2 => out.extend_from_slice(&[
Sdk::MeteoraDammV2Swap,
Sdk::MeteoraDammV2AddLiquidity,
Sdk::MeteoraDammV2RemoveLiquidity,
Sdk::MeteoraDammV2CreatePosition,
Sdk::MeteoraDammV2ClosePosition,
]),
StProtocol::OrcaWhirlpool => out.extend_from_slice(&[
Sdk::OrcaWhirlpoolSwap,
Sdk::OrcaWhirlpoolLiquidityIncreased,
Sdk::OrcaWhirlpoolLiquidityDecreased,
Sdk::OrcaWhirlpoolPoolInitialized,
]),
StProtocol::MeteoraPools => out.extend_from_slice(&[
Sdk::MeteoraPoolsSwap,
Sdk::MeteoraPoolsAddLiquidity,
Sdk::MeteoraPoolsRemoveLiquidity,
Sdk::MeteoraPoolsBootstrapLiquidity,
Sdk::MeteoraPoolsPoolCreated,
Sdk::MeteoraPoolsSetPoolFees,
]),
StProtocol::MeteoraDlmm => out.extend_from_slice(&[
Sdk::MeteoraDlmmSwap,
Sdk::MeteoraDlmmAddLiquidity,
Sdk::MeteoraDlmmRemoveLiquidity,
Sdk::MeteoraDlmmInitializePool,
Sdk::MeteoraDlmmInitializeBinArray,
Sdk::MeteoraDlmmCreatePosition,
Sdk::MeteoraDlmmClosePosition,
Sdk::MeteoraDlmmClaimFee,
]),
}
}
@@ -218,6 +371,10 @@ fn event_type_matches(filter_type: &EventType, event_type: &EventType) -> bool {
(filter_type, event_type),
(EventType::PumpFunBuy, EventType::PumpFunBuyExactSolIn)
| (EventType::TokenAccount, EventType::TokenInfo)
| (EventType::RaydiumClmmSwap, EventType::RaydiumClmmSwapV2)
| (EventType::RaydiumClmmSwapV2, EventType::RaydiumClmmSwap)
| (EventType::MeteoraDammV2Swap, EventType::MeteoraDammV2Swap2)
| (EventType::MeteoraDammV2Swap2, EventType::MeteoraDammV2Swap)
)
}
@@ -372,6 +529,17 @@ mod tests {
assert!(f.passes_event_type(&EventType::TokenInfo));
}
#[test]
fn sdk_generic_swap_filters_match_streamer_specific_aliases() {
let f =
EventTypeFilter { include: vec![EventType::RaydiumClmmSwapV2], ..Default::default() };
assert!(f.passes_event_type(&EventType::RaydiumClmmSwap));
let f =
EventTypeFilter { include: vec![EventType::MeteoraDammV2Swap2], ..Default::default() };
assert!(f.passes_event_type(&EventType::MeteoraDammV2Swap));
}
#[test]
fn build_sdk_filter_pumpfun_exact_sol_in_only() {
let f = EventTypeFilter {
@@ -392,9 +560,150 @@ mod tests {
}
#[test]
fn build_sdk_filter_none_when_orca_in_mix() {
fn build_sdk_filter_maps_all_public_sdk_filter_events() {
let sdk_filter_backed = [
EventType::BlockMeta,
EventType::PumpFunCreateToken,
EventType::PumpFunCreateV2Token,
EventType::PumpFunBuy,
EventType::PumpFunBuyExactSolIn,
EventType::PumpFunSell,
EventType::PumpFunMigrate,
EventType::PumpFunMigrateBondingCurveCreator,
EventType::PumpFeesCreateFeeSharingConfig,
EventType::PumpFeesInitializeFeeConfig,
EventType::PumpFeesResetFeeSharingConfig,
EventType::PumpFeesRevokeFeeSharingAuthority,
EventType::PumpFeesTransferFeeSharingAuthority,
EventType::PumpFeesUpdateAdmin,
EventType::PumpFeesUpdateFeeConfig,
EventType::PumpFeesUpdateFeeShares,
EventType::PumpFeesUpsertFeeTiers,
EventType::PumpSwapBuy,
EventType::PumpSwapSell,
EventType::PumpSwapCreatePool,
EventType::PumpSwapDeposit,
EventType::PumpSwapWithdraw,
EventType::BonkBuyExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactIn,
EventType::BonkSellExactOut,
EventType::BonkInitialize,
EventType::BonkInitializeV2,
EventType::BonkInitializeWithToken2022,
EventType::BonkMigrateToAmm,
EventType::RaydiumCpmmSwapBaseInput,
EventType::RaydiumCpmmSwapBaseOutput,
EventType::RaydiumCpmmDeposit,
EventType::RaydiumCpmmInitialize,
EventType::RaydiumCpmmWithdraw,
EventType::RaydiumClmmSwap,
EventType::RaydiumClmmSwapV2,
EventType::RaydiumClmmClosePosition,
EventType::RaydiumClmmIncreaseLiquidityV2,
EventType::RaydiumClmmDecreaseLiquidityV2,
EventType::RaydiumClmmCreatePool,
EventType::RaydiumClmmOpenPositionWithToken22Nft,
EventType::RaydiumClmmOpenPositionV2,
EventType::RaydiumClmmCollectFee,
EventType::RaydiumAmmV4SwapBaseIn,
EventType::RaydiumAmmV4SwapBaseOut,
EventType::RaydiumAmmV4Deposit,
EventType::RaydiumAmmV4Initialize2,
EventType::RaydiumAmmV4Withdraw,
EventType::RaydiumAmmV4WithdrawPnl,
EventType::OrcaWhirlpoolSwap,
EventType::OrcaWhirlpoolLiquidityIncreased,
EventType::OrcaWhirlpoolLiquidityDecreased,
EventType::OrcaWhirlpoolPoolInitialized,
EventType::MeteoraPoolsSwap,
EventType::MeteoraPoolsAddLiquidity,
EventType::MeteoraPoolsRemoveLiquidity,
EventType::MeteoraPoolsBootstrapLiquidity,
EventType::MeteoraPoolsPoolCreated,
EventType::MeteoraPoolsSetPoolFees,
EventType::MeteoraDammV2Swap,
EventType::MeteoraDammV2Swap2,
EventType::MeteoraDammV2AddLiquidity,
EventType::MeteoraDammV2RemoveLiquidity,
EventType::MeteoraDammV2CreatePosition,
EventType::MeteoraDammV2ClosePosition,
EventType::MeteoraDlmmSwap,
EventType::MeteoraDlmmAddLiquidity,
EventType::MeteoraDlmmRemoveLiquidity,
EventType::MeteoraDlmmInitializePool,
EventType::MeteoraDlmmInitializeBinArray,
EventType::MeteoraDlmmCreatePosition,
EventType::MeteoraDlmmClosePosition,
EventType::MeteoraDlmmClaimFee,
EventType::TokenAccount,
EventType::TokenInfo,
EventType::NonceAccount,
EventType::AccountPumpFunGlobal,
EventType::AccountPumpSwapGlobalConfig,
EventType::AccountPumpSwapPool,
];
for event_type in sdk_filter_backed {
let f = EventTypeFilter { include: vec![event_type.clone()], ..Default::default() };
assert!(
build_sdk_parse_event_filter(Some(&f)).is_some(),
"{event_type:?} should map to an SDK parse filter"
);
}
}
#[test]
fn build_sdk_filter_none_when_public_sdk_filter_enum_cannot_express_requested_type() {
let f = EventTypeFilter {
include: vec![EventType::PumpFunBuy, EventType::OrcaWhirlpoolSwap],
include: vec![EventType::PumpFunBuy, EventType::MeteoraDammV2InitializePool],
..Default::default()
};
assert!(build_sdk_parse_event_filter(Some(&f)).is_none());
}
#[test]
fn build_sdk_shred_filter_falls_back_to_protocol_group_for_sdk_enum_gap() {
let f = EventTypeFilter {
include: vec![EventType::MeteoraDammV2InitializePool],
..Default::default()
};
let sdk_f =
build_sdk_shred_parse_event_filter(&[Protocol::MeteoraDammV2], Some(&f)).unwrap();
assert!(sdk_f.should_include(SdkGrpcEventType::MeteoraDammV2Swap));
assert!(!sdk_f.should_include(SdkGrpcEventType::PumpFunBuy));
}
#[test]
fn build_sdk_shred_filter_skips_transaction_parse_for_account_only_gap() {
let f = EventTypeFilter {
include: vec![EventType::AccountRaydiumCpmmPoolState],
..Default::default()
};
let sdk_f = build_sdk_shred_parse_event_filter(&[Protocol::RaydiumCpmm], Some(&f)).unwrap();
assert!(!sdk_f.should_include(SdkGrpcEventType::RaydiumCpmmSwap));
assert!(!sdk_f.should_include(SdkGrpcEventType::PumpFunBuy));
}
#[test]
fn build_sdk_filter_exclude_only_orca_maps_upstream() {
let f = EventTypeFilter {
include: vec![],
exclude: vec![EventType::OrcaWhirlpoolSwap],
..Default::default()
};
let sdk_f = build_sdk_parse_event_filter(Some(&f)).expect("mapped");
assert!(!sdk_f.should_include(SdkGrpcEventType::OrcaWhirlpoolSwap));
assert!(sdk_f.should_include(SdkGrpcEventType::PumpFunBuy));
}
#[test]
fn build_sdk_filter_exclude_only_none_when_only_sdk_filter_enum_gap() {
let f = EventTypeFilter {
include: vec![],
exclude: vec![EventType::MeteoraDammV2InitializePool],
..Default::default()
};
assert!(build_sdk_parse_event_filter(Some(&f)).is_none());
@@ -404,7 +713,7 @@ mod tests {
fn build_sdk_filter_exclude_only_none_when_only_unmapped_types() {
let f = EventTypeFilter {
include: vec![],
exclude: vec![EventType::OrcaWhirlpoolSwap],
exclude: vec![EventType::SetComputeUnitLimit],
..Default::default()
};
assert!(build_sdk_parse_event_filter(Some(&f)).is_none());
-2
View File
@@ -1,6 +1,4 @@
pub mod filter;
pub mod high_performance_clock;
pub mod types;
pub mod utils;
pub use types::*;
pub use utils::*;
+1 -257
View File
@@ -4,8 +4,6 @@ use serde::{Deserialize, Serialize};
use solana_sdk::{pubkey::Pubkey, signature::Signature};
use std::{borrow::Cow, fmt, sync::Arc};
use crate::streaming::event_parser::DexEvent;
// Object pool size configuration
const EVENT_METADATA_POOL_SIZE: usize = 1000;
@@ -55,6 +53,7 @@ pub enum ProtocolType {
MeteoraPools,
MeteoraDlmm,
Common,
PumpFees,
}
/// Event type enumeration
@@ -294,258 +293,3 @@ impl EventMetadata {
self.swap_data = Some(swap_data);
}
}
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 {
fn program_id_index(&self) -> usize;
fn accounts(&self) -> &[u8];
fn data(&self) -> &[u8];
}
/// Adapter for standard Solana compiled instructions
impl InnerInstructionLike for solana_sdk::message::compiled_instruction::CompiledInstruction {
fn program_id_index(&self) -> usize {
self.program_id_index as usize
}
fn accounts(&self) -> &[u8] {
&self.accounts
}
fn data(&self) -> &[u8] {
&self.data
}
}
/// Adapter for gRPC inner instructions (yellowstone)
impl InnerInstructionLike for yellowstone_grpc_proto::prelude::InnerInstruction {
fn program_id_index(&self) -> usize {
self.program_id_index as usize
}
fn accounts(&self) -> &[u8] {
&self.accounts
}
fn data(&self) -> &[u8] {
&self.data
}
}
/// Extract event context (mint/token account/vault info) from a DexEvent
fn extract_swap_context(
event: &DexEvent,
) -> (
SwapData,
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;
let mut to_mint: Option<Pubkey> = None;
let mut user_from_token: Option<Pubkey> = None;
let mut user_to_token: Option<Pubkey> = None;
let mut from_vault: Option<Pubkey> = None;
let mut to_vault: Option<Pubkey> = None;
match event {
DexEvent::BonkTradeEvent(e) => {
from_mint = Some(e.base_token_mint);
to_mint = Some(e.quote_token_mint);
user_from_token = Some(e.user_base_token);
user_to_token = Some(e.user_quote_token);
from_vault = Some(e.base_vault);
to_vault = Some(e.quote_vault);
}
DexEvent::PumpFunTradeEvent(e) => {
swap_data.from_mint = if e.is_buy { *SOL_MINT } else { e.mint };
swap_data.to_mint = if e.is_buy { e.mint } else { *SOL_MINT };
}
DexEvent::PumpSwapBuyEvent(e) => {
swap_data.from_mint = e.quote_mint;
swap_data.to_mint = e.base_mint;
}
DexEvent::PumpSwapSellEvent(e) => {
swap_data.from_mint = e.base_mint;
swap_data.to_mint = e.quote_mint;
}
DexEvent::RaydiumCpmmSwapEvent(e) => {
from_mint = Some(e.input_token_mint);
to_mint = Some(e.output_token_mint);
user_from_token = Some(e.input_token_account);
user_to_token = Some(e.output_token_account);
from_vault = Some(e.input_vault);
to_vault = Some(e.output_vault);
}
DexEvent::RaydiumClmmSwapEvent(e) => {
swap_data.description =
Some("Unable to get from_mint and to_mint from RaydiumClmmSwapEvent".into());
user_from_token = Some(e.input_token_account);
user_to_token = Some(e.output_token_account);
from_vault = Some(e.input_vault);
to_vault = Some(e.output_vault);
}
DexEvent::RaydiumClmmSwapV2Event(e) => {
from_mint = Some(e.input_vault_mint);
to_mint = Some(e.output_vault_mint);
user_from_token = Some(e.input_token_account);
user_to_token = Some(e.output_token_account);
from_vault = Some(e.input_vault);
to_vault = Some(e.output_vault);
}
DexEvent::RaydiumAmmV4SwapEvent(e) => {
swap_data.description =
Some("Unable to get from_mint and to_mint from RaydiumAmmV4SwapEvent".into());
user_from_token = Some(e.user_source_token_account);
user_to_token = Some(e.user_destination_token_account);
from_vault = Some(e.pool_pc_token_account);
to_vault = Some(e.pool_coin_token_account);
}
_ => {}
}
(swap_data, from_mint, to_mint, user_from_token, user_to_token, from_vault, to_vault)
}
/// Generic swap data extraction that works with any instruction type implementing InnerInstructionLike
fn extract_swap_data_from_instructions<I: InnerInstructionLike>(
event: &DexEvent,
instructions: impl Iterator<Item = I>,
current_index: i32,
accounts: &[Pubkey],
) -> Option<SwapData> {
let (mut swap_data, fm, tm, uft, utt, fv, tv) = extract_swap_context(event);
let user_to_token = utt.unwrap_or_default();
let user_from_token = uft.unwrap_or_default();
let to_vault = tv.unwrap_or_default();
let from_vault = fv.unwrap_or_default();
let to_mint = tm.unwrap_or_default();
let from_mint = fm.unwrap_or_default();
let skip_count = (current_index + 1).max(0) as usize;
for instruction in instructions.skip(skip_count) {
let program_id_index = instruction.program_id_index();
let program_id = match accounts.get(program_id_index) {
Some(&pid) => pid,
None => break,
};
if !SYSTEM_PROGRAMS.contains(&program_id) {
break;
}
let data = instruction.data();
let accs = instruction.accounts();
if data.len() < 8 {
continue;
}
let get_pubkey = |i: usize| -> Option<Pubkey> {
let idx = accs.get(i).copied().map(|b| b as usize)?;
accounts.get(idx).copied()
};
let (source, destination, amount) = match data[0] {
12 if accs.len() >= 4 && data.len() >= 9 => {
let amt = u64::from_le_bytes(data[1..9].try_into().unwrap());
match (get_pubkey(0), get_pubkey(2)) {
(Some(s), Some(d)) => (s, d, amt),
_ => continue,
}
}
3 if accs.len() >= 3 && data.len() >= 9 => {
let amt = u64::from_le_bytes(data[1..9].try_into().unwrap());
match (get_pubkey(0), get_pubkey(1)) {
(Some(s), Some(d)) => (s, d, amt),
_ => continue,
}
}
2 if accs.len() >= 2 && data.len() >= 12 => {
let amt = u64::from_le_bytes(data[4..12].try_into().unwrap());
match (get_pubkey(0), get_pubkey(1)) {
(Some(s), Some(d)) => (s, d, amt),
_ => continue,
}
}
_ => continue,
};
match (source, destination) {
(s, d) if s == user_to_token && d == to_vault => {
swap_data.from_mint = to_mint;
swap_data.from_amount = amount;
}
(s, d) if s == from_vault && d == user_from_token => {
swap_data.to_mint = from_mint;
swap_data.to_amount = amount;
}
(s, d) if s == user_from_token && d == from_vault => {
swap_data.from_mint = from_mint;
swap_data.from_amount = amount;
}
(s, d) if s == to_vault && d == user_to_token => {
swap_data.to_mint = to_mint;
swap_data.to_amount = amount;
}
(s, d) if s == user_from_token && d == to_vault => {
swap_data.from_mint = from_mint;
swap_data.from_amount = amount;
}
(s, d) if s == from_vault && d == user_to_token => {
swap_data.to_mint = to_mint;
swap_data.to_amount = amount;
}
_ => {}
}
if swap_data.from_mint != Pubkey::default() && swap_data.to_mint != Pubkey::default() {
break;
}
if swap_data.from_amount != 0 && swap_data.to_amount != 0 {
break;
}
}
if swap_data.from_mint != Pubkey::default()
|| swap_data.to_mint != Pubkey::default()
|| swap_data.from_amount != 0
|| swap_data.to_amount != 0
{
Some(swap_data)
} else {
None
}
}
/// Parse token transfer data from standard Solana inner instructions
pub fn parse_swap_data_from_next_instructions(
event: &DexEvent,
inner_instruction: &solana_transaction_status::InnerInstructions,
current_index: i32,
accounts: &[Pubkey],
) -> Option<SwapData> {
extract_swap_data_from_instructions(
event,
inner_instruction.instructions.iter().map(|ix| ix.instruction.clone()),
current_index,
accounts,
)
}
/// Parse token transfer data from gRPC inner instructions
pub fn parse_swap_data_from_next_grpc_instructions(
event: &DexEvent,
inner_instruction: &yellowstone_grpc_proto::prelude::InnerInstructions,
current_index: i32,
accounts: &[Pubkey],
) -> Option<SwapData> {
extract_swap_data_from_instructions(
event,
inner_instruction.instructions.iter().cloned(),
current_index,
accounts,
)
}
-119
View File
@@ -1,119 +0,0 @@
use std::time::{SystemTime, UNIX_EPOCH};
/// 获取当前时间戳
pub fn current_timestamp() -> i64 {
SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64
}
/// 从字节数组中提取鉴别器和剩余数据
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.len() < length {
return None;
}
Some((&data[..length], &data[length..]))
}
/// 从日志中提取程序数据
pub fn extract_program_data(log: &str) -> Option<&str> {
const PROGRAM_DATA_PREFIX: &str = "Program data: ";
log.strip_prefix(PROGRAM_DATA_PREFIX)
}
/// 从日志中提取程序日志
pub fn extract_program_log<'a>(log: &'a str, prefix: &str) -> Option<&'a str> {
log.strip_prefix(prefix)
}
/// 安全地从字节数组中读取 i64
pub fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
if data.len() < offset + 8 {
return None;
}
let bytes: [u8; 8] = data[offset..offset + 8].try_into().ok()?;
Some(i64::from_le_bytes(bytes))
}
/// 安全地从字节数组中读取u64
pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
if data.len() < offset + 8 {
return None;
}
let bytes: [u8; 8] = data[offset..offset + 8].try_into().ok()?;
Some(u64::from_le_bytes(bytes))
}
pub fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
if data.len() < offset + 4 {
return None;
}
let bytes: [u8; 4] = data[offset..offset + 4].try_into().ok()?;
Some(i32::from_le_bytes(bytes))
}
pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
if data.len() < offset + 16 {
return None;
}
let bytes: [u8; 16] = data[offset..offset + 16].try_into().ok()?;
Some(u128::from_le_bytes(bytes))
}
pub fn read_u8_le(data: &[u8], offset: usize) -> Option<u8> {
if data.len() < offset + 1 {
return None;
}
let bytes: [u8; 1] = data[offset..offset + 1].try_into().ok()?;
Some(u8::from_le_bytes(bytes))
}
pub fn read_option_bool(data: &[u8], offset: &mut usize) -> Option<Option<bool>> {
let has_value = data.get(*offset).copied()?;
*offset += 1;
if has_value == 0 {
return Some(None);
}
let value = data.get(*offset).copied()?;
*offset += 1;
Some(Some(value != 0))
}
/// 安全地从字节数组中读取u32
pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
if data.len() < offset + 4 {
return None;
}
let bytes: [u8; 4] = data[offset..offset + 4].try_into().ok()?;
Some(u32::from_le_bytes(bytes))
}
/// 安全地从字节数组中读取u16
pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
if data.len() < offset + 2 {
return None;
}
let bytes: [u8; 2] = data[offset..offset + 2].try_into().ok()?;
Some(u16::from_le_bytes(bytes))
}
/// 安全地从字节数组中读取u8
pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
data.get(offset).copied()
}
/// 验证账户索引的有效性
pub fn validate_account_indices(indices: &[u8], account_count: usize) -> bool {
indices.iter().all(|&idx| (idx as usize) < account_count)
}
/// 格式化公钥为短字符串
pub fn format_pubkey_short(pubkey: &solana_sdk::pubkey::Pubkey) -> String {
let s = pubkey.to_string();
if s.len() <= 8 {
s
} else {
format!("{}...{}", &s[..4], &s[s.len() - 4..])
}
}
@@ -1,18 +1,10 @@
use crate::streaming::event_parser::common::filter::EventTypeFilter;
use crate::streaming::event_parser::common::high_performance_clock::elapsed_micros_since;
use crate::streaming::event_parser::common::{EventMetadata, EventType, ProtocolType};
use crate::streaming::event_parser::common::{EventMetadata, EventType};
use crate::streaming::event_parser::core::traits::DexEvent;
use crate::streaming::event_parser::Protocol;
use crate::streaming::grpc::AccountPretty;
use serde::{Deserialize, Serialize};
use solana_account_decoder::parse_nonce::parse_nonce;
use solana_sdk::pubkey::Pubkey;
use spl_token::solana_program::program_pack::Pack;
use spl_token::state::{Account, Mint};
use spl_token_2022::{
extension::StateWithExtensions,
state::{Account as Account2022, Mint as Mint2022},
};
/// 通用账户事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -61,186 +53,38 @@ impl AccountEventParser {
account: AccountPretty,
event_type_filter: Option<&EventTypeFilter>,
) -> Option<DexEvent> {
use crate::streaming::event_parser::core::dispatcher::EventDispatcher;
// 1. 尝试从账户 discriminator 解析(协议特定账户)
if account.data.len() >= 8 {
let discriminator = &account.data[0..8];
// 尝试识别协议类型
if let Some(protocol) = EventDispatcher::match_protocol_by_program_id(&account.owner) {
// 检查是否在请求的协议列表中
if protocols.contains(&protocol) {
// 构建临时元数据(protocol会被dispatcher设置,event_type会在parser中设置)
let metadata = EventMetadata {
slot: account.slot,
signature: account.signature,
protocol: ProtocolType::Common, // 会被 EventDispatcher::dispatch_account 设置
event_type: EventType::default(), // 会被具体 parser 设置
program_id: account.owner,
recv_us: account.recv_us,
handle_us: elapsed_micros_since(account.recv_us),
..Default::default()
};
// 使用 dispatcher 解析
if let Some(event) = EventDispatcher::dispatch_account(
protocol,
discriminator,
&account,
metadata,
) {
// 应用事件类型过滤
if let Some(filter) = event_type_filter {
if filter.passes_event_type(&event.metadata().event_type) {
return Some(event);
}
// 不匹配过滤器,继续尝试其他解析方式
} else {
return Some(event);
}
}
}
}
}
// 2. 尝试解析特殊账户类型(Token、Nonce等)
// 这些是通用的,不属于特定协议
let metadata = EventMetadata {
slot: account.slot,
signature: account.signature,
protocol: ProtocolType::Common,
event_type: EventType::default(),
program_id: account.owner,
recv_us: account.recv_us,
handle_us: elapsed_micros_since(account.recv_us),
..Default::default()
};
// 尝试解析 Nonce 账户
if let Some(event) = Self::parse_nonce_account_event(&account, metadata.clone()) {
if let Some(filter) = event_type_filter {
if filter.passes_event_type(&event.metadata().event_type) {
return Some(event);
}
} else {
return Some(event);
}
}
// 尝试解析 Token 账户
if let Some(event) = Self::parse_token_account_event(&account, metadata) {
if let Some(filter) = event_type_filter {
if filter.passes_event_type(&event.metadata().event_type) {
return Some(event);
}
} else {
return Some(event);
}
}
None
crate::streaming::parser_sdk_bridge::parse_sdk_account_event(
&account,
protocols,
event_type_filter,
)
}
pub fn parse_token_account_event(
account: &AccountPretty,
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::TokenAccount;
let pubkey = account.pubkey;
let executable = account.executable;
let lamports = account.lamports;
let owner = account.owner;
let rent_epoch = account.rent_epoch;
// 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,
executable,
lamports,
owner,
rent_epoch,
supply: mint.supply,
decimals: mint.decimals,
};
let recv_delta = elapsed_micros_since(account.recv_us);
event.metadata.handle_us = recv_delta;
return Some(DexEvent::TokenInfoEvent(event));
}
}
// 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,
executable,
lamports,
owner,
rent_epoch,
supply: mint.base.supply,
decimals: mint.base.decimals,
};
let recv_delta = elapsed_micros_since(account.recv_us);
event.metadata.handle_us = recv_delta;
return Some(DexEvent::TokenInfoEvent(event));
}
}
let amount = if account.owner.to_bytes() == spl_token_2022::ID.to_bytes() {
StateWithExtensions::<Account2022>::unpack(&account.data)
.ok()
.map(|info| info.base.amount)
} else {
Account::unpack(&account.data).ok().map(|info| info.amount)
};
let mut event = TokenAccountEvent {
metadata,
pubkey,
executable,
lamports,
owner,
rent_epoch,
amount,
token_owner: account.owner,
};
let recv_delta = elapsed_micros_since(account.recv_us);
event.metadata.handle_us = recv_delta;
Some(DexEvent::TokenAccountEvent(event))
let mut account = account.clone();
overlay_metadata(&mut account, &metadata);
let filter = EventTypeFilter::include_only([EventType::TokenAccount]);
crate::streaming::parser_sdk_bridge::parse_sdk_account_event(&account, &[], Some(&filter))
}
pub fn parse_nonce_account_event(
account: &AccountPretty,
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::NonceAccount;
let mut account = account.clone();
overlay_metadata(&mut account, &metadata);
let filter = EventTypeFilter::include_only([EventType::NonceAccount]);
crate::streaming::parser_sdk_bridge::parse_sdk_account_event(&account, &[], Some(&filter))
}
}
if let Ok(info) = parse_nonce(&account.data) {
match info {
solana_account_decoder::parse_nonce::UiNonceState::Initialized(details) => {
let mut event = NonceAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
nonce: details.blockhash,
authority: details.authority,
};
event.metadata.handle_us = elapsed_micros_since(account.recv_us);
return Some(DexEvent::NonceAccountEvent(event));
}
solana_account_decoder::parse_nonce::UiNonceState::Uninitialized => {}
}
}
None
fn overlay_metadata(account: &mut AccountPretty, metadata: &EventMetadata) {
account.slot = metadata.slot;
account.signature = metadata.signature;
if metadata.recv_us != 0 {
account.recv_us = metadata.recv_us;
}
}
@@ -1,15 +1,9 @@
use crate::streaming::event_parser::common::high_performance_clock::elapsed_micros_since;
use crate::streaming::event_parser::common::types::{EventType, ProtocolType};
use crate::streaming::event_parser::common::EventMetadata;
use crate::streaming::event_parser::core::traits::DexEvent;
use crate::streaming::event_parser::protocols::block::block_meta_event::BlockMetaEvent;
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
// Compute Budget Program ID
pub const COMPUTE_BUDGET_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("ComputeBudget111111111111111111111111111111");
/// SetComputeUnitLimit 事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
@@ -42,42 +36,4 @@ impl CommonEventParser {
block_meta_event.metadata.handle_us = elapsed_micros_since(recv_us);
DexEvent::BlockMetaEvent(block_meta_event)
}
/// 解析 Compute Budget 指令
pub fn parse_compute_budget_instruction(
instruction_data: &[u8],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
if instruction_data.is_empty() {
return None;
}
// 设置 protocol 为 Common
metadata.protocol = ProtocolType::Common;
// Compute Budget 指令使用单字节判别器
match instruction_data[0] {
// SetComputeUnitLimit: discriminator = 2
2 => {
if instruction_data.len() < 5 {
return None;
}
let units = u32::from_le_bytes(instruction_data[1..5].try_into().ok()?);
metadata.event_type = EventType::SetComputeUnitLimit;
let event = SetComputeUnitLimitEvent { metadata, units };
Some(DexEvent::SetComputeUnitLimitEvent(event))
}
// SetComputeUnitPrice: discriminator = 3
3 => {
if instruction_data.len() < 9 {
return None;
}
let micro_lamports = u64::from_le_bytes(instruction_data[1..9].try_into().ok()?);
metadata.event_type = EventType::SetComputeUnitPrice;
let event = SetComputeUnitPriceEvent { metadata, micro_lamports };
Some(DexEvent::SetComputeUnitPriceEvent(event))
}
_ => None,
}
}
}
+119 -257
View File
@@ -1,331 +1,193 @@
//! 事件路由入口(类比 sol-parser-sdk 的 `instr`,区分「原生字节解析」与「sdk 事件对齐」)。
//! SDK-backed event dispatcher kept for compatibility with older streamer APIs.
//!
//! ## 代码去哪找
//! - **`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。
//!
//! ## 设计原则
//! - **单一职责**: 每个函数只负责一件事(路由、解析、合并分离)
//! - **灵活性**: 调用方可以选择是否合并,或自定义合并逻辑
//! - **可测试性**: 每个函数都可以独立测试
//! Streamer no longer owns protocol parsers here. The dispatcher only adapts
//! streamer metadata and routes to `sol-parser-sdk` parsers, then converts SDK
//! events back to streamer `DexEvent`.
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, sol_parser_forward,
},
DexEvent, Protocol,
use crate::streaming::event_parser::{common::EventMetadata, DexEvent, Protocol};
use crate::streaming::parser_sdk_bridge::{
block_timestamp_from_stream_meta, convert_parser_event, fuse_streamer_ix_ctx,
parse_sdk_account_event,
};
use sol_parser_sdk::core::events::EventMetadata as PbEventMetadata;
use sol_parser_sdk::instr::{
all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner,
};
use solana_sdk::pubkey::Pubkey;
/// 中心事件解析调度器
///
/// 负责将解析请求路由到对应协议的解析函数
pub struct EventDispatcher;
impl EventDispatcher {
/// 解析 instruction 事件(只解析,不合并)
///
/// # 参数
/// - `protocol`: 协议类型
/// - `instruction_discriminator`: 指令判别器 (8 bytes)
/// - `instruction_data`: 指令数据
/// - `accounts`: 账户公钥列表
/// - `metadata`: 事件元数据
///
/// # 返回
/// 解析成功返回 `Some(DexEvent)`,否则返回 `None`
#[inline]
pub fn dispatch_instruction(
protocol: Protocol,
instruction_discriminator: &[u8],
instruction_data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
// 根据协议类型设置 metadata.protocol
use crate::streaming::event_parser::common::ProtocolType;
metadata.protocol = match protocol {
Protocol::PumpFun => ProtocolType::PumpFun,
Protocol::PumpSwap => ProtocolType::PumpSwap,
Protocol::Bonk => ProtocolType::Bonk,
Protocol::RaydiumCpmm => ProtocolType::RaydiumCpmm,
Protocol::RaydiumClmm => ProtocolType::RaydiumClmm,
Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4,
Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2,
Protocol::OrcaWhirlpool => ProtocolType::OrcaWhirlpool,
Protocol::MeteoraPools => ProtocolType::MeteoraPools,
Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm,
};
let mut full = Vec::with_capacity(instruction_discriminator.len() + instruction_data.len());
full.extend_from_slice(instruction_discriminator);
full.extend_from_slice(instruction_data);
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,
accounts,
metadata,
),
Protocol::PumpSwap => pumpswap::parse_pumpswap_instruction_data(
instruction_discriminator,
instruction_data,
accounts,
metadata,
),
Protocol::Bonk => bonk::parse_bonk_instruction_data(
instruction_discriminator,
instruction_data,
accounts,
metadata,
),
Protocol::RaydiumCpmm => raydium_cpmm::parse_raydium_cpmm_instruction_data(
instruction_discriminator,
instruction_data,
accounts,
metadata,
),
Protocol::RaydiumClmm => raydium_clmm::parse_raydium_clmm_instruction_data(
instruction_discriminator,
instruction_data,
accounts,
metadata,
),
Protocol::RaydiumAmmV4 => raydium_amm_v4::parse_raydium_amm_v4_instruction_data(
instruction_discriminator,
instruction_data,
accounts,
metadata,
),
Protocol::MeteoraDammV2 => meteora_damm_v2::parse_meteora_damm_v2_instruction_data(
instruction_discriminator,
instruction_data,
accounts,
metadata,
),
}
let program_id = Self::get_program_id(protocol);
let pb = sol_parser_sdk::instr::parse_instruction_unified(
&full,
accounts,
metadata.signature,
metadata.slot,
metadata.tx_index.unwrap_or(0),
Some(metadata.block_time_ms.saturating_mul(1000)),
metadata.recv_us,
None,
&program_id,
)?;
let ts = block_timestamp_from_stream_meta(&metadata);
let ev = convert_parser_event(pb, Some(&ts), metadata.recv_us)?;
Some(fuse_streamer_ix_ctx(ev, &metadata))
}
/// 解析 inner instruction 事件(只解析,不合并)
///
/// # 参数
/// - `protocol`: 协议类型
/// - `inner_instruction_discriminator`: 内联指令判别器 (16 bytes)
/// - `inner_instruction_data`: 内联指令数据
/// - `metadata`: 事件元数据
///
/// # 返回
/// 解析成功返回 `Some(DexEvent)`,否则返回 `None`
#[inline]
pub fn dispatch_inner_instruction(
protocol: Protocol,
inner_instruction_discriminator: &[u8],
inner_instruction_data: &[u8],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
// 根据协议类型设置 metadata.protocol
use crate::streaming::event_parser::common::ProtocolType;
metadata.protocol = match protocol {
Protocol::PumpFun => ProtocolType::PumpFun,
Protocol::PumpSwap => ProtocolType::PumpSwap,
Protocol::Bonk => ProtocolType::Bonk,
Protocol::RaydiumCpmm => ProtocolType::RaydiumCpmm,
Protocol::RaydiumClmm => ProtocolType::RaydiumClmm,
Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4,
Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2,
Protocol::OrcaWhirlpool => ProtocolType::OrcaWhirlpool,
Protocol::MeteoraPools => ProtocolType::MeteoraPools,
Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm,
};
let disc: [u8; 16] = inner_instruction_discriminator.try_into().ok()?;
let pm = pb_meta_from_streamer(&metadata);
match protocol {
Protocol::OrcaWhirlpool | Protocol::MeteoraPools | Protocol::MeteoraDlmm => {
sol_parser_forward::native::dispatch_inner_instruction(
protocol.clone(),
inner_instruction_discriminator,
inner_instruction_data,
&metadata,
)
let pb = match protocol {
Protocol::PumpFun => pump_inner::parse_pumpfun_inner_instruction(
&disc,
inner_instruction_data,
pm,
false,
),
Protocol::PumpSwap => {
pump_amm_inner::parse_pumpswap_inner_instruction(&disc, inner_instruction_data, pm)
}
Protocol::PumpFun => pumpfun::parse_pumpfun_inner_instruction_data(
inner_instruction_discriminator,
Protocol::PumpFees => all_inner::pump_fees::parse(&disc, inner_instruction_data, pm),
Protocol::Bonk | Protocol::RaydiumLaunchpad => {
all_inner::bonk::parse(&disc, inner_instruction_data, pm)
}
Protocol::RaydiumCpmm => {
all_inner::raydium_cpmm::parse(&disc, inner_instruction_data, pm)
}
Protocol::RaydiumClmm => raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
&disc,
inner_instruction_data,
metadata,
),
Protocol::PumpSwap => pumpswap::parse_pumpswap_inner_instruction_data(
inner_instruction_discriminator,
inner_instruction_data,
metadata,
),
Protocol::Bonk => bonk::parse_bonk_inner_instruction_data(
inner_instruction_discriminator,
inner_instruction_data,
metadata,
),
Protocol::RaydiumCpmm => raydium_cpmm::parse_raydium_cpmm_inner_instruction_data(
inner_instruction_discriminator,
inner_instruction_data,
metadata,
),
Protocol::RaydiumClmm => raydium_clmm::parse_raydium_clmm_inner_instruction_data(
inner_instruction_discriminator,
inner_instruction_data,
metadata,
),
Protocol::RaydiumAmmV4 => raydium_amm_v4::parse_raydium_amm_v4_inner_instruction_data(
inner_instruction_discriminator,
inner_instruction_data,
metadata,
pm,
),
Protocol::RaydiumAmmV4 => {
all_inner::raydium_amm::parse(&disc, inner_instruction_data, pm)
}
Protocol::MeteoraDammV2 => {
meteora_damm_v2::parse_meteora_damm_v2_inner_instruction_data(
inner_instruction_discriminator,
inner_instruction_data,
metadata,
)
all_inner::meteora_damm::parse(&disc, inner_instruction_data, pm)
}
}
Protocol::OrcaWhirlpool => all_inner::orca::parse(&disc, inner_instruction_data, pm),
Protocol::MeteoraPools => {
all_inner::meteora_amm::parse(&disc, inner_instruction_data, pm)
}
Protocol::MeteoraDlmm => {
all_inner::meteora_dlmm::parse(&disc, inner_instruction_data, pm)
}
}?;
let ts = block_timestamp_from_stream_meta(&metadata);
let ev = convert_parser_event(pb, Some(&ts), metadata.recv_us)?;
Some(fuse_streamer_ix_ctx(ev, &metadata))
}
/// 通过 program_id 匹配协议类型
#[inline]
pub fn match_protocol_by_program_id(program_id: &Pubkey) -> Option<Protocol> {
if program_id == &pumpfun::PUMPFUN_PROGRAM_ID {
if program_id == &program_ids::PUMPFUN_PROGRAM_ID {
Some(Protocol::PumpFun)
} else if program_id == &pumpswap::PUMPSWAP_PROGRAM_ID {
} else if program_id == &program_ids::PUMP_FEES_PROGRAM_ID {
Some(Protocol::PumpFees)
} else if program_id == &program_ids::PUMPSWAP_PROGRAM_ID {
Some(Protocol::PumpSwap)
} else if program_id == &bonk::BONK_PROGRAM_ID {
} else if program_id == &program_ids::BONK_PROGRAM_ID {
Some(Protocol::Bonk)
} else if program_id == &raydium_cpmm::RAYDIUM_CPMM_PROGRAM_ID {
} else if program_id == &program_ids::RAYDIUM_CPMM_PROGRAM_ID {
Some(Protocol::RaydiumCpmm)
} else if program_id == &raydium_clmm::RAYDIUM_CLMM_PROGRAM_ID {
} else if program_id == &program_ids::RAYDIUM_CLMM_PROGRAM_ID {
Some(Protocol::RaydiumClmm)
} else if program_id == &raydium_amm_v4::RAYDIUM_AMM_V4_PROGRAM_ID {
} else if program_id == &program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
Some(Protocol::RaydiumAmmV4)
} else if program_id == &meteora_damm_v2::METEORA_DAMM_V2_PROGRAM_ID {
} else if program_id == &program_ids::METEORA_DAMM_V2_PROGRAM_ID {
Some(Protocol::MeteoraDammV2)
} else if program_id == &sol_parser_forward::ORCA_WHIRLPOOL_PROGRAM_ID {
} else if program_id == &program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
Some(Protocol::OrcaWhirlpool)
} else if program_id == &sol_parser_forward::METEORA_POOLS_PROGRAM_ID {
} else if program_id == &program_ids::METEORA_POOLS_PROGRAM_ID {
Some(Protocol::MeteoraPools)
} else if program_id == &sol_parser_forward::METEORA_DLMM_PROGRAM_ID {
} else if program_id == &program_ids::METEORA_DLMM_PROGRAM_ID {
Some(Protocol::MeteoraDlmm)
} else {
None
}
}
/// 检查是否为 Compute Budget Program
#[inline]
pub fn is_compute_budget_program(program_id: &Pubkey) -> bool {
program_id == &COMPUTE_BUDGET_PROGRAM_ID
program_id == &solana_sdk::pubkey!("ComputeBudget111111111111111111111111111111")
}
/// 解析 Compute Budget 指令
///
/// # 参数
/// - `instruction_data`: 指令数据
/// - `metadata`: 事件元数据
///
/// # 返回
/// 解析成功返回 `Some(DexEvent)`,否则返回 `None`
#[inline]
pub fn dispatch_compute_budget_instruction(
instruction_data: &[u8],
metadata: EventMetadata,
_instruction_data: &[u8],
_metadata: EventMetadata,
) -> Option<DexEvent> {
CommonEventParser::parse_compute_budget_instruction(instruction_data, metadata)
None
}
/// 获取指定协议的 program_id
#[inline]
pub fn get_program_id(protocol: Protocol) -> Pubkey {
match protocol {
Protocol::PumpFun => pumpfun::PUMPFUN_PROGRAM_ID,
Protocol::PumpSwap => pumpswap::PUMPSWAP_PROGRAM_ID,
Protocol::Bonk => bonk::BONK_PROGRAM_ID,
Protocol::RaydiumCpmm => raydium_cpmm::RAYDIUM_CPMM_PROGRAM_ID,
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,
Protocol::PumpFun => program_ids::PUMPFUN_PROGRAM_ID,
Protocol::PumpFees => program_ids::PUMP_FEES_PROGRAM_ID,
Protocol::PumpSwap => program_ids::PUMPSWAP_PROGRAM_ID,
Protocol::Bonk | Protocol::RaydiumLaunchpad => program_ids::BONK_PROGRAM_ID,
Protocol::RaydiumCpmm => program_ids::RAYDIUM_CPMM_PROGRAM_ID,
Protocol::RaydiumClmm => program_ids::RAYDIUM_CLMM_PROGRAM_ID,
Protocol::RaydiumAmmV4 => program_ids::RAYDIUM_AMM_V4_PROGRAM_ID,
Protocol::MeteoraDammV2 => program_ids::METEORA_DAMM_V2_PROGRAM_ID,
Protocol::OrcaWhirlpool => program_ids::ORCA_WHIRLPOOL_PROGRAM_ID,
Protocol::MeteoraPools => program_ids::METEORA_POOLS_PROGRAM_ID,
Protocol::MeteoraDlmm => program_ids::METEORA_DLMM_PROGRAM_ID,
}
}
/// 批量获取 program_ids
pub fn get_program_ids(protocols: &[Protocol]) -> Vec<Pubkey> {
protocols.iter().map(|p| Self::get_program_id(p.clone())).collect()
let mut ids = Vec::with_capacity(protocols.len());
for protocol in protocols {
let id = Self::get_program_id(protocol.clone());
if !ids.contains(&id) {
ids.push(id);
}
}
ids
}
/// 解析账户数据
///
/// 根据账户的 discriminator 路由到对应协议的账户解析函数
///
/// # 参数
/// - `protocol`: 协议类型
/// - `discriminator`: 账户判别器
/// - `account`: 账户信息
/// - `metadata`: 事件元数据
///
/// # 返回
/// 解析成功返回 `Some(DexEvent)`,否则返回 `None`
pub fn dispatch_account(
protocol: Protocol,
discriminator: &[u8],
_discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
mut metadata: crate::streaming::event_parser::common::EventMetadata,
_metadata: EventMetadata,
) -> Option<DexEvent> {
// 根据协议类型设置 metadata.protocol
use crate::streaming::event_parser::common::ProtocolType;
metadata.protocol = match protocol {
Protocol::PumpFun => ProtocolType::PumpFun,
Protocol::PumpSwap => ProtocolType::PumpSwap,
Protocol::Bonk => ProtocolType::Bonk,
Protocol::RaydiumCpmm => ProtocolType::RaydiumCpmm,
Protocol::RaydiumClmm => ProtocolType::RaydiumClmm,
Protocol::RaydiumAmmV4 => ProtocolType::RaydiumAmmV4,
Protocol::MeteoraDammV2 => ProtocolType::MeteoraDammV2,
Protocol::OrcaWhirlpool => ProtocolType::OrcaWhirlpool,
Protocol::MeteoraPools => ProtocolType::MeteoraPools,
Protocol::MeteoraDlmm => ProtocolType::MeteoraDlmm,
};
parse_sdk_account_event(account, &[protocol], None)
}
}
match protocol {
Protocol::OrcaWhirlpool | Protocol::MeteoraPools | Protocol::MeteoraDlmm => None,
Protocol::PumpFun => {
pumpfun::parse_pumpfun_account_data(discriminator, account, metadata)
}
Protocol::PumpSwap => {
pumpswap::parse_pumpswap_account_data(discriminator, account, metadata)
}
Protocol::Bonk => bonk::parse_bonk_account_data(discriminator, account, metadata),
Protocol::RaydiumCpmm => {
raydium_cpmm::parse_raydium_cpmm_account_data(discriminator, account, metadata)
}
Protocol::RaydiumClmm => {
raydium_clmm::parse_raydium_clmm_account_data(discriminator, account, metadata)
}
Protocol::RaydiumAmmV4 => {
raydium_amm_v4::parse_raydium_amm_v4_account_data(discriminator, account, metadata)
}
Protocol::MeteoraDammV2 => {
// Meteora DAMM 目前不需要解析账户数据,返回 None
None
}
}
#[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(),
}
}
@@ -1,195 +0,0 @@
//! 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(())
}
@@ -1,87 +0,0 @@
//! 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(())
}
@@ -1,11 +0,0 @@
//! 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;
@@ -1,171 +0,0 @@
//! 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(())
}
@@ -1,9 +0,0 @@
//! 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,
}
@@ -1,265 +0,0 @@
//! 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(())
}
@@ -1,13 +0,0 @@
//! 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;
@@ -1,30 +1,13 @@
//! Protocol filtering and event enrichment for PumpFun / PumpSwap / Bonk / bot flags.
//! 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,
DexEvent,
};
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
// ================================================================================================
@@ -1,13 +1,9 @@
//! Transaction parser entry point with separate gRPC and standard ix paths.
//! Transaction parser entry point.
//!
//! | Module | Path |
//! |--------|------|
//! | [`grpc_path`] | Yellowstone gRPC |
//! | [`compiled_path`] | standard transaction / RPC replay |
//! | [`helpers`] | `should_handle`、`process_event` |
//! gRPC and ShredStream parsing are delegated to `sol-parser-sdk`; this layer
//! only adapts SDK events into streamer event structs and applies streamer
//! metadata enrichment.
mod compiled_path;
mod grpc_path;
pub(crate) mod helpers;
pub struct EventParser;
@@ -16,7 +12,7 @@ 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,
mut grpc_tx: yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo,
signature: solana_sdk::signature::Signature,
slot: Option<u64>,
block_time: Option<prost_types::Timestamp>,
@@ -25,19 +21,46 @@ impl EventParser {
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,
if grpc_tx.signature.is_empty() {
grpc_tx.signature = signature.as_ref().to_vec();
}
if grpc_tx.index == 0 {
grpc_tx.index = tx_index.unwrap_or(0);
}
let block_us = block_time.map(|t| t.seconds * 1_000_000 + t.nanos as i64 / 1_000);
let update = yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction {
slot: slot.unwrap_or(0),
transaction: Some(grpc_tx),
..Default::default()
};
let sdk_parse_filter =
crate::streaming::event_parser::common::filter::build_sdk_parse_event_filter(
event_type_filter,
);
let sdk_events = sol_parser_sdk::grpc::parse_subscribe_update_transaction_low_latency(
&update,
recv_us,
bot_wallet,
tx_index,
callback,
)
.await
block_us,
sdk_parse_filter.as_ref(),
);
for sdk_event in sdk_events {
if let Some(mut event) = crate::streaming::parser_sdk_bridge::adapt_parser_event(
sdk_event,
block_time.as_ref(),
recv_us,
protocols,
event_type_filter,
) {
event.metadata_mut().handle_us =
crate::streaming::event_parser::common::high_performance_clock::elapsed_micros_since(
recv_us,
);
event = helpers::process_event(event, bot_wallet);
callback(event);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
@@ -47,28 +70,45 @@ impl EventParser {
transaction: &solana_sdk::transaction::VersionedTransaction,
signature: solana_sdk::signature::Signature,
slot: Option<u64>,
block_time: Option<prost_types::Timestamp>,
_block_time: Option<prost_types::Timestamp>,
recv_us: i64,
accounts: &[solana_sdk::pubkey::Pubkey],
inner_instructions: &[solana_transaction_status::InnerInstructions],
_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,
let sdk_parse_filter =
crate::streaming::event_parser::common::filter::build_sdk_shred_parse_event_filter(
protocols,
event_type_filter,
);
let mut sdk_events = Vec::with_capacity(4);
sol_parser_sdk::shredstream::parse_transaction_dex_events_with_filter(
transaction,
signature,
slot,
block_time,
slot.unwrap_or(0),
tx_index.unwrap_or(0),
recv_us,
accounts,
inner_instructions,
bot_wallet,
tx_index,
callback,
)
.await
sdk_parse_filter.as_ref(),
&mut sdk_events,
);
for sdk_event in sdk_events {
if let Some(mut event) = crate::streaming::parser_sdk_bridge::adapt_parser_event(
sdk_event,
None,
recv_us,
protocols,
event_type_filter,
) {
event.metadata_mut().handle_us =
crate::streaming::event_parser::common::high_performance_clock::elapsed_micros_since(
recv_us,
);
event = helpers::process_event(event, bot_wallet);
callback(event);
}
}
Ok(())
}
}
+3 -2
View File
@@ -1,7 +1,8 @@
//! Solana DEX 交易解析:**对外类型** [`DexEvent`]、[`Protocol`]
//! **`common`**(元数据/过滤器)、**`core`**调度、合并、gRPC 解析入口)、**`protocols`**(按协议的 parser/events)。
//! **`common`**(元数据/过滤器)、**`core`**SDK 调度与入口)、**`protocols`**(按协议的事件类型与兼容 facade)。
//!
//! 与 **sol-parser-sdk** 对照:`protocols/*/parser` ≈ sdk `instr/*``parser_sdk_bridge` ≈ sdk 事件枚举与各实现的胶水层。
//! gRPC/ShredStream 解析统一委托给 **sol-parser-sdk**,本 crate 只做订阅、过滤映射、
//! SDK 事件到 streamer 事件的适配,以及兼容旧公开路径的轻量转发。
//!
//! [`DexEvent`]: crate::streaming::event_parser::DexEvent
//! [`Protocol`]: crate::streaming::event_parser::Protocol
@@ -73,13 +73,6 @@ pub struct BonkTradeEvent {
pub const BONK_TRADE_EVENT_LOG_SIZE: usize = 32 + 8 * 13 + 1 + 1 + 1;
pub fn bonk_trade_event_log_decode(data: &[u8]) -> Option<BonkTradeEvent> {
if data.len() < BONK_TRADE_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<BonkTradeEvent>(&data[..BONK_TRADE_EVENT_LOG_SIZE]).ok()
}
/// Create pool event
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct BonkPoolCreateEvent {
@@ -110,13 +103,6 @@ pub struct BonkPoolCreateEvent {
pub const BONK_POOL_CREATE_EVENT_LOG_SIZE: usize = 256;
pub fn bonk_pool_create_event_log_decode(data: &[u8]) -> Option<BonkPoolCreateEvent> {
if data.len() < BONK_POOL_CREATE_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<BonkPoolCreateEvent>(&data[..BONK_POOL_CREATE_EVENT_LOG_SIZE]).ok()
}
/// Create pool event
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct BonkMigrateToAmmEvent {
+8 -582
View File
@@ -1,605 +1,31 @@
use crate::streaming::event_parser::{
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::event_parser::{
common::{utils::*, EventMetadata, EventType},
protocols::bonk::{
bonk_pool_create_event_log_decode, bonk_trade_event_log_decode, discriminators, AmmFeeOn,
BonkMigrateToAmmEvent, BonkMigrateToCpswapEvent, BonkPoolCreateEvent, BonkTradeEvent,
ConstantCurve, CurveParams, FixedCurve, LinearCurve, MintParams, TradeDirection,
VestingParams,
},
DexEvent,
};
pub use sol_parser_sdk::instr::program_ids::BONK_PROGRAM_ID;
/// Bonk Program ID
pub const BONK_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj");
/// 解析 Bonk instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_bonk_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
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::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_WITH_TOKEN_2022 => {
parse_initialize_with_token_2022_instruction(data, accounts, metadata)
}
discriminators::MIGRATE_TO_AMM => {
parse_migrate_to_amm_instruction(data, accounts, metadata)
}
discriminators::MIGRATE_TO_CP_SWAP => {
parse_migrate_to_cpswap_instruction(data, accounts, metadata)
}
_ => None,
}
EventDispatcher::dispatch_instruction(Protocol::Bonk, discriminator, data, accounts, metadata)
}
/// 解析 Bonk inner instruction data
///
/// 根据判别器路由到具体的 inner instruction 解析函数
pub fn parse_bonk_inner_instruction_data(
discriminator: &[u8],
data: &[u8],
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),
_ => None,
}
EventDispatcher::dispatch_inner_instruction(Protocol::Bonk, discriminator, data, metadata)
}
/// 解析 Bonk 账户数据
///
/// 根据判别器路由到具体的账户解析函数
pub fn parse_bonk_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: crate::streaming::event_parser::common::EventMetadata,
) -> Option<crate::streaming::event_parser::DexEvent> {
match discriminator {
discriminators::POOL_STATE_ACCOUNT => {
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,
)
}
discriminators::PLATFORM_CONFIG_ACCOUNT => {
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> {
// 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) {
Some(DexEvent::BonkPoolCreateEvent(BonkPoolCreateEvent { metadata, ..event }))
} else {
None
}
}
/// Parse trade event
fn parse_trade_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
if let Some(event) = bonk_trade_event_log_decode(data) {
if metadata.event_type == EventType::BonkBuyExactIn
|| metadata.event_type == EventType::BonkBuyExactOut
{
if event.trade_direction != TradeDirection::Buy {
return None;
}
} else if (metadata.event_type == EventType::BonkSellExactIn
|| metadata.event_type == EventType::BonkSellExactOut)
&& event.trade_direction != TradeDirection::Sell
{
return None;
}
Some(DexEvent::BonkTradeEvent(BonkTradeEvent { metadata, ..event }))
} else {
None
}
}
/// Parse buy instruction event
fn parse_buy_exact_in_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkBuyExactIn;
if data.len() < 16 || accounts.len() < 18 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
Some(DexEvent::BonkTradeEvent(BonkTradeEvent {
metadata,
amount_in,
minimum_amount_out,
share_fee_rate,
payer: accounts[0],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
system_program: accounts[15],
platform_associated_account: accounts[16],
creator_associated_account: accounts[17],
trade_direction: TradeDirection::Buy,
..Default::default()
}))
}
fn parse_buy_exact_out_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkBuyExactOut;
if data.len() < 16 || accounts.len() < 18 {
return None;
}
let amount_out = read_u64_le(data, 0)?;
let maximum_amount_in = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
Some(DexEvent::BonkTradeEvent(BonkTradeEvent {
metadata,
amount_out,
maximum_amount_in,
share_fee_rate,
payer: accounts[0],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
system_program: accounts[15],
platform_associated_account: accounts[16],
creator_associated_account: accounts[17],
trade_direction: TradeDirection::Buy,
..Default::default()
}))
}
fn parse_sell_exact_in_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkSellExactIn;
if data.len() < 16 || accounts.len() < 18 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
Some(DexEvent::BonkTradeEvent(BonkTradeEvent {
metadata,
amount_in,
minimum_amount_out,
share_fee_rate,
payer: accounts[0],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
system_program: accounts[15],
platform_associated_account: accounts[16],
creator_associated_account: accounts[17],
trade_direction: TradeDirection::Sell,
..Default::default()
}))
}
fn parse_sell_exact_out_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkSellExactOut;
if data.len() < 16 || accounts.len() < 18 {
return None;
}
let amount_out = read_u64_le(data, 0)?;
let maximum_amount_in = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
Some(DexEvent::BonkTradeEvent(BonkTradeEvent {
metadata,
amount_out,
maximum_amount_in,
share_fee_rate,
payer: accounts[0],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
system_program: accounts[15],
platform_associated_account: accounts[16],
creator_associated_account: accounts[17],
trade_direction: TradeDirection::Sell,
..Default::default()
}))
}
/// Parse initialize event
fn parse_initialize_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkInitialize;
if data.len() < 24 {
return None;
}
let mut offset = 0;
let base_mint_param = parse_mint_params(data, &mut offset)?;
let curve_param = parse_curve_params(data, &mut offset)?;
let vesting_param = parse_vesting_params(data, &mut offset)?;
Some(DexEvent::BonkPoolCreateEvent(BonkPoolCreateEvent {
metadata,
payer: accounts[0],
creator: accounts[1],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[5],
base_mint: accounts[6],
quote_mint: accounts[7],
base_vault: accounts[8],
quote_vault: accounts[9],
base_mint_param,
curve_param,
vesting_param,
..Default::default()
}))
}
/// Parse initialize event
fn parse_initialize_v2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkInitializeV2;
if data.len() < 24 {
return None;
}
let mut offset = 0;
let base_mint_param = parse_mint_params(data, &mut offset)?;
let curve_param = parse_curve_params(data, &mut offset)?;
let vesting_param = parse_vesting_params(data, &mut offset)?;
let amm_fee_on = data[offset];
Some(DexEvent::BonkPoolCreateEvent(BonkPoolCreateEvent {
metadata,
payer: accounts[0],
creator: accounts[1],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[5],
base_mint: accounts[6],
quote_mint: accounts[7],
base_vault: accounts[8],
quote_vault: accounts[9],
base_mint_param,
curve_param,
vesting_param,
amm_fee_on: if amm_fee_on == 0 {
Some(AmmFeeOn::QuoteToken)
} else {
Some(AmmFeeOn::BothToken)
},
..Default::default()
}))
}
/// Parse initialize event
fn parse_initialize_with_token_2022_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkInitializeWithToken2022;
if data.len() < 24 {
return None;
}
let mut offset = 0;
let base_mint_param = parse_mint_params(data, &mut offset)?;
let curve_param = parse_curve_params(data, &mut offset)?;
let vesting_param = parse_vesting_params(data, &mut offset)?;
let amm_fee_on = data[offset];
Some(DexEvent::BonkPoolCreateEvent(BonkPoolCreateEvent {
metadata,
payer: accounts[0],
creator: accounts[1],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[5],
base_mint: accounts[6],
quote_mint: accounts[7],
base_vault: accounts[8],
quote_vault: accounts[9],
base_mint_param,
curve_param,
vesting_param,
amm_fee_on: if amm_fee_on == 0 {
Some(AmmFeeOn::QuoteToken)
} else {
Some(AmmFeeOn::BothToken)
},
..Default::default()
}))
}
/// Parse MintParams structure
fn parse_mint_params(data: &[u8], offset: &mut usize) -> Option<MintParams> {
// Read decimals (1 byte)
let decimals = read_u8(data, *offset)?;
*offset += 1;
// Read name string length and content
let name_len = read_u32_le(data, *offset)? as usize;
*offset += 4;
if data.len() < *offset + name_len {
return None;
}
let name = String::from_utf8(data[*offset..*offset + name_len].to_vec()).ok()?;
*offset += name_len;
// Read symbol string length and content
let symbol_len = read_u32_le(data, *offset)? as usize;
*offset += 4;
if data.len() < *offset + symbol_len {
return None;
}
let symbol = String::from_utf8(data[*offset..*offset + symbol_len].to_vec()).ok()?;
*offset += symbol_len;
// Read uri string length and content
let uri_len = read_u32_le(data, *offset)? as usize;
*offset += 4;
if data.len() < *offset + uri_len {
return None;
}
let uri = String::from_utf8(data[*offset..*offset + uri_len].to_vec()).ok()?;
*offset += uri_len;
Some(MintParams { decimals, name, symbol, uri })
}
/// Parse CurveParams structure
fn parse_curve_params(data: &[u8], offset: &mut usize) -> Option<CurveParams> {
// Read curve type identifier (1 byte)
let curve_type = read_u8(data, *offset)?;
*offset += 1;
match curve_type {
0 => {
// Constant curve
let supply = read_u64_le(data, *offset)?;
*offset += 8;
let total_base_sell = read_u64_le(data, *offset)?;
*offset += 8;
let total_quote_fund_raising = read_u64_le(data, *offset)?;
*offset += 8;
let migrate_type = read_u8(data, *offset)?;
*offset += 1;
Some(CurveParams::Constant {
data: ConstantCurve {
supply,
total_base_sell,
total_quote_fund_raising,
migrate_type,
},
})
}
1 => {
// Fixed curve
let supply = read_u64_le(data, *offset)?;
*offset += 8;
let total_quote_fund_raising = read_u64_le(data, *offset)?;
*offset += 8;
let migrate_type = read_u8(data, *offset)?;
*offset += 1;
Some(CurveParams::Fixed {
data: FixedCurve { supply, total_quote_fund_raising, migrate_type },
})
}
2 => {
// Linear curve
let supply = read_u64_le(data, *offset)?;
*offset += 8;
let total_quote_fund_raising = read_u64_le(data, *offset)?;
*offset += 8;
let migrate_type = read_u8(data, *offset)?;
*offset += 1;
Some(CurveParams::Linear {
data: LinearCurve { supply, total_quote_fund_raising, migrate_type },
})
}
_ => None,
}
}
/// Parse VestingParams structure
fn parse_vesting_params(data: &[u8], offset: &mut usize) -> Option<VestingParams> {
let total_locked_amount = read_u64_le(data, *offset)?;
*offset += 8;
let cliff_period = read_u64_le(data, *offset)?;
*offset += 8;
let unlock_period = read_u64_le(data, *offset)?;
*offset += 8;
Some(VestingParams { total_locked_amount, cliff_period, unlock_period })
}
/// Parse migrate to AMM event
fn parse_migrate_to_amm_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkMigrateToAmm;
if data.len() < 16 {
return None;
}
let base_lot_size = u64::from_le_bytes(data[0..8].try_into().unwrap());
let quote_lot_size = u64::from_le_bytes(data[8..16].try_into().unwrap());
let market_vault_signer_nonce = data[16];
Some(DexEvent::BonkMigrateToAmmEvent(BonkMigrateToAmmEvent {
metadata,
base_lot_size,
quote_lot_size,
market_vault_signer_nonce,
payer: accounts[0],
base_mint: accounts[1],
quote_mint: accounts[2],
openbook_program: accounts[3],
market: accounts[4],
request_queue: accounts[5],
event_queue: accounts[6],
bids: accounts[7],
asks: accounts[8],
market_vault_signer: accounts[9],
market_base_vault: accounts[10],
market_quote_vault: accounts[11],
amm_program: accounts[12],
amm_pool: accounts[13],
amm_authority: accounts[14],
amm_open_orders: accounts[15],
amm_lp_mint: accounts[16],
amm_base_vault: accounts[17],
amm_quote_vault: accounts[18],
amm_target_orders: accounts[19],
amm_config: accounts[20],
amm_create_fee_destination: accounts[21],
authority: accounts[22],
pool_state: accounts[23],
global_config: accounts[24],
base_vault: accounts[25],
quote_vault: accounts[26],
pool_lp_token: accounts[27],
spl_token_program: accounts[28],
associated_token_program: accounts[29],
system_program: accounts[30],
rent_program: accounts[31],
..Default::default()
}))
}
/// Parse migrate to CP Swap event
fn parse_migrate_to_cpswap_instruction(
_data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::BonkMigrateToCpswap;
Some(DexEvent::BonkMigrateToCpswapEvent(BonkMigrateToCpswapEvent {
metadata,
payer: accounts[0],
base_mint: accounts[1],
quote_mint: accounts[2],
platform_config: accounts[3],
cpswap_program: accounts[4],
cpswap_pool: accounts[5],
cpswap_authority: accounts[6],
cpswap_lp_mint: accounts[7],
cpswap_base_vault: accounts[8],
cpswap_quote_vault: accounts[9],
cpswap_config: accounts[10],
cpswap_create_pool_fee: accounts[11],
cpswap_observation: accounts[12],
lock_program: accounts[13],
lock_authority: accounts[14],
lock_lp_vault: accounts[15],
authority: accounts[16],
pool_state: accounts[17],
global_config: accounts[18],
base_vault: accounts[19],
quote_vault: accounts[20],
pool_lp_token: accounts[21],
base_token_program: accounts[22],
quote_token_program: accounts[23],
associated_token_program: accounts[24],
system_program: accounts[25],
rent_program: accounts[26],
metadata_program: accounts[27],
remaining_accounts: accounts[28..].to_vec(),
..Default::default()
}))
EventDispatcher::dispatch_account(Protocol::Bonk, discriminator, account, metadata)
}
@@ -2,17 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::{
event_parser::{
common::{EventMetadata, EventType},
protocols::bonk::{
BonkGlobalConfigAccountEvent, BonkPlatformConfigAccountEvent, BonkPoolStateAccountEvent,
},
DexEvent,
},
grpc::AccountPretty,
};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub enum TradeDirection {
#[default]
@@ -182,34 +171,6 @@ impl Default for PoolState {
}
pub const POOL_STATE_SIZE: usize = 8 + 1 * 5 + 8 * 10 + 32 * 7 + 8 * 8 + 8 * 5 + 1 + 1 + 8 + 54;
pub fn pool_state_decode(data: &[u8]) -> Option<PoolState> {
if data.len() < POOL_STATE_SIZE {
return None;
}
borsh::from_slice::<PoolState>(&data[..POOL_STATE_SIZE]).ok()
}
pub fn pool_state_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountBonkPoolState;
if account.data.len() < POOL_STATE_SIZE + 8 {
return None;
}
if let Some(pool_state) = pool_state_decode(&account.data[8..POOL_STATE_SIZE + 8]) {
Some(DexEvent::BonkPoolStateAccountEvent(BonkPoolStateAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
pool_state,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct GlobalConfig {
pub epoch: u64,
@@ -233,37 +194,6 @@ pub struct GlobalConfig {
pub const GLOBAL_CONFIG_SIZE: usize = 8 + 1 + 2 + 8 * 8 + 32 * 5 + 8 * 16;
pub fn global_config_decode(data: &[u8]) -> Option<GlobalConfig> {
if data.len() < GLOBAL_CONFIG_SIZE {
return None;
}
borsh::from_slice::<GlobalConfig>(&data[..GLOBAL_CONFIG_SIZE]).ok()
}
pub fn global_config_parser(
account: &AccountPretty,
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::AccountBonkGlobalConfig;
if account.data.len() < GLOBAL_CONFIG_SIZE + 8 {
return None;
}
if let Some(global_config) = global_config_decode(&account.data[8..GLOBAL_CONFIG_SIZE + 8]) {
Some(DexEvent::BonkGlobalConfigAccountEvent(BonkGlobalConfigAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
global_config,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct BondingCurveParam {
pub migrate_type: u8,
@@ -351,36 +281,3 @@ 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 fn platform_config_decode(data: &[u8]) -> Option<PlatformConfig> {
if data.len() < PLATFORM_CONFIG_SIZE {
return None;
}
borsh::from_slice::<PlatformConfig>(&data[..PLATFORM_CONFIG_SIZE]).ok()
}
pub fn platform_config_parser(
account: &AccountPretty,
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::AccountBonkPlatformConfig;
if account.data.len() < PLATFORM_CONFIG_SIZE + 8 {
return None;
}
if let Some(platform_config) =
platform_config_decode(&account.data[8..PLATFORM_CONFIG_SIZE + 8])
{
Some(DexEvent::BonkPlatformConfigAccountEvent(BonkPlatformConfigAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
platform_config,
}))
} else {
None
}
}
@@ -463,17 +463,3 @@ pub mod discriminators {
/// Decode swap event from CPI log
pub const METEORA_DAMM_V2_SWAP_EVENT_LOG_SIZE: usize = 180;
pub fn meteora_damm_v2_swap_event_decode(data: &[u8]) -> Option<MeteoraDammV2SwapEvent> {
if data.len() < METEORA_DAMM_V2_SWAP_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<MeteoraDammV2SwapEvent>(&data[..METEORA_DAMM_V2_SWAP_EVENT_LOG_SIZE]).ok()
}
/// Decode initialize pool event from CPI log
/// Note: discriminator (16 bytes) is already removed by the caller
pub fn meteora_damm_v2_initialize_pool_event_decode(
data: &[u8],
) -> Option<MeteoraDammV2InitializePoolEvent> {
borsh::from_slice::<MeteoraDammV2InitializePoolEvent>(&data).ok()
}
@@ -1,444 +1,42 @@
use crate::streaming::event_parser::{
common::{EventMetadata, EventType},
protocols::meteora_damm_v2::{
discriminators, meteora_damm_v2_initialize_pool_event_decode,
meteora_damm_v2_swap_event_decode, MeteoraDammV2InitializeCustomizablePoolEvent,
MeteoraDammV2InitializePoolEvent, MeteoraDammV2InitializePoolWithDynamicConfigEvent,
MeteoraDammV2Swap2Event, MeteoraDammV2SwapEvent,
},
DexEvent,
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
/// Meteora DAMM v2 程序ID
pub const METEORA_DAMM_V2_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG");
pub use sol_parser_sdk::instr::program_ids::METEORA_DAMM_V2_PROGRAM_ID;
/// 解析 Meteora DAMM v2 instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_meteora_damm_v2_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
discriminators::SWAP_IX => parse_swap_instruction(data, accounts, metadata),
discriminators::SWAP2_IX => parse_swap2_instruction(data, accounts, metadata),
discriminators::INITIALIZE_POOL_IX => {
parse_initialize_pool_instruction(data, accounts, metadata)
}
discriminators::INITIALIZE_CUSTOMIZABLE_POOL_IX => {
parse_initialize_customizable_pool_instruction(data, accounts, metadata)
}
discriminators::INITIALIZE_POOL_WITH_DYNAMIC_CONFIG_IX => {
parse_initialize_pool_with_dynamic_config_instruction(data, accounts, metadata)
}
_ => None,
}
EventDispatcher::dispatch_instruction(
Protocol::MeteoraDammV2,
discriminator,
data,
accounts,
metadata,
)
}
/// 解析 Meteora DAMM v2 inner instruction data (CPI events)
///
/// 根据判别器路由到具体的 inner instruction 解析函数
pub fn parse_meteora_damm_v2_inner_instruction_data(
discriminator: &[u8],
data: &[u8],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
discriminators::SWAP_EVENT => parse_swap_inner_instruction(data, metadata),
discriminators::INITIALIZE_POOL_EVENT => {
parse_initialize_pool_inner_instruction(data, metadata)
}
_ => None,
}
}
/// 解析 swap 指令
fn parse_swap_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::MeteoraDammV2Swap;
if data.len() < 16 || accounts.len() < 14 {
return None;
}
// 跳过 discriminator (8 bytes)
let amount_in = u64::from_le_bytes(data[0..8].try_into().unwrap());
let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().unwrap());
Some(DexEvent::MeteoraDammV2SwapEvent(MeteoraDammV2SwapEvent {
EventDispatcher::dispatch_inner_instruction(
Protocol::MeteoraDammV2,
discriminator,
data,
metadata,
pool_authority: accounts[0],
pool: accounts[1],
input_token_account: accounts[2],
output_token_account: accounts[3],
token_a_vault: accounts[4],
token_b_vault: accounts[5],
token_a_mint: accounts[6],
token_b_mint: accounts[7],
payer: accounts[8],
token_a_program: accounts[9],
token_b_program: accounts[10],
referral_token_account: Some(accounts[11]),
event_authority: accounts[12],
program: accounts[13],
amount_0: amount_in,
amount_1: minimum_amount_out,
..Default::default()
}))
)
}
/// 解析 swap2 指令
fn parse_swap2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
pub fn parse_meteora_damm_v2_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::MeteoraDammV2Swap2;
if data.len() < 16 || accounts.len() < 13 {
return None;
}
// 跳过 discriminator (8 bytes)
let amount_0 = u64::from_le_bytes(data[0..8].try_into().unwrap());
let amount_1 = u64::from_le_bytes(data[8..16].try_into().unwrap());
let swap_mode = data[16];
// swap2 可能有 15 个账户(带 referral)或 14 个账户
let has_referral = accounts.len() >= 15;
Some(DexEvent::MeteoraDammV2Swap2Event(MeteoraDammV2Swap2Event {
metadata,
pool_authority: accounts[0],
pool: accounts[1],
input_token_account: accounts[2],
output_token_account: accounts[3],
token_a_vault: accounts[4],
token_b_vault: accounts[5],
token_a_mint: accounts[6],
token_b_mint: accounts[7],
payer: accounts[8],
token_a_program: accounts[9],
token_b_program: accounts[10],
referral_token_account: if has_referral && accounts.len() > 11 {
Some(accounts[11])
} else {
None
},
event_authority: accounts[if has_referral { 12 } else { 11 }],
program: accounts[if has_referral { 13 } else { 12 }],
sysvar: accounts[if has_referral { 14 } else { 13 }],
amount_0,
amount_1,
swap_mode,
has_referral,
..Default::default()
}))
}
/// 解析 initialize_pool 指令
fn parse_initialize_pool_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::MeteoraDammV2InitializePool;
if accounts.len() < 20 {
return None;
}
// 解析 instruction data (不包含 discriminator,已被调用者移除)
// 结构: liquidity (u128 = 16 bytes) + sqrt_price (u128 = 16 bytes) + activation_point (Option<u64> = 1 + 8 bytes)
if data.len() < 33 {
return None;
}
let mut offset = 0;
// 读取 liquidity (u128)
let liquidity = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 sqrt_price (u128)
let sqrt_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 activation_point (Option<u64>)
let option_tag = data[offset];
offset += 1;
let _activation_point = if option_tag == 1 && data.len() >= offset + 8 {
Some(u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?))
} else {
None
};
Some(DexEvent::MeteoraDammV2InitializePoolEvent(MeteoraDammV2InitializePoolEvent {
metadata,
creator: accounts[0],
position_nft_mint: accounts[1],
position_nft_account: accounts[2],
payer: accounts[3],
config: accounts[4],
pool_authority: accounts[5],
pool: accounts[6],
position: accounts[7],
token_a_mint: accounts[8],
token_b_mint: accounts[9],
token_a_vault: accounts[10],
token_b_vault: accounts[11],
payer_token_a: accounts[12],
payer_token_b: accounts[13],
token_a_program: accounts[14],
token_b_program: accounts[15],
event_authority: accounts[18],
program: accounts[19],
remaining_accounts: accounts[20..].to_vec(),
liquidity,
sqrt_price,
..Default::default()
}))
}
/// 解析 initialize_customizable_pool 指令
fn parse_initialize_customizable_pool_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::MeteoraDammV2InitializeCustomizablePool;
if accounts.len() < 19 {
return None;
}
// 解析 instruction data (不包含 discriminator)
// 结构: PoolFeeParameters + sqrt_min_price + sqrt_max_price + has_alpha_vault + liquidity + sqrt_price + activation_type + collect_fee_mode + activation_point
if data.len() < 99 {
return None;
}
let mut offset = 0;
// 解析 PoolFeeParameters
use crate::streaming::event_parser::protocols::meteora_damm_v2::PoolFeeParameters;
use borsh::BorshDeserialize;
// PoolFeeParameters size: 8 + 2 + 8 + 8 + 1 + 3 + 1 + (optional DynamicFee)
// 先读取前 31 bytes (不包含 dynamic_fee option tag)
let pool_fees = PoolFeeParameters::deserialize(&mut &data[offset..]).ok()?;
// 计算 pool_fees 消耗的字节数
// BaseFee: 8 + 2 + 8 + 8 + 1 = 27 bytes
// padding: 3 bytes
// option tag: 1 byte
// 如果 dynamic_fee 存在: 2 + 16 + 2 + 2 + 2 + 4 + 4 = 32 bytes
let pool_fees_size = 31 + if pool_fees.dynamic_fee.is_some() { 32 } else { 0 };
offset += pool_fees_size;
// 读取 sqrt_min_price (u128)
let sqrt_min_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 sqrt_max_price (u128)
let sqrt_max_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 has_alpha_vault (bool)
let _has_alpha_vault = data[offset];
offset += 1;
// 读取 liquidity (u128)
let liquidity = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 sqrt_price (u128)
let sqrt_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 activation_type (u8)
let activation_type = data[offset];
offset += 1;
// 读取 collect_fee_mode (u8)
let collect_fee_mode = data[offset];
offset += 1;
// 读取 activation_point (Option<u64>)
let option_tag = data[offset];
let _activation_point = if option_tag == 1 && data.len() >= offset + 9 {
Some(u64::from_le_bytes(data[offset + 1..offset + 9].try_into().ok()?))
} else {
None
};
Some(DexEvent::MeteoraDammV2InitializeCustomizablePoolEvent(
MeteoraDammV2InitializeCustomizablePoolEvent {
metadata,
creator: accounts[0],
position_nft_mint: accounts[1],
position_nft_account: accounts[2],
payer: accounts[3],
pool_authority: accounts[4],
pool: accounts[5],
position: accounts[6],
token_a_mint: accounts[7],
token_b_mint: accounts[8],
token_a_vault: accounts[9],
token_b_vault: accounts[10],
payer_token_a: accounts[11],
payer_token_b: accounts[12],
token_a_program: accounts[13],
token_b_program: accounts[14],
token_2022_program: accounts[15],
system_program: accounts[16],
event_authority: accounts[17],
program: accounts[18],
remaining_accounts: accounts[19..].to_vec(),
pool_fees,
sqrt_min_price,
sqrt_max_price,
activation_type,
collect_fee_mode,
liquidity,
sqrt_price,
..Default::default()
},
))
}
/// 解析 initialize_pool_with_dynamic_config 指令
fn parse_initialize_pool_with_dynamic_config_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::MeteoraDammV2InitializePoolWithDynamicConfig;
if accounts.len() < 21 {
return None;
}
if data.len() < 99 {
return None;
}
let mut offset = 0;
// 解析 PoolFeeParameters
use crate::streaming::event_parser::protocols::meteora_damm_v2::PoolFeeParameters;
use borsh::BorshDeserialize;
let pool_fees = PoolFeeParameters::deserialize(&mut &data[offset..]).ok()?;
// 计算 pool_fees 消耗的字节数
// BaseFee: 8 + 2 + 8 + 8 + 1 = 27 bytes
// padding: 3 bytes
// option tag: 1 byte
// 如果 dynamic_fee 存在: 2 + 16 + 2 + 2 + 2 + 4 + 4 = 32 bytes
let pool_fees_size = 31 + if pool_fees.dynamic_fee.is_some() { 32 } else { 0 };
offset += pool_fees_size;
// 读取 sqrt_min_price (u128)
let sqrt_min_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 sqrt_max_price (u128)
let sqrt_max_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 has_alpha_vault (bool)
let _has_alpha_vault = data[offset];
offset += 1;
// 读取 liquidity (u128)
let liquidity = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 sqrt_price (u128)
let sqrt_price = u128::from_le_bytes(data[offset..offset + 16].try_into().ok()?);
offset += 16;
// 读取 activation_type (u8)
let activation_type = data[offset];
offset += 1;
// 读取 collect_fee_mode (u8)
let collect_fee_mode = data[offset];
offset += 1;
// 读取 activation_point (Option<u64>)
let option_tag = data[offset];
let _activation_point = if option_tag == 1 && data.len() >= offset + 9 {
Some(u64::from_le_bytes(data[offset + 1..offset + 9].try_into().ok()?))
} else {
None
};
Some(DexEvent::MeteoraDammV2InitializePoolWithDynamicConfigEvent(
MeteoraDammV2InitializePoolWithDynamicConfigEvent {
metadata,
creator: accounts[0],
position_nft_mint: accounts[1],
position_nft_account: accounts[2],
payer: accounts[3],
pool_creator_authority: accounts[4],
pool_authority: accounts[6],
pool: accounts[7],
position: accounts[8],
token_a_mint: accounts[9],
token_b_mint: accounts[10],
token_a_vault: accounts[11],
token_b_vault: accounts[12],
payer_token_a: accounts[13],
payer_token_b: accounts[14],
token_a_program: accounts[15],
token_b_program: accounts[16],
token_2022_program: accounts[17],
system_program: accounts[18],
event_authority: accounts[19],
program: accounts[20],
config: accounts[5],
pool_fees,
sqrt_min_price,
sqrt_max_price,
activation_type,
collect_fee_mode,
liquidity,
sqrt_price,
..Default::default()
},
))
}
/// 解析 swap inner instruction (CPI event)
fn parse_swap_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
if let Some(event) = meteora_damm_v2_swap_event_decode(data) {
Some(DexEvent::MeteoraDammV2SwapEvent(MeteoraDammV2SwapEvent { metadata, ..event }))
} else {
None
}
}
/// 解析 initialize pool inner instruction (CPI event)
fn parse_initialize_pool_inner_instruction(
data: &[u8],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::MeteoraDammV2InitializePool;
if let Some(event) = meteora_damm_v2_initialize_pool_event_decode(data) {
Some(DexEvent::MeteoraDammV2InitializePoolEvent(MeteoraDammV2InitializePoolEvent {
metadata,
..event
}))
} else {
None
}
EventDispatcher::dispatch_account(Protocol::MeteoraDammV2, discriminator, account, metadata)
}
@@ -97,136 +97,6 @@ pub struct PumpFunCreateV2TokenEvent {
pub program: Pubkey,
}
pub fn pumpfun_create_v2_token_event_log_decode(data: &[u8]) -> Option<PumpFunCreateV2TokenEvent> {
let mut offset = 0;
// Parse name string: [length (4 bytes u32)][string bytes]
if data.len() < offset + 4 {
return None;
}
let name_len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
offset += 4;
if data.len() < offset + name_len {
return None;
}
let name = String::from_utf8(data[offset..offset + name_len].to_vec()).ok()?;
offset += name_len;
// Parse symbol string
if data.len() < offset + 4 {
return None;
}
let symbol_len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
offset += 4;
if data.len() < offset + symbol_len {
return None;
}
let symbol = String::from_utf8(data[offset..offset + symbol_len].to_vec()).ok()?;
offset += symbol_len;
// Parse uri string
if data.len() < offset + 4 {
return None;
}
let uri_len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
offset += 4;
if data.len() < offset + uri_len {
return None;
}
let uri = String::from_utf8(data[offset..offset + uri_len].to_vec()).ok()?;
offset += uri_len;
// Parse Pubkey fields (32 bytes each)
if data.len() < offset + 32 {
return None;
}
let mint = Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?);
offset += 32;
if data.len() < offset + 32 {
return None;
}
let bonding_curve = Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?);
offset += 32;
if data.len() < offset + 32 {
return None;
}
let user = Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?);
offset += 32;
if data.len() < offset + 32 {
return None;
}
let creator = Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?);
offset += 32;
// Parse numeric fields
if data.len() < offset + 8 {
return None;
}
let timestamp = i64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
offset += 8;
if data.len() < offset + 8 {
return None;
}
let virtual_token_reserves = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
offset += 8;
if data.len() < offset + 8 {
return None;
}
let virtual_sol_reserves = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
offset += 8;
if data.len() < offset + 8 {
return None;
}
let real_token_reserves = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
offset += 8;
if data.len() < offset + 8 {
return None;
}
let token_total_supply = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
offset += 8;
// If data length allows, parse V2 extra fields: token_program (32 bytes) + is_mayhem_mode (1 byte) + is_cashback_enabled (1 byte)
let (token_program, is_mayhem_mode, is_cashback_enabled) = if data.len() >= offset + 34 {
let token_program = Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?);
let is_mayhem_mode = data[offset + 32] != 0;
let is_cashback_enabled = data[offset + 33] != 0;
(token_program, is_mayhem_mode, is_cashback_enabled)
} else if data.len() >= offset + 33 {
// Backward compat: only token_program + is_mayhem_mode, no is_cashback_enabled
let token_program = Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?);
let is_mayhem_mode = data[offset + 32] != 0;
(token_program, is_mayhem_mode, false)
} else {
(Pubkey::default(), false, false)
};
Some(PumpFunCreateV2TokenEvent {
name,
symbol,
uri,
mint,
bonding_curve,
user,
creator,
timestamp,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
token_total_supply,
token_program,
is_mayhem_mode,
is_cashback_enabled,
..Default::default()
})
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunTradeEvent {
#[borsh(skip)]
@@ -318,49 +188,6 @@ pub struct PumpFunTradeEvent {
/// Layout: mint(32)+sol_amount(8)+token_amount(8)+is_buy(1)+user(32)+timestamp(8)+virtual_sol(8)+virtual_token(8)+real_sol(8)+real_token(8)+fee_recipient(32)+fee_basis_points(8)+fee(8)+creator(32)+creator_fee_bps(8)+creator_fee(8)+track_volume(1)+total_unclaimed(8)+total_claimed(8)+current_sol_volume(8)+last_update_timestamp(8) = 250
pub const PUMPFUN_TRADE_EVENT_LOG_SIZE: usize = 250;
/// Decode TradeEvent log; if data.len() > 250 then parse ix_name, mayhem_mode, cashback (IDL-aligned).
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 offset = PUMPFUN_TRADE_EVENT_LOG_SIZE;
if offset < data.len() {
let (ix_name, inc) = read_borsh_string(data, offset).unwrap_or((String::new(), 0));
offset += inc;
event.ix_name = ix_name;
}
if offset + 1 <= data.len() {
event.mayhem_mode = data[offset] != 0;
offset += 1;
}
if offset + 8 <= data.len() {
event.cashback_fee_basis_points =
u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
offset += 8;
}
if offset + 8 <= data.len() {
event.cashback = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
}
event.is_cashback_coin = event.cashback_fee_basis_points > 0;
Some(event)
}
#[inline]
fn read_borsh_string(data: &[u8], start: usize) -> Option<(String, usize)> {
if start + 4 > data.len() {
return None;
}
let len = u32::from_le_bytes(data[start..start + 4].try_into().ok()?) as usize;
let start = start + 4;
if start + len > data.len() {
return None;
}
let s = String::from_utf8_lossy(&data[start..start + len]).to_string();
Some((s, 4 + len))
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunMigrateEvent {
#[borsh(skip)]
@@ -551,13 +378,6 @@ pub struct PumpFunMigrateBondingCurveCreatorEvent {
pub const PUMPFUN_MIGRATE_EVENT_LOG_SIZE: usize = 160;
pub fn pumpfun_migrate_event_log_decode(data: &[u8]) -> Option<PumpFunMigrateEvent> {
if data.len() < PUMPFUN_MIGRATE_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<PumpFunMigrateEvent>(&data[..PUMPFUN_MIGRATE_EVENT_LOG_SIZE]).ok()
}
/// Bonding curve
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunBondingCurveAccountEvent {
+12 -423
View File
@@ -1,448 +1,37 @@
use crate::streaming::event_parser::{
common::{EventMetadata, EventType},
protocols::pumpfun::{
discriminators, pumpfun_create_v2_token_event_log_decode, pumpfun_migrate_event_log_decode,
pumpfun_trade_event_log_decode, PumpFunCreateTokenEvent, PumpFunCreateV2TokenEvent,
PumpFunMigrateEvent, PumpFunTradeEvent,
},
DexEvent,
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
/// PumpFun程序ID
pub const PUMPFUN_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
pub use sol_parser_sdk::instr::program_ids::PUMPFUN_PROGRAM_ID;
/// 解析 PumpFun instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_pumpfun_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
discriminators::CREATE_TOKEN_IX => parse_create_token_instruction(data, accounts, metadata),
discriminators::CREATE_V2_TOKEN_IX => {
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::SELL_IX => parse_sell_instruction(data, accounts, metadata),
discriminators::MIGRATE_IX => parse_migrate_instruction(data, accounts, metadata),
_ => None,
}
EventDispatcher::dispatch_instruction(
Protocol::PumpFun,
discriminator,
data,
accounts,
metadata,
)
}
/// 解析 PumpFun inner instruction data
///
/// 根据判别器路由到具体的 inner instruction 解析函数
pub fn parse_pumpfun_inner_instruction_data(
discriminator: &[u8],
data: &[u8],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
discriminators::CREATE_TOKEN_EVENT => parse_create_token_inner_instruction(data, metadata),
discriminators::TRADE_EVENT => parse_trade_inner_instruction(data, metadata),
discriminators::COMPLETE_PUMP_AMM_MIGRATION_EVENT => {
parse_migrate_inner_instruction(data, metadata)
}
_ => None,
}
EventDispatcher::dispatch_inner_instruction(Protocol::PumpFun, discriminator, data, metadata)
}
/// 解析 PumpFun 账户数据
///
/// 根据判别器路由到具体的账户解析函数
pub fn parse_pumpfun_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: crate::streaming::event_parser::common::EventMetadata,
) -> Option<crate::streaming::event_parser::DexEvent> {
match discriminator {
discriminators::BONDING_CURVE_ACCOUNT => {
crate::streaming::event_parser::protocols::pumpfun::types::bonding_curve_parser(
account, metadata,
)
}
discriminators::GLOBAL_ACCOUNT => {
crate::streaming::event_parser::protocols::pumpfun::types::global_parser(
account, metadata,
)
}
_ => None,
}
}
/// 解析迁移事件
fn parse_migrate_inner_instruction(data: &[u8], mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunMigrate;
if let Some(event) = pumpfun_migrate_event_log_decode(data) {
Some(DexEvent::PumpFunMigrateEvent(PumpFunMigrateEvent { metadata, ..event }))
} else {
None
}
}
/// 解析创建代币日志事件
fn parse_create_token_inner_instruction(
data: &[u8],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunCreateToken;
if let Some(event) = pumpfun_create_v2_token_event_log_decode(data) {
Some(DexEvent::PumpFunCreateV2TokenEvent(PumpFunCreateV2TokenEvent { metadata, ..event }))
} else {
None
}
}
/// 解析交易事件 (inner instruction 不设置 event_type,因为不知道是 Buy 还是 Sell)
fn parse_trade_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// 注意:inner instruction 的 trade event 不设置 event_type
// 因为它会被合并到 instruction event 中,而 instruction event 已经设置了正确的 event_type
if let Some(event) = pumpfun_trade_event_log_decode(data) {
Some(DexEvent::PumpFunTradeEvent(PumpFunTradeEvent { metadata, ..event }))
} else {
None
}
}
/// 解析创建代币指令事件
/// 账户: 0: mint, 1: mint_authority, 2: bonding_curve, 3: associated_bonding_curve, 4: global,
/// 5: mpl_token_metadata, 6: metadata_account, 7: user, 8: system_program, 9: token_program,
/// 10: associated_token_program, 11: rent, 12: event_authority, 13: program.
/// 共 14 个固定账户,不足时返回 None 避免越界。
fn parse_create_token_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunCreateToken;
const CREATE_TOKEN_MIN_ACCOUNTS: usize = 14;
if data.len() < 16 || accounts.len() < CREATE_TOKEN_MIN_ACCOUNTS {
return None;
}
let mut offset = 0;
if offset + 4 > data.len() {
return None;
}
let name_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
if offset + name_len > data.len() {
return None;
}
let name = String::from_utf8_lossy(&data[offset..offset + name_len]);
offset += name_len;
if offset + 4 > data.len() {
return None;
}
let symbol_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
if offset + symbol_len > data.len() {
return None;
}
let symbol = String::from_utf8_lossy(&data[offset..offset + symbol_len]);
offset += symbol_len;
if offset + 4 > data.len() {
return None;
}
let uri_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
if offset + uri_len > data.len() {
return None;
}
let uri = String::from_utf8_lossy(&data[offset..offset + uri_len]);
offset += uri_len;
let creator = if offset + 32 <= data.len() {
Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?)
} else {
Pubkey::default()
};
Some(DexEvent::PumpFunCreateTokenEvent(PumpFunCreateTokenEvent {
metadata,
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
creator,
mint: accounts[0],
mint_authority: accounts[1],
bonding_curve: accounts[2],
associated_bonding_curve: accounts[3],
global: accounts[4],
mpl_token_metadata: accounts[5],
metadata_account: accounts[6],
user: accounts[7],
system_program: accounts[8],
token_program: accounts[9],
associated_token_program: accounts[10],
rent: accounts[11],
event_authority: accounts[12],
program: accounts[13],
..Default::default()
}))
}
/// 解析创建 V2 代币指令事件 (SPL-22 Token, Mayhem Mode)
/// 与 IDL create_v2 及区块浏览器一致,共 16 个固定账户:
/// 0: mint, 1: mint_authority, 2: bonding_curve, 3: associated_bonding_curve, 4: global,
/// 5: user, 6: system_program, 7: token_program, 8: associated_token_program, 9: mayhem_program_id,
/// 10: global_params, 11: sol_vault, 12: mayhem_state, 13: mayhem_token_vault, 14: event_authority, 15: program.
/// 不足 16 个账户时返回 None 避免越界。
/// 注意:shredstream 路径仅传入 static_account_keys,若交易使用 Address Lookup Tables
/// 无法解析 loaded_addresses,部分账户会以 default 填充,导致 token_program/global 等错误。
fn parse_create_v2_token_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
const CREATE_V2_MIN_ACCOUNTS: usize = 16;
// Guard: avoid index out of bounds (e.g. ALT-loaded tx with fewer static accounts). See issue #63.
if accounts.len() < CREATE_V2_MIN_ACCOUNTS || data.len() < 16 {
return None;
}
metadata.event_type = EventType::PumpFunCreateV2Token;
let mut offset = 0;
if offset + 4 > data.len() {
return None;
}
let name_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
if offset + name_len > data.len() {
return None;
}
let name = String::from_utf8_lossy(&data[offset..offset + name_len]);
offset += name_len;
if offset + 4 > data.len() {
return None;
}
let symbol_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
if offset + symbol_len > data.len() {
return None;
}
let symbol = String::from_utf8_lossy(&data[offset..offset + symbol_len]);
offset += symbol_len;
if offset + 4 > data.len() {
return None;
}
let uri_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
if offset + uri_len > data.len() {
return None;
}
let uri = String::from_utf8_lossy(&data[offset..offset + uri_len]);
offset += uri_len;
let creator = if offset + 32 <= data.len() {
Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?)
} else {
Pubkey::default()
};
// Safe slice: already guaranteed accounts.len() >= 16 above; avoid any index panic (issue #63).
let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
Some(DexEvent::PumpFunCreateV2TokenEvent(PumpFunCreateV2TokenEvent {
metadata,
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
creator,
mint: acc[0],
mint_authority: acc[1],
bonding_curve: acc[2],
associated_bonding_curve: acc[3],
global: acc[4],
user: acc[5],
system_program: acc[6],
token_program: acc[7],
associated_token_program: acc[8],
mayhem_program_id: acc[9],
global_params: acc[10],
sol_vault: acc[11],
mayhem_state: acc[12],
mayhem_token_vault: acc[13],
event_authority: acc[14],
program: acc[15],
..Default::default()
}))
}
/// Parse buy instruction event.
/// Buy has 16 fixed accounts + optional 17th (index 16, "Account" on block explorers):
/// 0: global, 1: fee_recipient, 2: mint, 3: bonding_curve, 4: associated_bonding_curve,
/// 5: associated_user, 6: user, 7: system_program, 8: token_program, 9: creator_vault,
/// 10: event_authority, 11: program, 12: global_volume_accumulator, 13: user_volume_accumulator,
/// 14: fee_config, 15: fee_program, 16 (optional): account.
fn parse_buy_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunBuy;
if data.len() < 16 || accounts.len() < 16 {
return None;
}
let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
let max_sol_cost = u64::from_le_bytes(data[8..16].try_into().unwrap());
Some(DexEvent::PumpFunTradeEvent(PumpFunTradeEvent {
metadata,
global: accounts[0],
fee_recipient: accounts[1],
mint: accounts[2],
bonding_curve: accounts[3],
associated_bonding_curve: accounts[4],
associated_user: accounts[5],
user: accounts[6],
system_program: accounts[7],
token_program: accounts[8],
creator_vault: accounts[9],
event_authority: accounts[10],
program: accounts[11],
global_volume_accumulator: accounts[12],
user_volume_accumulator: accounts[13],
fee_config: accounts[14],
fee_program: accounts[15],
account: accounts.get(16).copied(),
max_sol_cost,
amount,
is_buy: true,
..Default::default()
}))
}
/// Parse buy_exact_sol_in instruction event.
/// 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],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunBuyExactSolIn;
if data.len() < 16 || accounts.len() < 16 {
return None;
}
let spendable_sol_in = u64::from_le_bytes(data[0..8].try_into().unwrap());
let min_tokens_out = u64::from_le_bytes(data[8..16].try_into().unwrap());
Some(DexEvent::PumpFunTradeEvent(PumpFunTradeEvent {
metadata,
global: accounts[0],
fee_recipient: accounts[1],
mint: accounts[2],
bonding_curve: accounts[3],
associated_bonding_curve: accounts[4],
associated_user: accounts[5],
user: accounts[6],
system_program: accounts[7],
token_program: accounts[8],
creator_vault: accounts[9],
event_authority: accounts[10],
program: accounts[11],
global_volume_accumulator: accounts[12],
user_volume_accumulator: accounts[13],
fee_config: accounts[14],
fee_program: accounts[15],
account: accounts.get(16).copied(),
max_sol_cost: spendable_sol_in,
amount: min_tokens_out,
is_buy: true,
..Default::default()
}))
}
/// Parse sell instruction event.
/// Sell has 14 fixed accounts; some versions pass 17 accounts, index 16 = "Account" on block explorers.
fn parse_sell_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunSell;
if data.len() < 16 || accounts.len() < 14 {
return None;
}
let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
let min_sol_output = u64::from_le_bytes(data[8..16].try_into().unwrap());
Some(DexEvent::PumpFunTradeEvent(PumpFunTradeEvent {
metadata,
global: accounts[0],
fee_recipient: accounts[1],
mint: accounts[2],
bonding_curve: accounts[3],
associated_bonding_curve: accounts[4],
associated_user: accounts[5],
user: accounts[6],
system_program: accounts[7],
creator_vault: accounts[8],
token_program: accounts[9],
event_authority: accounts[10],
program: accounts[11],
global_volume_accumulator: Pubkey::default(),
user_volume_accumulator: Pubkey::default(),
fee_config: accounts[12],
fee_program: accounts[13],
account: accounts.get(16).copied(),
min_sol_output,
amount,
is_buy: false,
..Default::default()
}))
}
/// 解析迁移指令事件
/// 共 24 个固定账户: 0: global, 1: withdraw_authority, 2: mint, 3: bonding_curve, 4: associated_bonding_curve,
/// 5: user, 6: system_program, 7: token_program, 8: pump_amm, 9: pool, 10: pool_authority,
/// 11: pool_authority_mint_account, 12: pool_authority_wsol_account, 13: amm_global_config, 14: wsol_mint,
/// 15: lp_mint, 16: user_pool_token_account, 17: pool_base_token_account, 18: pool_quote_token_account,
/// 19: token_2022_program, 20: associated_token_program, 21: pump_amm_event_authority, 22: event_authority, 23: program.
fn parse_migrate_instruction(
_data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpFunMigrate;
if accounts.len() < 24 {
return None;
}
Some(DexEvent::PumpFunMigrateEvent(PumpFunMigrateEvent {
metadata,
global: accounts[0],
withdraw_authority: accounts[1],
mint: accounts[2],
bonding_curve: accounts[3],
associated_bonding_curve: accounts[4],
user: accounts[5],
system_program: accounts[6],
token_program: accounts[7],
pump_amm: accounts[8],
pool: accounts[9],
pool_authority: accounts[10],
pool_authority_mint_account: accounts[11],
pool_authority_wsol_account: accounts[12],
amm_global_config: accounts[13],
wsol_mint: accounts[14],
lp_mint: accounts[15],
user_pool_token_account: accounts[16],
pool_base_token_account: accounts[17],
pool_quote_token_account: accounts[18],
token_2022_program: accounts[19],
associated_token_program: accounts[20],
pump_amm_event_authority: accounts[21],
event_authority: accounts[22],
program: accounts[23],
..Default::default()
}))
EventDispatcher::dispatch_account(Protocol::PumpFun, discriminator, account, metadata)
}
@@ -2,15 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::{
event_parser::{
common::{EventMetadata, EventType},
protocols::pumpfun::{PumpFunBondingCurveAccountEvent, PumpFunGlobalAccountEvent},
DexEvent,
},
grpc::AccountPretty,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct BondingCurve {
pub virtual_token_reserves: u64,
@@ -26,37 +17,6 @@ pub struct BondingCurve {
pub const BONDING_CURVE_SIZE: usize = 8 * 5 + 1 + 32 + 1 + 1;
pub fn bonding_curve_decode(data: &[u8]) -> Option<BondingCurve> {
if data.len() < BONDING_CURVE_SIZE {
return None;
}
borsh::from_slice::<BondingCurve>(&data[..BONDING_CURVE_SIZE]).ok()
}
pub fn bonding_curve_parser(
account: &AccountPretty,
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::AccountPumpFunBondingCurve;
if account.data.len() < BONDING_CURVE_SIZE + 8 {
return None;
}
if let Some(bonding_curve) = bonding_curve_decode(&account.data[8..BONDING_CURVE_SIZE + 8]) {
Some(DexEvent::PumpFunBondingCurveAccountEvent(PumpFunBondingCurveAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
bonding_curve,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct Global {
pub initialized: bool,
@@ -84,31 +44,3 @@ pub struct Global {
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 {
return None;
}
borsh::from_slice::<Global>(&data[..GLOBAL_SIZE]).ok()
}
pub fn global_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountPumpFunGlobal;
if account.data.len() < GLOBAL_SIZE + 8 {
return None;
}
if let Some(global) = global_decode(&account.data[8..GLOBAL_SIZE + 8]) {
Some(DexEvent::PumpFunGlobalAccountEvent(PumpFunGlobalAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
global,
}))
} else {
None
}
}
@@ -2,7 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::event_parser::common::utils::{read_i64_le, read_u32_le, read_u64_le};
use crate::streaming::event_parser::common::EventMetadata;
use crate::streaming::event_parser::protocols::pumpswap::types::{GlobalConfig, Pool};
@@ -70,102 +69,6 @@ pub const PUMP_SWAP_BUY_EVENT_LOG_MIN: usize = 385;
/// Backwards-compatible name for the minimum BuyEvent payload size (legacy `borsh` slice length).
pub const PUMP_SWAP_BUY_EVENT_LOG_SIZE: usize = PUMP_SWAP_BUY_EVENT_LOG_MIN;
pub fn pump_swap_buy_event_log_decode(data: &[u8]) -> Option<PumpSwapBuyEvent> {
if data.len() < PUMP_SWAP_BUY_EVENT_LOG_MIN {
return None;
}
let timestamp = read_i64_le(data, 0)?;
let base_amount_out = read_u64_le(data, 8)?;
let max_quote_amount_in = read_u64_le(data, 16)?;
let user_base_token_reserves = read_u64_le(data, 24)?;
let user_quote_token_reserves = read_u64_le(data, 32)?;
let pool_base_token_reserves = read_u64_le(data, 40)?;
let pool_quote_token_reserves = read_u64_le(data, 48)?;
let quote_amount_in = read_u64_le(data, 56)?;
let lp_fee_basis_points = read_u64_le(data, 64)?;
let lp_fee = read_u64_le(data, 72)?;
let protocol_fee_basis_points = read_u64_le(data, 80)?;
let protocol_fee = read_u64_le(data, 88)?;
let quote_amount_in_with_lp_fee = read_u64_le(data, 96)?;
let user_quote_amount_in = read_u64_le(data, 104)?;
let pool = Pubkey::new_from_array(data.get(112..144)?.try_into().ok()?);
let user = Pubkey::new_from_array(data.get(144..176)?.try_into().ok()?);
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 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 track_volume = *data.get(352)? != 0;
let total_unclaimed_tokens = read_u64_le(data, 353)?;
let total_claimed_tokens = read_u64_le(data, 361)?;
let current_sol_volume = read_u64_le(data, 369)?;
let last_update_timestamp = read_i64_le(data, 377)?;
let mut offset = 385usize;
let min_base_amount_out = if data.len() >= offset + 8 {
let v = read_u64_le(data, offset)?;
offset += 8;
v
} else {
0
};
let ix_name = if data.len() >= offset + 4 {
let slen = read_u32_le(data, offset)? as usize;
let str_start = offset + 4;
if data.len() < str_start + slen {
return None;
}
offset = str_start + slen;
String::from_utf8_lossy(&data[str_start..offset]).into_owned()
} else {
String::new()
};
let cashback_fee_basis_points = read_u64_le(data, offset).unwrap_or(0);
let cashback = read_u64_le(data, offset + 8).unwrap_or(0);
Some(PumpSwapBuyEvent {
metadata: EventMetadata::default(),
timestamp,
base_amount_out,
max_quote_amount_in,
user_base_token_reserves,
user_quote_token_reserves,
pool_base_token_reserves,
pool_quote_token_reserves,
quote_amount_in,
lp_fee_basis_points,
lp_fee,
protocol_fee_basis_points,
protocol_fee,
quote_amount_in_with_lp_fee,
user_quote_amount_in,
pool,
user,
user_base_token_account,
user_quote_token_account,
protocol_fee_recipient,
protocol_fee_recipient_token_account,
coin_creator,
coin_creator_fee_basis_points,
coin_creator_fee,
track_volume,
total_unclaimed_tokens,
total_claimed_tokens,
current_sol_volume,
last_update_timestamp,
min_base_amount_out,
ix_name,
cashback_fee_basis_points,
cashback,
..Default::default()
})
}
/// 卖出事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapSellEvent {
@@ -221,72 +124,6 @@ pub const PUMP_SWAP_SELL_EVENT_WITH_CASHBACK: usize = 368;
/// Backwards-compatible name for the pre-cashback SellEvent payload size.
pub const PUMP_SWAP_SELL_EVENT_LOG_SIZE: usize = PUMP_SWAP_SELL_EVENT_LOG_MIN;
pub fn pump_swap_sell_event_log_decode(data: &[u8]) -> Option<PumpSwapSellEvent> {
if data.len() < PUMP_SWAP_SELL_EVENT_LOG_MIN {
return None;
}
let timestamp = read_i64_le(data, 0)?;
let base_amount_in = read_u64_le(data, 8)?;
let min_quote_amount_out = read_u64_le(data, 16)?;
let user_base_token_reserves = read_u64_le(data, 24)?;
let user_quote_token_reserves = read_u64_le(data, 32)?;
let pool_base_token_reserves = read_u64_le(data, 40)?;
let pool_quote_token_reserves = read_u64_le(data, 48)?;
let quote_amount_out = read_u64_le(data, 56)?;
let lp_fee_basis_points = read_u64_le(data, 64)?;
let lp_fee = read_u64_le(data, 72)?;
let protocol_fee_basis_points = read_u64_le(data, 80)?;
let protocol_fee = read_u64_le(data, 88)?;
let quote_amount_out_without_lp_fee = read_u64_le(data, 96)?;
let user_quote_amount_out = read_u64_le(data, 104)?;
let pool = Pubkey::new_from_array(data.get(112..144)?.try_into().ok()?);
let user = Pubkey::new_from_array(data.get(144..176)?.try_into().ok()?);
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 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
{
(read_u64_le(data, 352)?, read_u64_le(data, 360)?)
} else {
(0, 0)
};
Some(PumpSwapSellEvent {
metadata: EventMetadata::default(),
timestamp,
base_amount_in,
min_quote_amount_out,
user_base_token_reserves,
user_quote_token_reserves,
pool_base_token_reserves,
pool_quote_token_reserves,
quote_amount_out,
lp_fee_basis_points,
lp_fee,
protocol_fee_basis_points,
protocol_fee,
quote_amount_out_without_lp_fee,
user_quote_amount_out,
pool,
user,
user_base_token_account,
user_quote_token_account,
protocol_fee_recipient,
protocol_fee_recipient_token_account,
coin_creator,
coin_creator_fee_basis_points,
coin_creator_fee,
cashback_fee_basis_points,
cashback,
..Default::default()
})
}
/// 创建池子事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapCreatePoolEvent {
@@ -322,13 +159,6 @@ pub struct PumpSwapCreatePoolEvent {
pub const PUMP_SWAP_CREATE_POOL_EVENT_LOG_SIZE: usize = 325;
pub fn pump_swap_create_pool_event_log_decode(data: &[u8]) -> Option<PumpSwapCreatePoolEvent> {
if data.len() < PUMP_SWAP_CREATE_POOL_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<PumpSwapCreatePoolEvent>(&data[..PUMP_SWAP_CREATE_POOL_EVENT_LOG_SIZE]).ok()
}
/// 存款事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapDepositEvent {
@@ -362,13 +192,6 @@ pub struct PumpSwapDepositEvent {
pub const PUMP_SWAP_DEPOSIT_EVENT_LOG_SIZE: usize = 248;
pub fn pump_swap_deposit_event_log_decode(data: &[u8]) -> Option<PumpSwapDepositEvent> {
if data.len() < PUMP_SWAP_DEPOSIT_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<PumpSwapDepositEvent>(&data[..PUMP_SWAP_DEPOSIT_EVENT_LOG_SIZE]).ok()
}
/// 提款事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapWithdrawEvent {
@@ -402,13 +225,6 @@ pub struct PumpSwapWithdrawEvent {
pub const PUMP_SWAP_WITHDRAW_EVENT_LOG_SIZE: usize = 248;
pub fn pump_swap_withdraw_event_log_decode(data: &[u8]) -> Option<PumpSwapWithdrawEvent> {
if data.len() < PUMP_SWAP_WITHDRAW_EVENT_LOG_SIZE {
return None;
}
borsh::from_slice::<PumpSwapWithdrawEvent>(&data[..PUMP_SWAP_WITHDRAW_EVENT_LOG_SIZE]).ok()
}
/// 全局配置
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapGlobalConfigAccountEvent {
+12 -342
View File
@@ -1,367 +1,37 @@
use crate::streaming::event_parser::{
common::{read_u64_le, EventMetadata, EventType},
protocols::pumpswap::{
discriminators, pump_swap_buy_event_log_decode, pump_swap_create_pool_event_log_decode,
pump_swap_deposit_event_log_decode, pump_swap_sell_event_log_decode,
pump_swap_withdraw_event_log_decode, PumpSwapBuyEvent, PumpSwapCreatePoolEvent,
PumpSwapDepositEvent, PumpSwapSellEvent, PumpSwapWithdrawEvent,
},
DexEvent,
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
/// PumpSwap程序ID
pub const PUMPSWAP_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
pub use sol_parser_sdk::instr::program_ids::PUMPSWAP_PROGRAM_ID;
/// 解析 PumpSwap instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_pumpswap_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> 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::DEPOSIT_IX => parse_deposit_instruction(data, accounts, metadata),
discriminators::WITHDRAW_IX => parse_withdraw_instruction(data, accounts, metadata),
_ => None,
}
EventDispatcher::dispatch_instruction(
Protocol::PumpSwap,
discriminator,
data,
accounts,
metadata,
)
}
/// 解析 PumpSwap inner instruction data
///
/// 根据判别器路由到具体的 inner instruction 解析函数
pub fn parse_pumpswap_inner_instruction_data(
discriminator: &[u8],
data: &[u8],
metadata: EventMetadata,
) -> Option<DexEvent> {
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::DEPOSIT_EVENT => parse_deposit_inner_instruction(data, metadata),
discriminators::WITHDRAW_EVENT => parse_withdraw_inner_instruction(data, metadata),
_ => None,
}
EventDispatcher::dispatch_inner_instruction(Protocol::PumpSwap, discriminator, data, metadata)
}
/// 解析 PumpSwap 账户数据
///
/// 根据判别器路由到具体的账户解析函数
pub fn parse_pumpswap_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: crate::streaming::event_parser::common::EventMetadata,
) -> Option<crate::streaming::event_parser::DexEvent> {
match discriminator {
discriminators::GLOBAL_CONFIG_ACCOUNT => {
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,
)
}
_ => None,
}
}
/// 解析买入日志事件
fn parse_buy_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// Note: event_type will be set by instruction parser
if let Some(event) = pump_swap_buy_event_log_decode(data) {
Some(DexEvent::PumpSwapBuyEvent(PumpSwapBuyEvent { metadata, ..event }))
} else {
None
}
}
/// 解析卖出日志事件
fn parse_sell_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// Note: event_type will be set by instruction parser
if let Some(event) = pump_swap_sell_event_log_decode(data) {
Some(DexEvent::PumpSwapSellEvent(PumpSwapSellEvent { metadata, ..event }))
} else {
None
}
}
/// 解析创建池子日志事件
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 }))
} else {
None
}
}
/// 解析存款日志事件
fn parse_deposit_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// Note: event_type will be set by instruction parser
if let Some(event) = pump_swap_deposit_event_log_decode(data) {
Some(DexEvent::PumpSwapDepositEvent(PumpSwapDepositEvent { metadata, ..event }))
} else {
None
}
}
/// 解析提款日志事件
fn parse_withdraw_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
// Note: event_type will be set by instruction parser
if let Some(event) = pump_swap_withdraw_event_log_decode(data) {
Some(DexEvent::PumpSwapWithdrawEvent(PumpSwapWithdrawEvent { metadata, ..event }))
} else {
None
}
}
/// 解析买入指令事件
/// Buy 指令共 23 个固定账户(与 idl/pump_amm.json 一致):
/// 0: pool, 1: user, 2: global_config, 3: base_mint, 4: quote_mint, 5: user_base_token_account,
/// 6: user_quote_token_account, 7: pool_base_token_account, 8: pool_quote_token_account,
/// 9: protocol_fee_recipient, 10: protocol_fee_recipient_token_account, 11: base_token_program,
/// 12: quote_token_program, 13: system_program, 14: associated_token_program, 15: event_authority,
/// 16: program, 17: coin_creator_vault_ata, 18: coin_creator_vault_authority,
/// 19: global_volume_accumulator, 20: user_volume_accumulator, 21: fee_config, 22: fee_program.
fn parse_buy_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpSwapBuy;
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let base_amount_out = read_u64_le(data, 0)?;
let max_quote_amount_in = read_u64_le(data, 8)?;
Some(DexEvent::PumpSwapBuyEvent(PumpSwapBuyEvent {
metadata,
base_amount_out,
max_quote_amount_in,
ix_name: "buy".to_string(),
pool: accounts[0],
user: accounts[1],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[5],
user_quote_token_account: accounts[6],
pool_base_token_account: accounts[7],
pool_quote_token_account: accounts[8],
protocol_fee_recipient: accounts[9],
protocol_fee_recipient_token_account: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
..Default::default()
}))
}
/// 解析 buy_exact_quote_in 指令事件
/// 账户布局与 buy 相同,共 23 个固定账户(02217/18 为 coin_creator_vault_ata / coin_creator_vault_authority)。
/// 参数顺序与 buy 不同: spendable_quote_in (SOL), min_base_amount_out (token).
fn parse_buy_exact_quote_in_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpSwapBuy;
if data.len() < 16 || accounts.len() < 13 {
return None;
}
// 注意:buy_exact_quote_in 的参数顺序是先 quote (SOL) 再 base (token)
let spendable_quote_in = read_u64_le(data, 0)?;
let min_base_amount_out = read_u64_le(data, 8)?;
Some(DexEvent::PumpSwapBuyEvent(PumpSwapBuyEvent {
metadata,
base_amount_out: min_base_amount_out,
max_quote_amount_in: spendable_quote_in,
min_base_amount_out,
ix_name: "buy_exact_quote_in".to_string(),
pool: accounts[0],
user: accounts[1],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[5],
user_quote_token_account: accounts[6],
pool_base_token_account: accounts[7],
pool_quote_token_account: accounts[8],
protocol_fee_recipient: accounts[9],
protocol_fee_recipient_token_account: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
..Default::default()
}))
}
/// 解析卖出指令事件
/// Sell 指令共 21 个固定账户(与 idl/pump_amm.json 一致):
/// 0: pool, 1: user, 2: global_config, 3: base_mint, 4: quote_mint, 5: user_base_token_account,
/// 6: user_quote_token_account, 7: pool_base_token_account, 8: pool_quote_token_account,
/// 9: protocol_fee_recipient, 10: protocol_fee_recipient_token_account, 11: base_token_program,
/// 12: quote_token_program, 13: system_program, 14: associated_token_program, 15: event_authority,
/// 16: program, 17: coin_creator_vault_ata, 18: coin_creator_vault_authority, 19: fee_config, 20: fee_program.
fn parse_sell_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpSwapSell;
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let base_amount_in = read_u64_le(data, 0)?;
let min_quote_amount_out = read_u64_le(data, 8)?;
Some(DexEvent::PumpSwapSellEvent(PumpSwapSellEvent {
metadata,
base_amount_in,
min_quote_amount_out,
pool: accounts[0],
user: accounts[1],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[5],
user_quote_token_account: accounts[6],
pool_base_token_account: accounts[7],
pool_quote_token_account: accounts[8],
protocol_fee_recipient: accounts[9],
protocol_fee_recipient_token_account: accounts[10],
base_token_program: accounts[11],
quote_token_program: accounts[12],
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
..Default::default()
}))
}
/// 解析创建池子指令事件
fn parse_create_pool_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpSwapCreatePool;
if data.len() < 18 || accounts.len() < 11 {
return None;
}
let index = u16::from_le_bytes(data[0..2].try_into().ok()?);
let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?);
let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?);
let coin_creator = if data.len() >= 50 {
Pubkey::new_from_array(data[18..50].try_into().ok()?)
} else {
Pubkey::default()
};
Some(DexEvent::PumpSwapCreatePoolEvent(PumpSwapCreatePoolEvent {
metadata,
index,
base_amount_in,
quote_amount_in,
pool: accounts[0],
creator: accounts[2],
base_mint: accounts[3],
quote_mint: accounts[4],
lp_mint: accounts[5],
user_base_token_account: accounts[6],
user_quote_token_account: accounts[7],
user_pool_token_account: accounts[8],
pool_base_token_account: accounts[9],
pool_quote_token_account: accounts[10],
coin_creator,
..Default::default()
}))
}
/// 解析存款指令事件
fn parse_deposit_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpSwapDeposit;
if data.len() < 24 || accounts.len() < 11 {
return None;
}
let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?);
Some(DexEvent::PumpSwapDepositEvent(PumpSwapDepositEvent {
metadata,
lp_token_amount_out,
max_base_amount_in,
max_quote_amount_in,
pool: accounts[0],
user: accounts[2],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[6],
user_quote_token_account: accounts[7],
user_pool_token_account: accounts[8],
pool_base_token_account: accounts[9],
pool_quote_token_account: accounts[10],
..Default::default()
}))
}
/// 解析提款指令事件
fn parse_withdraw_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::PumpSwapWithdraw;
if data.len() < 24 || accounts.len() < 11 {
return None;
}
let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?);
Some(DexEvent::PumpSwapWithdrawEvent(PumpSwapWithdrawEvent {
metadata,
lp_token_amount_in,
min_base_amount_out,
min_quote_amount_out,
pool: accounts[0],
user: accounts[2],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[6],
user_quote_token_account: accounts[7],
user_pool_token_account: accounts[8],
pool_base_token_account: accounts[9],
pool_quote_token_account: accounts[10],
..Default::default()
}))
EventDispatcher::dispatch_account(Protocol::PumpSwap, discriminator, account, metadata)
}
@@ -2,15 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::{
event_parser::{
common::{EventMetadata, EventType},
protocols::pumpswap::{PumpSwapGlobalConfigAccountEvent, PumpSwapPoolAccountEvent},
DexEvent,
},
grpc::AccountPretty,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct GlobalConfig {
pub admin: Pubkey,
@@ -28,37 +19,6 @@ pub struct GlobalConfig {
pub const GLOBAL_CONFIG_SIZE: usize = 32 + 8 + 8 + 1 + 32 * 8 + 8 + 32 + 32 + 32 + 1 + 32 * 7;
pub fn global_config_decode(data: &[u8]) -> Option<GlobalConfig> {
if data.len() < GLOBAL_CONFIG_SIZE {
return None;
}
borsh::from_slice::<GlobalConfig>(&data[..GLOBAL_CONFIG_SIZE]).ok()
}
pub fn global_config_parser(
account: &AccountPretty,
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::AccountPumpSwapGlobalConfig;
if account.data.len() < GLOBAL_CONFIG_SIZE + 8 {
return None;
}
if let Some(config) = global_config_decode(&account.data[8..GLOBAL_CONFIG_SIZE + 8]) {
Some(DexEvent::PumpSwapGlobalConfigAccountEvent(PumpSwapGlobalConfigAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
global_config: config,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct Pool {
pub pool_bump: u8,
@@ -83,108 +43,3 @@ pub const POOL_BODY_LEGACY: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1;
pub const POOL_BODY: usize = POOL_BODY_LEGACY + 1 + 7;
pub const POOL_SIZE: usize = POOL_BODY;
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
if data.len() >= POOL_BODY {
return borsh::from_slice::<Pool>(&data[..POOL_BODY]).ok();
}
if data.len() < POOL_BODY_LEGACY {
return None;
}
let legacy = borsh::from_slice::<PoolLegacy>(&data[..POOL_BODY_LEGACY]).ok()?;
Some(legacy.into())
}
/// Pre-cashback on-chain layout (Borsh-compatible prefix of `Pool`).
#[derive(Clone, Debug, BorshDeserialize)]
struct PoolLegacy {
pub pool_bump: u8,
pub index: u16,
pub creator: Pubkey,
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
pub lp_mint: Pubkey,
pub pool_base_token_account: Pubkey,
pub pool_quote_token_account: Pubkey,
pub lp_supply: u64,
pub coin_creator: Pubkey,
pub is_mayhem_mode: bool,
}
impl From<PoolLegacy> for Pool {
fn from(p: PoolLegacy) -> Self {
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: false,
reserved: [0u8; 7],
}
}
}
pub fn pool_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountPumpSwapPool;
let body = account.data.get(8..)?;
if body.len() < POOL_BODY_LEGACY {
return None;
}
if let Some(pool) = pool_decode(body) {
Some(DexEvent::PumpSwapPoolAccountEvent(PumpSwapPoolAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
pool,
}))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_decode_legacy_and_extended() {
let keys: Vec<Pubkey> = (0..6).map(|_| Pubkey::new_unique()).collect();
let coin = Pubkey::new_unique();
let mut legacy = Vec::new();
legacy.push(9u8);
legacy.extend_from_slice(&7u16.to_le_bytes());
for k in &keys {
legacy.extend_from_slice(k.as_ref());
}
legacy.extend_from_slice(&99u64.to_le_bytes());
legacy.extend_from_slice(coin.as_ref());
legacy.push(1u8);
let p = pool_decode(&legacy).expect("legacy");
assert_eq!(p.pool_bump, 9);
assert_eq!(p.index, 7);
assert_eq!(p.lp_supply, 99);
assert!(p.is_mayhem_mode);
assert!(!p.is_cashback_coin);
assert_eq!(p.reserved, [0u8; 7]);
let mut ext = legacy.clone();
ext.push(1u8);
ext.extend_from_slice(&[2u8, 3, 4, 5, 6, 7, 8]);
let p2 = pool_decode(&ext).expect("extended");
assert!(p2.is_cashback_coin);
assert_eq!(p2.reserved, [2, 3, 4, 5, 6, 7, 8]);
}
}
+20 -301
View File
@@ -1,323 +1,42 @@
use crate::streaming::event_parser::{
common::{read_u64_le, EventMetadata, EventType},
protocols::raydium_amm_v4::{
discriminators, RaydiumAmmV4DepositEvent, RaydiumAmmV4Initialize2Event,
RaydiumAmmV4SwapEvent, RaydiumAmmV4WithdrawEvent, RaydiumAmmV4WithdrawPnlEvent,
},
DexEvent,
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
/// Raydium AMM V4程序ID
pub const RAYDIUM_AMM_V4_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
pub use sol_parser_sdk::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID;
/// 解析 Raydium AMM V4 instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_raydium_amm_v4_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
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),
_ => None,
}
EventDispatcher::dispatch_instruction(
Protocol::RaydiumAmmV4,
discriminator,
data,
accounts,
metadata,
)
}
/// 解析 Raydium AMM V4 inner instruction data
///
/// Raydium AMM V4 没有 inner instruction 事件
pub fn parse_raydium_amm_v4_inner_instruction_data(
_discriminator: &[u8],
_data: &[u8],
_metadata: EventMetadata,
discriminator: &[u8],
data: &[u8],
metadata: EventMetadata,
) -> Option<DexEvent> {
None
EventDispatcher::dispatch_inner_instruction(
Protocol::RaydiumAmmV4,
discriminator,
data,
metadata,
)
}
/// 解析 Raydium AMM V4 账户数据
///
/// 根据判别器路由到具体的账户解析函数
pub fn parse_raydium_amm_v4_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: crate::streaming::event_parser::common::EventMetadata,
) -> 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,
)
}
_ => None,
}
}
/// 解析提现指令事件
fn parse_withdraw_pnl_instruction(
_data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumAmmV4WithdrawPnl;
if accounts.len() < 17 {
return None;
}
Some(DexEvent::RaydiumAmmV4WithdrawPnlEvent(RaydiumAmmV4WithdrawPnlEvent {
metadata,
token_program: accounts[0],
amm: accounts[1],
amm_config: accounts[2],
amm_authority: accounts[3],
amm_open_orders: accounts[4],
pool_coin_token_account: accounts[5],
pool_pc_token_account: accounts[6],
coin_pnl_token_account: accounts[7],
pc_pnl_token_account: accounts[8],
pnl_owner_account: accounts[9],
amm_target_orders: accounts[10],
serum_program: accounts[11],
serum_market: accounts[12],
serum_event_queue: accounts[13],
serum_coin_vault_account: accounts[14],
serum_pc_vault_account: accounts[15],
serum_vault_signer: accounts[16],
}))
}
/// 解析移除流动性指令事件
fn parse_withdraw_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumAmmV4Withdraw;
if data.len() < 8 || accounts.len() < 22 {
return None;
}
let amount = read_u64_le(data, 0)?;
Some(DexEvent::RaydiumAmmV4WithdrawEvent(RaydiumAmmV4WithdrawEvent {
metadata,
amount,
token_program: accounts[0],
amm: accounts[1],
amm_authority: accounts[2],
amm_open_orders: accounts[3],
amm_target_orders: accounts[4],
lp_mint_address: accounts[5],
pool_coin_token_account: accounts[6],
pool_pc_token_account: accounts[7],
pool_withdraw_queue: accounts[8],
pool_temp_lp_token_account: accounts[9],
serum_program: accounts[10],
serum_market: accounts[11],
serum_coin_vault_account: accounts[12],
serum_pc_vault_account: accounts[13],
serum_vault_signer: accounts[14],
user_lp_token_account: accounts[15],
user_coin_token_account: accounts[16],
user_pc_token_account: accounts[17],
user_owner: accounts[18],
serum_event_queue: accounts[19],
serum_bids: accounts[20],
serum_asks: accounts[21],
}))
}
/// 解析初始化指令事件
fn parse_initialize2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumAmmV4Initialize2;
if data.len() < 25 || accounts.len() < 21 {
return None;
}
let nonce = data[0];
let open_time = read_u64_le(data, 1)?;
let init_pc_amount = read_u64_le(data, 9)?;
let init_coin_amount = read_u64_le(data, 17)?;
Some(DexEvent::RaydiumAmmV4Initialize2Event(RaydiumAmmV4Initialize2Event {
metadata,
nonce,
open_time,
init_pc_amount,
init_coin_amount,
token_program: accounts[0],
spl_associated_token_account: accounts[1],
system_program: accounts[2],
rent: accounts[3],
amm: accounts[4],
amm_authority: accounts[5],
amm_open_orders: accounts[6],
lp_mint: accounts[7],
coin_mint: accounts[8],
pc_mint: accounts[9],
pool_coin_token_account: accounts[10],
pool_pc_token_account: accounts[11],
pool_withdraw_queue: accounts[12],
amm_target_orders: accounts[13],
pool_temp_lp: accounts[14],
serum_program: accounts[15],
serum_market: accounts[16],
user_wallet: accounts[17],
user_token_coin: accounts[18],
user_token_pc: accounts[19],
user_lp_token_account: accounts[20],
}))
}
/// 解析添加流动性指令事件
fn parse_deposit_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumAmmV4Deposit;
if data.len() < 24 || accounts.len() < 14 {
return None;
}
let max_coin_amount = read_u64_le(data, 0)?;
let max_pc_amount = read_u64_le(data, 8)?;
let base_side = read_u64_le(data, 16)?;
Some(DexEvent::RaydiumAmmV4DepositEvent(RaydiumAmmV4DepositEvent {
metadata,
max_coin_amount,
max_pc_amount,
base_side,
token_program: accounts[0],
amm: accounts[1],
amm_authority: accounts[2],
amm_open_orders: accounts[3],
amm_target_orders: accounts[4],
lp_mint_address: accounts[5],
pool_coin_token_account: accounts[6],
pool_pc_token_account: accounts[7],
serum_market: accounts[8],
user_coin_token_account: accounts[9],
user_pc_token_account: accounts[10],
user_lp_token_account: accounts[11],
user_owner: accounts[12],
serum_event_queue: accounts[13],
}))
}
/// 解析买入指令事件
fn parse_swap_base_output_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumAmmV4SwapBaseOut;
if data.len() < 16 || accounts.len() < 17 {
return None;
}
let max_amount_in = read_u64_le(data, 0)?;
let amount_out = read_u64_le(data, 8)?;
let mut accounts = accounts.to_vec();
if accounts.len() == 17 {
// 添加一个默认的 Pubkey 作为 amm_target_orders 的占位符
// 因为在某些情况下,amm_target_orders 可能是可选的
accounts.insert(4, Pubkey::default());
}
Some(DexEvent::RaydiumAmmV4SwapEvent(RaydiumAmmV4SwapEvent {
metadata,
max_amount_in,
amount_out,
token_program: accounts[0],
amm: accounts[1],
amm_authority: accounts[2],
amm_open_orders: accounts[3],
amm_target_orders: Some(accounts[4]),
pool_coin_token_account: accounts[5],
pool_pc_token_account: accounts[6],
serum_program: accounts[7],
serum_market: accounts[8],
serum_bids: accounts[9],
serum_asks: accounts[10],
serum_event_queue: accounts[11],
serum_coin_vault_account: accounts[12],
serum_pc_vault_account: accounts[13],
serum_vault_signer: accounts[14],
user_source_token_account: accounts[15],
user_destination_token_account: accounts[16],
user_source_owner: accounts[17],
..Default::default()
}))
}
/// 解析买入指令事件
fn parse_swap_base_input_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumAmmV4SwapBaseIn;
if data.len() < 16 || accounts.len() < 17 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let mut accounts = accounts.to_vec();
if accounts.len() == 17 {
// 添加一个默认的 Pubkey 作为 amm_target_orders 的占位符
// 因为在某些情况下,amm_target_orders 可能是可选的
accounts.insert(4, Pubkey::default());
}
Some(DexEvent::RaydiumAmmV4SwapEvent(RaydiumAmmV4SwapEvent {
metadata,
amount_in,
minimum_amount_out,
token_program: accounts[0],
amm: accounts[1],
amm_authority: accounts[2],
amm_open_orders: accounts[3],
amm_target_orders: Some(accounts[4]),
pool_coin_token_account: accounts[5],
pool_pc_token_account: accounts[6],
serum_program: accounts[7],
serum_market: accounts[8],
serum_bids: accounts[9],
serum_asks: accounts[10],
serum_event_queue: accounts[11],
serum_coin_vault_account: accounts[12],
serum_pc_vault_account: accounts[13],
serum_vault_signer: accounts[14],
user_source_token_account: accounts[15],
user_destination_token_account: accounts[16],
user_source_owner: accounts[17],
..Default::default()
}))
EventDispatcher::dispatch_account(Protocol::RaydiumAmmV4, discriminator, account, metadata)
}
@@ -2,15 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::{
event_parser::{
common::{EventMetadata, EventType},
protocols::raydium_amm_v4::RaydiumAmmV4AmmInfoAccountEvent,
DexEvent,
},
grpc::AccountPretty,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct Fees {
pub min_separate_numerator: u64,
@@ -80,34 +71,6 @@ pub struct AmmInfo {
pub const AMM_INFO_SIZE: usize = 752;
pub fn amm_info_decode(data: &[u8]) -> Option<AmmInfo> {
if data.len() < AMM_INFO_SIZE {
return None;
}
borsh::from_slice::<AmmInfo>(&data[..AMM_INFO_SIZE]).ok()
}
pub fn amm_info_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountRaydiumAmmV4AmmInfo;
if account.data.len() < AMM_INFO_SIZE {
return None;
}
if let Some(amm_info) = amm_info_decode(&account.data[..AMM_INFO_SIZE]) {
Some(DexEvent::RaydiumAmmV4AmmInfoAccountEvent(RaydiumAmmV4AmmInfoAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
amm_info: amm_info,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct MarketState {
pub padding: [u8; 5],
@@ -135,10 +98,3 @@ pub struct MarketState {
}
pub const MARKET_STATE_SIZE: usize = 388;
pub fn market_state_decode(data: &[u8]) -> Option<MarketState> {
if data.len() < MARKET_STATE_SIZE {
return None;
}
borsh::from_slice::<MarketState>(&data[..MARKET_STATE_SIZE]).ok()
}
+20 -362
View File
@@ -1,384 +1,42 @@
use crate::streaming::event_parser::{
common::{
read_i32_le, read_option_bool, read_u128_le, read_u64_le, read_u8_le, EventMetadata,
EventType,
},
protocols::raydium_clmm::{
discriminators, RaydiumClmmClosePositionEvent, RaydiumClmmCreatePoolEvent,
RaydiumClmmDecreaseLiquidityV2Event, RaydiumClmmIncreaseLiquidityV2Event,
RaydiumClmmOpenPositionV2Event, RaydiumClmmOpenPositionWithToken22NftEvent,
RaydiumClmmSwapEvent, RaydiumClmmSwapV2Event,
},
DexEvent,
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
/// Raydium CLMM程序ID
pub const RAYDIUM_CLMM_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK");
pub use sol_parser_sdk::instr::program_ids::RAYDIUM_CLMM_PROGRAM_ID;
/// 解析 Raydium CLMM instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_raydium_clmm_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
discriminators::SWAP => parse_swap_instruction(data, accounts, metadata),
discriminators::SWAP_V2 => parse_swap_v2_instruction(data, accounts, metadata),
discriminators::CLOSE_POSITION => {
parse_close_position_instruction(data, accounts, metadata)
}
discriminators::DECREASE_LIQUIDITY_V2 => {
parse_decrease_liquidity_v2_instruction(data, accounts, metadata)
}
discriminators::CREATE_POOL => parse_create_pool_instruction(data, accounts, metadata),
discriminators::INCREASE_LIQUIDITY_V2 => {
parse_increase_liquidity_v2_instruction(data, accounts, metadata)
}
discriminators::OPEN_POSITION_WITH_TOKEN_22_NFT => {
parse_open_position_with_token_22_nft_instruction(data, accounts, metadata)
}
discriminators::OPEN_POSITION_V2 => {
parse_open_position_v2_instruction(data, accounts, metadata)
}
_ => None,
}
EventDispatcher::dispatch_instruction(
Protocol::RaydiumClmm,
discriminator,
data,
accounts,
metadata,
)
}
/// 解析 Raydium CLMM inner instruction data
///
/// Raydium CLMM 没有 inner instruction 事件
pub fn parse_raydium_clmm_inner_instruction_data(
_discriminator: &[u8],
_data: &[u8],
_metadata: EventMetadata,
discriminator: &[u8],
data: &[u8],
metadata: EventMetadata,
) -> Option<DexEvent> {
None
EventDispatcher::dispatch_inner_instruction(
Protocol::RaydiumClmm,
discriminator,
data,
metadata,
)
}
/// 解析 Raydium CLMM 账户数据
///
/// 根据判别器路由到具体的账户解析函数
pub fn parse_raydium_clmm_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: crate::streaming::event_parser::common::EventMetadata,
) -> Option<crate::streaming::event_parser::DexEvent> {
match discriminator {
discriminators::AMM_CONFIG => {
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,
)
}
discriminators::TICK_ARRAY_STATE => {
crate::streaming::event_parser::protocols::raydium_clmm::types::tick_array_state_parser(
account, metadata,
)
}
_ => None,
}
}
/// 解析打开仓位V2指令事件
fn parse_open_position_v2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmOpenPositionV2;
if data.len() < 51 || accounts.len() < 22 {
return None;
}
Some(DexEvent::RaydiumClmmOpenPositionV2Event(RaydiumClmmOpenPositionV2Event {
metadata,
tick_lower_index: read_i32_le(data, 0)?,
tick_upper_index: read_i32_le(data, 4)?,
tick_array_lower_start_index: read_i32_le(data, 8)?,
tick_array_upper_start_index: read_i32_le(data, 12)?,
liquidity: read_u128_le(data, 16)?,
amount0_max: read_u64_le(data, 32)?,
amount1_max: read_u64_le(data, 40)?,
with_metadata: read_u8_le(data, 48)? == 1,
base_flag: read_option_bool(data, &mut 49)?,
payer: accounts[0],
position_nft_owner: accounts[1],
position_nft_mint: accounts[2],
position_nft_account: accounts[3],
metadata_account: accounts[4],
pool_state: accounts[5],
protocol_position: accounts[6],
tick_array_lower: accounts[7],
tick_array_upper: accounts[8],
personal_position: accounts[9],
token_account0: accounts[10],
token_account1: accounts[11],
token_vault0: accounts[12],
token_vault1: accounts[13],
rent: accounts[14],
system_program: accounts[15],
token_program: accounts[16],
associated_token_program: accounts[17],
metadata_program: accounts[18],
token_program2022: accounts[19],
vault0_mint: accounts[20],
vault1_mint: accounts[21],
remaining_accounts: accounts[22..].to_vec(),
}))
}
/// 解析打开仓位v2指令事件
fn parse_open_position_with_token_22_nft_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmOpenPositionWithToken22Nft;
if data.len() < 51 || accounts.len() < 20 {
return None;
}
Some(DexEvent::RaydiumClmmOpenPositionWithToken22NftEvent(
RaydiumClmmOpenPositionWithToken22NftEvent {
metadata,
tick_lower_index: read_i32_le(data, 0)?,
tick_upper_index: read_i32_le(data, 4)?,
tick_array_lower_start_index: read_i32_le(data, 8)?,
tick_array_upper_start_index: read_i32_le(data, 12)?,
liquidity: read_u128_le(data, 16)?,
amount0_max: read_u64_le(data, 32)?,
amount1_max: read_u64_le(data, 40)?,
with_metadata: read_u8_le(data, 48)? == 1,
base_flag: read_option_bool(data, &mut 49)?,
payer: accounts[0],
position_nft_owner: accounts[1],
position_nft_mint: accounts[2],
position_nft_account: accounts[3],
pool_state: accounts[4],
protocol_position: accounts[5],
tick_array_lower: accounts[6],
tick_array_upper: accounts[7],
personal_position: accounts[8],
token_account0: accounts[9],
token_account1: accounts[10],
token_vault0: accounts[11],
token_vault1: accounts[12],
rent: accounts[13],
system_program: accounts[14],
token_program: accounts[15],
associated_token_program: accounts[16],
token_program2022: accounts[17],
vault0_mint: accounts[18],
vault1_mint: accounts[19],
},
))
}
/// 解析增加流动性v2指令事件
fn parse_increase_liquidity_v2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmIncreaseLiquidityV2;
if data.len() < 34 || accounts.len() < 15 {
return None;
}
Some(DexEvent::RaydiumClmmIncreaseLiquidityV2Event(RaydiumClmmIncreaseLiquidityV2Event {
metadata,
liquidity: read_u128_le(data, 0)?,
amount0_max: read_u64_le(data, 16)?,
amount1_max: read_u64_le(data, 24)?,
base_flag: read_option_bool(data, &mut 32)?,
nft_owner: accounts[0],
nft_account: accounts[1],
pool_state: accounts[2],
protocol_position: accounts[3],
personal_position: accounts[4],
tick_array_lower: accounts[5],
tick_array_upper: accounts[6],
token_account0: accounts[7],
token_account1: accounts[8],
token_vault0: accounts[9],
token_vault1: accounts[10],
token_program: accounts[11],
token_program2022: accounts[12],
vault0_mint: accounts[13],
vault1_mint: accounts[14],
}))
}
/// 解析创建池指令事件
fn parse_create_pool_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmCreatePool;
if data.len() < 24 || accounts.len() < 13 {
return None;
}
Some(DexEvent::RaydiumClmmCreatePoolEvent(RaydiumClmmCreatePoolEvent {
metadata,
sqrt_price_x64: read_u128_le(data, 0)?,
open_time: read_u64_le(data, 16)?,
pool_creator: accounts[0],
amm_config: accounts[1],
pool_state: accounts[2],
token_mint0: accounts[3],
token_mint1: accounts[4],
token_vault0: accounts[5],
token_vault1: accounts[6],
observation_state: accounts[7],
tick_array_bitmap: accounts[8],
token_program0: accounts[9],
token_program1: accounts[10],
system_program: accounts[11],
rent: accounts[12],
}))
}
/// 解析减少流动性v2指令事件
fn parse_decrease_liquidity_v2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmDecreaseLiquidityV2;
if data.len() < 32 || accounts.len() < 16 {
return None;
}
Some(DexEvent::RaydiumClmmDecreaseLiquidityV2Event(RaydiumClmmDecreaseLiquidityV2Event {
metadata,
liquidity: read_u128_le(data, 0)?,
amount0_min: read_u64_le(data, 16)?,
amount1_min: read_u64_le(data, 24)?,
nft_owner: accounts[0],
nft_account: accounts[1],
personal_position: accounts[2],
pool_state: accounts[3],
protocol_position: accounts[4],
token_vault0: accounts[5],
token_vault1: accounts[6],
tick_array_lower: accounts[7],
tick_array_upper: accounts[8],
recipient_token_account0: accounts[9],
recipient_token_account1: accounts[10],
token_program: accounts[11],
token_program2022: accounts[12],
memo_program: accounts[13],
vault0_mint: accounts[14],
vault1_mint: accounts[15],
remaining_accounts: accounts[16..].to_vec(),
}))
}
/// 解析关闭仓位指令事件
fn parse_close_position_instruction(
_data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmClosePosition;
if accounts.len() < 6 {
return None;
}
Some(DexEvent::RaydiumClmmClosePositionEvent(RaydiumClmmClosePositionEvent {
metadata,
nft_owner: accounts[0],
position_nft_mint: accounts[1],
position_nft_account: accounts[2],
personal_position: accounts[3],
system_program: accounts[4],
token_program: accounts[5],
}))
}
/// 解析交易指令事件
fn parse_swap_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmSwap;
if data.len() < 33 || accounts.len() < 10 {
return None;
}
let amount = read_u64_le(data, 0)?;
let other_amount_threshold = read_u64_le(data, 8)?;
let sqrt_price_limit_x64 = read_u128_le(data, 16)?;
let is_base_input = read_u8_le(data, 32)?;
Some(DexEvent::RaydiumClmmSwapEvent(RaydiumClmmSwapEvent {
metadata,
amount,
other_amount_threshold,
sqrt_price_limit_x64,
is_base_input: is_base_input == 1,
payer: accounts[0],
amm_config: accounts[1],
pool_state: accounts[2],
input_token_account: accounts[3],
output_token_account: accounts[4],
input_vault: accounts[5],
output_vault: accounts[6],
observation_state: accounts[7],
token_program: accounts[8],
tick_array: accounts[9],
remaining_accounts: accounts[10..].to_vec(),
}))
}
fn parse_swap_v2_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumClmmSwapV2;
if data.len() < 33 || accounts.len() < 13 {
return None;
}
let amount = read_u64_le(data, 0)?;
let other_amount_threshold = read_u64_le(data, 8)?;
let sqrt_price_limit_x64 = read_u128_le(data, 16)?;
let is_base_input = read_u8_le(data, 32)?;
Some(DexEvent::RaydiumClmmSwapV2Event(RaydiumClmmSwapV2Event {
metadata,
amount,
other_amount_threshold,
sqrt_price_limit_x64,
is_base_input: is_base_input == 1,
payer: accounts[0],
amm_config: accounts[1],
pool_state: accounts[2],
input_token_account: accounts[3],
output_token_account: accounts[4],
input_vault: accounts[5],
output_vault: accounts[6],
observation_state: accounts[7],
token_program: accounts[8],
token_program2022: accounts[9],
memo_program: accounts[10],
input_vault_mint: accounts[11],
output_vault_mint: accounts[12],
remaining_accounts: accounts[13..].to_vec(),
}))
EventDispatcher::dispatch_account(Protocol::RaydiumClmm, discriminator, account, metadata)
}
@@ -2,18 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::{
event_parser::{
common::{EventMetadata, EventType},
protocols::raydium_clmm::{
RaydiumClmmAmmConfigAccountEvent, RaydiumClmmPoolStateAccountEvent,
RaydiumClmmTickArrayStateAccountEvent,
},
DexEvent,
},
grpc::AccountPretty,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct AmmConfig {
pub bump: u8,
@@ -30,34 +18,6 @@ pub struct AmmConfig {
pub const AMM_CONFIG_SIZE: usize = 1 + 2 + 32 + 4 * 2 + 2 + 4 * 2 + 32 + 8 * 3;
pub fn amm_config_decode(data: &[u8]) -> Option<AmmConfig> {
if data.len() < AMM_CONFIG_SIZE {
return None;
}
borsh::from_slice::<AmmConfig>(&data[..AMM_CONFIG_SIZE]).ok()
}
pub fn amm_config_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountRaydiumClmmAmmConfig;
if account.data.len() < AMM_CONFIG_SIZE + 8 {
return None;
}
if let Some(amm_config) = amm_config_decode(&account.data[8..AMM_CONFIG_SIZE + 8]) {
Some(DexEvent::RaydiumClmmAmmConfigAccountEvent(RaydiumClmmAmmConfigAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
amm_config: amm_config,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct RewardInfo {
pub reward_state: u8,
@@ -117,34 +77,6 @@ pub struct PoolState {
pub const POOL_STATE_SIZE: usize = 1536;
pub fn pool_state_decode(data: &[u8]) -> Option<PoolState> {
if data.len() < POOL_STATE_SIZE {
return None;
}
borsh::from_slice::<PoolState>(&data[..POOL_STATE_SIZE]).ok()
}
pub fn pool_state_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountRaydiumClmmPoolState;
if account.data.len() < POOL_STATE_SIZE + 8 {
return None;
}
if let Some(pool_state) = pool_state_decode(&account.data[8..POOL_STATE_SIZE + 8]) {
Some(DexEvent::RaydiumClmmPoolStateAccountEvent(RaydiumClmmPoolStateAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
pool_state: pool_state,
}))
} else {
None
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct TickState {
pub tick: i32,
@@ -196,38 +128,3 @@ impl Default for TickArrayState {
}
pub const TICK_ARRAY_STATE_SIZE: usize = 10232;
pub fn tick_array_state_decode(data: &[u8]) -> Option<TickArrayState> {
if data.len() < TICK_ARRAY_STATE_SIZE {
return None;
}
borsh::from_slice::<TickArrayState>(&data[..TICK_ARRAY_STATE_SIZE]).ok()
}
pub fn tick_array_state_parser(
account: &AccountPretty,
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::AccountRaydiumClmmTickArrayState;
if account.data.len() < TICK_ARRAY_STATE_SIZE + 8 {
return None;
}
if let Some(tick_array_state) =
tick_array_state_decode(&account.data[8..TICK_ARRAY_STATE_SIZE + 8])
{
Some(DexEvent::RaydiumClmmTickArrayStateAccountEvent(
RaydiumClmmTickArrayStateAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
tick_array_state: tick_array_state,
},
))
} else {
None
}
}
+22 -224
View File
@@ -1,244 +1,42 @@
use crate::streaming::event_parser::{
common::EventMetadata, core::EventDispatcher, DexEvent, Protocol,
};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::event_parser::{
common::{read_u64_le, EventMetadata, EventType},
protocols::raydium_cpmm::{
discriminators, RaydiumCpmmDepositEvent, RaydiumCpmmInitializeEvent, RaydiumCpmmSwapEvent,
RaydiumCpmmWithdrawEvent,
},
DexEvent,
};
pub use sol_parser_sdk::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID;
/// Raydium CPMM程序ID
pub const RAYDIUM_CPMM_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C");
/// 解析 Raydium CPMM instruction data
///
/// 根据判别器路由到具体的 instruction 解析函数
pub fn parse_raydium_cpmm_instruction_data(
discriminator: &[u8],
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<DexEvent> {
match discriminator {
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::INITIALIZE => parse_initialize_instruction(data, accounts, metadata),
discriminators::WITHDRAW => parse_withdraw_instruction(data, accounts, metadata),
_ => None,
}
EventDispatcher::dispatch_instruction(
Protocol::RaydiumCpmm,
discriminator,
data,
accounts,
metadata,
)
}
/// 解析 Raydium CPMM inner instruction data
///
/// Raydium CPMM 没有 inner instruction 事件
pub fn parse_raydium_cpmm_inner_instruction_data(
_discriminator: &[u8],
_data: &[u8],
_metadata: EventMetadata,
discriminator: &[u8],
data: &[u8],
metadata: EventMetadata,
) -> Option<DexEvent> {
None
EventDispatcher::dispatch_inner_instruction(
Protocol::RaydiumCpmm,
discriminator,
data,
metadata,
)
}
/// 解析 Raydium CPMM 账户数据
///
/// 根据判别器路由到具体的账户解析函数
pub fn parse_raydium_cpmm_account_data(
discriminator: &[u8],
account: &crate::streaming::grpc::AccountPretty,
metadata: crate::streaming::event_parser::common::EventMetadata,
) -> Option<crate::streaming::event_parser::DexEvent> {
match discriminator {
discriminators::AMM_CONFIG => {
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,
)
}
_ => None,
}
}
/// 解析提款指令事件
fn parse_withdraw_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumCpmmWithdraw;
if data.len() < 24 || accounts.len() < 14 {
return None;
}
Some(DexEvent::RaydiumCpmmWithdrawEvent(RaydiumCpmmWithdrawEvent {
metadata,
lp_token_amount: read_u64_le(data, 0)?,
minimum_token0_amount: read_u64_le(data, 8)?,
minimum_token1_amount: read_u64_le(data, 16)?,
owner: accounts[0],
authority: accounts[1],
pool_state: accounts[2],
owner_lp_token: accounts[3],
token_0_account: accounts[4],
token_1_account: accounts[5],
token_0_vault: accounts[6],
token_1_vault: accounts[7],
token_program: accounts[8],
token_program2022: accounts[9],
vault_0_mint: accounts[10],
vault_1_mint: accounts[11],
lp_mint: accounts[12],
memo_program: accounts[13],
}))
}
/// 解析初始化指令事件
fn parse_initialize_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumCpmmInitialize;
if data.len() < 24 || accounts.len() < 20 {
return None;
}
Some(DexEvent::RaydiumCpmmInitializeEvent(RaydiumCpmmInitializeEvent {
metadata,
init_amount0: read_u64_le(data, 0)?,
init_amount1: read_u64_le(data, 8)?,
open_time: read_u64_le(data, 16)?,
creator: accounts[0],
amm_config: accounts[1],
authority: accounts[2],
pool_state: accounts[3],
token_0_mint: accounts[4],
token_1_mint: accounts[5],
lp_mint: accounts[6],
creator_token_0: accounts[7],
creator_token_1: accounts[8],
creator_lp_token: accounts[9],
token_0_vault: accounts[10],
token_1_vault: accounts[11],
create_pool_fee: accounts[12],
observation_state: accounts[13],
token_program: accounts[14],
token_0_program: accounts[15],
token_1_program: accounts[16],
associated_token_program: accounts[17],
system_program: accounts[18],
rent: accounts[19],
}))
}
/// 解析存款指令事件
fn parse_deposit_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumCpmmDeposit;
if data.len() < 24 || accounts.len() < 13 {
return None;
}
Some(DexEvent::RaydiumCpmmDepositEvent(RaydiumCpmmDepositEvent {
metadata,
lp_token_amount: read_u64_le(data, 0)?,
maximum_token0_amount: read_u64_le(data, 8)?,
maximum_token1_amount: read_u64_le(data, 16)?,
owner: accounts[0],
authority: accounts[1],
pool_state: accounts[2],
owner_lp_token: accounts[3],
token_0_account: accounts[4],
token_1_account: accounts[5],
token_0_vault: accounts[6],
token_1_vault: accounts[7],
token_program: accounts[8],
token_program2022: accounts[9],
vault_0_mint: accounts[10],
vault_1_mint: accounts[11],
lp_mint: accounts[12],
}))
}
/// 解析买入指令事件
fn parse_swap_base_input_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumCpmmSwapBaseInput;
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
Some(DexEvent::RaydiumCpmmSwapEvent(RaydiumCpmmSwapEvent {
metadata,
amount_in,
minimum_amount_out,
payer: accounts[0],
authority: accounts[1],
amm_config: accounts[2],
pool_state: accounts[3],
input_token_account: accounts[4],
output_token_account: accounts[5],
input_vault: accounts[6],
output_vault: accounts[7],
input_token_program: accounts[8],
output_token_program: accounts[9],
input_token_mint: accounts[10],
output_token_mint: accounts[11],
observation_state: accounts[12],
..Default::default()
}))
}
fn parse_swap_base_output_instruction(
data: &[u8],
accounts: &[Pubkey],
mut metadata: EventMetadata,
) -> Option<DexEvent> {
metadata.event_type = EventType::RaydiumCpmmSwapBaseOutput;
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let max_amount_in = read_u64_le(data, 0)?;
let amount_out = read_u64_le(data, 8)?;
Some(DexEvent::RaydiumCpmmSwapEvent(RaydiumCpmmSwapEvent {
metadata,
max_amount_in,
amount_out,
payer: accounts[0],
authority: accounts[1],
amm_config: accounts[2],
pool_state: accounts[3],
input_token_account: accounts[4],
output_token_account: accounts[5],
input_vault: accounts[6],
output_vault: accounts[7],
input_token_program: accounts[8],
output_token_program: accounts[9],
input_token_mint: accounts[10],
output_token_mint: accounts[11],
observation_state: accounts[12],
..Default::default()
}))
EventDispatcher::dispatch_account(Protocol::RaydiumCpmm, discriminator, account, metadata)
}
@@ -2,17 +2,6 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::{
event_parser::{
common::{EventMetadata, EventType},
protocols::raydium_cpmm::{
RaydiumCpmmAmmConfigAccountEvent, RaydiumCpmmPoolStateAccountEvent,
},
DexEvent,
},
grpc::AccountPretty,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct AmmConfig {
pub bump: u8,
@@ -30,34 +19,6 @@ pub struct AmmConfig {
pub const AMM_CONFIG_SIZE: usize = 228;
pub fn amm_config_decode(data: &[u8]) -> Option<AmmConfig> {
if data.len() < AMM_CONFIG_SIZE {
return None;
}
borsh::from_slice::<AmmConfig>(&data[..AMM_CONFIG_SIZE]).ok()
}
pub fn amm_config_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountRaydiumCpmmAmmConfig;
if account.data.len() < AMM_CONFIG_SIZE + 8 {
return None;
}
if let Some(amm_config) = amm_config_decode(&account.data[8..AMM_CONFIG_SIZE + 8]) {
Some(DexEvent::RaydiumCpmmAmmConfigAccountEvent(RaydiumCpmmAmmConfigAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
amm_config: amm_config,
}))
} else {
None
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PoolState {
pub amm_config: Pubkey,
@@ -91,31 +52,3 @@ pub struct PoolState {
}
pub const POOL_STATE_SIZE: usize = 629;
pub fn pool_state_decode(data: &[u8]) -> Option<PoolState> {
if data.len() < POOL_STATE_SIZE {
return None;
}
borsh::from_slice::<PoolState>(&data[..POOL_STATE_SIZE]).ok()
}
pub fn pool_state_parser(account: &AccountPretty, mut metadata: EventMetadata) -> Option<DexEvent> {
metadata.event_type = EventType::AccountRaydiumCpmmPoolState;
if account.data.len() < POOL_STATE_SIZE + 8 {
return None;
}
if let Some(pool_state) = pool_state_decode(&account.data[8..POOL_STATE_SIZE + 8]) {
Some(DexEvent::RaydiumCpmmPoolStateAccountEvent(RaydiumCpmmPoolStateAccountEvent {
metadata,
pubkey: account.pubkey,
executable: account.executable,
lamports: account.lamports,
owner: account.owner,
rent_epoch: account.rent_epoch,
pool_state: pool_state,
}))
} else {
None
}
}
@@ -1,12 +1,7 @@
//! `sol-parser-sdk` 产出、经 `parser_sdk_bridge` 映射的协议事件类型;`native` 将 sdk 指令解析接到 Yellowstone/shred 路径。
//! `sol-parser-sdk` compatibility event types and parser facades.
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");
pub use sol_parser_sdk::instr::program_ids::{
METEORA_DLMM_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID, ORCA_WHIRLPOOL_PROGRAM_ID,
};
@@ -1,34 +1,10 @@
//! 将 `sol-parser-sdk` 的 Orca Whirlpool / Meteora Pools / DLMM 顶层与 inner 指令解析接到 streamer `DexEvent`。
//! Backward-compatible SDK parser facade.
use crate::streaming::event_parser::common::EventMetadata;
use crate::streaming::event_parser::core::dispatcher::EventDispatcher;
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],
@@ -36,44 +12,13 @@ pub fn dispatch_instruction(
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))
EventDispatcher::dispatch_instruction(
protocol,
instruction_discriminator,
instruction_data,
accounts,
stream_meta.clone(),
)
}
pub fn dispatch_inner_instruction(
@@ -82,17 +27,10 @@ pub fn dispatch_inner_instruction(
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))
EventDispatcher::dispatch_inner_instruction(
protocol,
inner_instruction_discriminator,
inner_instruction_data,
stream_meta.clone(),
)
}
+19 -12
View File
@@ -1,16 +1,9 @@
use crate::streaming::event_parser::protocols::{
bonk::parser::BONK_PROGRAM_ID,
meteora_damm_v2::parser::METEORA_DAMM_V2_PROGRAM_ID,
pumpfun::parser::PUMPFUN_PROGRAM_ID,
pumpswap::parser::PUMPSWAP_PROGRAM_ID,
raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID,
raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID,
raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID,
sol_parser_forward::{
METEORA_DLMM_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID, ORCA_WHIRLPOOL_PROGRAM_ID,
},
};
use anyhow::{anyhow, Result};
use sol_parser_sdk::instr::program_ids::{
BONK_PROGRAM_ID, METEORA_DAMM_V2_PROGRAM_ID, METEORA_DLMM_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID,
ORCA_WHIRLPOOL_PROGRAM_ID, PUMPFUN_PROGRAM_ID, PUMPSWAP_PROGRAM_ID, PUMP_FEES_PROGRAM_ID,
RAYDIUM_AMM_V4_PROGRAM_ID, RAYDIUM_CLMM_PROGRAM_ID, RAYDIUM_CPMM_PROGRAM_ID,
};
use solana_sdk::pubkey::Pubkey;
/// 支持的协议
@@ -18,7 +11,10 @@ use solana_sdk::pubkey::Pubkey;
pub enum Protocol {
PumpSwap,
PumpFun,
PumpFees,
/// Backward-compatible alias for Raydium Launchpad / LaunchLab.
Bonk,
RaydiumLaunchpad,
RaydiumCpmm,
RaydiumClmm,
RaydiumAmmV4,
@@ -33,7 +29,9 @@ impl Protocol {
match self {
Protocol::PumpSwap => vec![PUMPSWAP_PROGRAM_ID],
Protocol::PumpFun => vec![PUMPFUN_PROGRAM_ID],
Protocol::PumpFees => vec![PUMP_FEES_PROGRAM_ID],
Protocol::Bonk => vec![BONK_PROGRAM_ID],
Protocol::RaydiumLaunchpad => vec![BONK_PROGRAM_ID],
Protocol::RaydiumCpmm => vec![RAYDIUM_CPMM_PROGRAM_ID],
Protocol::RaydiumClmm => vec![RAYDIUM_CLMM_PROGRAM_ID],
Protocol::RaydiumAmmV4 => vec![RAYDIUM_AMM_V4_PROGRAM_ID],
@@ -50,7 +48,9 @@ impl std::fmt::Display for Protocol {
match self {
Protocol::PumpSwap => write!(f, "PumpSwap"),
Protocol::PumpFun => write!(f, "PumpFun"),
Protocol::PumpFees => write!(f, "PumpFees"),
Protocol::Bonk => write!(f, "Bonk"),
Protocol::RaydiumLaunchpad => write!(f, "RaydiumLaunchpad"),
Protocol::RaydiumCpmm => write!(f, "RaydiumCpmm"),
Protocol::RaydiumClmm => write!(f, "RaydiumClmm"),
Protocol::RaydiumAmmV4 => write!(f, "RaydiumAmmV4"),
@@ -69,7 +69,10 @@ impl std::str::FromStr for Protocol {
match s.to_lowercase().as_str() {
"pumpswap" => Ok(Protocol::PumpSwap),
"pumpfun" => Ok(Protocol::PumpFun),
"pumpfees" | "pump_fees" => Ok(Protocol::PumpFees),
"bonk" => Ok(Protocol::Bonk),
"raydiumlaunchpad" | "raydium_launchpad" | "raydium_launchlab" | "launchpad"
| "launchlab" => Ok(Protocol::RaydiumLaunchpad),
"raydiumcpmm" | "raydium_cpmm" => Ok(Protocol::RaydiumCpmm),
"raydiumclmm" | "raydium_clmm" => Ok(Protocol::RaydiumClmm),
"raydiumammv4" | "raydium_amm_v4" => Ok(Protocol::RaydiumAmmV4),
@@ -90,6 +93,8 @@ mod tests {
#[test]
fn parses_display_style_protocol_names() {
for protocol in [
Protocol::PumpFees,
Protocol::RaydiumLaunchpad,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
Protocol::RaydiumAmmV4,
@@ -106,6 +111,8 @@ mod tests {
#[test]
fn parses_snake_case_protocol_aliases() {
assert_eq!(Protocol::from_str("raydium_cpmm").unwrap(), Protocol::RaydiumCpmm);
assert_eq!(Protocol::from_str("pump_fees").unwrap(), Protocol::PumpFees);
assert_eq!(Protocol::from_str("raydium_launchpad").unwrap(), Protocol::RaydiumLaunchpad);
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);