Skip to main content

sol_parser_sdk/grpc/
instruction_parser.rs

1//! Instruction 解析器 - 完整支持 instruction + inner instruction
2//!
3//! 设计原则:
4//! - 简洁:单一入口函数,清晰的解析流程
5//! - 高性能:零拷贝,内联优化,并行处理
6//! - 可读性:每个步骤都有明确的注释
7
8use crate::core::{
9    events::*, merger::try_merge_events, pumpfun_fee_enrich::enrich_pumpfun_same_tx_post_merge,
10};
11use crate::grpc::types::EventTypeFilter;
12use crate::instr::read_pubkey_fast;
13use solana_sdk::pubkey::Pubkey;
14use solana_sdk::signature::Signature;
15use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
16
17#[derive(Debug)]
18struct IndexedInstructionEvent {
19    outer_idx: usize,
20    inner_idx: Option<usize>,
21    stack_height: Option<u32>,
22    is_event_cpi: bool,
23    event: DexEvent,
24}
25
26#[inline(always)]
27fn is_dlmm_event(event: &DexEvent) -> bool {
28    matches!(
29        event,
30        DexEvent::MeteoraDlmmSwap(_)
31            | DexEvent::MeteoraDlmmAddLiquidity(_)
32            | DexEvent::MeteoraDlmmRemoveLiquidity(_)
33            | DexEvent::MeteoraDlmmInitializePool(_)
34            | DexEvent::MeteoraDlmmInitializeBinArray(_)
35            | DexEvent::MeteoraDlmmCreatePosition(_)
36            | DexEvent::MeteoraDlmmClosePosition(_)
37            | DexEvent::MeteoraDlmmClaimFee(_)
38    )
39}
40
41/// 解析交易中的所有指令事件(instruction + inner instruction)
42///
43/// # 解析流程
44/// 1. 解析主指令(outer instructions)- 8字节 discriminator
45/// 2. 解析内部指令(inner instructions)- 16字节 discriminator
46/// 3. 合并相关事件(instruction + inner instruction)
47/// 4. 填充账户上下文
48///
49/// # 性能优化
50/// - 零分配泄漏:`program_invokes` 全程 `Pubkey` 键,与账户填充 / `fill_data` 共用同一表
51/// - 零拷贝读取指令账户字节、`read_pubkey_fast` 解码
52/// - 热路径 `#[inline]`
53/// - `should_parse_instructions` 提前跳过整段 ix 解析
54#[inline]
55pub fn parse_instructions_enhanced(
56    meta: &TransactionStatusMeta,
57    transaction: &Option<Transaction>,
58    sig: Signature,
59    slot: u64,
60    tx_idx: u64,
61    block_us: Option<i64>,
62    grpc_us: i64,
63    filter: Option<&EventTypeFilter>,
64) -> Vec<DexEvent> {
65    let needs_pumpfun = filter.map(EventTypeFilter::includes_pumpfun).unwrap_or(true);
66    let is_created_buy =
67        needs_pumpfun && crate::logs::optimized_matcher::detect_pumpfun_create(&meta.log_messages);
68    let mut events = parse_instructions_enhanced_with_created_buy(
69        meta,
70        transaction,
71        sig,
72        slot,
73        tx_idx,
74        block_us,
75        grpc_us,
76        filter,
77        is_created_buy,
78    );
79    for event in &mut events {
80        crate::core::common_filler::fill_token_balances(event, meta, transaction);
81    }
82    crate::grpc::transaction_meta::fill_recent_blockhash(&mut events, transaction);
83    events
84}
85
86#[inline]
87pub(crate) fn parse_instructions_enhanced_with_created_buy(
88    meta: &TransactionStatusMeta,
89    transaction: &Option<Transaction>,
90    sig: Signature,
91    slot: u64,
92    tx_idx: u64,
93    block_us: Option<i64>,
94    grpc_us: i64,
95    filter: Option<&EventTypeFilter>,
96    is_created_buy: bool,
97) -> Vec<DexEvent> {
98    let Some(tx) = transaction else { return Vec::new() };
99    let Some(msg) = &tx.message else { return Vec::new() };
100
101    // 提前检查:是否需要解析 instruction(根据 filter)
102    if !should_parse_instructions(filter) {
103        return Vec::new();
104    }
105
106    // 构建账户查找表
107    let keys_len = msg.account_keys.len();
108    let writable_len = meta.loaded_writable_addresses.len();
109    let get_key = |i: usize| -> Option<&Vec<u8>> {
110        let bytes = if i < keys_len {
111            msg.account_keys.get(i)
112        } else if i < keys_len + writable_len {
113            meta.loaded_writable_addresses.get(i - keys_len)
114        } else {
115            meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
116        }?;
117        // Yellowstone carries complete account addresses. Invalid lengths are
118        // malformed input, not unresolved shred ALT entries or the System program.
119        (bytes.len() == 32).then_some(bytes)
120    };
121
122    // Creation context is available in the message even when logs are absent or
123    // create events are filtered out. Associate it by mint, not transaction-wide.
124    let mut created_mints = Vec::new();
125    if filter.is_none_or(EventTypeFilter::includes_pumpfun) {
126        for ix in &msg.instructions {
127            if get_key(ix.program_id_index as usize).map(|k| read_pubkey_fast(k))
128                != Some(crate::instr::program_ids::PUMPFUN_PROGRAM_ID)
129            {
130                continue;
131            }
132            use crate::instr::pump::discriminators::{CREATE, CREATE_V2};
133            let Some(disc) = ix.data.get(..8) else { continue };
134            if disc != CREATE && disc != CREATE_V2 {
135                continue;
136            }
137            let Some(mint) = ix.accounts.first().and_then(|i| get_key(*i as usize)) else {
138                continue;
139            };
140            let mayhem = disc == CREATE_V2
141                && crate::instr::utils::parse_create_v2_tail_fields(&ix.data[8..])
142                    .is_some_and(|(_, mayhem, _, _, _)| mayhem);
143            created_mints.push((read_pubkey_fast(mint), mayhem));
144        }
145    }
146
147    let mut result = Vec::with_capacity(8);
148    let mut invokes = crate::core::invoke_context::InvokeContext::default();
149    // 步骤 1: 解析所有主指令
150    for (i, ix) in msg.instructions.iter().enumerate() {
151        let pid = get_key(ix.program_id_index as usize)
152            .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
153
154        if crate::grpc::program_ids::needs_invoke_context(&pid) {
155            invokes.push(pid, (i as i32, -1));
156        }
157
158        // 解析主指令(8字节 discriminator)
159        if let Some(mut event) = parse_outer_instruction(
160            &ix.data,
161            &pid,
162            sig,
163            slot,
164            tx_idx,
165            block_us,
166            grpc_us,
167            &ix.accounts,
168            &get_key,
169            filter,
170            is_created_buy,
171        ) {
172            event = clear_pump_instruction_execution_amounts(event);
173            match &mut event {
174                DexEvent::PumpFunBuy(trade)
175                | DexEvent::PumpFunBuyExactSolIn(trade)
176                | DexEvent::PumpFunTrade(trade)
177                    if trade.is_buy =>
178                {
179                    trade.is_created_buy |=
180                        created_mints.iter().any(|(mint, _)| *mint == trade.mint);
181                    trade.mayhem_mode |=
182                        created_mints.iter().any(|(mint, mayhem)| *mint == trade.mint && *mayhem);
183                }
184                _ => {}
185            }
186            result.push(IndexedInstructionEvent {
187                outer_idx: i,
188                inner_idx: None,
189                stack_height: Some(1),
190                is_event_cpi: false,
191                event,
192            });
193        }
194    }
195
196    // 步骤 2: 解析所有 inner instructions
197    for inner in &meta.inner_instructions {
198        let outer_idx = inner.index as usize;
199
200        for (j, inner_ix) in inner.instructions.iter().enumerate() {
201            let pid = get_key(inner_ix.program_id_index as usize)
202                .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
203
204            if crate::grpc::program_ids::needs_invoke_context(&pid) {
205                invokes.push(pid, (outer_idx as i32, j as i32));
206            }
207
208            let event = parse_inner_compiled_instruction_if_supported(
209                &inner_ix.data,
210                &pid,
211                sig,
212                slot,
213                tx_idx,
214                block_us,
215                grpc_us,
216                &inner_ix.accounts,
217                &get_key,
218                filter,
219            )
220            .map(clear_pump_instruction_execution_amounts)
221            .or_else(|| {
222                parse_inner_instruction(
223                    &inner_ix.data,
224                    &pid,
225                    sig,
226                    slot,
227                    tx_idx,
228                    block_us,
229                    grpc_us,
230                    filter,
231                    is_created_buy,
232                )
233            });
234
235            if let Some(event) = event {
236                result.push(IndexedInstructionEvent {
237                    outer_idx,
238                    inner_idx: Some(j),
239                    stack_height: inner_ix.stack_height,
240                    is_event_cpi: (pid == crate::instr::program_ids::METEORA_DLMM_PROGRAM_ID
241                        || pid == crate::instr::program_ids::PUMPFUN_PROGRAM_ID
242                        || pid == crate::instr::program_ids::PUMPSWAP_PROGRAM_ID)
243                        && crate::instr::all_inner::meteora_dlmm::is_event_cpi(&inner_ix.data),
244                    event,
245                });
246            }
247        }
248    }
249
250    // 步骤 3: 合并相关事件(instruction + inner instruction)
251    let mut final_result = merge_instruction_events(result);
252    enrich_pumpfun_same_tx_post_merge(&mut final_result);
253    crate::core::common_filler::fill_pump_creation_context(&mut final_result, meta, transaction);
254
255    // 步骤 4: 填充账户上下文(invokes 与 fill_data 均使用 Pubkey 键,无堆泄漏)
256    for event in &mut final_result {
257        // Every instruction in the transaction shares the caller's receive
258        // timestamp. Parsers using create_metadata_simple sample their own clock.
259        if let Some(metadata) = event.metadata_mut() {
260            metadata.grpc_recv_us = grpc_us;
261        }
262        crate::core::account_dispatcher::fill_accounts_with_invoke_context(
263            event,
264            meta,
265            transaction,
266            &invokes,
267        );
268        crate::core::common_filler::fill_data_with_invoke_context(
269            event,
270            meta,
271            transaction,
272            &invokes,
273        );
274    }
275
276    final_result
277}
278
279// ============================================================================
280// 辅助函数
281// ============================================================================
282
283#[inline(always)]
284fn parse_compiled_instruction<'a>(
285    data: &[u8],
286    program_id: &Pubkey,
287    sig: Signature,
288    slot: u64,
289    tx_idx: u64,
290    block_us: Option<i64>,
291    grpc_us: i64,
292    account_indices: &[u8],
293    get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
294    filter: Option<&EventTypeFilter>,
295) -> Option<DexEvent> {
296    // Anchor programs use an 8-byte discriminator; AMM V4 uses a one-byte tag.
297    if data.is_empty()
298        || (*program_id != crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID && data.len() < 8)
299    {
300        return None;
301    }
302
303    // Reject filtered, unknown or truncated CPMM instructions before decoding
304    // account arrays (including the heap fallback for large remaining accounts).
305    if *program_id == crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID
306        && !crate::instr::raydium_cpmm::instruction_may_parse(data, account_indices.len(), filter)
307    {
308        return None;
309    }
310
311    // 常见 DEX 指令账户数远小于 64;栈上缓冲避免每笔 outer 一次 Vec 分配
312    const STACK_CAP: usize = 64;
313    if account_indices.len() <= STACK_CAP {
314        let mut stack = [Pubkey::default(); STACK_CAP];
315        let mut n = 0usize;
316        for &idx in account_indices {
317            let k = get_key(idx as usize)?;
318            stack[n] = Pubkey::try_from(k.as_slice()).ok()?;
319            n += 1;
320        }
321        crate::instr::parse_instruction_unified(
322            data,
323            &stack[..n],
324            sig,
325            slot,
326            tx_idx,
327            block_us,
328            grpc_us,
329            filter,
330            program_id,
331        )
332    } else {
333        let accounts: Vec<Pubkey> = account_indices
334            .iter()
335            .map(|&idx| get_key(idx as usize).and_then(|key| Pubkey::try_from(key.as_slice()).ok()))
336            .collect::<Option<_>>()?;
337        crate::instr::parse_instruction_unified(
338            data, &accounts, sig, slot, tx_idx, block_us, grpc_us, filter, program_id,
339        )
340    }
341}
342
343#[inline(always)]
344fn is_supported_inner_compiled_instruction(data: &[u8], program_id: &Pubkey) -> bool {
345    crate::instr::normal_instruction_data_may_parse(program_id, data)
346}
347
348#[inline(always)]
349fn parse_inner_compiled_instruction_if_supported<'a>(
350    data: &[u8],
351    program_id: &Pubkey,
352    sig: Signature,
353    slot: u64,
354    tx_idx: u64,
355    block_us: Option<i64>,
356    grpc_us: i64,
357    account_indices: &[u8],
358    get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
359    filter: Option<&EventTypeFilter>,
360) -> Option<DexEvent> {
361    if !is_supported_inner_compiled_instruction(data, program_id) {
362        return None;
363    }
364    parse_compiled_instruction(
365        data,
366        program_id,
367        sig,
368        slot,
369        tx_idx,
370        block_us,
371        grpc_us,
372        account_indices,
373        get_key,
374        filter,
375    )
376}
377
378/// 解析单个主指令(outer instruction)
379///
380/// 主指令使用 8 字节 discriminator
381#[inline(always)]
382fn parse_outer_instruction<'a>(
383    data: &[u8],
384    program_id: &Pubkey,
385    sig: Signature,
386    slot: u64,
387    tx_idx: u64,
388    block_us: Option<i64>,
389    grpc_us: i64,
390    account_indices: &[u8],
391    get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
392    filter: Option<&EventTypeFilter>,
393    _is_created_buy: bool,
394) -> Option<DexEvent> {
395    if !crate::instr::normal_instruction_data_may_parse(program_id, data) {
396        return None;
397    }
398    parse_compiled_instruction(
399        data,
400        program_id,
401        sig,
402        slot,
403        tx_idx,
404        block_us,
405        grpc_us,
406        account_indices,
407        get_key,
408        filter,
409    )
410}
411
412/// 解析单个 inner instruction
413///
414/// Inner instructions 使用 16 字节 discriminator(前8字节是event hash,后8字节是magic)
415#[inline(always)]
416fn parse_inner_instruction(
417    data: &[u8],
418    program_id: &Pubkey,
419    sig: Signature,
420    slot: u64,
421    tx_idx: u64,
422    block_us: Option<i64>,
423    grpc_us: i64,
424    filter: Option<&EventTypeFilter>,
425    is_created_buy: bool,
426) -> Option<DexEvent> {
427    // 检查数据长度(至少16字节 discriminator)
428    if data.len() < 16 {
429        return None;
430    }
431
432    let metadata = EventMetadata {
433        signature: sig,
434        slot,
435        tx_index: tx_idx,
436        block_time_us: block_us.unwrap_or(0),
437        grpc_recv_us: grpc_us,
438        recent_blockhash: None, // set later on merged events in parse_instructions_enhanced
439    };
440
441    // 提取 16 字节 discriminator
442    let mut discriminator = [0u8; 16];
443    discriminator.copy_from_slice(&data[..16]);
444    let inner_data = &data[16..];
445    let upgrade_disc = if discriminator[..8] == [228, 69, 165, 46, 81, 203, 154, 29] {
446        Some(u64::from_le_bytes(discriminator[8..].try_into().ok()?))
447    } else if discriminator[8..] == [155, 167, 108, 32, 122, 76, 173, 64] {
448        Some(u64::from_le_bytes(discriminator[..8].try_into().ok()?))
449    } else {
450        None
451    };
452    if let Some(disc) = upgrade_disc {
453        if crate::logs::pump_upgrade::event_type(disc, Some(program_id)).is_some() {
454            let event =
455                crate::logs::pump_upgrade::parse(disc, inner_data, metadata, Some(program_id));
456            return event.filter(|e| filter.map(|f| f.should_include_dex_event(e)).unwrap_or(true));
457        }
458    }
459
460    use crate::instr::{all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner};
461
462    // 根据 program_id 路由到对应的 inner instruction 解析器
463    let event = if *program_id == program_ids::PUMPFUN_PROGRAM_ID {
464        if let Some(f) = filter {
465            if !f.includes_pumpfun() {
466                return None;
467            }
468        }
469        pump_inner::parse_pumpfun_inner_instruction(
470            &discriminator,
471            inner_data,
472            metadata,
473            is_created_buy,
474        )
475    } else if *program_id == program_ids::PUMPSWAP_PROGRAM_ID {
476        if let Some(f) = filter {
477            if !f.includes_pumpswap() {
478                return None;
479            }
480        }
481        pump_amm_inner::parse_pumpswap_inner_instruction(&discriminator, inner_data, metadata)
482    } else if *program_id == program_ids::PUMP_FEES_PROGRAM_ID {
483        if let Some(f) = filter {
484            if !f.includes_pump_fees() {
485                return None;
486            }
487        }
488        all_inner::pump_fees::parse(&discriminator, inner_data, metadata)
489    } else if *program_id == program_ids::RAYDIUM_CLMM_PROGRAM_ID {
490        if let Some(f) = filter {
491            if !f.includes_raydium_clmm() {
492                return None;
493            }
494        }
495        raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
496            &discriminator,
497            inner_data,
498            metadata,
499        )
500    } else if *program_id == program_ids::RAYDIUM_CPMM_PROGRAM_ID {
501        if let Some(f) = filter {
502            if !f.includes_raydium_cpmm() {
503                return None;
504            }
505        }
506        all_inner::raydium_cpmm::parse(&discriminator, inner_data, metadata)
507    } else if *program_id == program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
508        if let Some(f) = filter {
509            if !f.includes_raydium_amm_v4() {
510                return None;
511            }
512        }
513        all_inner::raydium_amm::parse(&discriminator, inner_data, metadata)
514    } else if *program_id == program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
515        if let Some(f) = filter {
516            if !f.includes_orca_whirlpool() {
517                return None;
518            }
519        }
520        all_inner::orca::parse(&discriminator, inner_data, metadata)
521    } else if *program_id == program_ids::METEORA_POOLS_PROGRAM_ID {
522        if let Some(f) = filter {
523            if !f.includes_meteora_pools() {
524                return None;
525            }
526        }
527        all_inner::meteora_amm::parse(&discriminator, inner_data, metadata)
528    } else if *program_id == program_ids::METEORA_DAMM_V2_PROGRAM_ID {
529        if let Some(f) = filter {
530            if !f.includes_meteora_damm_v2() {
531                return None;
532            }
533        }
534        all_inner::meteora_damm::parse(&discriminator, inner_data, metadata)
535    } else if *program_id == program_ids::METEORA_DBC_PROGRAM_ID {
536        if filter.is_some_and(|f| !f.includes_meteora_dbc()) {
537            return None;
538        }
539        crate::logs::meteora_dbc::parse_event_cpi(data, metadata)
540    } else if *program_id == program_ids::METEORA_DLMM_PROGRAM_ID {
541        if let Some(f) = filter {
542            if !f.includes_meteora_dlmm() {
543                return None;
544            }
545        }
546        all_inner::meteora_dlmm::parse(&discriminator, inner_data, metadata)
547    } else if *program_id == program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID {
548        if let Some(f) = filter {
549            if !f.includes_raydium_launchlab() {
550                return None;
551            }
552        }
553        all_inner::raydium_launchlab::parse(&discriminator, inner_data, metadata)
554    } else {
555        None
556    };
557
558    if filter.map(|f| event.as_ref().is_some_and(|e| f.should_include_dex_event(e))).unwrap_or(true)
559    {
560        event
561    } else {
562        None
563    }
564}
565
566/// 合并相关的 instruction 和 inner instruction 事件
567///
568#[inline(always)]
569fn is_pump_trade(event: &DexEvent) -> bool {
570    matches!(
571        event,
572        DexEvent::PumpFunTrade(_)
573            | DexEvent::PumpFunBuy(_)
574            | DexEvent::PumpFunSell(_)
575            | DexEvent::PumpFunBuyExactSolIn(_)
576            | DexEvent::PumpSwapBuy(_)
577            | DexEvent::PumpSwapSell(_)
578    )
579}
580
581// Compiled buy/sell payloads contain budgets and limits, never executed amounts.
582fn clear_pump_instruction_execution_amounts(mut event: DexEvent) -> DexEvent {
583    match &mut event {
584        DexEvent::PumpFunTrade(e)
585        | DexEvent::PumpFunBuy(e)
586        | DexEvent::PumpFunSell(e)
587        | DexEvent::PumpFunBuyExactSolIn(e) => {
588            e.sol_amount = 0;
589            e.quote_amount = 0;
590            e.token_amount = 0;
591        }
592        _ => {}
593    }
594    event
595}
596
597fn pump_trade_has_instruction_name(event: &DexEvent) -> bool {
598    match event {
599        DexEvent::PumpFunTrade(e)
600        | DexEvent::PumpFunBuy(e)
601        | DexEvent::PumpFunSell(e)
602        | DexEvent::PumpFunBuyExactSolIn(e) => !e.ix_name.is_empty(),
603        DexEvent::PumpSwapBuy(_) | DexEvent::PumpSwapSell(_) => true,
604        _ => false,
605    }
606}
607/// Pair Pump event CPI with its nearest trade invocation, once per call.
608fn merge_instruction_events(events: Vec<IndexedInstructionEvent>) -> Vec<DexEvent> {
609    if events.is_empty() {
610        return Vec::new();
611    }
612
613    // 按 (outer_idx, inner_idx) 排序,确保顺序:同一 outer 下 **主指令在前、inner 在后**
614    // (`None` 若用 MAX 会把 outer 排到 inner 后面,导致无法 merge)
615    let mut events = events;
616    events.sort_unstable_by_key(|event| {
617        (event.outer_idx, event.inner_idx.map_or(0, |inner_idx| inner_idx + 1))
618    });
619
620    let mut result = Vec::with_capacity(events.len());
621    let mut outer_target: Option<(usize, usize)> = None;
622    // Solana's instruction stack is shallow; keep DLMM parent candidates on
623    // the stack so nested DLMM CPIs do not require a hot-path heap allocation.
624    let mut dlmm_targets: [Option<(usize, Option<u32>, usize)>; 8] = [None; 8];
625    let mut dlmm_targets_len = 0usize;
626
627    let mut pump_targets: Vec<(usize, Option<u32>, usize, bool)> = Vec::new();
628    for indexed in events {
629        let IndexedInstructionEvent { outer_idx, inner_idx, stack_height, is_event_cpi, event } =
630            indexed;
631        match inner_idx {
632            None => {
633                let is_dlmm = is_dlmm_event(&event);
634                let target_idx = result.len();
635                result.push(event);
636                outer_target = Some((outer_idx, target_idx));
637                pump_targets.clear();
638                if is_pump_trade(&result[target_idx]) {
639                    pump_targets.push((outer_idx, stack_height, target_idx, false));
640                }
641                dlmm_targets_len = 0;
642                if is_dlmm {
643                    dlmm_targets[0] = Some((outer_idx, stack_height, target_idx));
644                    dlmm_targets_len = 1;
645                }
646            }
647            Some(_) => {
648                if is_pump_trade(&event)
649                    && (is_event_cpi || pump_trade_has_instruction_name(&event))
650                {
651                    if is_event_cpi {
652                        let candidate = pump_targets.iter().rposition(|t| {
653                            t.0 == outer_idx
654                                && match (t.1, stack_height) {
655                                    (Some(parent), Some(child)) => child == parent + 1,
656                                    _ => true,
657                                }
658                        });
659                        if let Some(i) = candidate {
660                            let target = &mut pump_targets[i];
661                            if !target.3 {
662                                target.3 = true;
663                                let mut unmerged = None;
664                                if !try_merge_events(&mut result[target.2], event, &mut unmerged) {
665                                    result.push(unmerged.unwrap());
666                                }
667                                continue;
668                            }
669                        }
670                        result.push(event);
671                    } else {
672                        if let Some(height) = stack_height {
673                            while pump_targets.last().is_some_and(|t| {
674                                t.0 != outer_idx || t.1.is_some_and(|h| h >= height)
675                            }) {
676                                pump_targets.pop();
677                            }
678                        } else {
679                            pump_targets.clear();
680                        }
681                        let target_idx = result.len();
682                        result.push(event);
683                        pump_targets.push((outer_idx, stack_height, target_idx, false));
684                    }
685                    continue;
686                }
687                if is_event_cpi && is_dlmm_event(&event) {
688                    let target = (0..dlmm_targets_len).rev().find_map(|idx| {
689                        let (target_outer, target_height, target_idx) = dlmm_targets[idx]?;
690                        let is_direct_child = match (target_height, stack_height) {
691                            (Some(parent), Some(child)) => child == parent + 1,
692                            _ => true,
693                        };
694                        (target_outer == outer_idx && is_direct_child).then_some((idx, target_idx))
695                    });
696                    if let Some((candidate_idx, target_idx)) = target {
697                        dlmm_targets_len = candidate_idx + 1;
698                        if target_idx < result.len() {
699                            let mut unmerged = None;
700                            if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
701                                continue;
702                            }
703                            result.push(unmerged.expect("unmerged event remains available"));
704                            continue;
705                        }
706                    }
707                    result.push(event);
708                    continue;
709                }
710
711                let is_dlmm = is_dlmm_event(&event);
712                let target_idx = if let Some((target_outer, target_idx)) = outer_target {
713                    if target_outer == outer_idx {
714                        let mut unmerged = None;
715                        if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
716                            target_idx
717                        } else {
718                            let target_idx = result.len();
719                            result.push(unmerged.expect("unmerged event remains available"));
720                            target_idx
721                        }
722                    } else {
723                        let target_idx = result.len();
724                        result.push(event);
725                        target_idx
726                    }
727                } else {
728                    let target_idx = result.len();
729                    result.push(event);
730                    target_idx
731                };
732
733                if is_dlmm {
734                    if let Some(height) = stack_height {
735                        while dlmm_targets_len > 0 {
736                            let Some((candidate_outer, candidate_height, _)) =
737                                dlmm_targets[dlmm_targets_len - 1]
738                            else {
739                                break;
740                            };
741                            if candidate_outer != outer_idx
742                                || candidate_height.is_some_and(|candidate| candidate >= height)
743                            {
744                                dlmm_targets_len -= 1;
745                            } else {
746                                break;
747                            }
748                        }
749                    } else {
750                        dlmm_targets_len = 0;
751                    }
752                    if dlmm_targets_len < dlmm_targets.len() {
753                        dlmm_targets[dlmm_targets_len] =
754                            Some((outer_idx, stack_height, target_idx));
755                        dlmm_targets_len += 1;
756                    }
757                }
758            }
759        }
760    }
761
762    result
763}
764
765/// 检查是否需要解析 instructions(根据 filter)
766#[inline(always)]
767fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
768    // 如果没有 filter,总是解析
769    let Some(filter) = filter else { return true };
770
771    // 如果 filter.include_only 为空,总是解析
772    if filter.include_only.is_none() {
773        return true;
774    }
775
776    // PumpFun:outer BUY/SELL carries instruction args while inner/log TradeEvent
777    // carries executed amounts. Parse both and merge them by order.
778    if filter.includes_pumpfun() {
779        return true;
780    }
781
782    if filter.includes_pump_fees() {
783        return true;
784    }
785
786    filter.includes_pumpswap()
787        || filter.includes_raydium_launchlab()
788        || filter.includes_raydium_cpmm()
789        || filter.includes_raydium_clmm()
790        || filter.includes_raydium_amm_v4()
791        || filter.includes_orca_whirlpool()
792        || filter.includes_meteora_pools()
793        || filter.includes_meteora_damm_v2()
794        || filter.includes_meteora_dlmm()
795        || filter.includes_meteora_dbc()
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
802    use yellowstone_grpc_proto::prelude::{
803        CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
804    };
805
806    fn pk(s: &str) -> Pubkey {
807        s.parse().unwrap()
808    }
809
810    fn usdc_mint() -> Pubkey {
811        pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
812    }
813
814    fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
815        key.to_bytes().to_vec()
816    }
817
818    fn decode_b58(s: &str) -> Vec<u8> {
819        bs58::decode(s).into_vec().unwrap()
820    }
821
822    fn str_arg(s: &str, out: &mut Vec<u8>) {
823        out.extend_from_slice(&(s.len() as u32).to_le_bytes());
824        out.extend_from_slice(s.as_bytes());
825    }
826
827    fn create_v2_data() -> Vec<u8> {
828        let mut data = Vec::new();
829        data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
830        str_arg("Alt Coin", &mut data);
831        str_arg("ALT", &mut data);
832        str_arg("https://example.invalid/alt.json", &mut data);
833        data.extend_from_slice(Pubkey::new_unique().as_ref());
834        data.push(1);
835        data.push(1);
836        data
837    }
838
839    fn grpc_pumpfun_create_v2_tx(
840        static_len: usize,
841        writable_len: usize,
842        program_idx: u8,
843        ix_accounts: Vec<u8>,
844        account_overrides: Vec<(usize, Pubkey)>,
845    ) -> (TransactionStatusMeta, Option<Transaction>) {
846        let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
847        account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
848        let readonly_len = account_overrides
849            .iter()
850            .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
851            .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
852            .max()
853            .unwrap_or_default();
854        let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
855        let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
856        for (global_idx, key) in account_overrides {
857            if global_idx < static_len {
858                account_keys[global_idx] = key;
859            } else if global_idx < static_len + writable_len {
860                loaded_writable[global_idx - static_len] = key;
861            } else {
862                loaded_readonly[global_idx - static_len - writable_len] = key;
863            }
864        }
865
866        let meta = TransactionStatusMeta {
867            loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
868            loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
869            ..Default::default()
870        };
871        let tx = Transaction {
872            signatures: vec![Signature::default().as_ref().to_vec()],
873            message: Some(Message {
874                header: Some(MessageHeader {
875                    num_required_signatures: 1,
876                    num_readonly_signed_accounts: 0,
877                    num_readonly_unsigned_accounts: 0,
878                }),
879                account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
880                recent_blockhash: vec![0; 32],
881                instructions: vec![CompiledInstruction {
882                    program_id_index: program_idx as u32,
883                    accounts: ix_accounts,
884                    data: create_v2_data(),
885                }],
886                versioned: true,
887                address_table_lookups: Vec::new(),
888                config: None,
889            }),
890        };
891        (meta, Some(tx))
892    }
893
894    fn create_v2_accounts(
895        account_len: usize,
896        program_idx: u8,
897        mint_idx: u8,
898        user_idx: u8,
899        token_program_idx: u8,
900        quote_tail: Option<(u8, u8, u8)>,
901    ) -> Vec<u8> {
902        let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
903        accounts[0] = mint_idx;
904        accounts[5] = user_idx;
905        accounts[7] = token_program_idx;
906        if account_len > 15 {
907            accounts[15] = program_idx;
908        }
909        if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
910            accounts[16] = quote_idx;
911            accounts[17] = quote_vault_idx;
912            accounts[18] = quote_token_program_idx;
913        }
914        accounts
915    }
916
917    fn parse_create_v2_from_grpc(
918        meta: &TransactionStatusMeta,
919        tx: &Option<Transaction>,
920    ) -> crate::core::events::PumpFunCreateTokenEvent {
921        let events = parse_instructions_enhanced(
922            meta,
923            tx,
924            Signature::default(),
925            123,
926            0,
927            Some(456),
928            789,
929            None,
930        );
931        assert_eq!(events.len(), 1);
932        match &events[0] {
933            DexEvent::PumpFunCreate(e) => {
934                assert_eq!(e.ix_name, "create_v2");
935                e.clone()
936            }
937            DexEvent::PumpFunCreateV2(e) => {
938                assert_eq!(e.ix_name, "create_v2");
939                crate::core::events::PumpFunCreateTokenEvent {
940                    depth: 0,
941                    metadata: e.metadata.clone(),
942                    name: e.name.clone(),
943                    symbol: e.symbol.clone(),
944                    uri: e.uri.clone(),
945                    mint: e.mint,
946                    bonding_curve: e.bonding_curve,
947                    user: e.user,
948                    creator: e.creator,
949                    timestamp: e.timestamp,
950                    virtual_token_reserves: e.virtual_token_reserves,
951                    virtual_sol_reserves: e.virtual_sol_reserves,
952                    real_token_reserves: e.real_token_reserves,
953                    token_total_supply: e.token_total_supply,
954                    mint_authority: e.mint_authority,
955                    associated_bonding_curve: e.associated_bonding_curve,
956                    global: e.global,
957                    system_program: e.system_program,
958                    token_program: e.token_program,
959                    associated_token_program: e.associated_token_program,
960                    mayhem_program_id: e.mayhem_program_id,
961                    global_params: e.global_params,
962                    sol_vault: e.sol_vault,
963                    mayhem_state: e.mayhem_state,
964                    mayhem_token_vault: e.mayhem_token_vault,
965                    event_authority: e.event_authority,
966                    program: e.program,
967                    quote_mint: e.quote_mint,
968                    quote_vault: e.quote_vault,
969                    quote_token_program: e.quote_token_program,
970                    virtual_quote_reserves: e.virtual_quote_reserves,
971                    creator_fee_bps: e.creator_fee_bps,
972                    is_holder_reward: e.is_holder_reward,
973                    ix_name: e.ix_name.clone(),
974                    is_mayhem_mode: e.is_mayhem_mode,
975                    is_cashback_enabled: e.is_cashback_enabled,
976                    observed_fee_recipient: e.observed_fee_recipient,
977                }
978            }
979            other => panic!("expected PumpFun create_v2 event, got {other:?}"),
980        }
981    }
982
983    #[test]
984    fn test_should_parse_instructions() {
985        // 无 filter - 应该解析
986        assert!(should_parse_instructions(None));
987
988        // 有 filter 但 include_only 为空 - 应该解析
989        let filter = EventTypeFilter { include_only: None, exclude_types: None };
990        assert!(should_parse_instructions(Some(&filter)));
991
992        // 包含需要 instruction 解析的事件类型
993        use crate::grpc::types::EventType;
994        let filter = EventTypeFilter {
995            include_only: Some(vec![EventType::PumpFunMigrate]),
996            exclude_types: None,
997        };
998        assert!(should_parse_instructions(Some(&filter)));
999
1000        // PumpFun 订阅:需要 instruction+inner,避免仅日志时截断丢腿
1001        let filter = EventTypeFilter {
1002            include_only: Some(vec![EventType::PumpFunTrade]),
1003            exclude_types: None,
1004        };
1005        assert!(should_parse_instructions(Some(&filter)));
1006
1007        for event_type in [
1008            EventType::PumpSwapTrade,
1009            EventType::PumpFeesUpdateFeeShares,
1010            EventType::RaydiumLaunchlabTrade,
1011            EventType::RaydiumCpmmSwap,
1012            EventType::RaydiumClmmSwap,
1013            EventType::RaydiumAmmV4Swap,
1014            EventType::OrcaWhirlpoolSwap,
1015            EventType::MeteoraPoolsSwap,
1016            EventType::MeteoraDammV2Swap,
1017            EventType::MeteoraDammV2InitializePool,
1018            EventType::MeteoraDlmmSwap,
1019        ] {
1020            let filter = EventTypeFilter::include_only(vec![event_type]);
1021            assert!(
1022                should_parse_instructions(Some(&filter)),
1023                "instruction parsing should be enabled for {event_type:?}"
1024            );
1025        }
1026
1027        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
1028        assert!(
1029            should_parse_instructions(Some(&filter)),
1030            "DBC event CPI must parse even with a DBC-only filter"
1031        );
1032
1033        let filter = EventTypeFilter::include_only(vec![
1034            EventType::AccountPumpFunGlobal,
1035            EventType::AccountRaydiumClmmPoolState,
1036            EventType::AccountRaydiumCpmmPoolState,
1037            EventType::AccountOrcaWhirlpool,
1038        ]);
1039        assert!(
1040            !should_parse_instructions(Some(&filter)),
1041            "account-only non-Pump filters should stay on the account update path"
1042        );
1043    }
1044
1045    #[test]
1046    fn test_merge_instruction_events() {
1047        use solana_sdk::signature::Signature;
1048
1049        let metadata = EventMetadata {
1050            signature: Signature::default(),
1051            slot: 100,
1052            tx_index: 1,
1053            block_time_us: 1000,
1054            grpc_recv_us: 2000,
1055            recent_blockhash: None,
1056        };
1057
1058        // 模拟:outer instruction + inner instruction(应该合并)
1059        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1060            metadata: metadata.clone(),
1061            bonding_curve: Pubkey::new_unique(),
1062            ..Default::default()
1063        });
1064
1065        let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1066            metadata: metadata.clone(),
1067            sol_amount: 1000,
1068            token_amount: 2000,
1069            ..Default::default()
1070        });
1071
1072        let events = vec![
1073            IndexedInstructionEvent {
1074                outer_idx: 0,
1075                inner_idx: None,
1076                stack_height: Some(1),
1077                is_event_cpi: false,
1078                event: outer_event,
1079            },
1080            IndexedInstructionEvent {
1081                outer_idx: 0,
1082                inner_idx: Some(0),
1083                stack_height: Some(2),
1084                is_event_cpi: false,
1085                event: inner_event,
1086            },
1087        ];
1088
1089        let result = merge_instruction_events(events);
1090
1091        // 应该合并为1个事件
1092        assert_eq!(result.len(), 1);
1093
1094        // 验证合并结果包含两者的数据
1095        if let DexEvent::PumpFunTrade(trade) = &result[0] {
1096            assert_eq!(trade.sol_amount, 1000); // 来自 inner
1097            assert_eq!(trade.token_amount, 2000); // 来自 inner
1098            assert_ne!(trade.bonding_curve, Pubkey::default()); // 来自 outer
1099        } else {
1100            panic!("Expected PumpFunTrade event");
1101        }
1102    }
1103
1104    #[test]
1105    fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
1106        use solana_sdk::signature::Signature;
1107
1108        let metadata = EventMetadata {
1109            signature: Signature::default(),
1110            slot: 100,
1111            tx_index: 1,
1112            block_time_us: 1000,
1113            grpc_recv_us: 2000,
1114            recent_blockhash: None,
1115        };
1116
1117        let bc = Pubkey::new_unique();
1118        let fee = Pubkey::new_unique();
1119
1120        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1121            metadata: metadata.clone(),
1122            bonding_curve: bc,
1123            ..Default::default()
1124        });
1125
1126        let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1127            metadata: metadata.clone(),
1128            sol_amount: 1000,
1129            token_amount: 2000,
1130            is_buy: true,
1131            ..Default::default()
1132        });
1133
1134        // 第二段 inner 仅有 fee_recipient,无成交量 —— 不应抹掉第一段金额
1135        let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1136            metadata: metadata.clone(),
1137            fee_recipient: fee,
1138            ..Default::default()
1139        });
1140
1141        let events = vec![
1142            IndexedInstructionEvent {
1143                outer_idx: 0,
1144                inner_idx: None,
1145                stack_height: Some(1),
1146                is_event_cpi: false,
1147                event: outer_event,
1148            },
1149            IndexedInstructionEvent {
1150                outer_idx: 0,
1151                inner_idx: Some(0),
1152                stack_height: Some(2),
1153                is_event_cpi: false,
1154                event: inner_trade,
1155            },
1156            IndexedInstructionEvent {
1157                outer_idx: 0,
1158                inner_idx: Some(1),
1159                stack_height: Some(2),
1160                is_event_cpi: false,
1161                event: inner_fee_only,
1162            },
1163        ];
1164
1165        let result = merge_instruction_events(events);
1166        assert_eq!(result.len(), 1);
1167        if let DexEvent::PumpFunTrade(trade) = &result[0] {
1168            assert_eq!(trade.bonding_curve, bc);
1169            assert_eq!(trade.sol_amount, 1000);
1170            assert_eq!(trade.token_amount, 2000);
1171            assert_eq!(trade.fee_recipient, fee);
1172        } else {
1173            panic!("Expected PumpFunTrade event");
1174        }
1175    }
1176
1177    fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1178        DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1179            metadata: EventMetadata::default(),
1180            token_x_mint: Pubkey::default(),
1181            token_y_mint: Pubkey::default(),
1182            user_token_in: Pubkey::default(),
1183            user_token_out: Pubkey::default(),
1184            min_amount_out: 0,
1185            pool,
1186            from: Pubkey::default(),
1187            start_bin_id: 0,
1188            end_bin_id: 0,
1189            amount_in,
1190            amount_out,
1191            swap_for_y: false,
1192            fee: 0,
1193            protocol_fee: 0,
1194            fee_bps: 0,
1195            host_fee: 0,
1196            ..Default::default()
1197        })
1198    }
1199
1200    fn dlmm_add_liquidity() -> DexEvent {
1201        DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1202            metadata: EventMetadata::default(),
1203            pool: Pubkey::default(),
1204            from: Pubkey::default(),
1205            position: Pubkey::default(),
1206            amounts: [0; 2],
1207            active_bin_id: 0,
1208        })
1209    }
1210
1211    #[test]
1212    fn merge_preserves_unrelated_inner_events() {
1213        let events = vec![
1214            IndexedInstructionEvent {
1215                outer_idx: 0,
1216                inner_idx: None,
1217                stack_height: Some(1),
1218                is_event_cpi: false,
1219                event: dlmm_swap(Pubkey::default(), 0, 0),
1220            },
1221            IndexedInstructionEvent {
1222                outer_idx: 0,
1223                inner_idx: Some(0),
1224                stack_height: Some(2),
1225                is_event_cpi: false,
1226                event: dlmm_add_liquidity(),
1227            },
1228        ];
1229
1230        let result = merge_instruction_events(events);
1231        assert_eq!(result.len(), 2);
1232        assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1233        assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1234    }
1235
1236    #[test]
1237    fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1238        let first_pool = Pubkey::new_unique();
1239        let second_pool = Pubkey::new_unique();
1240        let events = vec![
1241            IndexedInstructionEvent {
1242                outer_idx: 0,
1243                inner_idx: Some(0),
1244                stack_height: Some(2),
1245                is_event_cpi: false,
1246                event: dlmm_swap(first_pool, 1, 0),
1247            },
1248            IndexedInstructionEvent {
1249                outer_idx: 0,
1250                inner_idx: Some(1),
1251                stack_height: Some(3),
1252                is_event_cpi: true,
1253                event: dlmm_swap(first_pool, 10, 9),
1254            },
1255            IndexedInstructionEvent {
1256                outer_idx: 0,
1257                inner_idx: Some(2),
1258                stack_height: Some(2),
1259                is_event_cpi: false,
1260                event: dlmm_swap(second_pool, 2, 0),
1261            },
1262            IndexedInstructionEvent {
1263                outer_idx: 0,
1264                inner_idx: Some(3),
1265                stack_height: Some(3),
1266                is_event_cpi: true,
1267                event: dlmm_swap(second_pool, 20, 18),
1268            },
1269        ];
1270
1271        let result = merge_instruction_events(events);
1272        assert_eq!(result.len(), 2);
1273        let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1274        let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1275        assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1276        assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1277    }
1278
1279    #[test]
1280    fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1281        let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1282        let static_keys = [
1283            "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1284            "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1285            "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1286            "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1287            "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1288            "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1289            "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1290            "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1291            "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1292            "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1293            "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1294            "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1295            "ComputeBudget111111111111111111111111111111",
1296            "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1297            "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1298            "SysvarRent111111111111111111111111111111111",
1299        ];
1300        let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1301        let loaded_readonly = [
1302            "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1303            "So11111111111111111111111111111111111111112",
1304            "11111111111111111111111111111111",
1305            "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1306            "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1307            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1308            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1309            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1310            "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1311            "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1312        ];
1313        let meta = TransactionStatusMeta {
1314            loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1315            loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1316            inner_instructions: vec![InnerInstructions {
1317                index: 2,
1318                instructions: vec![InnerInstruction {
1319                    program_id_index: 20,
1320                    accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1321                    data: decode_b58(
1322                        "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1323                    ),
1324                    stack_height: Some(2),
1325                }],
1326            }],
1327            ..Default::default()
1328        };
1329        let tx = Some(Transaction {
1330            signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1331            message: Some(Message {
1332                header: Some(MessageHeader::default()),
1333                account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1334                recent_blockhash: vec![0; 32],
1335                instructions: vec![
1336                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1337                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1338                    CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1339                ],
1340                versioned: true,
1341                address_table_lookups: Vec::new(),
1342                config: None,
1343            }),
1344        });
1345
1346        let events = parse_instructions_enhanced(
1347            &meta,
1348            &tx,
1349            signature.parse().unwrap(),
1350            427_039_576,
1351            0,
1352            Some(1_781_687_252_000_000),
1353            789,
1354            None,
1355        );
1356
1357        assert_eq!(events.len(), 1, "{signature}");
1358        match &events[0] {
1359            DexEvent::PumpSwapCreatePool(e) => {
1360                assert_eq!(e.index, 0, "{signature}");
1361                assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1362                assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1363                assert_eq!(
1364                    e.coin_creator,
1365                    pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1366                    "{signature}"
1367                );
1368                assert!(!e.is_mayhem_mode, "{signature}");
1369                assert!(e.is_cashback_coin, "{signature}");
1370                assert_eq!(
1371                    e.pool,
1372                    pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1373                    "{signature}"
1374                );
1375                assert_eq!(
1376                    e.creator,
1377                    pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1378                    "{signature}"
1379                );
1380                assert_eq!(
1381                    e.base_mint,
1382                    pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1383                    "{signature}"
1384                );
1385                assert_eq!(
1386                    e.quote_mint,
1387                    pk("So11111111111111111111111111111111111111112"),
1388                    "{signature}"
1389                );
1390                assert_eq!(
1391                    e.lp_mint,
1392                    pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1393                    "{signature}"
1394                );
1395                assert_eq!(
1396                    e.user_base_token_account,
1397                    pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1398                    "{signature}"
1399                );
1400                assert_eq!(
1401                    e.user_quote_token_account,
1402                    pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1403                    "{signature}"
1404                );
1405            }
1406            other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1407        }
1408    }
1409
1410    #[test]
1411    fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1412        struct Case {
1413            signature: &'static str,
1414            name: &'static str,
1415            static_len: usize,
1416            writable_len: usize,
1417            program_idx: u8,
1418            account_len: usize,
1419            mint_idx: u8,
1420            mint: &'static str,
1421            user_idx: u8,
1422            user: &'static str,
1423            token_program_idx: u8,
1424            quote_idx: u8,
1425            quote_mint: Pubkey,
1426            quote_vault_idx: u8,
1427            quote_vault: &'static str,
1428            quote_token_program_idx: u8,
1429        }
1430        let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1431        let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1432        let cases = [
1433            Case {
1434                signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1435                name: "19-account WSOL quote in ALT",
1436                static_len: 15,
1437                writable_len: 7,
1438                program_idx: 12,
1439                account_len: 19,
1440                mint_idx: 1,
1441                mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1442                user_idx: 0,
1443                user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1444                token_program_idx: 24,
1445                quote_idx: 27,
1446                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1447                quote_vault_idx: 7,
1448                quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1449                quote_token_program_idx: 28,
1450            },
1451            Case {
1452                signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1453                name: "19-account WSOL quote in ALT with exact quote buy",
1454                static_len: 20,
1455                writable_len: 7,
1456                program_idx: 15,
1457                account_len: 19,
1458                mint_idx: 1,
1459                mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1460                user_idx: 0,
1461                user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1462                token_program_idx: 31,
1463                quote_idx: 28,
1464                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1465                quote_vault_idx: 4,
1466                quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1467                quote_token_program_idx: 30,
1468            },
1469            Case {
1470                signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1471                name: "19-account USDC quote in ALT",
1472                static_len: 19,
1473                writable_len: 6,
1474                program_idx: 16,
1475                account_len: 19,
1476                mint_idx: 1,
1477                mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1478                user_idx: 0,
1479                user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1480                token_program_idx: 27,
1481                quote_idx: 30,
1482                quote_mint: usdc_mint(),
1483                quote_vault_idx: 6,
1484                quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1485                quote_token_program_idx: 31,
1486            },
1487            Case {
1488                signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1489                name: "20-account WSOL quote in ALT",
1490                static_len: 15,
1491                writable_len: 7,
1492                program_idx: 12,
1493                account_len: 20,
1494                mint_idx: 1,
1495                mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1496                user_idx: 0,
1497                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1498                token_program_idx: 24,
1499                quote_idx: 27,
1500                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1501                quote_vault_idx: 7,
1502                quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1503                quote_token_program_idx: 28,
1504            },
1505            Case {
1506                signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1507                name: "19-account WSOL quote in ALT with later buy",
1508                static_len: 18,
1509                writable_len: 7,
1510                program_idx: 13,
1511                account_len: 19,
1512                mint_idx: 1,
1513                mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1514                user_idx: 0,
1515                user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1516                token_program_idx: 27,
1517                quote_idx: 30,
1518                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1519                quote_vault_idx: 7,
1520                quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1521                quote_token_program_idx: 31,
1522            },
1523            Case {
1524                signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1525                name: "19-account USDC quote in ALT exact quote buy",
1526                static_len: 19,
1527                writable_len: 6,
1528                program_idx: 15,
1529                account_len: 19,
1530                mint_idx: 1,
1531                mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1532                user_idx: 0,
1533                user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1534                token_program_idx: 28,
1535                quote_idx: 33,
1536                quote_mint: usdc_mint(),
1537                quote_vault_idx: 7,
1538                quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1539                quote_token_program_idx: 27,
1540            },
1541            Case {
1542                signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1543                name: "20-account WSOL quote in ALT with jit account",
1544                static_len: 15,
1545                writable_len: 7,
1546                program_idx: 12,
1547                account_len: 20,
1548                mint_idx: 1,
1549                mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1550                user_idx: 0,
1551                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1552                token_program_idx: 24,
1553                quote_idx: 27,
1554                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1555                quote_vault_idx: 7,
1556                quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1557                quote_token_program_idx: 28,
1558            },
1559        ];
1560
1561        for case in cases {
1562            let (meta, tx) = grpc_pumpfun_create_v2_tx(
1563                case.static_len,
1564                case.writable_len,
1565                case.program_idx,
1566                create_v2_accounts(
1567                    case.account_len,
1568                    case.program_idx,
1569                    case.mint_idx,
1570                    case.user_idx,
1571                    case.token_program_idx,
1572                    Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1573                ),
1574                vec![
1575                    (case.mint_idx as usize, pk(case.mint)),
1576                    (case.user_idx as usize, pk(case.user)),
1577                    (case.token_program_idx as usize, token_2022_program),
1578                    (case.quote_idx as usize, case.quote_mint),
1579                    (case.quote_vault_idx as usize, pk(case.quote_vault)),
1580                    (case.quote_token_program_idx as usize, spl_token_program),
1581                ],
1582            );
1583            let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1584                let idx = global_idx as usize;
1585                if idx < case.static_len {
1586                    ("static", idx)
1587                } else if idx < case.static_len + case.writable_len {
1588                    ("writable", idx - case.static_len)
1589                } else {
1590                    ("readonly", idx - case.static_len - case.writable_len)
1591                }
1592            };
1593            assert_eq!(
1594                meta.loaded_writable_addresses.len(),
1595                case.writable_len,
1596                "{}: {}",
1597                case.name,
1598                case.signature
1599            );
1600            for (global_idx, expected_key) in [
1601                (case.token_program_idx, token_2022_program),
1602                (case.quote_idx, case.quote_mint),
1603                (case.quote_token_program_idx, spl_token_program),
1604            ] {
1605                match loaded_key_location(global_idx) {
1606                    ("static", _) => {}
1607                    ("writable", offset) => assert_eq!(
1608                        read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1609                        expected_key,
1610                        "{}: writable loaded key {global_idx}: {}",
1611                        case.name,
1612                        case.signature
1613                    ),
1614                    ("readonly", offset) => assert_eq!(
1615                        read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1616                        expected_key,
1617                        "{}: readonly loaded key {global_idx}: {}",
1618                        case.name,
1619                        case.signature
1620                    ),
1621                    _ => unreachable!(),
1622                }
1623            }
1624
1625            let create = parse_create_v2_from_grpc(&meta, &tx);
1626
1627            assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1628            assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1629            assert_eq!(
1630                create.token_program, token_2022_program,
1631                "{}: {}",
1632                case.name, case.signature
1633            );
1634            assert_eq!(
1635                create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1636                "{}: {}",
1637                case.name, case.signature
1638            );
1639            assert_eq!(create.quote_vault, Pubkey::default(), "{}: {}", case.name, case.signature);
1640            assert_eq!(
1641                create.quote_token_program,
1642                Pubkey::default(),
1643                "{}: {}",
1644                case.name,
1645                case.signature
1646            );
1647        }
1648    }
1649
1650    #[test]
1651    fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1652        let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1653        let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1654        let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1655        let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1656        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1657            16,
1658            5,
1659            12,
1660            create_v2_accounts(16, 12, 1, 0, 24, None),
1661            vec![(1, mint), (0, user), (24, token_program)],
1662        );
1663
1664        let create = parse_create_v2_from_grpc(&meta, &tx);
1665
1666        assert_eq!(create.mint, mint, "{signature}");
1667        assert_eq!(create.user, user, "{signature}");
1668        assert_eq!(create.token_program, token_program, "{signature}");
1669        assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1670        assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1671        assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1672    }
1673
1674    #[test]
1675    fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1676        let quote_vault = Pubkey::new_unique();
1677        let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1678        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1679            19,
1680            6,
1681            16,
1682            create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1683            vec![
1684                (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1685                (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1686                (6, quote_vault),
1687                (31, quote_token_program),
1688            ],
1689        );
1690
1691        let create = parse_create_v2_from_grpc(&meta, &tx);
1692
1693        assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
1694        assert_eq!(create.quote_vault, Pubkey::default());
1695        assert_eq!(create.quote_token_program, Pubkey::default());
1696    }
1697    #[test]
1698    fn grpc_amm_v4_accepts_one_byte_withdraw_pnl_instruction() {
1699        let mut keys: Vec<_> = (0..14).map(|_| Pubkey::new_unique()).collect();
1700        keys.push(crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID);
1701        let tx = Some(Transaction {
1702            message: Some(Message {
1703                account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
1704                instructions: vec![CompiledInstruction {
1705                    program_id_index: 14,
1706                    accounts: (0..14).collect(),
1707                    data: vec![crate::instr::raydium_amm::discriminators::WITHDRAW_PNL],
1708                }],
1709                ..Default::default()
1710            }),
1711            ..Default::default()
1712        });
1713        let events = parse_instructions_enhanced(
1714            &TransactionStatusMeta::default(),
1715            &tx,
1716            Signature::default(),
1717            42,
1718            0,
1719            None,
1720            0,
1721            None,
1722        );
1723        assert_eq!(events.len(), 1);
1724        let DexEvent::RaydiumAmmV4WithdrawPnl(e) = &events[0] else { panic!("withdraw pnl") };
1725        assert_eq!(e.amm, keys[1]);
1726    }
1727}
1728
1729#[cfg(test)]
1730mod cpmm_account_decode_boundaries {
1731    use super::*;
1732
1733    #[test]
1734    fn invalid_or_same_family_filtered_cpmm_does_not_resolve_accounts() {
1735        use crate::instr::raydium_cpmm::discriminators::*;
1736        let swaps_only =
1737            EventTypeFilter::include_only(vec![crate::grpc::EventType::RaydiumCpmmSwap]);
1738        let get_key = |_index: usize| -> Option<&Vec<u8>> { panic!("rejected account resolved") };
1739        for (data, count, filter) in [
1740            (COLLECT_CREATOR_FEE.to_vec(), 15, Some(&swaps_only)),
1741            (COLLECT_CREATOR_FEE_PERMISSIONLESS.to_vec(), 65, Some(&swaps_only)),
1742            (SWAP_BASE_IN.to_vec(), 13, None),
1743            (SWAP_BASE_OUT.to_vec(), 65, None),
1744            (DEPOSIT.to_vec(), 13, None),
1745            (COLLECT_CREATOR_FEE.to_vec(), 14, None),
1746            (COLLECT_CREATOR_FEE_PERMISSIONLESS.to_vec(), 15, None),
1747            (vec![255; 8], 65, None),
1748        ] {
1749            assert!(parse_compiled_instruction(
1750                &data,
1751                &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1752                Signature::default(),
1753                1,
1754                0,
1755                None,
1756                0,
1757                &vec![0; count],
1758                &get_key,
1759                filter,
1760            )
1761            .is_none());
1762        }
1763    }
1764
1765    #[test]
1766    fn excluded_cpmm_never_decodes_the_instruction_account_array() {
1767        let filter = EventTypeFilter::include_only(vec![crate::grpc::EventType::RaydiumClmmSwap]);
1768        let key = vec![1; 32];
1769        let get_key = |_index: usize| -> Option<&Vec<u8>> { panic!("excluded account decoded") };
1770        for count in [15, 65] {
1771            assert!(parse_compiled_instruction(
1772                &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1773                &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1774                Signature::default(),
1775                1,
1776                0,
1777                None,
1778                0,
1779                &vec![0; count],
1780                &get_key,
1781                Some(&filter),
1782            )
1783            .is_none());
1784            // No filter still decodes the complete account array and parses it.
1785            let valid = |_index: usize| Some(&key);
1786            assert!(parse_compiled_instruction(
1787                &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1788                &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1789                Signature::default(),
1790                1,
1791                0,
1792                None,
1793                0,
1794                &vec![0; count],
1795                &valid,
1796                None,
1797            )
1798            .is_some());
1799        }
1800    }
1801
1802    #[test]
1803    fn stack_and_heap_account_decoders_reject_noncanonical_pubkey_lengths() {
1804        for count in [15, 65] {
1805            for length in [0, 31, 33, 64] {
1806                let key = vec![1; length];
1807                let get_key = |_index: usize| Some(&key);
1808                assert!(parse_compiled_instruction(
1809                    &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1810                    &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1811                    Signature::default(),
1812                    1,
1813                    0,
1814                    None,
1815                    0,
1816                    &vec![0; count],
1817                    &get_key,
1818                    None,
1819                )
1820                .is_none());
1821            }
1822        }
1823    }
1824}
1825
1826#[cfg(test)]
1827mod pump_occurrence_tests {
1828    use super::*;
1829    #[test]
1830    fn repeated_trades_pair_with_own_cpi() {
1831        let mint = Pubkey::new_unique();
1832        let user = Pubkey::new_unique();
1833        for known in [true, false] {
1834            let event = |amount, executed| {
1835                DexEvent::PumpFunTrade(PumpFunTradeEvent {
1836                    mint,
1837                    user,
1838                    is_buy: true,
1839                    ix_name: "buy_v3".into(),
1840                    amount: if executed { 0 } else { amount },
1841                    token_amount: if executed { amount } else { 0 },
1842                    ..Default::default()
1843                })
1844            };
1845            let item = |inner_idx, height, is_cpi, event| IndexedInstructionEvent {
1846                outer_idx: 0,
1847                inner_idx,
1848                stack_height: if known { Some(height) } else { None },
1849                is_event_cpi: is_cpi,
1850                event,
1851            };
1852            let result = merge_instruction_events(vec![
1853                item(None, 1, false, event(100, false)),
1854                item(Some(0), 2, true, event(90, true)),
1855                item(Some(1), 2, false, event(200, false)),
1856                item(Some(2), 3, true, event(180, true)),
1857            ]);
1858            assert_eq!(result.len(), 2);
1859            for (e, (amount, tokens)) in result.iter().zip([(100, 90), (200, 180)]) {
1860                let DexEvent::PumpFunTrade(e) = e else { panic!() };
1861                assert_eq!((e.amount, e.token_amount), (amount, tokens));
1862            }
1863        }
1864    }
1865}
1866
1867#[cfg(test)]
1868mod swap_occurrence_tests {
1869    use super::*;
1870    #[test]
1871    fn repeated_swap_calls_pair_with_their_own_cpi() {
1872        let pool = Pubkey::new_unique();
1873        let user = Pubkey::new_unique();
1874        let accounts = [Pubkey::new_unique(), Pubkey::new_unique()];
1875        for buy in [true, false] {
1876            for known in [true, false] {
1877                let event = |n: usize, execution| {
1878                    let account = if execution { Pubkey::default() } else { accounts[n] };
1879                    let amount = if execution { 90 * (n as u64 + 1) } else { 0 };
1880                    if buy {
1881                        DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
1882                            pool,
1883                            user,
1884                            user_base_token_account: account,
1885                            quote_amount_in: amount,
1886                            ..Default::default()
1887                        })
1888                    } else {
1889                        DexEvent::PumpSwapSell(PumpSwapSellEvent {
1890                            pool,
1891                            user,
1892                            user_base_token_account: account,
1893                            quote_amount_out: amount,
1894                            ..Default::default()
1895                        })
1896                    }
1897                };
1898                let item = |inner_idx, height, is_cpi, event| IndexedInstructionEvent {
1899                    outer_idx: 0,
1900                    inner_idx,
1901                    stack_height: if known { Some(height) } else { None },
1902                    is_event_cpi: is_cpi,
1903                    event,
1904                };
1905                let out = merge_instruction_events(vec![
1906                    item(None, 1, false, event(0, false)),
1907                    item(Some(0), 2, true, event(0, true)),
1908                    item(Some(1), 2, false, event(1, false)),
1909                    item(Some(2), 3, true, event(1, true)),
1910                ]);
1911                assert_eq!(out.len(), 2);
1912                for (i, e) in out.iter().enumerate() {
1913                    match e {
1914                        DexEvent::PumpSwapBuy(e) => assert_eq!(
1915                            (e.user_base_token_account, e.quote_amount_in),
1916                            (accounts[i], 90 * (i as u64 + 1))
1917                        ),
1918                        DexEvent::PumpSwapSell(e) => assert_eq!(
1919                            (e.user_base_token_account, e.quote_amount_out),
1920                            (accounts[i], 90 * (i as u64 + 1))
1921                        ),
1922                        _ => panic!(),
1923                    }
1924                }
1925            }
1926        }
1927    }
1928}