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