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 std::collections::HashMap;
16use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
17
18#[derive(Debug)]
19struct IndexedInstructionEvent {
20    outer_idx: usize,
21    inner_idx: Option<usize>,
22    stack_height: Option<u32>,
23    is_dlmm_event_cpi: bool,
24    event: DexEvent,
25}
26
27#[inline(always)]
28fn is_dlmm_event(event: &DexEvent) -> bool {
29    matches!(
30        event,
31        DexEvent::MeteoraDlmmSwap(_)
32            | DexEvent::MeteoraDlmmAddLiquidity(_)
33            | DexEvent::MeteoraDlmmRemoveLiquidity(_)
34            | DexEvent::MeteoraDlmmInitializePool(_)
35            | DexEvent::MeteoraDlmmInitializeBinArray(_)
36            | DexEvent::MeteoraDlmmCreatePosition(_)
37            | DexEvent::MeteoraDlmmClosePosition(_)
38            | DexEvent::MeteoraDlmmClaimFee(_)
39    )
40}
41
42/// 解析交易中的所有指令事件(instruction + inner instruction)
43///
44/// # 解析流程
45/// 1. 解析主指令(outer instructions)- 8字节 discriminator
46/// 2. 解析内部指令(inner instructions)- 16字节 discriminator
47/// 3. 合并相关事件(instruction + inner instruction)
48/// 4. 填充账户上下文
49///
50/// # 性能优化
51/// - 零分配泄漏:`program_invokes` 全程 `Pubkey` 键,与账户填充 / `fill_data` 共用同一表
52/// - 零拷贝读取指令账户字节、`read_pubkey_fast` 解码
53/// - 热路径 `#[inline]`
54/// - `should_parse_instructions` 提前跳过整段 ix 解析
55#[inline]
56pub fn parse_instructions_enhanced(
57    meta: &TransactionStatusMeta,
58    transaction: &Option<Transaction>,
59    sig: Signature,
60    slot: u64,
61    tx_idx: u64,
62    block_us: Option<i64>,
63    grpc_us: i64,
64    filter: Option<&EventTypeFilter>,
65) -> Vec<DexEvent> {
66    let Some(tx) = transaction else { return Vec::new() };
67    let Some(msg) = &tx.message else { return Vec::new() };
68
69    let recent_blockhash = if msg.recent_blockhash.is_empty() {
70        None
71    } else {
72        Some(bs58::encode(&msg.recent_blockhash).into_string())
73    };
74
75    // 提前检查:是否需要解析 instruction(根据 filter)
76    if !should_parse_instructions(filter) {
77        return Vec::new();
78    }
79
80    // 与 log 解析一致:同笔交易内若有 PumpFun create,则本 tx 的 buy 事件标记为 is_created_buy(创建者首次买入)
81    let is_created_buy = crate::logs::optimized_matcher::detect_pumpfun_create(&meta.log_messages);
82
83    // 构建账户查找表
84    let keys_len = msg.account_keys.len();
85    let writable_len = meta.loaded_writable_addresses.len();
86    let get_key = |i: usize| -> Option<&Vec<u8>> {
87        if i < keys_len {
88            msg.account_keys.get(i)
89        } else if i < keys_len + writable_len {
90            meta.loaded_writable_addresses.get(i - keys_len)
91        } else {
92            meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
93        }
94    };
95
96    let mut result = Vec::with_capacity(8);
97    let mut invokes: HashMap<Pubkey, Vec<(i32, i32)>> = HashMap::with_capacity(8);
98
99    // 步骤 1: 解析所有主指令
100    for (i, ix) in msg.instructions.iter().enumerate() {
101        let pid = get_key(ix.program_id_index as usize)
102            .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
103
104        invokes.entry(pid).or_default().push((i as i32, -1));
105
106        // 解析主指令(8字节 discriminator)
107        if let Some(event) = parse_outer_instruction(
108            &ix.data,
109            &pid,
110            sig,
111            slot,
112            tx_idx,
113            block_us,
114            grpc_us,
115            &ix.accounts,
116            &get_key,
117            filter,
118            is_created_buy,
119        ) {
120            result.push(IndexedInstructionEvent {
121                outer_idx: i,
122                inner_idx: None,
123                stack_height: Some(1),
124                is_dlmm_event_cpi: false,
125                event,
126            });
127        }
128    }
129
130    // 步骤 2: 解析所有 inner instructions
131    for inner in &meta.inner_instructions {
132        let outer_idx = inner.index as usize;
133
134        for (j, inner_ix) in inner.instructions.iter().enumerate() {
135            let pid = get_key(inner_ix.program_id_index as usize)
136                .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
137
138            invokes.entry(pid).or_default().push((outer_idx as i32, j as i32));
139
140            let event = parse_inner_compiled_instruction_if_supported(
141                &inner_ix.data,
142                &pid,
143                sig,
144                slot,
145                tx_idx,
146                block_us,
147                grpc_us,
148                &inner_ix.accounts,
149                &get_key,
150                filter,
151            )
152            .or_else(|| {
153                parse_inner_instruction(
154                    &inner_ix.data,
155                    &pid,
156                    sig,
157                    slot,
158                    tx_idx,
159                    block_us,
160                    grpc_us,
161                    filter,
162                    is_created_buy,
163                )
164            });
165
166            if let Some(event) = event {
167                result.push(IndexedInstructionEvent {
168                    outer_idx,
169                    inner_idx: Some(j),
170                    stack_height: inner_ix.stack_height,
171                    is_dlmm_event_cpi: pid == crate::instr::program_ids::METEORA_DLMM_PROGRAM_ID
172                        && crate::instr::all_inner::meteora_dlmm::is_event_cpi(&inner_ix.data),
173                    event,
174                });
175            }
176        }
177    }
178
179    // 步骤 3: 合并相关事件(instruction + inner instruction)
180    let mut merged = merge_instruction_events(result);
181    enrich_pumpfun_same_tx_post_merge(&mut merged);
182
183    for e in merged.iter_mut() {
184        if let Some(m) = e.metadata_mut() {
185            m.recent_blockhash = recent_blockhash.clone();
186        }
187    }
188
189    // 步骤 4: 填充账户上下文(invokes 与 fill_data 均使用 Pubkey 键,无堆泄漏)
190    let mut final_result = Vec::with_capacity(merged.len());
191    for mut event in merged {
192        crate::core::account_dispatcher::fill_accounts_with_owned_keys(
193            &mut event,
194            meta,
195            transaction,
196            &invokes,
197        );
198        crate::core::common_filler::fill_data(&mut event, meta, transaction, &invokes);
199        final_result.push(event);
200    }
201
202    final_result
203}
204
205// ============================================================================
206// 辅助函数
207// ============================================================================
208
209#[inline(always)]
210fn parse_compiled_instruction<'a>(
211    data: &[u8],
212    program_id: &Pubkey,
213    sig: Signature,
214    slot: u64,
215    tx_idx: u64,
216    block_us: Option<i64>,
217    grpc_us: i64,
218    account_indices: &[u8],
219    get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
220    filter: Option<&EventTypeFilter>,
221) -> Option<DexEvent> {
222    // 检查指令数据长度(至少8字节 discriminator)
223    if data.len() < 8 {
224        return None;
225    }
226
227    // 常见 DEX 指令账户数远小于 64;栈上缓冲避免每笔 outer 一次 Vec 分配
228    const STACK_CAP: usize = 64;
229    if account_indices.len() <= STACK_CAP {
230        let mut stack = [Pubkey::default(); STACK_CAP];
231        let mut n = 0usize;
232        for &idx in account_indices {
233            let k = get_key(idx as usize)?;
234            stack[n] = read_pubkey_fast(k);
235            n += 1;
236        }
237        crate::instr::parse_instruction_unified(
238            data,
239            &stack[..n],
240            sig,
241            slot,
242            tx_idx,
243            block_us,
244            grpc_us,
245            filter,
246            program_id,
247        )
248    } else {
249        let accounts: Vec<Pubkey> = account_indices
250            .iter()
251            .map(|&idx| get_key(idx as usize).map(|k| read_pubkey_fast(k)))
252            .collect::<Option<_>>()?;
253        crate::instr::parse_instruction_unified(
254            data, &accounts, sig, slot, tx_idx, block_us, grpc_us, filter, program_id,
255        )
256    }
257}
258
259#[inline(always)]
260fn is_supported_inner_compiled_instruction(data: &[u8], program_id: &Pubkey) -> bool {
261    crate::instr::normal_instruction_data_may_parse(program_id, data)
262}
263
264#[inline(always)]
265fn parse_inner_compiled_instruction_if_supported<'a>(
266    data: &[u8],
267    program_id: &Pubkey,
268    sig: Signature,
269    slot: u64,
270    tx_idx: u64,
271    block_us: Option<i64>,
272    grpc_us: i64,
273    account_indices: &[u8],
274    get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
275    filter: Option<&EventTypeFilter>,
276) -> Option<DexEvent> {
277    if !is_supported_inner_compiled_instruction(data, program_id) {
278        return None;
279    }
280    parse_compiled_instruction(
281        data,
282        program_id,
283        sig,
284        slot,
285        tx_idx,
286        block_us,
287        grpc_us,
288        account_indices,
289        get_key,
290        filter,
291    )
292}
293
294/// 解析单个主指令(outer instruction)
295///
296/// 主指令使用 8 字节 discriminator
297#[inline(always)]
298fn parse_outer_instruction<'a>(
299    data: &[u8],
300    program_id: &Pubkey,
301    sig: Signature,
302    slot: u64,
303    tx_idx: u64,
304    block_us: Option<i64>,
305    grpc_us: i64,
306    account_indices: &[u8],
307    get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
308    filter: Option<&EventTypeFilter>,
309    _is_created_buy: bool,
310) -> Option<DexEvent> {
311    parse_compiled_instruction(
312        data,
313        program_id,
314        sig,
315        slot,
316        tx_idx,
317        block_us,
318        grpc_us,
319        account_indices,
320        get_key,
321        filter,
322    )
323}
324
325/// 解析单个 inner instruction
326///
327/// Inner instructions 使用 16 字节 discriminator(前8字节是event hash,后8字节是magic)
328#[inline(always)]
329fn parse_inner_instruction(
330    data: &[u8],
331    program_id: &Pubkey,
332    sig: Signature,
333    slot: u64,
334    tx_idx: u64,
335    block_us: Option<i64>,
336    grpc_us: i64,
337    filter: Option<&EventTypeFilter>,
338    is_created_buy: bool,
339) -> Option<DexEvent> {
340    // 检查数据长度(至少16字节 discriminator)
341    if data.len() < 16 {
342        return None;
343    }
344
345    let metadata = EventMetadata {
346        signature: sig,
347        slot,
348        tx_index: tx_idx,
349        block_time_us: block_us.unwrap_or(0),
350        grpc_recv_us: grpc_us,
351        recent_blockhash: None, // set later on merged events in parse_instructions_enhanced
352    };
353
354    // 提取 16 字节 discriminator
355    let mut discriminator = [0u8; 16];
356    discriminator.copy_from_slice(&data[..16]);
357    let inner_data = &data[16..];
358
359    use crate::instr::{all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner};
360
361    // 根据 program_id 路由到对应的 inner instruction 解析器
362    let event = if *program_id == program_ids::PUMPFUN_PROGRAM_ID {
363        if let Some(f) = filter {
364            if !f.includes_pumpfun() {
365                return None;
366            }
367        }
368        pump_inner::parse_pumpfun_inner_instruction(
369            &discriminator,
370            inner_data,
371            metadata,
372            is_created_buy,
373        )
374    } else if *program_id == program_ids::PUMPSWAP_PROGRAM_ID {
375        if let Some(f) = filter {
376            if !f.includes_pumpswap() {
377                return None;
378            }
379        }
380        pump_amm_inner::parse_pumpswap_inner_instruction(&discriminator, inner_data, metadata)
381    } else if *program_id == program_ids::PUMP_FEES_PROGRAM_ID {
382        if let Some(f) = filter {
383            if !f.includes_pump_fees() {
384                return None;
385            }
386        }
387        all_inner::pump_fees::parse(&discriminator, inner_data, metadata)
388    } else if *program_id == program_ids::RAYDIUM_CLMM_PROGRAM_ID {
389        if let Some(f) = filter {
390            if !f.includes_raydium_clmm() {
391                return None;
392            }
393        }
394        raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
395            &discriminator,
396            inner_data,
397            metadata,
398        )
399    } else if *program_id == program_ids::RAYDIUM_CPMM_PROGRAM_ID {
400        if let Some(f) = filter {
401            if !f.includes_raydium_cpmm() {
402                return None;
403            }
404        }
405        all_inner::raydium_cpmm::parse(&discriminator, inner_data, metadata)
406    } else if *program_id == program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
407        if let Some(f) = filter {
408            if !f.includes_raydium_amm_v4() {
409                return None;
410            }
411        }
412        all_inner::raydium_amm::parse(&discriminator, inner_data, metadata)
413    } else if *program_id == program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
414        if let Some(f) = filter {
415            if !f.includes_orca_whirlpool() {
416                return None;
417            }
418        }
419        all_inner::orca::parse(&discriminator, inner_data, metadata)
420    } else if *program_id == program_ids::METEORA_POOLS_PROGRAM_ID {
421        if let Some(f) = filter {
422            if !f.includes_meteora_pools() {
423                return None;
424            }
425        }
426        all_inner::meteora_amm::parse(&discriminator, inner_data, metadata)
427    } else if *program_id == program_ids::METEORA_DAMM_V2_PROGRAM_ID {
428        if let Some(f) = filter {
429            if !f.includes_meteora_damm_v2() {
430                return None;
431            }
432        }
433        all_inner::meteora_damm::parse(&discriminator, inner_data, metadata)
434    } else if *program_id == program_ids::METEORA_DLMM_PROGRAM_ID {
435        if let Some(f) = filter {
436            if !f.includes_meteora_dlmm() {
437                return None;
438            }
439        }
440        all_inner::meteora_dlmm::parse(&discriminator, inner_data, metadata)
441    } else if *program_id == program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID {
442        if let Some(f) = filter {
443            if !f.includes_raydium_launchlab() {
444                return None;
445            }
446        }
447        all_inner::raydium_launchlab::parse(&discriminator, inner_data, metadata)
448    } else {
449        None
450    };
451
452    if filter.map(|f| event.as_ref().is_some_and(|e| f.should_include_dex_event(e))).unwrap_or(true)
453    {
454        event
455    } else {
456        None
457    }
458}
459
460/// 合并相关的 instruction 和 inner instruction 事件
461///
462/// 合并策略:
463/// 1. 同一个 outer_idx 的 instruction 和 inner instruction 可以合并
464/// 2. Inner instruction 在 outer instruction 之后出现(排序保证主指令在前)
465/// 3. 同一 outer 下若有多个 inner,依次链式合并进同一条事件,再输出
466/// 4. 合并后返回更完整的事件
467#[inline(always)]
468fn merge_instruction_events(events: Vec<IndexedInstructionEvent>) -> Vec<DexEvent> {
469    if events.is_empty() {
470        return Vec::new();
471    }
472
473    // 按 (outer_idx, inner_idx) 排序,确保顺序:同一 outer 下 **主指令在前、inner 在后**
474    // (`None` 若用 MAX 会把 outer 排到 inner 后面,导致无法 merge)
475    let mut events = events;
476    events.sort_unstable_by_key(|event| {
477        (event.outer_idx, event.inner_idx.map_or(0, |inner_idx| inner_idx + 1))
478    });
479
480    let mut result = Vec::with_capacity(events.len());
481    let mut outer_target: Option<(usize, usize)> = None;
482    // Solana's instruction stack is shallow; keep DLMM parent candidates on
483    // the stack so nested DLMM CPIs do not require a hot-path heap allocation.
484    let mut dlmm_targets: [Option<(usize, Option<u32>, usize)>; 8] = [None; 8];
485    let mut dlmm_targets_len = 0usize;
486
487    for indexed in events {
488        let IndexedInstructionEvent {
489            outer_idx,
490            inner_idx,
491            stack_height,
492            is_dlmm_event_cpi,
493            event,
494        } = indexed;
495        match inner_idx {
496            None => {
497                let is_dlmm = is_dlmm_event(&event);
498                let target_idx = result.len();
499                result.push(event);
500                outer_target = Some((outer_idx, target_idx));
501                dlmm_targets_len = 0;
502                if is_dlmm {
503                    dlmm_targets[0] = Some((outer_idx, stack_height, target_idx));
504                    dlmm_targets_len = 1;
505                }
506            }
507            Some(_) => {
508                if is_dlmm_event_cpi {
509                    let target = (0..dlmm_targets_len).rev().find_map(|idx| {
510                        let (target_outer, target_height, target_idx) = dlmm_targets[idx]?;
511                        let is_direct_child = match (target_height, stack_height) {
512                            (Some(parent), Some(child)) => child == parent + 1,
513                            _ => true,
514                        };
515                        (target_outer == outer_idx && is_direct_child).then_some((idx, target_idx))
516                    });
517                    if let Some((candidate_idx, target_idx)) = target {
518                        dlmm_targets_len = candidate_idx + 1;
519                        if target_idx < result.len() {
520                            match try_merge_events(&mut result[target_idx], event) {
521                                Ok(()) => continue,
522                                Err(event) => result.push(event),
523                            }
524                            continue;
525                        }
526                    }
527                    result.push(event);
528                    continue;
529                }
530
531                let is_dlmm = is_dlmm_event(&event);
532                let target_idx = if let Some((target_outer, target_idx)) = outer_target {
533                    if target_outer == outer_idx {
534                        match try_merge_events(&mut result[target_idx], event) {
535                            Ok(()) => target_idx,
536                            Err(event) => {
537                                let target_idx = result.len();
538                                result.push(event);
539                                target_idx
540                            }
541                        }
542                    } else {
543                        let target_idx = result.len();
544                        result.push(event);
545                        target_idx
546                    }
547                } else {
548                    let target_idx = result.len();
549                    result.push(event);
550                    target_idx
551                };
552
553                if is_dlmm {
554                    if let Some(height) = stack_height {
555                        while dlmm_targets_len > 0 {
556                            let Some((candidate_outer, candidate_height, _)) =
557                                dlmm_targets[dlmm_targets_len - 1]
558                            else {
559                                break;
560                            };
561                            if candidate_outer != outer_idx
562                                || candidate_height.is_some_and(|candidate| candidate >= height)
563                            {
564                                dlmm_targets_len -= 1;
565                            } else {
566                                break;
567                            }
568                        }
569                    } else {
570                        dlmm_targets_len = 0;
571                    }
572                    if dlmm_targets_len < dlmm_targets.len() {
573                        dlmm_targets[dlmm_targets_len] =
574                            Some((outer_idx, stack_height, target_idx));
575                        dlmm_targets_len += 1;
576                    }
577                }
578            }
579        }
580    }
581
582    result
583}
584
585/// 检查是否需要解析 instructions(根据 filter)
586#[inline(always)]
587fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
588    // 如果没有 filter,总是解析
589    let Some(filter) = filter else { return true };
590
591    // 如果 filter.include_only 为空,总是解析
592    if filter.include_only.is_none() {
593        return true;
594    }
595
596    // PumpFun:outer BUY/SELL carries instruction args while inner/log TradeEvent
597    // carries executed amounts. Parse both and merge them by order.
598    if filter.includes_pumpfun() {
599        return true;
600    }
601
602    if filter.includes_pump_fees() {
603        return true;
604    }
605
606    filter.includes_pumpswap()
607        || filter.includes_raydium_launchlab()
608        || filter.includes_raydium_cpmm()
609        || filter.includes_raydium_clmm()
610        || filter.includes_raydium_amm_v4()
611        || filter.includes_orca_whirlpool()
612        || filter.includes_meteora_pools()
613        || filter.includes_meteora_damm_v2()
614        || filter.includes_meteora_dlmm()
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620    use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
621    use yellowstone_grpc_proto::prelude::{
622        CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
623    };
624
625    fn pk(s: &str) -> Pubkey {
626        s.parse().unwrap()
627    }
628
629    fn usdc_mint() -> Pubkey {
630        pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
631    }
632
633    fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
634        key.to_bytes().to_vec()
635    }
636
637    fn decode_b58(s: &str) -> Vec<u8> {
638        bs58::decode(s).into_vec().unwrap()
639    }
640
641    fn str_arg(s: &str, out: &mut Vec<u8>) {
642        out.extend_from_slice(&(s.len() as u32).to_le_bytes());
643        out.extend_from_slice(s.as_bytes());
644    }
645
646    fn create_v2_data() -> Vec<u8> {
647        let mut data = Vec::new();
648        data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
649        str_arg("Alt Coin", &mut data);
650        str_arg("ALT", &mut data);
651        str_arg("https://example.invalid/alt.json", &mut data);
652        data.extend_from_slice(Pubkey::new_unique().as_ref());
653        data.push(1);
654        data.push(1);
655        data
656    }
657
658    fn grpc_pumpfun_create_v2_tx(
659        static_len: usize,
660        writable_len: usize,
661        program_idx: u8,
662        ix_accounts: Vec<u8>,
663        account_overrides: Vec<(usize, Pubkey)>,
664    ) -> (TransactionStatusMeta, Option<Transaction>) {
665        let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
666        account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
667        let readonly_len = account_overrides
668            .iter()
669            .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
670            .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
671            .max()
672            .unwrap_or_default();
673        let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
674        let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
675        for (global_idx, key) in account_overrides {
676            if global_idx < static_len {
677                account_keys[global_idx] = key;
678            } else if global_idx < static_len + writable_len {
679                loaded_writable[global_idx - static_len] = key;
680            } else {
681                loaded_readonly[global_idx - static_len - writable_len] = key;
682            }
683        }
684
685        let meta = TransactionStatusMeta {
686            loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
687            loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
688            ..Default::default()
689        };
690        let tx = Transaction {
691            signatures: vec![Signature::default().as_ref().to_vec()],
692            message: Some(Message {
693                header: Some(MessageHeader {
694                    num_required_signatures: 1,
695                    num_readonly_signed_accounts: 0,
696                    num_readonly_unsigned_accounts: 0,
697                }),
698                account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
699                recent_blockhash: vec![0; 32],
700                instructions: vec![CompiledInstruction {
701                    program_id_index: program_idx as u32,
702                    accounts: ix_accounts,
703                    data: create_v2_data(),
704                }],
705                versioned: true,
706                address_table_lookups: Vec::new(),
707            }),
708        };
709        (meta, Some(tx))
710    }
711
712    fn create_v2_accounts(
713        account_len: usize,
714        program_idx: u8,
715        mint_idx: u8,
716        user_idx: u8,
717        token_program_idx: u8,
718        quote_tail: Option<(u8, u8, u8)>,
719    ) -> Vec<u8> {
720        let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
721        accounts[0] = mint_idx;
722        accounts[5] = user_idx;
723        accounts[7] = token_program_idx;
724        if account_len > 15 {
725            accounts[15] = program_idx;
726        }
727        if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
728            accounts[16] = quote_idx;
729            accounts[17] = quote_vault_idx;
730            accounts[18] = quote_token_program_idx;
731        }
732        accounts
733    }
734
735    fn parse_create_v2_from_grpc(
736        meta: &TransactionStatusMeta,
737        tx: &Option<Transaction>,
738    ) -> crate::core::events::PumpFunCreateTokenEvent {
739        let events = parse_instructions_enhanced(
740            meta,
741            tx,
742            Signature::default(),
743            123,
744            0,
745            Some(456),
746            789,
747            None,
748        );
749        assert_eq!(events.len(), 1);
750        match &events[0] {
751            DexEvent::PumpFunCreate(e) => {
752                assert_eq!(e.ix_name, "create_v2");
753                e.clone()
754            }
755            DexEvent::PumpFunCreateV2(e) => {
756                assert_eq!(e.ix_name, "create_v2");
757                crate::core::events::PumpFunCreateTokenEvent {
758                    metadata: e.metadata.clone(),
759                    name: e.name.clone(),
760                    symbol: e.symbol.clone(),
761                    uri: e.uri.clone(),
762                    mint: e.mint,
763                    bonding_curve: e.bonding_curve,
764                    user: e.user,
765                    creator: e.creator,
766                    timestamp: e.timestamp,
767                    virtual_token_reserves: e.virtual_token_reserves,
768                    virtual_sol_reserves: e.virtual_sol_reserves,
769                    real_token_reserves: e.real_token_reserves,
770                    token_total_supply: e.token_total_supply,
771                    mint_authority: e.mint_authority,
772                    associated_bonding_curve: e.associated_bonding_curve,
773                    global: e.global,
774                    system_program: e.system_program,
775                    token_program: e.token_program,
776                    associated_token_program: e.associated_token_program,
777                    mayhem_program_id: e.mayhem_program_id,
778                    global_params: e.global_params,
779                    sol_vault: e.sol_vault,
780                    mayhem_state: e.mayhem_state,
781                    mayhem_token_vault: e.mayhem_token_vault,
782                    event_authority: e.event_authority,
783                    program: e.program,
784                    quote_mint: e.quote_mint,
785                    quote_vault: e.quote_vault,
786                    quote_token_program: e.quote_token_program,
787                    virtual_quote_reserves: e.virtual_quote_reserves,
788                    ix_name: e.ix_name.clone(),
789                    is_mayhem_mode: e.is_mayhem_mode,
790                    is_cashback_enabled: e.is_cashback_enabled,
791                    observed_fee_recipient: e.observed_fee_recipient,
792                }
793            }
794            other => panic!("expected PumpFun create_v2 event, got {other:?}"),
795        }
796    }
797
798    #[test]
799    fn test_should_parse_instructions() {
800        // 无 filter - 应该解析
801        assert!(should_parse_instructions(None));
802
803        // 有 filter 但 include_only 为空 - 应该解析
804        let filter = EventTypeFilter { include_only: None, exclude_types: None };
805        assert!(should_parse_instructions(Some(&filter)));
806
807        // 包含需要 instruction 解析的事件类型
808        use crate::grpc::types::EventType;
809        let filter = EventTypeFilter {
810            include_only: Some(vec![EventType::PumpFunMigrate]),
811            exclude_types: None,
812        };
813        assert!(should_parse_instructions(Some(&filter)));
814
815        // PumpFun 订阅:需要 instruction+inner,避免仅日志时截断丢腿
816        let filter = EventTypeFilter {
817            include_only: Some(vec![EventType::PumpFunTrade]),
818            exclude_types: None,
819        };
820        assert!(should_parse_instructions(Some(&filter)));
821
822        for event_type in [
823            EventType::PumpSwapTrade,
824            EventType::PumpFeesUpdateFeeShares,
825            EventType::RaydiumLaunchlabTrade,
826            EventType::RaydiumCpmmSwap,
827            EventType::RaydiumClmmSwap,
828            EventType::RaydiumAmmV4Swap,
829            EventType::OrcaWhirlpoolSwap,
830            EventType::MeteoraPoolsSwap,
831            EventType::MeteoraDammV2Swap,
832            EventType::MeteoraDammV2InitializePool,
833            EventType::MeteoraDlmmSwap,
834        ] {
835            let filter = EventTypeFilter::include_only(vec![event_type]);
836            assert!(
837                should_parse_instructions(Some(&filter)),
838                "instruction parsing should be enabled for {event_type:?}"
839            );
840        }
841
842        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
843        assert!(
844            !should_parse_instructions(Some(&filter)),
845            "DBC events are log-only until an instruction parser is implemented"
846        );
847
848        let filter = EventTypeFilter::include_only(vec![
849            EventType::AccountPumpFunGlobal,
850            EventType::AccountRaydiumClmmPoolState,
851            EventType::AccountRaydiumCpmmPoolState,
852            EventType::AccountOrcaWhirlpool,
853        ]);
854        assert!(
855            !should_parse_instructions(Some(&filter)),
856            "account-only non-Pump filters should stay on the account update path"
857        );
858    }
859
860    #[test]
861    fn test_merge_instruction_events() {
862        use solana_sdk::signature::Signature;
863
864        let metadata = EventMetadata {
865            signature: Signature::default(),
866            slot: 100,
867            tx_index: 1,
868            block_time_us: 1000,
869            grpc_recv_us: 2000,
870            recent_blockhash: None,
871        };
872
873        // 模拟:outer instruction + inner instruction(应该合并)
874        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
875            metadata: metadata.clone(),
876            bonding_curve: Pubkey::new_unique(),
877            ..Default::default()
878        });
879
880        let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
881            metadata: metadata.clone(),
882            sol_amount: 1000,
883            token_amount: 2000,
884            ..Default::default()
885        });
886
887        let events = vec![
888            IndexedInstructionEvent {
889                outer_idx: 0,
890                inner_idx: None,
891                stack_height: Some(1),
892                is_dlmm_event_cpi: false,
893                event: outer_event,
894            },
895            IndexedInstructionEvent {
896                outer_idx: 0,
897                inner_idx: Some(0),
898                stack_height: Some(2),
899                is_dlmm_event_cpi: false,
900                event: inner_event,
901            },
902        ];
903
904        let result = merge_instruction_events(events);
905
906        // 应该合并为1个事件
907        assert_eq!(result.len(), 1);
908
909        // 验证合并结果包含两者的数据
910        if let DexEvent::PumpFunTrade(trade) = &result[0] {
911            assert_eq!(trade.sol_amount, 1000); // 来自 inner
912            assert_eq!(trade.token_amount, 2000); // 来自 inner
913            assert_ne!(trade.bonding_curve, Pubkey::default()); // 来自 outer
914        } else {
915            panic!("Expected PumpFunTrade event");
916        }
917    }
918
919    #[test]
920    fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
921        use solana_sdk::signature::Signature;
922
923        let metadata = EventMetadata {
924            signature: Signature::default(),
925            slot: 100,
926            tx_index: 1,
927            block_time_us: 1000,
928            grpc_recv_us: 2000,
929            recent_blockhash: None,
930        };
931
932        let bc = Pubkey::new_unique();
933        let fee = Pubkey::new_unique();
934
935        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
936            metadata: metadata.clone(),
937            bonding_curve: bc,
938            ..Default::default()
939        });
940
941        let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
942            metadata: metadata.clone(),
943            sol_amount: 1000,
944            token_amount: 2000,
945            is_buy: true,
946            ..Default::default()
947        });
948
949        // 第二段 inner 仅有 fee_recipient,无成交量 —— 不应抹掉第一段金额
950        let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
951            metadata: metadata.clone(),
952            fee_recipient: fee,
953            ..Default::default()
954        });
955
956        let events = vec![
957            IndexedInstructionEvent {
958                outer_idx: 0,
959                inner_idx: None,
960                stack_height: Some(1),
961                is_dlmm_event_cpi: false,
962                event: outer_event,
963            },
964            IndexedInstructionEvent {
965                outer_idx: 0,
966                inner_idx: Some(0),
967                stack_height: Some(2),
968                is_dlmm_event_cpi: false,
969                event: inner_trade,
970            },
971            IndexedInstructionEvent {
972                outer_idx: 0,
973                inner_idx: Some(1),
974                stack_height: Some(2),
975                is_dlmm_event_cpi: false,
976                event: inner_fee_only,
977            },
978        ];
979
980        let result = merge_instruction_events(events);
981        assert_eq!(result.len(), 1);
982        if let DexEvent::PumpFunTrade(trade) = &result[0] {
983            assert_eq!(trade.bonding_curve, bc);
984            assert_eq!(trade.sol_amount, 1000);
985            assert_eq!(trade.token_amount, 2000);
986            assert_eq!(trade.fee_recipient, fee);
987        } else {
988            panic!("Expected PumpFunTrade event");
989        }
990    }
991
992    fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
993        DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
994            metadata: EventMetadata::default(),
995            token_x_mint: Pubkey::default(),
996            token_y_mint: Pubkey::default(),
997            pool,
998            from: Pubkey::default(),
999            start_bin_id: 0,
1000            end_bin_id: 0,
1001            amount_in,
1002            amount_out,
1003            swap_for_y: false,
1004            fee: 0,
1005            protocol_fee: 0,
1006            fee_bps: 0,
1007            host_fee: 0,
1008        })
1009    }
1010
1011    fn dlmm_add_liquidity() -> DexEvent {
1012        DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1013            metadata: EventMetadata::default(),
1014            pool: Pubkey::default(),
1015            from: Pubkey::default(),
1016            position: Pubkey::default(),
1017            amounts: [0; 2],
1018            active_bin_id: 0,
1019        })
1020    }
1021
1022    #[test]
1023    fn merge_preserves_unrelated_inner_events() {
1024        let events = vec![
1025            IndexedInstructionEvent {
1026                outer_idx: 0,
1027                inner_idx: None,
1028                stack_height: Some(1),
1029                is_dlmm_event_cpi: false,
1030                event: dlmm_swap(Pubkey::default(), 0, 0),
1031            },
1032            IndexedInstructionEvent {
1033                outer_idx: 0,
1034                inner_idx: Some(0),
1035                stack_height: Some(2),
1036                is_dlmm_event_cpi: false,
1037                event: dlmm_add_liquidity(),
1038            },
1039        ];
1040
1041        let result = merge_instruction_events(events);
1042        assert_eq!(result.len(), 2);
1043        assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1044        assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1045    }
1046
1047    #[test]
1048    fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1049        let first_pool = Pubkey::new_unique();
1050        let second_pool = Pubkey::new_unique();
1051        let events = vec![
1052            IndexedInstructionEvent {
1053                outer_idx: 0,
1054                inner_idx: Some(0),
1055                stack_height: Some(2),
1056                is_dlmm_event_cpi: false,
1057                event: dlmm_swap(first_pool, 1, 0),
1058            },
1059            IndexedInstructionEvent {
1060                outer_idx: 0,
1061                inner_idx: Some(1),
1062                stack_height: Some(3),
1063                is_dlmm_event_cpi: true,
1064                event: dlmm_swap(first_pool, 10, 9),
1065            },
1066            IndexedInstructionEvent {
1067                outer_idx: 0,
1068                inner_idx: Some(2),
1069                stack_height: Some(2),
1070                is_dlmm_event_cpi: false,
1071                event: dlmm_swap(second_pool, 2, 0),
1072            },
1073            IndexedInstructionEvent {
1074                outer_idx: 0,
1075                inner_idx: Some(3),
1076                stack_height: Some(3),
1077                is_dlmm_event_cpi: true,
1078                event: dlmm_swap(second_pool, 20, 18),
1079            },
1080        ];
1081
1082        let result = merge_instruction_events(events);
1083        assert_eq!(result.len(), 2);
1084        let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1085        let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1086        assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1087        assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1088    }
1089
1090    #[test]
1091    fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1092        let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1093        let static_keys = [
1094            "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1095            "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1096            "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1097            "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1098            "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1099            "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1100            "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1101            "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1102            "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1103            "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1104            "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1105            "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1106            "ComputeBudget111111111111111111111111111111",
1107            "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1108            "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1109            "SysvarRent111111111111111111111111111111111",
1110        ];
1111        let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1112        let loaded_readonly = [
1113            "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1114            "So11111111111111111111111111111111111111112",
1115            "11111111111111111111111111111111",
1116            "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1117            "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1118            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1119            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1120            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1121            "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1122            "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1123        ];
1124        let meta = TransactionStatusMeta {
1125            loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1126            loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1127            inner_instructions: vec![InnerInstructions {
1128                index: 2,
1129                instructions: vec![InnerInstruction {
1130                    program_id_index: 20,
1131                    accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1132                    data: decode_b58(
1133                        "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1134                    ),
1135                    stack_height: Some(2),
1136                }],
1137            }],
1138            ..Default::default()
1139        };
1140        let tx = Some(Transaction {
1141            signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1142            message: Some(Message {
1143                header: Some(MessageHeader::default()),
1144                account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1145                recent_blockhash: vec![0; 32],
1146                instructions: vec![
1147                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1148                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1149                    CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1150                ],
1151                versioned: true,
1152                address_table_lookups: Vec::new(),
1153            }),
1154        });
1155
1156        let events = parse_instructions_enhanced(
1157            &meta,
1158            &tx,
1159            signature.parse().unwrap(),
1160            427_039_576,
1161            0,
1162            Some(1_781_687_252_000_000),
1163            789,
1164            None,
1165        );
1166
1167        assert_eq!(events.len(), 1, "{signature}");
1168        match &events[0] {
1169            DexEvent::PumpSwapCreatePool(e) => {
1170                assert_eq!(e.index, 0, "{signature}");
1171                assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1172                assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1173                assert_eq!(
1174                    e.coin_creator,
1175                    pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1176                    "{signature}"
1177                );
1178                assert!(!e.is_mayhem_mode, "{signature}");
1179                assert!(e.is_cashback_coin, "{signature}");
1180                assert_eq!(
1181                    e.pool,
1182                    pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1183                    "{signature}"
1184                );
1185                assert_eq!(
1186                    e.creator,
1187                    pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1188                    "{signature}"
1189                );
1190                assert_eq!(
1191                    e.base_mint,
1192                    pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1193                    "{signature}"
1194                );
1195                assert_eq!(
1196                    e.quote_mint,
1197                    pk("So11111111111111111111111111111111111111112"),
1198                    "{signature}"
1199                );
1200                assert_eq!(
1201                    e.lp_mint,
1202                    pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1203                    "{signature}"
1204                );
1205                assert_eq!(
1206                    e.user_base_token_account,
1207                    pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1208                    "{signature}"
1209                );
1210                assert_eq!(
1211                    e.user_quote_token_account,
1212                    pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1213                    "{signature}"
1214                );
1215            }
1216            other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1217        }
1218    }
1219
1220    #[test]
1221    fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1222        struct Case {
1223            signature: &'static str,
1224            name: &'static str,
1225            static_len: usize,
1226            writable_len: usize,
1227            program_idx: u8,
1228            account_len: usize,
1229            mint_idx: u8,
1230            mint: &'static str,
1231            user_idx: u8,
1232            user: &'static str,
1233            token_program_idx: u8,
1234            quote_idx: u8,
1235            quote_mint: Pubkey,
1236            quote_vault_idx: u8,
1237            quote_vault: &'static str,
1238            quote_token_program_idx: u8,
1239        }
1240        let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1241        let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1242        let cases = [
1243            Case {
1244                signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1245                name: "19-account WSOL quote in ALT",
1246                static_len: 15,
1247                writable_len: 7,
1248                program_idx: 12,
1249                account_len: 19,
1250                mint_idx: 1,
1251                mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1252                user_idx: 0,
1253                user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1254                token_program_idx: 24,
1255                quote_idx: 27,
1256                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1257                quote_vault_idx: 7,
1258                quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1259                quote_token_program_idx: 28,
1260            },
1261            Case {
1262                signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1263                name: "19-account WSOL quote in ALT with exact quote buy",
1264                static_len: 20,
1265                writable_len: 7,
1266                program_idx: 15,
1267                account_len: 19,
1268                mint_idx: 1,
1269                mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1270                user_idx: 0,
1271                user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1272                token_program_idx: 31,
1273                quote_idx: 28,
1274                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1275                quote_vault_idx: 4,
1276                quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1277                quote_token_program_idx: 30,
1278            },
1279            Case {
1280                signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1281                name: "19-account USDC quote in ALT",
1282                static_len: 19,
1283                writable_len: 6,
1284                program_idx: 16,
1285                account_len: 19,
1286                mint_idx: 1,
1287                mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1288                user_idx: 0,
1289                user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1290                token_program_idx: 27,
1291                quote_idx: 30,
1292                quote_mint: usdc_mint(),
1293                quote_vault_idx: 6,
1294                quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1295                quote_token_program_idx: 31,
1296            },
1297            Case {
1298                signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1299                name: "20-account WSOL quote in ALT",
1300                static_len: 15,
1301                writable_len: 7,
1302                program_idx: 12,
1303                account_len: 20,
1304                mint_idx: 1,
1305                mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1306                user_idx: 0,
1307                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1308                token_program_idx: 24,
1309                quote_idx: 27,
1310                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1311                quote_vault_idx: 7,
1312                quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1313                quote_token_program_idx: 28,
1314            },
1315            Case {
1316                signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1317                name: "19-account WSOL quote in ALT with later buy",
1318                static_len: 18,
1319                writable_len: 7,
1320                program_idx: 13,
1321                account_len: 19,
1322                mint_idx: 1,
1323                mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1324                user_idx: 0,
1325                user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1326                token_program_idx: 27,
1327                quote_idx: 30,
1328                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1329                quote_vault_idx: 7,
1330                quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1331                quote_token_program_idx: 31,
1332            },
1333            Case {
1334                signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1335                name: "19-account USDC quote in ALT exact quote buy",
1336                static_len: 19,
1337                writable_len: 6,
1338                program_idx: 15,
1339                account_len: 19,
1340                mint_idx: 1,
1341                mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1342                user_idx: 0,
1343                user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1344                token_program_idx: 28,
1345                quote_idx: 33,
1346                quote_mint: usdc_mint(),
1347                quote_vault_idx: 7,
1348                quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1349                quote_token_program_idx: 27,
1350            },
1351            Case {
1352                signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1353                name: "20-account WSOL quote in ALT with jit account",
1354                static_len: 15,
1355                writable_len: 7,
1356                program_idx: 12,
1357                account_len: 20,
1358                mint_idx: 1,
1359                mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1360                user_idx: 0,
1361                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1362                token_program_idx: 24,
1363                quote_idx: 27,
1364                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1365                quote_vault_idx: 7,
1366                quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1367                quote_token_program_idx: 28,
1368            },
1369        ];
1370
1371        for case in cases {
1372            let (meta, tx) = grpc_pumpfun_create_v2_tx(
1373                case.static_len,
1374                case.writable_len,
1375                case.program_idx,
1376                create_v2_accounts(
1377                    case.account_len,
1378                    case.program_idx,
1379                    case.mint_idx,
1380                    case.user_idx,
1381                    case.token_program_idx,
1382                    Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1383                ),
1384                vec![
1385                    (case.mint_idx as usize, pk(case.mint)),
1386                    (case.user_idx as usize, pk(case.user)),
1387                    (case.token_program_idx as usize, token_2022_program),
1388                    (case.quote_idx as usize, case.quote_mint),
1389                    (case.quote_vault_idx as usize, pk(case.quote_vault)),
1390                    (case.quote_token_program_idx as usize, spl_token_program),
1391                ],
1392            );
1393            let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1394                let idx = global_idx as usize;
1395                if idx < case.static_len {
1396                    ("static", idx)
1397                } else if idx < case.static_len + case.writable_len {
1398                    ("writable", idx - case.static_len)
1399                } else {
1400                    ("readonly", idx - case.static_len - case.writable_len)
1401                }
1402            };
1403            assert_eq!(
1404                meta.loaded_writable_addresses.len(),
1405                case.writable_len,
1406                "{}: {}",
1407                case.name,
1408                case.signature
1409            );
1410            for (global_idx, expected_key) in [
1411                (case.token_program_idx, token_2022_program),
1412                (case.quote_idx, case.quote_mint),
1413                (case.quote_token_program_idx, spl_token_program),
1414            ] {
1415                match loaded_key_location(global_idx) {
1416                    ("static", _) => {}
1417                    ("writable", offset) => assert_eq!(
1418                        read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1419                        expected_key,
1420                        "{}: writable loaded key {global_idx}: {}",
1421                        case.name,
1422                        case.signature
1423                    ),
1424                    ("readonly", offset) => assert_eq!(
1425                        read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1426                        expected_key,
1427                        "{}: readonly loaded key {global_idx}: {}",
1428                        case.name,
1429                        case.signature
1430                    ),
1431                    _ => unreachable!(),
1432                }
1433            }
1434
1435            let create = parse_create_v2_from_grpc(&meta, &tx);
1436
1437            assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1438            assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1439            assert_eq!(
1440                create.token_program, token_2022_program,
1441                "{}: {}",
1442                case.name, case.signature
1443            );
1444            assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1445            assert_eq!(
1446                create.quote_vault,
1447                pk(case.quote_vault),
1448                "{}: {}",
1449                case.name,
1450                case.signature
1451            );
1452            assert_eq!(
1453                create.quote_token_program, spl_token_program,
1454                "{}: {}",
1455                case.name, case.signature
1456            );
1457        }
1458    }
1459
1460    #[test]
1461    fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1462        let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1463        let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1464        let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1465        let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1466        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1467            16,
1468            5,
1469            12,
1470            create_v2_accounts(16, 12, 1, 0, 24, None),
1471            vec![(1, mint), (0, user), (24, token_program)],
1472        );
1473
1474        let create = parse_create_v2_from_grpc(&meta, &tx);
1475
1476        assert_eq!(create.mint, mint, "{signature}");
1477        assert_eq!(create.user, user, "{signature}");
1478        assert_eq!(create.token_program, token_program, "{signature}");
1479        assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1480        assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1481        assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1482    }
1483
1484    #[test]
1485    fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1486        let quote_vault = Pubkey::new_unique();
1487        let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1488        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1489            19,
1490            6,
1491            16,
1492            create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1493            vec![
1494                (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1495                (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1496                (6, quote_vault),
1497                (31, quote_token_program),
1498            ],
1499        );
1500
1501        let create = parse_create_v2_from_grpc(&meta, &tx);
1502
1503        assert_eq!(create.quote_mint, Pubkey::default());
1504        assert_eq!(create.quote_vault, Pubkey::default());
1505        assert_eq!(create.quote_token_program, Pubkey::default());
1506    }
1507}