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