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            pool,
996            from: Pubkey::default(),
997            start_bin_id: 0,
998            end_bin_id: 0,
999            amount_in,
1000            amount_out,
1001            swap_for_y: false,
1002            fee: 0,
1003            protocol_fee: 0,
1004            fee_bps: 0,
1005            host_fee: 0,
1006        })
1007    }
1008
1009    fn dlmm_add_liquidity() -> DexEvent {
1010        DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1011            metadata: EventMetadata::default(),
1012            pool: Pubkey::default(),
1013            from: Pubkey::default(),
1014            position: Pubkey::default(),
1015            amounts: [0; 2],
1016            active_bin_id: 0,
1017        })
1018    }
1019
1020    #[test]
1021    fn merge_preserves_unrelated_inner_events() {
1022        let events = vec![
1023            IndexedInstructionEvent {
1024                outer_idx: 0,
1025                inner_idx: None,
1026                stack_height: Some(1),
1027                is_dlmm_event_cpi: false,
1028                event: dlmm_swap(Pubkey::default(), 0, 0),
1029            },
1030            IndexedInstructionEvent {
1031                outer_idx: 0,
1032                inner_idx: Some(0),
1033                stack_height: Some(2),
1034                is_dlmm_event_cpi: false,
1035                event: dlmm_add_liquidity(),
1036            },
1037        ];
1038
1039        let result = merge_instruction_events(events);
1040        assert_eq!(result.len(), 2);
1041        assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1042        assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1043    }
1044
1045    #[test]
1046    fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1047        let first_pool = Pubkey::new_unique();
1048        let second_pool = Pubkey::new_unique();
1049        let events = vec![
1050            IndexedInstructionEvent {
1051                outer_idx: 0,
1052                inner_idx: Some(0),
1053                stack_height: Some(2),
1054                is_dlmm_event_cpi: false,
1055                event: dlmm_swap(first_pool, 1, 0),
1056            },
1057            IndexedInstructionEvent {
1058                outer_idx: 0,
1059                inner_idx: Some(1),
1060                stack_height: Some(3),
1061                is_dlmm_event_cpi: true,
1062                event: dlmm_swap(first_pool, 10, 9),
1063            },
1064            IndexedInstructionEvent {
1065                outer_idx: 0,
1066                inner_idx: Some(2),
1067                stack_height: Some(2),
1068                is_dlmm_event_cpi: false,
1069                event: dlmm_swap(second_pool, 2, 0),
1070            },
1071            IndexedInstructionEvent {
1072                outer_idx: 0,
1073                inner_idx: Some(3),
1074                stack_height: Some(3),
1075                is_dlmm_event_cpi: true,
1076                event: dlmm_swap(second_pool, 20, 18),
1077            },
1078        ];
1079
1080        let result = merge_instruction_events(events);
1081        assert_eq!(result.len(), 2);
1082        let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1083        let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1084        assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1085        assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1086    }
1087
1088    #[test]
1089    fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1090        let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1091        let static_keys = [
1092            "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1093            "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1094            "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1095            "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1096            "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1097            "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1098            "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1099            "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1100            "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1101            "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1102            "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1103            "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1104            "ComputeBudget111111111111111111111111111111",
1105            "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1106            "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1107            "SysvarRent111111111111111111111111111111111",
1108        ];
1109        let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1110        let loaded_readonly = [
1111            "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1112            "So11111111111111111111111111111111111111112",
1113            "11111111111111111111111111111111",
1114            "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1115            "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1116            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1117            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1118            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1119            "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1120            "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1121        ];
1122        let meta = TransactionStatusMeta {
1123            loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1124            loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1125            inner_instructions: vec![InnerInstructions {
1126                index: 2,
1127                instructions: vec![InnerInstruction {
1128                    program_id_index: 20,
1129                    accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1130                    data: decode_b58(
1131                        "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1132                    ),
1133                    stack_height: Some(2),
1134                }],
1135            }],
1136            ..Default::default()
1137        };
1138        let tx = Some(Transaction {
1139            signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1140            message: Some(Message {
1141                header: Some(MessageHeader::default()),
1142                account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1143                recent_blockhash: vec![0; 32],
1144                instructions: vec![
1145                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1146                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1147                    CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1148                ],
1149                versioned: true,
1150                address_table_lookups: Vec::new(),
1151            }),
1152        });
1153
1154        let events = parse_instructions_enhanced(
1155            &meta,
1156            &tx,
1157            signature.parse().unwrap(),
1158            427_039_576,
1159            0,
1160            Some(1_781_687_252_000_000),
1161            789,
1162            None,
1163        );
1164
1165        assert_eq!(events.len(), 1, "{signature}");
1166        match &events[0] {
1167            DexEvent::PumpSwapCreatePool(e) => {
1168                assert_eq!(e.index, 0, "{signature}");
1169                assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1170                assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1171                assert_eq!(
1172                    e.coin_creator,
1173                    pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1174                    "{signature}"
1175                );
1176                assert!(!e.is_mayhem_mode, "{signature}");
1177                assert!(e.is_cashback_coin, "{signature}");
1178                assert_eq!(
1179                    e.pool,
1180                    pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1181                    "{signature}"
1182                );
1183                assert_eq!(
1184                    e.creator,
1185                    pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1186                    "{signature}"
1187                );
1188                assert_eq!(
1189                    e.base_mint,
1190                    pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1191                    "{signature}"
1192                );
1193                assert_eq!(
1194                    e.quote_mint,
1195                    pk("So11111111111111111111111111111111111111112"),
1196                    "{signature}"
1197                );
1198                assert_eq!(
1199                    e.lp_mint,
1200                    pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1201                    "{signature}"
1202                );
1203                assert_eq!(
1204                    e.user_base_token_account,
1205                    pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1206                    "{signature}"
1207                );
1208                assert_eq!(
1209                    e.user_quote_token_account,
1210                    pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1211                    "{signature}"
1212                );
1213            }
1214            other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1215        }
1216    }
1217
1218    #[test]
1219    fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1220        struct Case {
1221            signature: &'static str,
1222            name: &'static str,
1223            static_len: usize,
1224            writable_len: usize,
1225            program_idx: u8,
1226            account_len: usize,
1227            mint_idx: u8,
1228            mint: &'static str,
1229            user_idx: u8,
1230            user: &'static str,
1231            token_program_idx: u8,
1232            quote_idx: u8,
1233            quote_mint: Pubkey,
1234            quote_vault_idx: u8,
1235            quote_vault: &'static str,
1236            quote_token_program_idx: u8,
1237        }
1238        let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1239        let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1240        let cases = [
1241            Case {
1242                signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1243                name: "19-account WSOL quote in ALT",
1244                static_len: 15,
1245                writable_len: 7,
1246                program_idx: 12,
1247                account_len: 19,
1248                mint_idx: 1,
1249                mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1250                user_idx: 0,
1251                user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1252                token_program_idx: 24,
1253                quote_idx: 27,
1254                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1255                quote_vault_idx: 7,
1256                quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1257                quote_token_program_idx: 28,
1258            },
1259            Case {
1260                signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1261                name: "19-account WSOL quote in ALT with exact quote buy",
1262                static_len: 20,
1263                writable_len: 7,
1264                program_idx: 15,
1265                account_len: 19,
1266                mint_idx: 1,
1267                mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1268                user_idx: 0,
1269                user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1270                token_program_idx: 31,
1271                quote_idx: 28,
1272                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1273                quote_vault_idx: 4,
1274                quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1275                quote_token_program_idx: 30,
1276            },
1277            Case {
1278                signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1279                name: "19-account USDC quote in ALT",
1280                static_len: 19,
1281                writable_len: 6,
1282                program_idx: 16,
1283                account_len: 19,
1284                mint_idx: 1,
1285                mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1286                user_idx: 0,
1287                user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1288                token_program_idx: 27,
1289                quote_idx: 30,
1290                quote_mint: usdc_mint(),
1291                quote_vault_idx: 6,
1292                quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1293                quote_token_program_idx: 31,
1294            },
1295            Case {
1296                signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1297                name: "20-account WSOL quote in ALT",
1298                static_len: 15,
1299                writable_len: 7,
1300                program_idx: 12,
1301                account_len: 20,
1302                mint_idx: 1,
1303                mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1304                user_idx: 0,
1305                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1306                token_program_idx: 24,
1307                quote_idx: 27,
1308                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1309                quote_vault_idx: 7,
1310                quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1311                quote_token_program_idx: 28,
1312            },
1313            Case {
1314                signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1315                name: "19-account WSOL quote in ALT with later buy",
1316                static_len: 18,
1317                writable_len: 7,
1318                program_idx: 13,
1319                account_len: 19,
1320                mint_idx: 1,
1321                mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1322                user_idx: 0,
1323                user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1324                token_program_idx: 27,
1325                quote_idx: 30,
1326                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1327                quote_vault_idx: 7,
1328                quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1329                quote_token_program_idx: 31,
1330            },
1331            Case {
1332                signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1333                name: "19-account USDC quote in ALT exact quote buy",
1334                static_len: 19,
1335                writable_len: 6,
1336                program_idx: 15,
1337                account_len: 19,
1338                mint_idx: 1,
1339                mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1340                user_idx: 0,
1341                user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1342                token_program_idx: 28,
1343                quote_idx: 33,
1344                quote_mint: usdc_mint(),
1345                quote_vault_idx: 7,
1346                quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1347                quote_token_program_idx: 27,
1348            },
1349            Case {
1350                signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1351                name: "20-account WSOL quote in ALT with jit account",
1352                static_len: 15,
1353                writable_len: 7,
1354                program_idx: 12,
1355                account_len: 20,
1356                mint_idx: 1,
1357                mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1358                user_idx: 0,
1359                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1360                token_program_idx: 24,
1361                quote_idx: 27,
1362                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1363                quote_vault_idx: 7,
1364                quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1365                quote_token_program_idx: 28,
1366            },
1367        ];
1368
1369        for case in cases {
1370            let (meta, tx) = grpc_pumpfun_create_v2_tx(
1371                case.static_len,
1372                case.writable_len,
1373                case.program_idx,
1374                create_v2_accounts(
1375                    case.account_len,
1376                    case.program_idx,
1377                    case.mint_idx,
1378                    case.user_idx,
1379                    case.token_program_idx,
1380                    Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1381                ),
1382                vec![
1383                    (case.mint_idx as usize, pk(case.mint)),
1384                    (case.user_idx as usize, pk(case.user)),
1385                    (case.token_program_idx as usize, token_2022_program),
1386                    (case.quote_idx as usize, case.quote_mint),
1387                    (case.quote_vault_idx as usize, pk(case.quote_vault)),
1388                    (case.quote_token_program_idx as usize, spl_token_program),
1389                ],
1390            );
1391            let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1392                let idx = global_idx as usize;
1393                if idx < case.static_len {
1394                    ("static", idx)
1395                } else if idx < case.static_len + case.writable_len {
1396                    ("writable", idx - case.static_len)
1397                } else {
1398                    ("readonly", idx - case.static_len - case.writable_len)
1399                }
1400            };
1401            assert_eq!(
1402                meta.loaded_writable_addresses.len(),
1403                case.writable_len,
1404                "{}: {}",
1405                case.name,
1406                case.signature
1407            );
1408            for (global_idx, expected_key) in [
1409                (case.token_program_idx, token_2022_program),
1410                (case.quote_idx, case.quote_mint),
1411                (case.quote_token_program_idx, spl_token_program),
1412            ] {
1413                match loaded_key_location(global_idx) {
1414                    ("static", _) => {}
1415                    ("writable", offset) => assert_eq!(
1416                        read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1417                        expected_key,
1418                        "{}: writable loaded key {global_idx}: {}",
1419                        case.name,
1420                        case.signature
1421                    ),
1422                    ("readonly", offset) => assert_eq!(
1423                        read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1424                        expected_key,
1425                        "{}: readonly loaded key {global_idx}: {}",
1426                        case.name,
1427                        case.signature
1428                    ),
1429                    _ => unreachable!(),
1430                }
1431            }
1432
1433            let create = parse_create_v2_from_grpc(&meta, &tx);
1434
1435            assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1436            assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1437            assert_eq!(
1438                create.token_program, token_2022_program,
1439                "{}: {}",
1440                case.name, case.signature
1441            );
1442            assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1443            assert_eq!(
1444                create.quote_vault,
1445                pk(case.quote_vault),
1446                "{}: {}",
1447                case.name,
1448                case.signature
1449            );
1450            assert_eq!(
1451                create.quote_token_program, spl_token_program,
1452                "{}: {}",
1453                case.name, case.signature
1454            );
1455        }
1456    }
1457
1458    #[test]
1459    fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1460        let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1461        let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1462        let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1463        let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1464        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1465            16,
1466            5,
1467            12,
1468            create_v2_accounts(16, 12, 1, 0, 24, None),
1469            vec![(1, mint), (0, user), (24, token_program)],
1470        );
1471
1472        let create = parse_create_v2_from_grpc(&meta, &tx);
1473
1474        assert_eq!(create.mint, mint, "{signature}");
1475        assert_eq!(create.user, user, "{signature}");
1476        assert_eq!(create.token_program, token_program, "{signature}");
1477        assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1478        assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1479        assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1480    }
1481
1482    #[test]
1483    fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1484        let quote_vault = Pubkey::new_unique();
1485        let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1486        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1487            19,
1488            6,
1489            16,
1490            create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1491            vec![
1492                (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1493                (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1494                (6, quote_vault),
1495                (31, quote_token_program),
1496            ],
1497        );
1498
1499        let create = parse_create_v2_from_grpc(&meta, &tx);
1500
1501        assert_eq!(create.quote_mint, Pubkey::default());
1502        assert_eq!(create.quote_vault, Pubkey::default());
1503        assert_eq!(create.quote_token_program, Pubkey::default());
1504    }
1505}