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 let mut unmerged = None;
543 if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
544 continue;
545 }
546 result.push(unmerged.expect("unmerged event remains available"));
547 continue;
548 }
549 }
550 result.push(event);
551 continue;
552 }
553
554 let is_dlmm = is_dlmm_event(&event);
555 let target_idx = if let Some((target_outer, target_idx)) = outer_target {
556 if target_outer == outer_idx {
557 let mut unmerged = None;
558 if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
559 target_idx
560 } else {
561 let target_idx = result.len();
562 result.push(unmerged.expect("unmerged event remains available"));
563 target_idx
564 }
565 } else {
566 let target_idx = result.len();
567 result.push(event);
568 target_idx
569 }
570 } else {
571 let target_idx = result.len();
572 result.push(event);
573 target_idx
574 };
575
576 if is_dlmm {
577 if let Some(height) = stack_height {
578 while dlmm_targets_len > 0 {
579 let Some((candidate_outer, candidate_height, _)) =
580 dlmm_targets[dlmm_targets_len - 1]
581 else {
582 break;
583 };
584 if candidate_outer != outer_idx
585 || candidate_height.is_some_and(|candidate| candidate >= height)
586 {
587 dlmm_targets_len -= 1;
588 } else {
589 break;
590 }
591 }
592 } else {
593 dlmm_targets_len = 0;
594 }
595 if dlmm_targets_len < dlmm_targets.len() {
596 dlmm_targets[dlmm_targets_len] =
597 Some((outer_idx, stack_height, target_idx));
598 dlmm_targets_len += 1;
599 }
600 }
601 }
602 }
603 }
604
605 result
606}
607
608#[inline(always)]
610fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
611 let Some(filter) = filter else { return true };
613
614 if filter.include_only.is_none() {
616 return true;
617 }
618
619 if filter.includes_pumpfun() {
622 return true;
623 }
624
625 if filter.includes_pump_fees() {
626 return true;
627 }
628
629 filter.includes_pumpswap()
630 || filter.includes_raydium_launchlab()
631 || filter.includes_raydium_cpmm()
632 || filter.includes_raydium_clmm()
633 || filter.includes_raydium_amm_v4()
634 || filter.includes_orca_whirlpool()
635 || filter.includes_meteora_pools()
636 || filter.includes_meteora_damm_v2()
637 || filter.includes_meteora_dlmm()
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
644 use yellowstone_grpc_proto::prelude::{
645 CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
646 };
647
648 fn pk(s: &str) -> Pubkey {
649 s.parse().unwrap()
650 }
651
652 fn usdc_mint() -> Pubkey {
653 pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
654 }
655
656 fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
657 key.to_bytes().to_vec()
658 }
659
660 fn decode_b58(s: &str) -> Vec<u8> {
661 bs58::decode(s).into_vec().unwrap()
662 }
663
664 fn str_arg(s: &str, out: &mut Vec<u8>) {
665 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
666 out.extend_from_slice(s.as_bytes());
667 }
668
669 fn create_v2_data() -> Vec<u8> {
670 let mut data = Vec::new();
671 data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
672 str_arg("Alt Coin", &mut data);
673 str_arg("ALT", &mut data);
674 str_arg("https://example.invalid/alt.json", &mut data);
675 data.extend_from_slice(Pubkey::new_unique().as_ref());
676 data.push(1);
677 data.push(1);
678 data
679 }
680
681 fn grpc_pumpfun_create_v2_tx(
682 static_len: usize,
683 writable_len: usize,
684 program_idx: u8,
685 ix_accounts: Vec<u8>,
686 account_overrides: Vec<(usize, Pubkey)>,
687 ) -> (TransactionStatusMeta, Option<Transaction>) {
688 let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
689 account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
690 let readonly_len = account_overrides
691 .iter()
692 .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
693 .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
694 .max()
695 .unwrap_or_default();
696 let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
697 let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
698 for (global_idx, key) in account_overrides {
699 if global_idx < static_len {
700 account_keys[global_idx] = key;
701 } else if global_idx < static_len + writable_len {
702 loaded_writable[global_idx - static_len] = key;
703 } else {
704 loaded_readonly[global_idx - static_len - writable_len] = key;
705 }
706 }
707
708 let meta = TransactionStatusMeta {
709 loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
710 loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
711 ..Default::default()
712 };
713 let tx = Transaction {
714 signatures: vec![Signature::default().as_ref().to_vec()],
715 message: Some(Message {
716 header: Some(MessageHeader {
717 num_required_signatures: 1,
718 num_readonly_signed_accounts: 0,
719 num_readonly_unsigned_accounts: 0,
720 }),
721 account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
722 recent_blockhash: vec![0; 32],
723 instructions: vec![CompiledInstruction {
724 program_id_index: program_idx as u32,
725 accounts: ix_accounts,
726 data: create_v2_data(),
727 }],
728 versioned: true,
729 address_table_lookups: Vec::new(),
730 config: None,
731 }),
732 };
733 (meta, Some(tx))
734 }
735
736 fn create_v2_accounts(
737 account_len: usize,
738 program_idx: u8,
739 mint_idx: u8,
740 user_idx: u8,
741 token_program_idx: u8,
742 quote_tail: Option<(u8, u8, u8)>,
743 ) -> Vec<u8> {
744 let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
745 accounts[0] = mint_idx;
746 accounts[5] = user_idx;
747 accounts[7] = token_program_idx;
748 if account_len > 15 {
749 accounts[15] = program_idx;
750 }
751 if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
752 accounts[16] = quote_idx;
753 accounts[17] = quote_vault_idx;
754 accounts[18] = quote_token_program_idx;
755 }
756 accounts
757 }
758
759 fn parse_create_v2_from_grpc(
760 meta: &TransactionStatusMeta,
761 tx: &Option<Transaction>,
762 ) -> crate::core::events::PumpFunCreateTokenEvent {
763 let events = parse_instructions_enhanced(
764 meta,
765 tx,
766 Signature::default(),
767 123,
768 0,
769 Some(456),
770 789,
771 None,
772 );
773 assert_eq!(events.len(), 1);
774 match &events[0] {
775 DexEvent::PumpFunCreate(e) => {
776 assert_eq!(e.ix_name, "create_v2");
777 e.clone()
778 }
779 DexEvent::PumpFunCreateV2(e) => {
780 assert_eq!(e.ix_name, "create_v2");
781 crate::core::events::PumpFunCreateTokenEvent {
782 metadata: e.metadata.clone(),
783 name: e.name.clone(),
784 symbol: e.symbol.clone(),
785 uri: e.uri.clone(),
786 mint: e.mint,
787 bonding_curve: e.bonding_curve,
788 user: e.user,
789 creator: e.creator,
790 timestamp: e.timestamp,
791 virtual_token_reserves: e.virtual_token_reserves,
792 virtual_sol_reserves: e.virtual_sol_reserves,
793 real_token_reserves: e.real_token_reserves,
794 token_total_supply: e.token_total_supply,
795 mint_authority: e.mint_authority,
796 associated_bonding_curve: e.associated_bonding_curve,
797 global: e.global,
798 system_program: e.system_program,
799 token_program: e.token_program,
800 associated_token_program: e.associated_token_program,
801 mayhem_program_id: e.mayhem_program_id,
802 global_params: e.global_params,
803 sol_vault: e.sol_vault,
804 mayhem_state: e.mayhem_state,
805 mayhem_token_vault: e.mayhem_token_vault,
806 event_authority: e.event_authority,
807 program: e.program,
808 quote_mint: e.quote_mint,
809 quote_vault: e.quote_vault,
810 quote_token_program: e.quote_token_program,
811 virtual_quote_reserves: e.virtual_quote_reserves,
812 ix_name: e.ix_name.clone(),
813 is_mayhem_mode: e.is_mayhem_mode,
814 is_cashback_enabled: e.is_cashback_enabled,
815 observed_fee_recipient: e.observed_fee_recipient,
816 }
817 }
818 other => panic!("expected PumpFun create_v2 event, got {other:?}"),
819 }
820 }
821
822 #[test]
823 fn test_should_parse_instructions() {
824 assert!(should_parse_instructions(None));
826
827 let filter = EventTypeFilter { include_only: None, exclude_types: None };
829 assert!(should_parse_instructions(Some(&filter)));
830
831 use crate::grpc::types::EventType;
833 let filter = EventTypeFilter {
834 include_only: Some(vec![EventType::PumpFunMigrate]),
835 exclude_types: None,
836 };
837 assert!(should_parse_instructions(Some(&filter)));
838
839 let filter = EventTypeFilter {
841 include_only: Some(vec![EventType::PumpFunTrade]),
842 exclude_types: None,
843 };
844 assert!(should_parse_instructions(Some(&filter)));
845
846 for event_type in [
847 EventType::PumpSwapTrade,
848 EventType::PumpFeesUpdateFeeShares,
849 EventType::RaydiumLaunchlabTrade,
850 EventType::RaydiumCpmmSwap,
851 EventType::RaydiumClmmSwap,
852 EventType::RaydiumAmmV4Swap,
853 EventType::OrcaWhirlpoolSwap,
854 EventType::MeteoraPoolsSwap,
855 EventType::MeteoraDammV2Swap,
856 EventType::MeteoraDammV2InitializePool,
857 EventType::MeteoraDlmmSwap,
858 ] {
859 let filter = EventTypeFilter::include_only(vec![event_type]);
860 assert!(
861 should_parse_instructions(Some(&filter)),
862 "instruction parsing should be enabled for {event_type:?}"
863 );
864 }
865
866 let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
867 assert!(
868 !should_parse_instructions(Some(&filter)),
869 "DBC events are log-only until an instruction parser is implemented"
870 );
871
872 let filter = EventTypeFilter::include_only(vec![
873 EventType::AccountPumpFunGlobal,
874 EventType::AccountRaydiumClmmPoolState,
875 EventType::AccountRaydiumCpmmPoolState,
876 EventType::AccountOrcaWhirlpool,
877 ]);
878 assert!(
879 !should_parse_instructions(Some(&filter)),
880 "account-only non-Pump filters should stay on the account update path"
881 );
882 }
883
884 #[test]
885 fn test_merge_instruction_events() {
886 use solana_sdk::signature::Signature;
887
888 let metadata = EventMetadata {
889 signature: Signature::default(),
890 slot: 100,
891 tx_index: 1,
892 block_time_us: 1000,
893 grpc_recv_us: 2000,
894 recent_blockhash: None,
895 };
896
897 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
899 metadata: metadata.clone(),
900 bonding_curve: Pubkey::new_unique(),
901 ..Default::default()
902 });
903
904 let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
905 metadata: metadata.clone(),
906 sol_amount: 1000,
907 token_amount: 2000,
908 ..Default::default()
909 });
910
911 let events = vec![
912 IndexedInstructionEvent {
913 outer_idx: 0,
914 inner_idx: None,
915 stack_height: Some(1),
916 is_dlmm_event_cpi: false,
917 event: outer_event,
918 },
919 IndexedInstructionEvent {
920 outer_idx: 0,
921 inner_idx: Some(0),
922 stack_height: Some(2),
923 is_dlmm_event_cpi: false,
924 event: inner_event,
925 },
926 ];
927
928 let result = merge_instruction_events(events);
929
930 assert_eq!(result.len(), 1);
932
933 if let DexEvent::PumpFunTrade(trade) = &result[0] {
935 assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
939 panic!("Expected PumpFunTrade event");
940 }
941 }
942
943 #[test]
944 fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
945 use solana_sdk::signature::Signature;
946
947 let metadata = EventMetadata {
948 signature: Signature::default(),
949 slot: 100,
950 tx_index: 1,
951 block_time_us: 1000,
952 grpc_recv_us: 2000,
953 recent_blockhash: None,
954 };
955
956 let bc = Pubkey::new_unique();
957 let fee = Pubkey::new_unique();
958
959 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
960 metadata: metadata.clone(),
961 bonding_curve: bc,
962 ..Default::default()
963 });
964
965 let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
966 metadata: metadata.clone(),
967 sol_amount: 1000,
968 token_amount: 2000,
969 is_buy: true,
970 ..Default::default()
971 });
972
973 let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
975 metadata: metadata.clone(),
976 fee_recipient: fee,
977 ..Default::default()
978 });
979
980 let events = vec![
981 IndexedInstructionEvent {
982 outer_idx: 0,
983 inner_idx: None,
984 stack_height: Some(1),
985 is_dlmm_event_cpi: false,
986 event: outer_event,
987 },
988 IndexedInstructionEvent {
989 outer_idx: 0,
990 inner_idx: Some(0),
991 stack_height: Some(2),
992 is_dlmm_event_cpi: false,
993 event: inner_trade,
994 },
995 IndexedInstructionEvent {
996 outer_idx: 0,
997 inner_idx: Some(1),
998 stack_height: Some(2),
999 is_dlmm_event_cpi: false,
1000 event: inner_fee_only,
1001 },
1002 ];
1003
1004 let result = merge_instruction_events(events);
1005 assert_eq!(result.len(), 1);
1006 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1007 assert_eq!(trade.bonding_curve, bc);
1008 assert_eq!(trade.sol_amount, 1000);
1009 assert_eq!(trade.token_amount, 2000);
1010 assert_eq!(trade.fee_recipient, fee);
1011 } else {
1012 panic!("Expected PumpFunTrade event");
1013 }
1014 }
1015
1016 fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1017 DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1018 metadata: EventMetadata::default(),
1019 token_x_mint: Pubkey::default(),
1020 token_y_mint: Pubkey::default(),
1021 user_token_in: Pubkey::default(),
1022 user_token_out: Pubkey::default(),
1023 min_amount_out: 0,
1024 pool,
1025 from: Pubkey::default(),
1026 start_bin_id: 0,
1027 end_bin_id: 0,
1028 amount_in,
1029 amount_out,
1030 swap_for_y: false,
1031 fee: 0,
1032 protocol_fee: 0,
1033 fee_bps: 0,
1034 host_fee: 0,
1035 })
1036 }
1037
1038 fn dlmm_add_liquidity() -> DexEvent {
1039 DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1040 metadata: EventMetadata::default(),
1041 pool: Pubkey::default(),
1042 from: Pubkey::default(),
1043 position: Pubkey::default(),
1044 amounts: [0; 2],
1045 active_bin_id: 0,
1046 })
1047 }
1048
1049 #[test]
1050 fn merge_preserves_unrelated_inner_events() {
1051 let events = vec![
1052 IndexedInstructionEvent {
1053 outer_idx: 0,
1054 inner_idx: None,
1055 stack_height: Some(1),
1056 is_dlmm_event_cpi: false,
1057 event: dlmm_swap(Pubkey::default(), 0, 0),
1058 },
1059 IndexedInstructionEvent {
1060 outer_idx: 0,
1061 inner_idx: Some(0),
1062 stack_height: Some(2),
1063 is_dlmm_event_cpi: false,
1064 event: dlmm_add_liquidity(),
1065 },
1066 ];
1067
1068 let result = merge_instruction_events(events);
1069 assert_eq!(result.len(), 2);
1070 assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1071 assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1072 }
1073
1074 #[test]
1075 fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1076 let first_pool = Pubkey::new_unique();
1077 let second_pool = Pubkey::new_unique();
1078 let events = vec![
1079 IndexedInstructionEvent {
1080 outer_idx: 0,
1081 inner_idx: Some(0),
1082 stack_height: Some(2),
1083 is_dlmm_event_cpi: false,
1084 event: dlmm_swap(first_pool, 1, 0),
1085 },
1086 IndexedInstructionEvent {
1087 outer_idx: 0,
1088 inner_idx: Some(1),
1089 stack_height: Some(3),
1090 is_dlmm_event_cpi: true,
1091 event: dlmm_swap(first_pool, 10, 9),
1092 },
1093 IndexedInstructionEvent {
1094 outer_idx: 0,
1095 inner_idx: Some(2),
1096 stack_height: Some(2),
1097 is_dlmm_event_cpi: false,
1098 event: dlmm_swap(second_pool, 2, 0),
1099 },
1100 IndexedInstructionEvent {
1101 outer_idx: 0,
1102 inner_idx: Some(3),
1103 stack_height: Some(3),
1104 is_dlmm_event_cpi: true,
1105 event: dlmm_swap(second_pool, 20, 18),
1106 },
1107 ];
1108
1109 let result = merge_instruction_events(events);
1110 assert_eq!(result.len(), 2);
1111 let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1112 let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1113 assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1114 assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1115 }
1116
1117 #[test]
1118 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1119 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1120 let static_keys = [
1121 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1122 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1123 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1124 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1125 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1126 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1127 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1128 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1129 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1130 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1131 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1132 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1133 "ComputeBudget111111111111111111111111111111",
1134 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1135 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1136 "SysvarRent111111111111111111111111111111111",
1137 ];
1138 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1139 let loaded_readonly = [
1140 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1141 "So11111111111111111111111111111111111111112",
1142 "11111111111111111111111111111111",
1143 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1144 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1145 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1146 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1147 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1148 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1149 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1150 ];
1151 let meta = TransactionStatusMeta {
1152 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1153 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1154 inner_instructions: vec![InnerInstructions {
1155 index: 2,
1156 instructions: vec![InnerInstruction {
1157 program_id_index: 20,
1158 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1159 data: decode_b58(
1160 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1161 ),
1162 stack_height: Some(2),
1163 }],
1164 }],
1165 ..Default::default()
1166 };
1167 let tx = Some(Transaction {
1168 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1169 message: Some(Message {
1170 header: Some(MessageHeader::default()),
1171 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1172 recent_blockhash: vec![0; 32],
1173 instructions: vec![
1174 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1175 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1176 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1177 ],
1178 versioned: true,
1179 address_table_lookups: Vec::new(),
1180 config: None,
1181 }),
1182 });
1183
1184 let events = parse_instructions_enhanced(
1185 &meta,
1186 &tx,
1187 signature.parse().unwrap(),
1188 427_039_576,
1189 0,
1190 Some(1_781_687_252_000_000),
1191 789,
1192 None,
1193 );
1194
1195 assert_eq!(events.len(), 1, "{signature}");
1196 match &events[0] {
1197 DexEvent::PumpSwapCreatePool(e) => {
1198 assert_eq!(e.index, 0, "{signature}");
1199 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1200 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1201 assert_eq!(
1202 e.coin_creator,
1203 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1204 "{signature}"
1205 );
1206 assert!(!e.is_mayhem_mode, "{signature}");
1207 assert!(e.is_cashback_coin, "{signature}");
1208 assert_eq!(
1209 e.pool,
1210 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1211 "{signature}"
1212 );
1213 assert_eq!(
1214 e.creator,
1215 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1216 "{signature}"
1217 );
1218 assert_eq!(
1219 e.base_mint,
1220 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1221 "{signature}"
1222 );
1223 assert_eq!(
1224 e.quote_mint,
1225 pk("So11111111111111111111111111111111111111112"),
1226 "{signature}"
1227 );
1228 assert_eq!(
1229 e.lp_mint,
1230 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1231 "{signature}"
1232 );
1233 assert_eq!(
1234 e.user_base_token_account,
1235 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1236 "{signature}"
1237 );
1238 assert_eq!(
1239 e.user_quote_token_account,
1240 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1241 "{signature}"
1242 );
1243 }
1244 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1245 }
1246 }
1247
1248 #[test]
1249 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1250 struct Case {
1251 signature: &'static str,
1252 name: &'static str,
1253 static_len: usize,
1254 writable_len: usize,
1255 program_idx: u8,
1256 account_len: usize,
1257 mint_idx: u8,
1258 mint: &'static str,
1259 user_idx: u8,
1260 user: &'static str,
1261 token_program_idx: u8,
1262 quote_idx: u8,
1263 quote_mint: Pubkey,
1264 quote_vault_idx: u8,
1265 quote_vault: &'static str,
1266 quote_token_program_idx: u8,
1267 }
1268 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1269 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1270 let cases = [
1271 Case {
1272 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1273 name: "19-account WSOL quote in ALT",
1274 static_len: 15,
1275 writable_len: 7,
1276 program_idx: 12,
1277 account_len: 19,
1278 mint_idx: 1,
1279 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1280 user_idx: 0,
1281 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1282 token_program_idx: 24,
1283 quote_idx: 27,
1284 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1285 quote_vault_idx: 7,
1286 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1287 quote_token_program_idx: 28,
1288 },
1289 Case {
1290 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1291 name: "19-account WSOL quote in ALT with exact quote buy",
1292 static_len: 20,
1293 writable_len: 7,
1294 program_idx: 15,
1295 account_len: 19,
1296 mint_idx: 1,
1297 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1298 user_idx: 0,
1299 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1300 token_program_idx: 31,
1301 quote_idx: 28,
1302 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1303 quote_vault_idx: 4,
1304 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1305 quote_token_program_idx: 30,
1306 },
1307 Case {
1308 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1309 name: "19-account USDC quote in ALT",
1310 static_len: 19,
1311 writable_len: 6,
1312 program_idx: 16,
1313 account_len: 19,
1314 mint_idx: 1,
1315 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1316 user_idx: 0,
1317 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1318 token_program_idx: 27,
1319 quote_idx: 30,
1320 quote_mint: usdc_mint(),
1321 quote_vault_idx: 6,
1322 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1323 quote_token_program_idx: 31,
1324 },
1325 Case {
1326 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1327 name: "20-account WSOL quote in ALT",
1328 static_len: 15,
1329 writable_len: 7,
1330 program_idx: 12,
1331 account_len: 20,
1332 mint_idx: 1,
1333 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1334 user_idx: 0,
1335 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1336 token_program_idx: 24,
1337 quote_idx: 27,
1338 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1339 quote_vault_idx: 7,
1340 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1341 quote_token_program_idx: 28,
1342 },
1343 Case {
1344 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1345 name: "19-account WSOL quote in ALT with later buy",
1346 static_len: 18,
1347 writable_len: 7,
1348 program_idx: 13,
1349 account_len: 19,
1350 mint_idx: 1,
1351 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1352 user_idx: 0,
1353 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1354 token_program_idx: 27,
1355 quote_idx: 30,
1356 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1357 quote_vault_idx: 7,
1358 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1359 quote_token_program_idx: 31,
1360 },
1361 Case {
1362 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1363 name: "19-account USDC quote in ALT exact quote buy",
1364 static_len: 19,
1365 writable_len: 6,
1366 program_idx: 15,
1367 account_len: 19,
1368 mint_idx: 1,
1369 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1370 user_idx: 0,
1371 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1372 token_program_idx: 28,
1373 quote_idx: 33,
1374 quote_mint: usdc_mint(),
1375 quote_vault_idx: 7,
1376 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1377 quote_token_program_idx: 27,
1378 },
1379 Case {
1380 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1381 name: "20-account WSOL quote in ALT with jit account",
1382 static_len: 15,
1383 writable_len: 7,
1384 program_idx: 12,
1385 account_len: 20,
1386 mint_idx: 1,
1387 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1388 user_idx: 0,
1389 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1390 token_program_idx: 24,
1391 quote_idx: 27,
1392 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1393 quote_vault_idx: 7,
1394 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1395 quote_token_program_idx: 28,
1396 },
1397 ];
1398
1399 for case in cases {
1400 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1401 case.static_len,
1402 case.writable_len,
1403 case.program_idx,
1404 create_v2_accounts(
1405 case.account_len,
1406 case.program_idx,
1407 case.mint_idx,
1408 case.user_idx,
1409 case.token_program_idx,
1410 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1411 ),
1412 vec![
1413 (case.mint_idx as usize, pk(case.mint)),
1414 (case.user_idx as usize, pk(case.user)),
1415 (case.token_program_idx as usize, token_2022_program),
1416 (case.quote_idx as usize, case.quote_mint),
1417 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1418 (case.quote_token_program_idx as usize, spl_token_program),
1419 ],
1420 );
1421 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1422 let idx = global_idx as usize;
1423 if idx < case.static_len {
1424 ("static", idx)
1425 } else if idx < case.static_len + case.writable_len {
1426 ("writable", idx - case.static_len)
1427 } else {
1428 ("readonly", idx - case.static_len - case.writable_len)
1429 }
1430 };
1431 assert_eq!(
1432 meta.loaded_writable_addresses.len(),
1433 case.writable_len,
1434 "{}: {}",
1435 case.name,
1436 case.signature
1437 );
1438 for (global_idx, expected_key) in [
1439 (case.token_program_idx, token_2022_program),
1440 (case.quote_idx, case.quote_mint),
1441 (case.quote_token_program_idx, spl_token_program),
1442 ] {
1443 match loaded_key_location(global_idx) {
1444 ("static", _) => {}
1445 ("writable", offset) => assert_eq!(
1446 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1447 expected_key,
1448 "{}: writable loaded key {global_idx}: {}",
1449 case.name,
1450 case.signature
1451 ),
1452 ("readonly", offset) => assert_eq!(
1453 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1454 expected_key,
1455 "{}: readonly loaded key {global_idx}: {}",
1456 case.name,
1457 case.signature
1458 ),
1459 _ => unreachable!(),
1460 }
1461 }
1462
1463 let create = parse_create_v2_from_grpc(&meta, &tx);
1464
1465 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1466 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1467 assert_eq!(
1468 create.token_program, token_2022_program,
1469 "{}: {}",
1470 case.name, case.signature
1471 );
1472 assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1473 assert_eq!(
1474 create.quote_vault,
1475 pk(case.quote_vault),
1476 "{}: {}",
1477 case.name,
1478 case.signature
1479 );
1480 assert_eq!(
1481 create.quote_token_program, spl_token_program,
1482 "{}: {}",
1483 case.name, case.signature
1484 );
1485 }
1486 }
1487
1488 #[test]
1489 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1490 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1491 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1492 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1493 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1494 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1495 16,
1496 5,
1497 12,
1498 create_v2_accounts(16, 12, 1, 0, 24, None),
1499 vec![(1, mint), (0, user), (24, token_program)],
1500 );
1501
1502 let create = parse_create_v2_from_grpc(&meta, &tx);
1503
1504 assert_eq!(create.mint, mint, "{signature}");
1505 assert_eq!(create.user, user, "{signature}");
1506 assert_eq!(create.token_program, token_program, "{signature}");
1507 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1508 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1509 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1510 }
1511
1512 #[test]
1513 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1514 let quote_vault = Pubkey::new_unique();
1515 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1516 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1517 19,
1518 6,
1519 16,
1520 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1521 vec![
1522 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1523 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1524 (6, quote_vault),
1525 (31, quote_token_program),
1526 ],
1527 );
1528
1529 let create = parse_create_v2_from_grpc(&meta, &tx);
1530
1531 assert_eq!(create.quote_mint, Pubkey::default());
1532 assert_eq!(create.quote_vault, Pubkey::default());
1533 assert_eq!(create.quote_token_program, Pubkey::default());
1534 }
1535}