sol-parser-sdk 0.4.0

A lightweight Rust library for real-time event streaming from Solana DEX trading programs. Supports PumpFun, PumpSwap, Bonk, and Raydium protocols with Yellowstone gRPC and ShredStream.
Documentation
//! Yellowstone gRPC 单笔交易解析会并行跑 **log** 与 **instruction** 两路,结果直接拼接,
//! 同一链上事实可能被输出成两条 `DexEvent`。本模块在合并阶段按「业务指纹」去重,
//! 并对同指纹事件做 **log 优先、ix 补充** 的字段合并(见 `merge_grpc_instruction_into_log`)。
//!
//! **去重键必须能区分「不同用户」**:交易类事件用 **`user`(钱包)**、池子 / mint、买卖方向等;
//! **刻意不包含成交量**:instruction 侧数值可能与程序日志不一致,若用金额做键会导致 log/ix 无法配对,
//! 合并后仍以 **log 数值为准**(见 [`crate::core::merger::merge_grpc_instruction_into_log`])。
//!
//! **同签多笔**:同一 `(mint, user, is_buy, ix_lane)` 可能出现多次(例如捆绑里同一钱包连买两笔)。
//! PumpFun 键上增加 **`lane_occurrence`**(在本路 `log_events` / `instr_events` 各自列表中的出现次序,从 0 递增),
//! 与 log、ix 两路各自遍历顺序一致时,仍能与首条 log 正确配对合并。
//!
//! **合并策略**:先收录 **log** 侧事件,再对同指纹的 **instruction** 侧调用
//! `merge_grpc_instruction_into_log` —— **以日志为权威**,指令只补缺账户等字段。

use std::collections::HashMap;

use solana_sdk::pubkey::Pubkey;

use crate::core::events::DexEvent;

#[derive(Clone, Hash, PartialEq, Eq)]
enum LogInstrDedupKey {
    PumpFunTrade {
        mint: Pubkey,
        user: Pubkey,
        is_buy: bool,
        /// 指令种类桶:`0=buy/未知`、`1=sell`、`2=buy_exact_sol_in`(日志侧 `ix_name` 常为空,归入 buy 桶以便与 ix 配对)。
        ix_lane: u8,
        /// 同签、同 `(mint,user,is_buy,ix_lane)` 下第几条(log 路与 ix 路各自从 0 计数)。
        lane_occurrence: u16,
    },
    PumpFunCreate {
        mint: Pubkey,
    },
    PumpFunCreateV2 {
        mint: Pubkey,
    },
    PumpFunMigrate {
        mint: Pubkey,
        pool: Pubkey,
        user: Pubkey,
    },
    BonkTrade {
        pool: Pubkey,
        user: Pubkey,
        is_buy: bool,
    },
    BonkPoolCreate {
        pool: Pubkey,
    },
    BonkMigrateAmm {
        old_pool: Pubkey,
        new_pool: Pubkey,
        user: Pubkey,
    },
    PumpSwapTrade {
        mint: Pubkey,
        user: Pubkey,
        is_buy: bool,
        ix_lane: u8,
    },
    PumpSwapBuy {
        pool: Pubkey,
        user: Pubkey,
    },
    PumpSwapSell {
        pool: Pubkey,
        user: Pubkey,
    },
    PumpSwapCreatePool {
        pool: Pubkey,
        base_mint: Pubkey,
        quote_mint: Pubkey,
    },
    PumpSwapLiquidityAdded {
        pool: Pubkey,
        user: Pubkey,
    },
    PumpSwapLiquidityRemoved {
        pool: Pubkey,
        user: Pubkey,
    },
    /// `sender` 可能仅 ix 填全,不参与键以免与 log 配对失败。
    RaydiumClmmSwap {
        pool: Pubkey,
        zero_for_one: bool,
    },
    /// 仅 `amm`:用户 ATA 常仅一路有,用 amm+ATA 会导致无法去重。
    RaydiumAmmV4Swap {
        amm: Pubkey,
    },
    MeteoraDlmmSwap {
        pool: Pubkey,
        from: Pubkey,
        swap_for_y: bool,
    },
}

#[inline]
fn pumpfun_ix_lane(ix_name: &str) -> u8 {
    match ix_name {
        "sell" => 1,
        "buy_exact_sol_in" => 2,
        _ => 0,
    }
}

#[inline]
fn pumpswap_ix_lane(ix_name: &str) -> u8 {
    pumpfun_ix_lane(ix_name)
}

type PumpFunLaneBase = (Pubkey, Pubkey, bool, u8);

#[inline]
fn pumpfun_trade_key_with_occ(
    t: &crate::core::events::PumpFunTradeEvent,
    lane_occurrence: u16,
) -> LogInstrDedupKey {
    LogInstrDedupKey::PumpFunTrade {
        mint: t.mint,
        user: t.user,
        is_buy: t.is_buy,
        ix_lane: pumpfun_ix_lane(t.ix_name.as_str()),
        lane_occurrence,
    }
}

/// 非 PumpFun 买卖事件的去重键。PumpFun `Trade/Buy/Sell/BuyExactSolIn` 必须用 [`next_pumpfun_dedup_key`] 带 `lane_occurrence`。
#[inline]
fn log_instr_dedup_key(ev: &DexEvent) -> Option<LogInstrDedupKey> {
    use DexEvent::*;
    match ev {
        PumpFunCreate(c) => Some(LogInstrDedupKey::PumpFunCreate { mint: c.mint }),
        PumpFunCreateV2(c) => Some(LogInstrDedupKey::PumpFunCreateV2 { mint: c.mint }),
        PumpFunMigrate(m) => Some(LogInstrDedupKey::PumpFunMigrate {
            mint: m.mint,
            pool: m.pool,
            user: m.user,
        }),
        BonkTrade(t) => Some(LogInstrDedupKey::BonkTrade {
            pool: t.pool_state,
            user: t.user,
            is_buy: t.is_buy,
        }),
        BonkPoolCreate(p) => Some(LogInstrDedupKey::BonkPoolCreate {
            pool: p.pool_state,
        }),
        BonkMigrateAmm(m) => Some(LogInstrDedupKey::BonkMigrateAmm {
            old_pool: m.old_pool,
            new_pool: m.new_pool,
            user: m.user,
        }),
        PumpSwapTrade(t) => Some(LogInstrDedupKey::PumpSwapTrade {
            mint: t.mint,
            user: t.user,
            is_buy: t.is_buy,
            ix_lane: pumpswap_ix_lane(t.ix_name.as_str()),
        }),
        PumpSwapBuy(b) => Some(LogInstrDedupKey::PumpSwapBuy {
            pool: b.pool,
            user: b.user,
        }),
        PumpSwapSell(s) => Some(LogInstrDedupKey::PumpSwapSell {
            pool: s.pool,
            user: s.user,
        }),
        PumpSwapCreatePool(c) => Some(LogInstrDedupKey::PumpSwapCreatePool {
            pool: c.pool,
            base_mint: c.base_mint,
            quote_mint: c.quote_mint,
        }),
        PumpSwapLiquidityAdded(a) => Some(LogInstrDedupKey::PumpSwapLiquidityAdded {
            pool: a.pool,
            user: a.user,
        }),
        PumpSwapLiquidityRemoved(r) => Some(LogInstrDedupKey::PumpSwapLiquidityRemoved {
            pool: r.pool,
            user: r.user,
        }),
        RaydiumClmmSwap(s) => Some(LogInstrDedupKey::RaydiumClmmSwap {
            pool: s.pool_state,
            zero_for_one: s.zero_for_one,
        }),
        // CPMM swap 事件体无用户/ATA,仅用池+金额去重会误伤多用户同额;不参与 log/ix 折叠。
        RaydiumCpmmSwap(_) => None,
        RaydiumAmmV4Swap(s) => Some(LogInstrDedupKey::RaydiumAmmV4Swap { amm: s.amm }),
        // Orca swap 事件体当前无 token authority / 用户 ATA 字段,无法安全区分用户。
        OrcaWhirlpoolSwap(_) => None,
        MeteoraDammV2Swap(_) => None,
        MeteoraDlmmSwap(s) => Some(LogInstrDedupKey::MeteoraDlmmSwap {
            pool: s.pool,
            from: s.from,
            swap_for_y: s.swap_for_y,
        }),
        // 无稳定链上指纹或其它路径:不去重
        _ => None,
    }
}

#[inline]
fn next_pumpfun_dedup_key(
    ev: &DexEvent,
    lane_count: &mut HashMap<PumpFunLaneBase, u16>,
) -> Option<LogInstrDedupKey> {
    use DexEvent::*;
    match ev {
        PumpFunTrade(t) | PumpFunBuy(t) | PumpFunSell(t) | PumpFunBuyExactSolIn(t) => {
            let lane = pumpfun_ix_lane(t.ix_name.as_str());
            let base = (t.mint, t.user, t.is_buy, lane);
            let entry = lane_count.entry(base).or_insert(0);
            let occ = *entry;
            *entry = occ.saturating_add(1);
            Some(pumpfun_trade_key_with_occ(t, occ))
        }
        _ => log_instr_dedup_key(ev),
    }
}

/// 合并 log + instruction 两路解析结果:**同一指纹只保留一条**;log 与 ix 同时存在时 **log 优先、ix 补充**。
pub(crate) fn dedupe_log_instruction_events(
    log_events: Vec<DexEvent>,
    instr_events: Vec<DexEvent>,
) -> Vec<DexEvent> {
    let cap = log_events.len().saturating_add(instr_events.len());
    let mut out: Vec<DexEvent> = Vec::with_capacity(cap);
    let mut idx_by_key: HashMap<LogInstrDedupKey, usize> = HashMap::new();
    let mut pump_lane_log: HashMap<PumpFunLaneBase, u16> = HashMap::new();

    for e in log_events {
        if let Some(k) = next_pumpfun_dedup_key(&e, &mut pump_lane_log) {
            idx_by_key.insert(k, out.len());
            out.push(e);
        } else {
            out.push(e);
        }
    }

    let mut pump_lane_ix: HashMap<PumpFunLaneBase, u16> = HashMap::new();
    for e in instr_events {
        if let Some(k) = next_pumpfun_dedup_key(&e, &mut pump_lane_ix) {
            if let Some(&idx) = idx_by_key.get(&k) {
                crate::core::merger::merge_grpc_instruction_into_log(&mut out[idx], e);
            } else {
                idx_by_key.insert(k, out.len());
                out.push(e);
            }
        } else {
            out.push(e);
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::events::{EventMetadata, PumpFunTradeEvent};
    use solana_sdk::{pubkey::Pubkey, signature::Signature};

    fn dummy_meta() -> EventMetadata {
        EventMetadata {
            signature: Signature::default(),
            slot: 1,
            tx_index: 0,
            block_time_us: 0,
            grpc_recv_us: 0,
            recent_blockhash: None,
        }
    }

    #[test]
    fn pumpfun_log_ix_duplicate_collapses() {
        let mint = Pubkey::new_unique();
        let user = Pubkey::new_unique();
        let creator = Pubkey::new_unique();

        let mut t1 = PumpFunTradeEvent::default();
        t1.metadata = dummy_meta();
        t1.mint = mint;
        t1.user = user;
        t1.creator = creator;
        t1.sol_amount = 1_000;
        t1.token_amount = 2_000;
        t1.is_buy = true;
        t1.ix_name = "buy".to_string();

        let mut t2 = t1.clone();
        t2.sol_amount = 9_999_999; // 模拟 ix 侧金额与日志不一致(应保留日志)
        t2.bonding_curve = Pubkey::new_unique(); // ix 补充账户
        let bc = t2.bonding_curve;

        let log = vec![DexEvent::PumpFunTrade(t1)];
        let ix = vec![DexEvent::PumpFunBuy(t2)];
        let merged = dedupe_log_instruction_events(log, ix);
        assert_eq!(merged.len(), 1, "log+ix 同一笔买卖应合并为 1 条事件");
        match &merged[0] {
            DexEvent::PumpFunTrade(t) => {
                assert_eq!(t.bonding_curve, bc);
                assert_eq!(t.sol_amount, 1_000, "应保留日志侧金额");
            }
            e => panic!("expected PumpFunTrade (保留 log 变体), got {:?}", e),
        }
    }

    #[test]
    fn pumpfun_same_user_two_buys_log_ix_pairs_merge() {
        let mint = Pubkey::new_unique();
        let user = Pubkey::new_unique();
        let creator = Pubkey::new_unique();
        let bc1 = Pubkey::new_unique();
        let bc2 = Pubkey::new_unique();

        let mut l1 = PumpFunTradeEvent::default();
        l1.metadata = dummy_meta();
        l1.mint = mint;
        l1.user = user;
        l1.creator = creator;
        l1.sol_amount = 100;
        l1.token_amount = 200;
        l1.is_buy = true;
        l1.ix_name = "buy".to_string();

        let mut l2 = l1.clone();
        l2.sol_amount = 300;
        l2.token_amount = 400;

        let mut i1 = l1.clone();
        i1.sol_amount = 9_999;
        i1.bonding_curve = bc1;
        let mut i2 = l2.clone();
        i2.sol_amount = 8_888;
        i2.bonding_curve = bc2;

        let merged = dedupe_log_instruction_events(
            vec![DexEvent::PumpFunTrade(l1), DexEvent::PumpFunTrade(l2)],
            vec![DexEvent::PumpFunBuy(i1), DexEvent::PumpFunBuy(i2)],
        );
        assert_eq!(merged.len(), 2);
        match (&merged[0], &merged[1]) {
            (DexEvent::PumpFunTrade(a), DexEvent::PumpFunTrade(b)) => {
                assert_eq!(a.sol_amount, 100);
                assert_eq!(a.bonding_curve, bc1);
                assert_eq!(b.sol_amount, 300);
                assert_eq!(b.bonding_curve, bc2);
            }
            e => panic!("expected two PumpFunTrade, got {:?}", e),
        }
    }

    #[test]
    fn pumpfun_same_user_two_buys_in_one_tx_both_kept() {
        let mint = Pubkey::new_unique();
        let user = Pubkey::new_unique();

        let mut first = PumpFunTradeEvent::default();
        first.metadata = dummy_meta();
        first.mint = mint;
        first.user = user;
        first.sol_amount = 1_000_000;
        first.token_amount = 100;
        first.is_buy = true;
        first.ix_name = "buy".to_string();

        let mut second = first.clone();
        second.sol_amount = 2_000_000;
        second.token_amount = 150;

        let merged = dedupe_log_instruction_events(
            vec![DexEvent::PumpFunBuy(first), DexEvent::PumpFunBuy(second)],
            vec![],
        );
        assert_eq!(merged.len(), 2, "同钱包同 mint 连买两笔不得被压成一条");
    }

    #[test]
    fn pumpfun_two_distinct_users_same_amounts_not_merged() {
        let mint = Pubkey::new_unique();
        let u1 = Pubkey::new_unique();
        let u2 = Pubkey::new_unique();

        let mut a = PumpFunTradeEvent::default();
        a.metadata = dummy_meta();
        a.mint = mint;
        a.user = u1;
        a.sol_amount = 100;
        a.token_amount = 200;
        a.is_buy = true;

        let mut b = a.clone();
        b.user = u2;

        let merged = dedupe_log_instruction_events(
            vec![DexEvent::PumpFunBuy(a)],
            vec![DexEvent::PumpFunBuy(b)],
        );
        assert_eq!(merged.len(), 2, "不同 user 即使金额相同也不得合并");
    }
}