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