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,
ix_lane: u8,
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,
},
RaydiumClmmSwap {
pool: Pubkey,
zero_for_one: bool,
},
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,
}
}
#[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,
}),
RaydiumCpmmSwap(_) => None,
RaydiumAmmV4Swap(s) => Some(LogInstrDedupKey::RaydiumAmmV4Swap { amm: s.amm }),
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),
}
}
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; t2.bonding_curve = Pubkey::new_unique(); 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 即使金额相同也不得合并");
}
}