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