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