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