mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-16 10:28:05 +00:00
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:
@@ -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());
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
pub mod filter;
|
||||
pub mod high_performance_clock;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
pub use types::*;
|
||||
pub use utils::*;
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Executable → Regular
+8
-582
@@ -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 {
|
||||
|
||||
Executable → Regular
+12
-423
@@ -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 {
|
||||
|
||||
Executable → Regular
+12
-342
@@ -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 个固定账户(0–22,17/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]);
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
+20
-301
@@ -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()
|
||||
}
|
||||
|
||||
Executable → Regular
+20
-362
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
+22
-224
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user