Skip to main content

sol_parser_sdk/grpc/
instruction_parser.rs

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