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