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