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