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                            match try_merge_events(&mut result[target_idx], event) {
543                                Ok(()) => continue,
544                                Err(event) => result.push(event),
545                            }
546                            continue;
547                        }
548                    }
549                    result.push(event);
550                    continue;
551                }
552
553                let is_dlmm = is_dlmm_event(&event);
554                let target_idx = if let Some((target_outer, target_idx)) = outer_target {
555                    if target_outer == outer_idx {
556                        match try_merge_events(&mut result[target_idx], event) {
557                            Ok(()) => target_idx,
558                            Err(event) => {
559                                let target_idx = result.len();
560                                result.push(event);
561                                target_idx
562                            }
563                        }
564                    } else {
565                        let target_idx = result.len();
566                        result.push(event);
567                        target_idx
568                    }
569                } else {
570                    let target_idx = result.len();
571                    result.push(event);
572                    target_idx
573                };
574
575                if is_dlmm {
576                    if let Some(height) = stack_height {
577                        while dlmm_targets_len > 0 {
578                            let Some((candidate_outer, candidate_height, _)) =
579                                dlmm_targets[dlmm_targets_len - 1]
580                            else {
581                                break;
582                            };
583                            if candidate_outer != outer_idx
584                                || candidate_height.is_some_and(|candidate| candidate >= height)
585                            {
586                                dlmm_targets_len -= 1;
587                            } else {
588                                break;
589                            }
590                        }
591                    } else {
592                        dlmm_targets_len = 0;
593                    }
594                    if dlmm_targets_len < dlmm_targets.len() {
595                        dlmm_targets[dlmm_targets_len] =
596                            Some((outer_idx, stack_height, target_idx));
597                        dlmm_targets_len += 1;
598                    }
599                }
600            }
601        }
602    }
603
604    result
605}
606
607/// 检查是否需要解析 instructions(根据 filter)
608#[inline(always)]
609fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
610    // 如果没有 filter,总是解析
611    let Some(filter) = filter else { return true };
612
613    // 如果 filter.include_only 为空,总是解析
614    if filter.include_only.is_none() {
615        return true;
616    }
617
618    // PumpFun:outer BUY/SELL carries instruction args while inner/log TradeEvent
619    // carries executed amounts. Parse both and merge them by order.
620    if filter.includes_pumpfun() {
621        return true;
622    }
623
624    if filter.includes_pump_fees() {
625        return true;
626    }
627
628    filter.includes_pumpswap()
629        || filter.includes_raydium_launchlab()
630        || filter.includes_raydium_cpmm()
631        || filter.includes_raydium_clmm()
632        || filter.includes_raydium_amm_v4()
633        || filter.includes_orca_whirlpool()
634        || filter.includes_meteora_pools()
635        || filter.includes_meteora_damm_v2()
636        || filter.includes_meteora_dlmm()
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
643    use yellowstone_grpc_proto::prelude::{
644        CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
645    };
646
647    fn pk(s: &str) -> Pubkey {
648        s.parse().unwrap()
649    }
650
651    fn usdc_mint() -> Pubkey {
652        pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
653    }
654
655    fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
656        key.to_bytes().to_vec()
657    }
658
659    fn decode_b58(s: &str) -> Vec<u8> {
660        bs58::decode(s).into_vec().unwrap()
661    }
662
663    fn str_arg(s: &str, out: &mut Vec<u8>) {
664        out.extend_from_slice(&(s.len() as u32).to_le_bytes());
665        out.extend_from_slice(s.as_bytes());
666    }
667
668    fn create_v2_data() -> Vec<u8> {
669        let mut data = Vec::new();
670        data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
671        str_arg("Alt Coin", &mut data);
672        str_arg("ALT", &mut data);
673        str_arg("https://example.invalid/alt.json", &mut data);
674        data.extend_from_slice(Pubkey::new_unique().as_ref());
675        data.push(1);
676        data.push(1);
677        data
678    }
679
680    fn grpc_pumpfun_create_v2_tx(
681        static_len: usize,
682        writable_len: usize,
683        program_idx: u8,
684        ix_accounts: Vec<u8>,
685        account_overrides: Vec<(usize, Pubkey)>,
686    ) -> (TransactionStatusMeta, Option<Transaction>) {
687        let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
688        account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
689        let readonly_len = account_overrides
690            .iter()
691            .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
692            .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
693            .max()
694            .unwrap_or_default();
695        let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
696        let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
697        for (global_idx, key) in account_overrides {
698            if global_idx < static_len {
699                account_keys[global_idx] = key;
700            } else if global_idx < static_len + writable_len {
701                loaded_writable[global_idx - static_len] = key;
702            } else {
703                loaded_readonly[global_idx - static_len - writable_len] = key;
704            }
705        }
706
707        let meta = TransactionStatusMeta {
708            loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
709            loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
710            ..Default::default()
711        };
712        let tx = Transaction {
713            signatures: vec![Signature::default().as_ref().to_vec()],
714            message: Some(Message {
715                header: Some(MessageHeader {
716                    num_required_signatures: 1,
717                    num_readonly_signed_accounts: 0,
718                    num_readonly_unsigned_accounts: 0,
719                }),
720                account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
721                recent_blockhash: vec![0; 32],
722                instructions: vec![CompiledInstruction {
723                    program_id_index: program_idx as u32,
724                    accounts: ix_accounts,
725                    data: create_v2_data(),
726                }],
727                versioned: true,
728                address_table_lookups: Vec::new(),
729            }),
730        };
731        (meta, Some(tx))
732    }
733
734    fn create_v2_accounts(
735        account_len: usize,
736        program_idx: u8,
737        mint_idx: u8,
738        user_idx: u8,
739        token_program_idx: u8,
740        quote_tail: Option<(u8, u8, u8)>,
741    ) -> Vec<u8> {
742        let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
743        accounts[0] = mint_idx;
744        accounts[5] = user_idx;
745        accounts[7] = token_program_idx;
746        if account_len > 15 {
747            accounts[15] = program_idx;
748        }
749        if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
750            accounts[16] = quote_idx;
751            accounts[17] = quote_vault_idx;
752            accounts[18] = quote_token_program_idx;
753        }
754        accounts
755    }
756
757    fn parse_create_v2_from_grpc(
758        meta: &TransactionStatusMeta,
759        tx: &Option<Transaction>,
760    ) -> crate::core::events::PumpFunCreateTokenEvent {
761        let events = parse_instructions_enhanced(
762            meta,
763            tx,
764            Signature::default(),
765            123,
766            0,
767            Some(456),
768            789,
769            None,
770        );
771        assert_eq!(events.len(), 1);
772        match &events[0] {
773            DexEvent::PumpFunCreate(e) => {
774                assert_eq!(e.ix_name, "create_v2");
775                e.clone()
776            }
777            DexEvent::PumpFunCreateV2(e) => {
778                assert_eq!(e.ix_name, "create_v2");
779                crate::core::events::PumpFunCreateTokenEvent {
780                    metadata: e.metadata.clone(),
781                    name: e.name.clone(),
782                    symbol: e.symbol.clone(),
783                    uri: e.uri.clone(),
784                    mint: e.mint,
785                    bonding_curve: e.bonding_curve,
786                    user: e.user,
787                    creator: e.creator,
788                    timestamp: e.timestamp,
789                    virtual_token_reserves: e.virtual_token_reserves,
790                    virtual_sol_reserves: e.virtual_sol_reserves,
791                    real_token_reserves: e.real_token_reserves,
792                    token_total_supply: e.token_total_supply,
793                    mint_authority: e.mint_authority,
794                    associated_bonding_curve: e.associated_bonding_curve,
795                    global: e.global,
796                    system_program: e.system_program,
797                    token_program: e.token_program,
798                    associated_token_program: e.associated_token_program,
799                    mayhem_program_id: e.mayhem_program_id,
800                    global_params: e.global_params,
801                    sol_vault: e.sol_vault,
802                    mayhem_state: e.mayhem_state,
803                    mayhem_token_vault: e.mayhem_token_vault,
804                    event_authority: e.event_authority,
805                    program: e.program,
806                    quote_mint: e.quote_mint,
807                    quote_vault: e.quote_vault,
808                    quote_token_program: e.quote_token_program,
809                    virtual_quote_reserves: e.virtual_quote_reserves,
810                    ix_name: e.ix_name.clone(),
811                    is_mayhem_mode: e.is_mayhem_mode,
812                    is_cashback_enabled: e.is_cashback_enabled,
813                    observed_fee_recipient: e.observed_fee_recipient,
814                }
815            }
816            other => panic!("expected PumpFun create_v2 event, got {other:?}"),
817        }
818    }
819
820    #[test]
821    fn test_should_parse_instructions() {
822        // 无 filter - 应该解析
823        assert!(should_parse_instructions(None));
824
825        // 有 filter 但 include_only 为空 - 应该解析
826        let filter = EventTypeFilter { include_only: None, exclude_types: None };
827        assert!(should_parse_instructions(Some(&filter)));
828
829        // 包含需要 instruction 解析的事件类型
830        use crate::grpc::types::EventType;
831        let filter = EventTypeFilter {
832            include_only: Some(vec![EventType::PumpFunMigrate]),
833            exclude_types: None,
834        };
835        assert!(should_parse_instructions(Some(&filter)));
836
837        // PumpFun 订阅:需要 instruction+inner,避免仅日志时截断丢腿
838        let filter = EventTypeFilter {
839            include_only: Some(vec![EventType::PumpFunTrade]),
840            exclude_types: None,
841        };
842        assert!(should_parse_instructions(Some(&filter)));
843
844        for event_type in [
845            EventType::PumpSwapTrade,
846            EventType::PumpFeesUpdateFeeShares,
847            EventType::RaydiumLaunchlabTrade,
848            EventType::RaydiumCpmmSwap,
849            EventType::RaydiumClmmSwap,
850            EventType::RaydiumAmmV4Swap,
851            EventType::OrcaWhirlpoolSwap,
852            EventType::MeteoraPoolsSwap,
853            EventType::MeteoraDammV2Swap,
854            EventType::MeteoraDammV2InitializePool,
855            EventType::MeteoraDlmmSwap,
856        ] {
857            let filter = EventTypeFilter::include_only(vec![event_type]);
858            assert!(
859                should_parse_instructions(Some(&filter)),
860                "instruction parsing should be enabled for {event_type:?}"
861            );
862        }
863
864        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
865        assert!(
866            !should_parse_instructions(Some(&filter)),
867            "DBC events are log-only until an instruction parser is implemented"
868        );
869
870        let filter = EventTypeFilter::include_only(vec![
871            EventType::AccountPumpFunGlobal,
872            EventType::AccountRaydiumClmmPoolState,
873            EventType::AccountRaydiumCpmmPoolState,
874            EventType::AccountOrcaWhirlpool,
875        ]);
876        assert!(
877            !should_parse_instructions(Some(&filter)),
878            "account-only non-Pump filters should stay on the account update path"
879        );
880    }
881
882    #[test]
883    fn test_merge_instruction_events() {
884        use solana_sdk::signature::Signature;
885
886        let metadata = EventMetadata {
887            signature: Signature::default(),
888            slot: 100,
889            tx_index: 1,
890            block_time_us: 1000,
891            grpc_recv_us: 2000,
892            recent_blockhash: None,
893        };
894
895        // 模拟:outer instruction + inner instruction(应该合并)
896        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
897            metadata: metadata.clone(),
898            bonding_curve: Pubkey::new_unique(),
899            ..Default::default()
900        });
901
902        let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
903            metadata: metadata.clone(),
904            sol_amount: 1000,
905            token_amount: 2000,
906            ..Default::default()
907        });
908
909        let events = vec![
910            IndexedInstructionEvent {
911                outer_idx: 0,
912                inner_idx: None,
913                stack_height: Some(1),
914                is_dlmm_event_cpi: false,
915                event: outer_event,
916            },
917            IndexedInstructionEvent {
918                outer_idx: 0,
919                inner_idx: Some(0),
920                stack_height: Some(2),
921                is_dlmm_event_cpi: false,
922                event: inner_event,
923            },
924        ];
925
926        let result = merge_instruction_events(events);
927
928        // 应该合并为1个事件
929        assert_eq!(result.len(), 1);
930
931        // 验证合并结果包含两者的数据
932        if let DexEvent::PumpFunTrade(trade) = &result[0] {
933            assert_eq!(trade.sol_amount, 1000); // 来自 inner
934            assert_eq!(trade.token_amount, 2000); // 来自 inner
935            assert_ne!(trade.bonding_curve, Pubkey::default()); // 来自 outer
936        } else {
937            panic!("Expected PumpFunTrade event");
938        }
939    }
940
941    #[test]
942    fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
943        use solana_sdk::signature::Signature;
944
945        let metadata = EventMetadata {
946            signature: Signature::default(),
947            slot: 100,
948            tx_index: 1,
949            block_time_us: 1000,
950            grpc_recv_us: 2000,
951            recent_blockhash: None,
952        };
953
954        let bc = Pubkey::new_unique();
955        let fee = Pubkey::new_unique();
956
957        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
958            metadata: metadata.clone(),
959            bonding_curve: bc,
960            ..Default::default()
961        });
962
963        let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
964            metadata: metadata.clone(),
965            sol_amount: 1000,
966            token_amount: 2000,
967            is_buy: true,
968            ..Default::default()
969        });
970
971        // 第二段 inner 仅有 fee_recipient,无成交量 —— 不应抹掉第一段金额
972        let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
973            metadata: metadata.clone(),
974            fee_recipient: fee,
975            ..Default::default()
976        });
977
978        let events = vec![
979            IndexedInstructionEvent {
980                outer_idx: 0,
981                inner_idx: None,
982                stack_height: Some(1),
983                is_dlmm_event_cpi: false,
984                event: outer_event,
985            },
986            IndexedInstructionEvent {
987                outer_idx: 0,
988                inner_idx: Some(0),
989                stack_height: Some(2),
990                is_dlmm_event_cpi: false,
991                event: inner_trade,
992            },
993            IndexedInstructionEvent {
994                outer_idx: 0,
995                inner_idx: Some(1),
996                stack_height: Some(2),
997                is_dlmm_event_cpi: false,
998                event: inner_fee_only,
999            },
1000        ];
1001
1002        let result = merge_instruction_events(events);
1003        assert_eq!(result.len(), 1);
1004        if let DexEvent::PumpFunTrade(trade) = &result[0] {
1005            assert_eq!(trade.bonding_curve, bc);
1006            assert_eq!(trade.sol_amount, 1000);
1007            assert_eq!(trade.token_amount, 2000);
1008            assert_eq!(trade.fee_recipient, fee);
1009        } else {
1010            panic!("Expected PumpFunTrade event");
1011        }
1012    }
1013
1014    fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1015        DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1016            metadata: EventMetadata::default(),
1017            token_x_mint: Pubkey::default(),
1018            token_y_mint: Pubkey::default(),
1019            min_amount_out: 0,
1020            pool,
1021            from: Pubkey::default(),
1022            start_bin_id: 0,
1023            end_bin_id: 0,
1024            amount_in,
1025            amount_out,
1026            swap_for_y: false,
1027            fee: 0,
1028            protocol_fee: 0,
1029            fee_bps: 0,
1030            host_fee: 0,
1031        })
1032    }
1033
1034    fn dlmm_add_liquidity() -> DexEvent {
1035        DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1036            metadata: EventMetadata::default(),
1037            pool: Pubkey::default(),
1038            from: Pubkey::default(),
1039            position: Pubkey::default(),
1040            amounts: [0; 2],
1041            active_bin_id: 0,
1042        })
1043    }
1044
1045    #[test]
1046    fn merge_preserves_unrelated_inner_events() {
1047        let events = vec![
1048            IndexedInstructionEvent {
1049                outer_idx: 0,
1050                inner_idx: None,
1051                stack_height: Some(1),
1052                is_dlmm_event_cpi: false,
1053                event: dlmm_swap(Pubkey::default(), 0, 0),
1054            },
1055            IndexedInstructionEvent {
1056                outer_idx: 0,
1057                inner_idx: Some(0),
1058                stack_height: Some(2),
1059                is_dlmm_event_cpi: false,
1060                event: dlmm_add_liquidity(),
1061            },
1062        ];
1063
1064        let result = merge_instruction_events(events);
1065        assert_eq!(result.len(), 2);
1066        assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1067        assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1068    }
1069
1070    #[test]
1071    fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1072        let first_pool = Pubkey::new_unique();
1073        let second_pool = Pubkey::new_unique();
1074        let events = vec![
1075            IndexedInstructionEvent {
1076                outer_idx: 0,
1077                inner_idx: Some(0),
1078                stack_height: Some(2),
1079                is_dlmm_event_cpi: false,
1080                event: dlmm_swap(first_pool, 1, 0),
1081            },
1082            IndexedInstructionEvent {
1083                outer_idx: 0,
1084                inner_idx: Some(1),
1085                stack_height: Some(3),
1086                is_dlmm_event_cpi: true,
1087                event: dlmm_swap(first_pool, 10, 9),
1088            },
1089            IndexedInstructionEvent {
1090                outer_idx: 0,
1091                inner_idx: Some(2),
1092                stack_height: Some(2),
1093                is_dlmm_event_cpi: false,
1094                event: dlmm_swap(second_pool, 2, 0),
1095            },
1096            IndexedInstructionEvent {
1097                outer_idx: 0,
1098                inner_idx: Some(3),
1099                stack_height: Some(3),
1100                is_dlmm_event_cpi: true,
1101                event: dlmm_swap(second_pool, 20, 18),
1102            },
1103        ];
1104
1105        let result = merge_instruction_events(events);
1106        assert_eq!(result.len(), 2);
1107        let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1108        let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1109        assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1110        assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1111    }
1112
1113    #[test]
1114    fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1115        let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1116        let static_keys = [
1117            "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1118            "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1119            "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1120            "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1121            "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1122            "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1123            "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1124            "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1125            "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1126            "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1127            "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1128            "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1129            "ComputeBudget111111111111111111111111111111",
1130            "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1131            "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1132            "SysvarRent111111111111111111111111111111111",
1133        ];
1134        let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1135        let loaded_readonly = [
1136            "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1137            "So11111111111111111111111111111111111111112",
1138            "11111111111111111111111111111111",
1139            "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1140            "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1141            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1142            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1143            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1144            "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1145            "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1146        ];
1147        let meta = TransactionStatusMeta {
1148            loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1149            loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1150            inner_instructions: vec![InnerInstructions {
1151                index: 2,
1152                instructions: vec![InnerInstruction {
1153                    program_id_index: 20,
1154                    accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1155                    data: decode_b58(
1156                        "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1157                    ),
1158                    stack_height: Some(2),
1159                }],
1160            }],
1161            ..Default::default()
1162        };
1163        let tx = Some(Transaction {
1164            signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1165            message: Some(Message {
1166                header: Some(MessageHeader::default()),
1167                account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1168                recent_blockhash: vec![0; 32],
1169                instructions: vec![
1170                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1171                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1172                    CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1173                ],
1174                versioned: true,
1175                address_table_lookups: Vec::new(),
1176            }),
1177        });
1178
1179        let events = parse_instructions_enhanced(
1180            &meta,
1181            &tx,
1182            signature.parse().unwrap(),
1183            427_039_576,
1184            0,
1185            Some(1_781_687_252_000_000),
1186            789,
1187            None,
1188        );
1189
1190        assert_eq!(events.len(), 1, "{signature}");
1191        match &events[0] {
1192            DexEvent::PumpSwapCreatePool(e) => {
1193                assert_eq!(e.index, 0, "{signature}");
1194                assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1195                assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1196                assert_eq!(
1197                    e.coin_creator,
1198                    pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1199                    "{signature}"
1200                );
1201                assert!(!e.is_mayhem_mode, "{signature}");
1202                assert!(e.is_cashback_coin, "{signature}");
1203                assert_eq!(
1204                    e.pool,
1205                    pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1206                    "{signature}"
1207                );
1208                assert_eq!(
1209                    e.creator,
1210                    pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1211                    "{signature}"
1212                );
1213                assert_eq!(
1214                    e.base_mint,
1215                    pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1216                    "{signature}"
1217                );
1218                assert_eq!(
1219                    e.quote_mint,
1220                    pk("So11111111111111111111111111111111111111112"),
1221                    "{signature}"
1222                );
1223                assert_eq!(
1224                    e.lp_mint,
1225                    pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1226                    "{signature}"
1227                );
1228                assert_eq!(
1229                    e.user_base_token_account,
1230                    pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1231                    "{signature}"
1232                );
1233                assert_eq!(
1234                    e.user_quote_token_account,
1235                    pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1236                    "{signature}"
1237                );
1238            }
1239            other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1240        }
1241    }
1242
1243    #[test]
1244    fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1245        struct Case {
1246            signature: &'static str,
1247            name: &'static str,
1248            static_len: usize,
1249            writable_len: usize,
1250            program_idx: u8,
1251            account_len: usize,
1252            mint_idx: u8,
1253            mint: &'static str,
1254            user_idx: u8,
1255            user: &'static str,
1256            token_program_idx: u8,
1257            quote_idx: u8,
1258            quote_mint: Pubkey,
1259            quote_vault_idx: u8,
1260            quote_vault: &'static str,
1261            quote_token_program_idx: u8,
1262        }
1263        let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1264        let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1265        let cases = [
1266            Case {
1267                signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1268                name: "19-account WSOL quote in ALT",
1269                static_len: 15,
1270                writable_len: 7,
1271                program_idx: 12,
1272                account_len: 19,
1273                mint_idx: 1,
1274                mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1275                user_idx: 0,
1276                user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1277                token_program_idx: 24,
1278                quote_idx: 27,
1279                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1280                quote_vault_idx: 7,
1281                quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1282                quote_token_program_idx: 28,
1283            },
1284            Case {
1285                signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1286                name: "19-account WSOL quote in ALT with exact quote buy",
1287                static_len: 20,
1288                writable_len: 7,
1289                program_idx: 15,
1290                account_len: 19,
1291                mint_idx: 1,
1292                mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1293                user_idx: 0,
1294                user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1295                token_program_idx: 31,
1296                quote_idx: 28,
1297                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1298                quote_vault_idx: 4,
1299                quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1300                quote_token_program_idx: 30,
1301            },
1302            Case {
1303                signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1304                name: "19-account USDC quote in ALT",
1305                static_len: 19,
1306                writable_len: 6,
1307                program_idx: 16,
1308                account_len: 19,
1309                mint_idx: 1,
1310                mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1311                user_idx: 0,
1312                user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1313                token_program_idx: 27,
1314                quote_idx: 30,
1315                quote_mint: usdc_mint(),
1316                quote_vault_idx: 6,
1317                quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1318                quote_token_program_idx: 31,
1319            },
1320            Case {
1321                signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1322                name: "20-account WSOL quote in ALT",
1323                static_len: 15,
1324                writable_len: 7,
1325                program_idx: 12,
1326                account_len: 20,
1327                mint_idx: 1,
1328                mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1329                user_idx: 0,
1330                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1331                token_program_idx: 24,
1332                quote_idx: 27,
1333                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1334                quote_vault_idx: 7,
1335                quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1336                quote_token_program_idx: 28,
1337            },
1338            Case {
1339                signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1340                name: "19-account WSOL quote in ALT with later buy",
1341                static_len: 18,
1342                writable_len: 7,
1343                program_idx: 13,
1344                account_len: 19,
1345                mint_idx: 1,
1346                mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1347                user_idx: 0,
1348                user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1349                token_program_idx: 27,
1350                quote_idx: 30,
1351                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1352                quote_vault_idx: 7,
1353                quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1354                quote_token_program_idx: 31,
1355            },
1356            Case {
1357                signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1358                name: "19-account USDC quote in ALT exact quote buy",
1359                static_len: 19,
1360                writable_len: 6,
1361                program_idx: 15,
1362                account_len: 19,
1363                mint_idx: 1,
1364                mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1365                user_idx: 0,
1366                user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1367                token_program_idx: 28,
1368                quote_idx: 33,
1369                quote_mint: usdc_mint(),
1370                quote_vault_idx: 7,
1371                quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1372                quote_token_program_idx: 27,
1373            },
1374            Case {
1375                signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1376                name: "20-account WSOL quote in ALT with jit account",
1377                static_len: 15,
1378                writable_len: 7,
1379                program_idx: 12,
1380                account_len: 20,
1381                mint_idx: 1,
1382                mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1383                user_idx: 0,
1384                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1385                token_program_idx: 24,
1386                quote_idx: 27,
1387                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1388                quote_vault_idx: 7,
1389                quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1390                quote_token_program_idx: 28,
1391            },
1392        ];
1393
1394        for case in cases {
1395            let (meta, tx) = grpc_pumpfun_create_v2_tx(
1396                case.static_len,
1397                case.writable_len,
1398                case.program_idx,
1399                create_v2_accounts(
1400                    case.account_len,
1401                    case.program_idx,
1402                    case.mint_idx,
1403                    case.user_idx,
1404                    case.token_program_idx,
1405                    Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1406                ),
1407                vec![
1408                    (case.mint_idx as usize, pk(case.mint)),
1409                    (case.user_idx as usize, pk(case.user)),
1410                    (case.token_program_idx as usize, token_2022_program),
1411                    (case.quote_idx as usize, case.quote_mint),
1412                    (case.quote_vault_idx as usize, pk(case.quote_vault)),
1413                    (case.quote_token_program_idx as usize, spl_token_program),
1414                ],
1415            );
1416            let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1417                let idx = global_idx as usize;
1418                if idx < case.static_len {
1419                    ("static", idx)
1420                } else if idx < case.static_len + case.writable_len {
1421                    ("writable", idx - case.static_len)
1422                } else {
1423                    ("readonly", idx - case.static_len - case.writable_len)
1424                }
1425            };
1426            assert_eq!(
1427                meta.loaded_writable_addresses.len(),
1428                case.writable_len,
1429                "{}: {}",
1430                case.name,
1431                case.signature
1432            );
1433            for (global_idx, expected_key) in [
1434                (case.token_program_idx, token_2022_program),
1435                (case.quote_idx, case.quote_mint),
1436                (case.quote_token_program_idx, spl_token_program),
1437            ] {
1438                match loaded_key_location(global_idx) {
1439                    ("static", _) => {}
1440                    ("writable", offset) => assert_eq!(
1441                        read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1442                        expected_key,
1443                        "{}: writable loaded key {global_idx}: {}",
1444                        case.name,
1445                        case.signature
1446                    ),
1447                    ("readonly", offset) => assert_eq!(
1448                        read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1449                        expected_key,
1450                        "{}: readonly loaded key {global_idx}: {}",
1451                        case.name,
1452                        case.signature
1453                    ),
1454                    _ => unreachable!(),
1455                }
1456            }
1457
1458            let create = parse_create_v2_from_grpc(&meta, &tx);
1459
1460            assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1461            assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1462            assert_eq!(
1463                create.token_program, token_2022_program,
1464                "{}: {}",
1465                case.name, case.signature
1466            );
1467            assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1468            assert_eq!(
1469                create.quote_vault,
1470                pk(case.quote_vault),
1471                "{}: {}",
1472                case.name,
1473                case.signature
1474            );
1475            assert_eq!(
1476                create.quote_token_program, spl_token_program,
1477                "{}: {}",
1478                case.name, case.signature
1479            );
1480        }
1481    }
1482
1483    #[test]
1484    fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1485        let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1486        let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1487        let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1488        let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1489        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1490            16,
1491            5,
1492            12,
1493            create_v2_accounts(16, 12, 1, 0, 24, None),
1494            vec![(1, mint), (0, user), (24, token_program)],
1495        );
1496
1497        let create = parse_create_v2_from_grpc(&meta, &tx);
1498
1499        assert_eq!(create.mint, mint, "{signature}");
1500        assert_eq!(create.user, user, "{signature}");
1501        assert_eq!(create.token_program, token_program, "{signature}");
1502        assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1503        assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1504        assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1505    }
1506
1507    #[test]
1508    fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1509        let quote_vault = Pubkey::new_unique();
1510        let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1511        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1512            19,
1513            6,
1514            16,
1515            create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1516            vec![
1517                (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1518                (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1519                (6, quote_vault),
1520                (31, quote_token_program),
1521            ],
1522        );
1523
1524        let create = parse_create_v2_from_grpc(&meta, &tx);
1525
1526        assert_eq!(create.quote_mint, Pubkey::default());
1527        assert_eq!(create.quote_vault, Pubkey::default());
1528        assert_eq!(create.quote_token_program, Pubkey::default());
1529    }
1530}