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