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                    ix_name: e.ix_name.clone(),
817                    is_mayhem_mode: e.is_mayhem_mode,
818                    is_cashback_enabled: e.is_cashback_enabled,
819                    observed_fee_recipient: e.observed_fee_recipient,
820                }
821            }
822            other => panic!("expected PumpFun create_v2 event, got {other:?}"),
823        }
824    }
825
826    #[test]
827    fn test_should_parse_instructions() {
828        // 无 filter - 应该解析
829        assert!(should_parse_instructions(None));
830
831        // 有 filter 但 include_only 为空 - 应该解析
832        let filter = EventTypeFilter { include_only: None, exclude_types: None };
833        assert!(should_parse_instructions(Some(&filter)));
834
835        // 包含需要 instruction 解析的事件类型
836        use crate::grpc::types::EventType;
837        let filter = EventTypeFilter {
838            include_only: Some(vec![EventType::PumpFunMigrate]),
839            exclude_types: None,
840        };
841        assert!(should_parse_instructions(Some(&filter)));
842
843        // PumpFun 订阅:需要 instruction+inner,避免仅日志时截断丢腿
844        let filter = EventTypeFilter {
845            include_only: Some(vec![EventType::PumpFunTrade]),
846            exclude_types: None,
847        };
848        assert!(should_parse_instructions(Some(&filter)));
849
850        for event_type in [
851            EventType::PumpSwapTrade,
852            EventType::PumpFeesUpdateFeeShares,
853            EventType::RaydiumLaunchlabTrade,
854            EventType::RaydiumCpmmSwap,
855            EventType::RaydiumClmmSwap,
856            EventType::RaydiumAmmV4Swap,
857            EventType::OrcaWhirlpoolSwap,
858            EventType::MeteoraPoolsSwap,
859            EventType::MeteoraDammV2Swap,
860            EventType::MeteoraDammV2InitializePool,
861            EventType::MeteoraDlmmSwap,
862        ] {
863            let filter = EventTypeFilter::include_only(vec![event_type]);
864            assert!(
865                should_parse_instructions(Some(&filter)),
866                "instruction parsing should be enabled for {event_type:?}"
867            );
868        }
869
870        let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
871        assert!(
872            !should_parse_instructions(Some(&filter)),
873            "DBC events are log-only until an instruction parser is implemented"
874        );
875
876        let filter = EventTypeFilter::include_only(vec![
877            EventType::AccountPumpFunGlobal,
878            EventType::AccountRaydiumClmmPoolState,
879            EventType::AccountRaydiumCpmmPoolState,
880            EventType::AccountOrcaWhirlpool,
881        ]);
882        assert!(
883            !should_parse_instructions(Some(&filter)),
884            "account-only non-Pump filters should stay on the account update path"
885        );
886    }
887
888    #[test]
889    fn test_merge_instruction_events() {
890        use solana_sdk::signature::Signature;
891
892        let metadata = EventMetadata {
893            signature: Signature::default(),
894            slot: 100,
895            tx_index: 1,
896            block_time_us: 1000,
897            grpc_recv_us: 2000,
898            recent_blockhash: None,
899        };
900
901        // 模拟:outer instruction + inner instruction(应该合并)
902        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
903            metadata: metadata.clone(),
904            bonding_curve: Pubkey::new_unique(),
905            ..Default::default()
906        });
907
908        let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
909            metadata: metadata.clone(),
910            sol_amount: 1000,
911            token_amount: 2000,
912            ..Default::default()
913        });
914
915        let events = vec![
916            IndexedInstructionEvent {
917                outer_idx: 0,
918                inner_idx: None,
919                stack_height: Some(1),
920                is_dlmm_event_cpi: false,
921                event: outer_event,
922            },
923            IndexedInstructionEvent {
924                outer_idx: 0,
925                inner_idx: Some(0),
926                stack_height: Some(2),
927                is_dlmm_event_cpi: false,
928                event: inner_event,
929            },
930        ];
931
932        let result = merge_instruction_events(events);
933
934        // 应该合并为1个事件
935        assert_eq!(result.len(), 1);
936
937        // 验证合并结果包含两者的数据
938        if let DexEvent::PumpFunTrade(trade) = &result[0] {
939            assert_eq!(trade.sol_amount, 1000); // 来自 inner
940            assert_eq!(trade.token_amount, 2000); // 来自 inner
941            assert_ne!(trade.bonding_curve, Pubkey::default()); // 来自 outer
942        } else {
943            panic!("Expected PumpFunTrade event");
944        }
945    }
946
947    #[test]
948    fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
949        use solana_sdk::signature::Signature;
950
951        let metadata = EventMetadata {
952            signature: Signature::default(),
953            slot: 100,
954            tx_index: 1,
955            block_time_us: 1000,
956            grpc_recv_us: 2000,
957            recent_blockhash: None,
958        };
959
960        let bc = Pubkey::new_unique();
961        let fee = Pubkey::new_unique();
962
963        let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
964            metadata: metadata.clone(),
965            bonding_curve: bc,
966            ..Default::default()
967        });
968
969        let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
970            metadata: metadata.clone(),
971            sol_amount: 1000,
972            token_amount: 2000,
973            is_buy: true,
974            ..Default::default()
975        });
976
977        // 第二段 inner 仅有 fee_recipient,无成交量 —— 不应抹掉第一段金额
978        let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
979            metadata: metadata.clone(),
980            fee_recipient: fee,
981            ..Default::default()
982        });
983
984        let events = vec![
985            IndexedInstructionEvent {
986                outer_idx: 0,
987                inner_idx: None,
988                stack_height: Some(1),
989                is_dlmm_event_cpi: false,
990                event: outer_event,
991            },
992            IndexedInstructionEvent {
993                outer_idx: 0,
994                inner_idx: Some(0),
995                stack_height: Some(2),
996                is_dlmm_event_cpi: false,
997                event: inner_trade,
998            },
999            IndexedInstructionEvent {
1000                outer_idx: 0,
1001                inner_idx: Some(1),
1002                stack_height: Some(2),
1003                is_dlmm_event_cpi: false,
1004                event: inner_fee_only,
1005            },
1006        ];
1007
1008        let result = merge_instruction_events(events);
1009        assert_eq!(result.len(), 1);
1010        if let DexEvent::PumpFunTrade(trade) = &result[0] {
1011            assert_eq!(trade.bonding_curve, bc);
1012            assert_eq!(trade.sol_amount, 1000);
1013            assert_eq!(trade.token_amount, 2000);
1014            assert_eq!(trade.fee_recipient, fee);
1015        } else {
1016            panic!("Expected PumpFunTrade event");
1017        }
1018    }
1019
1020    fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1021        DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1022            metadata: EventMetadata::default(),
1023            token_x_mint: Pubkey::default(),
1024            token_y_mint: Pubkey::default(),
1025            user_token_in: Pubkey::default(),
1026            user_token_out: Pubkey::default(),
1027            min_amount_out: 0,
1028            pool,
1029            from: Pubkey::default(),
1030            start_bin_id: 0,
1031            end_bin_id: 0,
1032            amount_in,
1033            amount_out,
1034            swap_for_y: false,
1035            fee: 0,
1036            protocol_fee: 0,
1037            fee_bps: 0,
1038            host_fee: 0,
1039        })
1040    }
1041
1042    fn dlmm_add_liquidity() -> DexEvent {
1043        DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1044            metadata: EventMetadata::default(),
1045            pool: Pubkey::default(),
1046            from: Pubkey::default(),
1047            position: Pubkey::default(),
1048            amounts: [0; 2],
1049            active_bin_id: 0,
1050        })
1051    }
1052
1053    #[test]
1054    fn merge_preserves_unrelated_inner_events() {
1055        let events = vec![
1056            IndexedInstructionEvent {
1057                outer_idx: 0,
1058                inner_idx: None,
1059                stack_height: Some(1),
1060                is_dlmm_event_cpi: false,
1061                event: dlmm_swap(Pubkey::default(), 0, 0),
1062            },
1063            IndexedInstructionEvent {
1064                outer_idx: 0,
1065                inner_idx: Some(0),
1066                stack_height: Some(2),
1067                is_dlmm_event_cpi: false,
1068                event: dlmm_add_liquidity(),
1069            },
1070        ];
1071
1072        let result = merge_instruction_events(events);
1073        assert_eq!(result.len(), 2);
1074        assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1075        assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1076    }
1077
1078    #[test]
1079    fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1080        let first_pool = Pubkey::new_unique();
1081        let second_pool = Pubkey::new_unique();
1082        let events = vec![
1083            IndexedInstructionEvent {
1084                outer_idx: 0,
1085                inner_idx: Some(0),
1086                stack_height: Some(2),
1087                is_dlmm_event_cpi: false,
1088                event: dlmm_swap(first_pool, 1, 0),
1089            },
1090            IndexedInstructionEvent {
1091                outer_idx: 0,
1092                inner_idx: Some(1),
1093                stack_height: Some(3),
1094                is_dlmm_event_cpi: true,
1095                event: dlmm_swap(first_pool, 10, 9),
1096            },
1097            IndexedInstructionEvent {
1098                outer_idx: 0,
1099                inner_idx: Some(2),
1100                stack_height: Some(2),
1101                is_dlmm_event_cpi: false,
1102                event: dlmm_swap(second_pool, 2, 0),
1103            },
1104            IndexedInstructionEvent {
1105                outer_idx: 0,
1106                inner_idx: Some(3),
1107                stack_height: Some(3),
1108                is_dlmm_event_cpi: true,
1109                event: dlmm_swap(second_pool, 20, 18),
1110            },
1111        ];
1112
1113        let result = merge_instruction_events(events);
1114        assert_eq!(result.len(), 2);
1115        let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1116        let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1117        assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1118        assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1119    }
1120
1121    #[test]
1122    fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1123        let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1124        let static_keys = [
1125            "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1126            "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1127            "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1128            "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1129            "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1130            "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1131            "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1132            "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1133            "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1134            "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1135            "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1136            "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1137            "ComputeBudget111111111111111111111111111111",
1138            "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1139            "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1140            "SysvarRent111111111111111111111111111111111",
1141        ];
1142        let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1143        let loaded_readonly = [
1144            "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1145            "So11111111111111111111111111111111111111112",
1146            "11111111111111111111111111111111",
1147            "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1148            "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1149            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1150            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1151            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1152            "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1153            "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1154        ];
1155        let meta = TransactionStatusMeta {
1156            loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1157            loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1158            inner_instructions: vec![InnerInstructions {
1159                index: 2,
1160                instructions: vec![InnerInstruction {
1161                    program_id_index: 20,
1162                    accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1163                    data: decode_b58(
1164                        "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1165                    ),
1166                    stack_height: Some(2),
1167                }],
1168            }],
1169            ..Default::default()
1170        };
1171        let tx = Some(Transaction {
1172            signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1173            message: Some(Message {
1174                header: Some(MessageHeader::default()),
1175                account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1176                recent_blockhash: vec![0; 32],
1177                instructions: vec![
1178                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1179                    CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1180                    CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1181                ],
1182                versioned: true,
1183                address_table_lookups: Vec::new(),
1184                config: None,
1185            }),
1186        });
1187
1188        let events = parse_instructions_enhanced(
1189            &meta,
1190            &tx,
1191            signature.parse().unwrap(),
1192            427_039_576,
1193            0,
1194            Some(1_781_687_252_000_000),
1195            789,
1196            None,
1197        );
1198
1199        assert_eq!(events.len(), 1, "{signature}");
1200        match &events[0] {
1201            DexEvent::PumpSwapCreatePool(e) => {
1202                assert_eq!(e.index, 0, "{signature}");
1203                assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1204                assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1205                assert_eq!(
1206                    e.coin_creator,
1207                    pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1208                    "{signature}"
1209                );
1210                assert!(!e.is_mayhem_mode, "{signature}");
1211                assert!(e.is_cashback_coin, "{signature}");
1212                assert_eq!(
1213                    e.pool,
1214                    pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1215                    "{signature}"
1216                );
1217                assert_eq!(
1218                    e.creator,
1219                    pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1220                    "{signature}"
1221                );
1222                assert_eq!(
1223                    e.base_mint,
1224                    pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1225                    "{signature}"
1226                );
1227                assert_eq!(
1228                    e.quote_mint,
1229                    pk("So11111111111111111111111111111111111111112"),
1230                    "{signature}"
1231                );
1232                assert_eq!(
1233                    e.lp_mint,
1234                    pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1235                    "{signature}"
1236                );
1237                assert_eq!(
1238                    e.user_base_token_account,
1239                    pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1240                    "{signature}"
1241                );
1242                assert_eq!(
1243                    e.user_quote_token_account,
1244                    pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1245                    "{signature}"
1246                );
1247            }
1248            other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1249        }
1250    }
1251
1252    #[test]
1253    fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1254        struct Case {
1255            signature: &'static str,
1256            name: &'static str,
1257            static_len: usize,
1258            writable_len: usize,
1259            program_idx: u8,
1260            account_len: usize,
1261            mint_idx: u8,
1262            mint: &'static str,
1263            user_idx: u8,
1264            user: &'static str,
1265            token_program_idx: u8,
1266            quote_idx: u8,
1267            quote_mint: Pubkey,
1268            quote_vault_idx: u8,
1269            quote_vault: &'static str,
1270            quote_token_program_idx: u8,
1271        }
1272        let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1273        let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1274        let cases = [
1275            Case {
1276                signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1277                name: "19-account WSOL quote in ALT",
1278                static_len: 15,
1279                writable_len: 7,
1280                program_idx: 12,
1281                account_len: 19,
1282                mint_idx: 1,
1283                mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1284                user_idx: 0,
1285                user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1286                token_program_idx: 24,
1287                quote_idx: 27,
1288                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1289                quote_vault_idx: 7,
1290                quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1291                quote_token_program_idx: 28,
1292            },
1293            Case {
1294                signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1295                name: "19-account WSOL quote in ALT with exact quote buy",
1296                static_len: 20,
1297                writable_len: 7,
1298                program_idx: 15,
1299                account_len: 19,
1300                mint_idx: 1,
1301                mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1302                user_idx: 0,
1303                user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1304                token_program_idx: 31,
1305                quote_idx: 28,
1306                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1307                quote_vault_idx: 4,
1308                quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1309                quote_token_program_idx: 30,
1310            },
1311            Case {
1312                signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1313                name: "19-account USDC quote in ALT",
1314                static_len: 19,
1315                writable_len: 6,
1316                program_idx: 16,
1317                account_len: 19,
1318                mint_idx: 1,
1319                mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1320                user_idx: 0,
1321                user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1322                token_program_idx: 27,
1323                quote_idx: 30,
1324                quote_mint: usdc_mint(),
1325                quote_vault_idx: 6,
1326                quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1327                quote_token_program_idx: 31,
1328            },
1329            Case {
1330                signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1331                name: "20-account WSOL quote in ALT",
1332                static_len: 15,
1333                writable_len: 7,
1334                program_idx: 12,
1335                account_len: 20,
1336                mint_idx: 1,
1337                mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1338                user_idx: 0,
1339                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1340                token_program_idx: 24,
1341                quote_idx: 27,
1342                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1343                quote_vault_idx: 7,
1344                quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1345                quote_token_program_idx: 28,
1346            },
1347            Case {
1348                signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1349                name: "19-account WSOL quote in ALT with later buy",
1350                static_len: 18,
1351                writable_len: 7,
1352                program_idx: 13,
1353                account_len: 19,
1354                mint_idx: 1,
1355                mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1356                user_idx: 0,
1357                user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1358                token_program_idx: 27,
1359                quote_idx: 30,
1360                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1361                quote_vault_idx: 7,
1362                quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1363                quote_token_program_idx: 31,
1364            },
1365            Case {
1366                signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1367                name: "19-account USDC quote in ALT exact quote buy",
1368                static_len: 19,
1369                writable_len: 6,
1370                program_idx: 15,
1371                account_len: 19,
1372                mint_idx: 1,
1373                mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1374                user_idx: 0,
1375                user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1376                token_program_idx: 28,
1377                quote_idx: 33,
1378                quote_mint: usdc_mint(),
1379                quote_vault_idx: 7,
1380                quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1381                quote_token_program_idx: 27,
1382            },
1383            Case {
1384                signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1385                name: "20-account WSOL quote in ALT with jit account",
1386                static_len: 15,
1387                writable_len: 7,
1388                program_idx: 12,
1389                account_len: 20,
1390                mint_idx: 1,
1391                mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1392                user_idx: 0,
1393                user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1394                token_program_idx: 24,
1395                quote_idx: 27,
1396                quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1397                quote_vault_idx: 7,
1398                quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1399                quote_token_program_idx: 28,
1400            },
1401        ];
1402
1403        for case in cases {
1404            let (meta, tx) = grpc_pumpfun_create_v2_tx(
1405                case.static_len,
1406                case.writable_len,
1407                case.program_idx,
1408                create_v2_accounts(
1409                    case.account_len,
1410                    case.program_idx,
1411                    case.mint_idx,
1412                    case.user_idx,
1413                    case.token_program_idx,
1414                    Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1415                ),
1416                vec![
1417                    (case.mint_idx as usize, pk(case.mint)),
1418                    (case.user_idx as usize, pk(case.user)),
1419                    (case.token_program_idx as usize, token_2022_program),
1420                    (case.quote_idx as usize, case.quote_mint),
1421                    (case.quote_vault_idx as usize, pk(case.quote_vault)),
1422                    (case.quote_token_program_idx as usize, spl_token_program),
1423                ],
1424            );
1425            let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1426                let idx = global_idx as usize;
1427                if idx < case.static_len {
1428                    ("static", idx)
1429                } else if idx < case.static_len + case.writable_len {
1430                    ("writable", idx - case.static_len)
1431                } else {
1432                    ("readonly", idx - case.static_len - case.writable_len)
1433                }
1434            };
1435            assert_eq!(
1436                meta.loaded_writable_addresses.len(),
1437                case.writable_len,
1438                "{}: {}",
1439                case.name,
1440                case.signature
1441            );
1442            for (global_idx, expected_key) in [
1443                (case.token_program_idx, token_2022_program),
1444                (case.quote_idx, case.quote_mint),
1445                (case.quote_token_program_idx, spl_token_program),
1446            ] {
1447                match loaded_key_location(global_idx) {
1448                    ("static", _) => {}
1449                    ("writable", offset) => assert_eq!(
1450                        read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1451                        expected_key,
1452                        "{}: writable loaded key {global_idx}: {}",
1453                        case.name,
1454                        case.signature
1455                    ),
1456                    ("readonly", offset) => assert_eq!(
1457                        read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1458                        expected_key,
1459                        "{}: readonly loaded key {global_idx}: {}",
1460                        case.name,
1461                        case.signature
1462                    ),
1463                    _ => unreachable!(),
1464                }
1465            }
1466
1467            let create = parse_create_v2_from_grpc(&meta, &tx);
1468
1469            assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1470            assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1471            assert_eq!(
1472                create.token_program, token_2022_program,
1473                "{}: {}",
1474                case.name, case.signature
1475            );
1476            assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1477            assert_eq!(
1478                create.quote_vault,
1479                pk(case.quote_vault),
1480                "{}: {}",
1481                case.name,
1482                case.signature
1483            );
1484            assert_eq!(
1485                create.quote_token_program, spl_token_program,
1486                "{}: {}",
1487                case.name, case.signature
1488            );
1489        }
1490    }
1491
1492    #[test]
1493    fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1494        let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1495        let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1496        let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1497        let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1498        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1499            16,
1500            5,
1501            12,
1502            create_v2_accounts(16, 12, 1, 0, 24, None),
1503            vec![(1, mint), (0, user), (24, token_program)],
1504        );
1505
1506        let create = parse_create_v2_from_grpc(&meta, &tx);
1507
1508        assert_eq!(create.mint, mint, "{signature}");
1509        assert_eq!(create.user, user, "{signature}");
1510        assert_eq!(create.token_program, token_program, "{signature}");
1511        assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1512        assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1513        assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1514    }
1515
1516    #[test]
1517    fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1518        let quote_vault = Pubkey::new_unique();
1519        let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1520        let (meta, tx) = grpc_pumpfun_create_v2_tx(
1521            19,
1522            6,
1523            16,
1524            create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1525            vec![
1526                (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1527                (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1528                (6, quote_vault),
1529                (31, quote_token_program),
1530            ],
1531        );
1532
1533        let create = parse_create_v2_from_grpc(&meta, &tx);
1534
1535        assert_eq!(create.quote_mint, Pubkey::default());
1536        assert_eq!(create.quote_vault, Pubkey::default());
1537        assert_eq!(create.quote_token_program, Pubkey::default());
1538    }
1539}