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