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