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_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_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_event_cpi: (pid == crate::instr::program_ids::METEORA_DLMM_PROGRAM_ID
239 || pid == crate::instr::program_ids::PUMPFUN_PROGRAM_ID
240 || pid == crate::instr::program_ids::PUMPSWAP_PROGRAM_ID)
241 && crate::instr::all_inner::meteora_dlmm::is_event_cpi(&inner_ix.data),
242 event,
243 });
244 }
245 }
246 }
247
248 let mut final_result = merge_instruction_events(result);
250 enrich_pumpfun_same_tx_post_merge(&mut final_result);
251
252 for event in &mut final_result {
254 if let Some(metadata) = event.metadata_mut() {
257 metadata.grpc_recv_us = grpc_us;
258 }
259 crate::core::account_dispatcher::fill_accounts_with_invoke_context(
260 event,
261 meta,
262 transaction,
263 &invokes,
264 );
265 crate::core::common_filler::fill_data_with_invoke_context(
266 event,
267 meta,
268 transaction,
269 &invokes,
270 );
271 }
272
273 final_result
274}
275
276#[inline(always)]
281fn parse_compiled_instruction<'a>(
282 data: &[u8],
283 program_id: &Pubkey,
284 sig: Signature,
285 slot: u64,
286 tx_idx: u64,
287 block_us: Option<i64>,
288 grpc_us: i64,
289 account_indices: &[u8],
290 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
291 filter: Option<&EventTypeFilter>,
292) -> Option<DexEvent> {
293 if data.is_empty()
295 || (*program_id != crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID && data.len() < 8)
296 {
297 return None;
298 }
299
300 if *program_id == crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID
303 && !crate::instr::raydium_cpmm::instruction_may_parse(data, account_indices.len(), filter)
304 {
305 return None;
306 }
307
308 const STACK_CAP: usize = 64;
310 if account_indices.len() <= STACK_CAP {
311 let mut stack = [Pubkey::default(); STACK_CAP];
312 let mut n = 0usize;
313 for &idx in account_indices {
314 let k = get_key(idx as usize)?;
315 stack[n] = Pubkey::try_from(k.as_slice()).ok()?;
316 n += 1;
317 }
318 crate::instr::parse_instruction_unified(
319 data,
320 &stack[..n],
321 sig,
322 slot,
323 tx_idx,
324 block_us,
325 grpc_us,
326 filter,
327 program_id,
328 )
329 } else {
330 let accounts: Vec<Pubkey> = account_indices
331 .iter()
332 .map(|&idx| get_key(idx as usize).and_then(|key| Pubkey::try_from(key.as_slice()).ok()))
333 .collect::<Option<_>>()?;
334 crate::instr::parse_instruction_unified(
335 data, &accounts, sig, slot, tx_idx, block_us, grpc_us, filter, program_id,
336 )
337 }
338}
339
340#[inline(always)]
341fn is_supported_inner_compiled_instruction(data: &[u8], program_id: &Pubkey) -> bool {
342 crate::instr::normal_instruction_data_may_parse(program_id, data)
343}
344
345#[inline(always)]
346fn parse_inner_compiled_instruction_if_supported<'a>(
347 data: &[u8],
348 program_id: &Pubkey,
349 sig: Signature,
350 slot: u64,
351 tx_idx: u64,
352 block_us: Option<i64>,
353 grpc_us: i64,
354 account_indices: &[u8],
355 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
356 filter: Option<&EventTypeFilter>,
357) -> Option<DexEvent> {
358 if !is_supported_inner_compiled_instruction(data, program_id) {
359 return None;
360 }
361 parse_compiled_instruction(
362 data,
363 program_id,
364 sig,
365 slot,
366 tx_idx,
367 block_us,
368 grpc_us,
369 account_indices,
370 get_key,
371 filter,
372 )
373}
374
375#[inline(always)]
379fn parse_outer_instruction<'a>(
380 data: &[u8],
381 program_id: &Pubkey,
382 sig: Signature,
383 slot: u64,
384 tx_idx: u64,
385 block_us: Option<i64>,
386 grpc_us: i64,
387 account_indices: &[u8],
388 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
389 filter: Option<&EventTypeFilter>,
390 _is_created_buy: bool,
391) -> Option<DexEvent> {
392 if !crate::instr::normal_instruction_data_may_parse(program_id, data) {
393 return None;
394 }
395 parse_compiled_instruction(
396 data,
397 program_id,
398 sig,
399 slot,
400 tx_idx,
401 block_us,
402 grpc_us,
403 account_indices,
404 get_key,
405 filter,
406 )
407}
408
409#[inline(always)]
413fn parse_inner_instruction(
414 data: &[u8],
415 program_id: &Pubkey,
416 sig: Signature,
417 slot: u64,
418 tx_idx: u64,
419 block_us: Option<i64>,
420 grpc_us: i64,
421 filter: Option<&EventTypeFilter>,
422 is_created_buy: bool,
423) -> Option<DexEvent> {
424 if data.len() < 16 {
426 return None;
427 }
428
429 let metadata = EventMetadata {
430 signature: sig,
431 slot,
432 tx_index: tx_idx,
433 block_time_us: block_us.unwrap_or(0),
434 grpc_recv_us: grpc_us,
435 recent_blockhash: None, };
437
438 let mut discriminator = [0u8; 16];
440 discriminator.copy_from_slice(&data[..16]);
441 let inner_data = &data[16..];
442 let upgrade_disc = if discriminator[..8] == [228, 69, 165, 46, 81, 203, 154, 29] {
443 Some(u64::from_le_bytes(discriminator[8..].try_into().ok()?))
444 } else if discriminator[8..] == [155, 167, 108, 32, 122, 76, 173, 64] {
445 Some(u64::from_le_bytes(discriminator[..8].try_into().ok()?))
446 } else {
447 None
448 };
449 if let Some(disc) = upgrade_disc {
450 if crate::logs::pump_upgrade::event_type(disc, Some(program_id)).is_some() {
451 let event =
452 crate::logs::pump_upgrade::parse(disc, inner_data, metadata, Some(program_id));
453 return event.filter(|e| filter.map(|f| f.should_include_dex_event(e)).unwrap_or(true));
454 }
455 }
456
457 use crate::instr::{all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner};
458
459 let event = if *program_id == program_ids::PUMPFUN_PROGRAM_ID {
461 if let Some(f) = filter {
462 if !f.includes_pumpfun() {
463 return None;
464 }
465 }
466 pump_inner::parse_pumpfun_inner_instruction(
467 &discriminator,
468 inner_data,
469 metadata,
470 is_created_buy,
471 )
472 } else if *program_id == program_ids::PUMPSWAP_PROGRAM_ID {
473 if let Some(f) = filter {
474 if !f.includes_pumpswap() {
475 return None;
476 }
477 }
478 pump_amm_inner::parse_pumpswap_inner_instruction(&discriminator, inner_data, metadata)
479 } else if *program_id == program_ids::PUMP_FEES_PROGRAM_ID {
480 if let Some(f) = filter {
481 if !f.includes_pump_fees() {
482 return None;
483 }
484 }
485 all_inner::pump_fees::parse(&discriminator, inner_data, metadata)
486 } else if *program_id == program_ids::RAYDIUM_CLMM_PROGRAM_ID {
487 if let Some(f) = filter {
488 if !f.includes_raydium_clmm() {
489 return None;
490 }
491 }
492 raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
493 &discriminator,
494 inner_data,
495 metadata,
496 )
497 } else if *program_id == program_ids::RAYDIUM_CPMM_PROGRAM_ID {
498 if let Some(f) = filter {
499 if !f.includes_raydium_cpmm() {
500 return None;
501 }
502 }
503 all_inner::raydium_cpmm::parse(&discriminator, inner_data, metadata)
504 } else if *program_id == program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
505 if let Some(f) = filter {
506 if !f.includes_raydium_amm_v4() {
507 return None;
508 }
509 }
510 all_inner::raydium_amm::parse(&discriminator, inner_data, metadata)
511 } else if *program_id == program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
512 if let Some(f) = filter {
513 if !f.includes_orca_whirlpool() {
514 return None;
515 }
516 }
517 all_inner::orca::parse(&discriminator, inner_data, metadata)
518 } else if *program_id == program_ids::METEORA_POOLS_PROGRAM_ID {
519 if let Some(f) = filter {
520 if !f.includes_meteora_pools() {
521 return None;
522 }
523 }
524 all_inner::meteora_amm::parse(&discriminator, inner_data, metadata)
525 } else if *program_id == program_ids::METEORA_DAMM_V2_PROGRAM_ID {
526 if let Some(f) = filter {
527 if !f.includes_meteora_damm_v2() {
528 return None;
529 }
530 }
531 all_inner::meteora_damm::parse(&discriminator, inner_data, metadata)
532 } else if *program_id == program_ids::METEORA_DBC_PROGRAM_ID {
533 if filter.is_some_and(|f| !f.includes_meteora_dbc()) {
534 return None;
535 }
536 crate::logs::meteora_dbc::parse_event_cpi(data, metadata)
537 } else if *program_id == program_ids::METEORA_DLMM_PROGRAM_ID {
538 if let Some(f) = filter {
539 if !f.includes_meteora_dlmm() {
540 return None;
541 }
542 }
543 all_inner::meteora_dlmm::parse(&discriminator, inner_data, metadata)
544 } else if *program_id == program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID {
545 if let Some(f) = filter {
546 if !f.includes_raydium_launchlab() {
547 return None;
548 }
549 }
550 all_inner::raydium_launchlab::parse(&discriminator, inner_data, metadata)
551 } else {
552 None
553 };
554
555 if filter.map(|f| event.as_ref().is_some_and(|e| f.should_include_dex_event(e))).unwrap_or(true)
556 {
557 event
558 } else {
559 None
560 }
561}
562
563#[inline(always)]
566fn is_pump_trade(event: &DexEvent) -> bool {
567 matches!(
568 event,
569 DexEvent::PumpFunTrade(_)
570 | DexEvent::PumpFunBuy(_)
571 | DexEvent::PumpFunSell(_)
572 | DexEvent::PumpFunBuyExactSolIn(_)
573 | DexEvent::PumpSwapBuy(_)
574 | DexEvent::PumpSwapSell(_)
575 )
576}
577
578fn pump_trade_has_instruction_name(event: &DexEvent) -> bool {
579 match event {
580 DexEvent::PumpFunTrade(e)
581 | DexEvent::PumpFunBuy(e)
582 | DexEvent::PumpFunSell(e)
583 | DexEvent::PumpFunBuyExactSolIn(e) => !e.ix_name.is_empty(),
584 DexEvent::PumpSwapBuy(_) | DexEvent::PumpSwapSell(_) => true,
585 _ => false,
586 }
587}
588fn merge_instruction_events(events: Vec<IndexedInstructionEvent>) -> Vec<DexEvent> {
590 if events.is_empty() {
591 return Vec::new();
592 }
593
594 let mut events = events;
597 events.sort_unstable_by_key(|event| {
598 (event.outer_idx, event.inner_idx.map_or(0, |inner_idx| inner_idx + 1))
599 });
600
601 let mut result = Vec::with_capacity(events.len());
602 let mut outer_target: Option<(usize, usize)> = None;
603 let mut dlmm_targets: [Option<(usize, Option<u32>, usize)>; 8] = [None; 8];
606 let mut dlmm_targets_len = 0usize;
607
608 let mut pump_targets: Vec<(usize, Option<u32>, usize, bool)> = Vec::new();
609 for indexed in events {
610 let IndexedInstructionEvent { outer_idx, inner_idx, stack_height, is_event_cpi, event } =
611 indexed;
612 match inner_idx {
613 None => {
614 let is_dlmm = is_dlmm_event(&event);
615 let target_idx = result.len();
616 result.push(event);
617 outer_target = Some((outer_idx, target_idx));
618 pump_targets.clear();
619 if is_pump_trade(&result[target_idx]) {
620 pump_targets.push((outer_idx, stack_height, target_idx, false));
621 }
622 dlmm_targets_len = 0;
623 if is_dlmm {
624 dlmm_targets[0] = Some((outer_idx, stack_height, target_idx));
625 dlmm_targets_len = 1;
626 }
627 }
628 Some(_) => {
629 if is_pump_trade(&event)
630 && (is_event_cpi || pump_trade_has_instruction_name(&event))
631 {
632 if is_event_cpi {
633 let candidate = pump_targets.iter().rposition(|t| {
634 t.0 == outer_idx
635 && match (t.1, stack_height) {
636 (Some(parent), Some(child)) => child == parent + 1,
637 _ => true,
638 }
639 });
640 if let Some(i) = candidate {
641 let target = &mut pump_targets[i];
642 if !target.3 {
643 target.3 = true;
644 let mut unmerged = None;
645 if !try_merge_events(&mut result[target.2], event, &mut unmerged) {
646 result.push(unmerged.unwrap());
647 }
648 continue;
649 }
650 }
651 result.push(event);
652 } else {
653 if let Some(height) = stack_height {
654 while pump_targets.last().is_some_and(|t| {
655 t.0 != outer_idx || t.1.is_some_and(|h| h >= height)
656 }) {
657 pump_targets.pop();
658 }
659 } else {
660 pump_targets.clear();
661 }
662 let target_idx = result.len();
663 result.push(event);
664 pump_targets.push((outer_idx, stack_height, target_idx, false));
665 }
666 continue;
667 }
668 if is_event_cpi && is_dlmm_event(&event) {
669 let target = (0..dlmm_targets_len).rev().find_map(|idx| {
670 let (target_outer, target_height, target_idx) = dlmm_targets[idx]?;
671 let is_direct_child = match (target_height, stack_height) {
672 (Some(parent), Some(child)) => child == parent + 1,
673 _ => true,
674 };
675 (target_outer == outer_idx && is_direct_child).then_some((idx, target_idx))
676 });
677 if let Some((candidate_idx, target_idx)) = target {
678 dlmm_targets_len = candidate_idx + 1;
679 if target_idx < result.len() {
680 let mut unmerged = None;
681 if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
682 continue;
683 }
684 result.push(unmerged.expect("unmerged event remains available"));
685 continue;
686 }
687 }
688 result.push(event);
689 continue;
690 }
691
692 let is_dlmm = is_dlmm_event(&event);
693 let target_idx = if let Some((target_outer, target_idx)) = outer_target {
694 if target_outer == outer_idx {
695 let mut unmerged = None;
696 if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
697 target_idx
698 } else {
699 let target_idx = result.len();
700 result.push(unmerged.expect("unmerged event remains available"));
701 target_idx
702 }
703 } else {
704 let target_idx = result.len();
705 result.push(event);
706 target_idx
707 }
708 } else {
709 let target_idx = result.len();
710 result.push(event);
711 target_idx
712 };
713
714 if is_dlmm {
715 if let Some(height) = stack_height {
716 while dlmm_targets_len > 0 {
717 let Some((candidate_outer, candidate_height, _)) =
718 dlmm_targets[dlmm_targets_len - 1]
719 else {
720 break;
721 };
722 if candidate_outer != outer_idx
723 || candidate_height.is_some_and(|candidate| candidate >= height)
724 {
725 dlmm_targets_len -= 1;
726 } else {
727 break;
728 }
729 }
730 } else {
731 dlmm_targets_len = 0;
732 }
733 if dlmm_targets_len < dlmm_targets.len() {
734 dlmm_targets[dlmm_targets_len] =
735 Some((outer_idx, stack_height, target_idx));
736 dlmm_targets_len += 1;
737 }
738 }
739 }
740 }
741 }
742
743 result
744}
745
746#[inline(always)]
748fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
749 let Some(filter) = filter else { return true };
751
752 if filter.include_only.is_none() {
754 return true;
755 }
756
757 if filter.includes_pumpfun() {
760 return true;
761 }
762
763 if filter.includes_pump_fees() {
764 return true;
765 }
766
767 filter.includes_pumpswap()
768 || filter.includes_raydium_launchlab()
769 || filter.includes_raydium_cpmm()
770 || filter.includes_raydium_clmm()
771 || filter.includes_raydium_amm_v4()
772 || filter.includes_orca_whirlpool()
773 || filter.includes_meteora_pools()
774 || filter.includes_meteora_damm_v2()
775 || filter.includes_meteora_dlmm()
776 || filter.includes_meteora_dbc()
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
783 use yellowstone_grpc_proto::prelude::{
784 CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
785 };
786
787 fn pk(s: &str) -> Pubkey {
788 s.parse().unwrap()
789 }
790
791 fn usdc_mint() -> Pubkey {
792 pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
793 }
794
795 fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
796 key.to_bytes().to_vec()
797 }
798
799 fn decode_b58(s: &str) -> Vec<u8> {
800 bs58::decode(s).into_vec().unwrap()
801 }
802
803 fn str_arg(s: &str, out: &mut Vec<u8>) {
804 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
805 out.extend_from_slice(s.as_bytes());
806 }
807
808 fn create_v2_data() -> Vec<u8> {
809 let mut data = Vec::new();
810 data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
811 str_arg("Alt Coin", &mut data);
812 str_arg("ALT", &mut data);
813 str_arg("https://example.invalid/alt.json", &mut data);
814 data.extend_from_slice(Pubkey::new_unique().as_ref());
815 data.push(1);
816 data.push(1);
817 data
818 }
819
820 fn grpc_pumpfun_create_v2_tx(
821 static_len: usize,
822 writable_len: usize,
823 program_idx: u8,
824 ix_accounts: Vec<u8>,
825 account_overrides: Vec<(usize, Pubkey)>,
826 ) -> (TransactionStatusMeta, Option<Transaction>) {
827 let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
828 account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
829 let readonly_len = account_overrides
830 .iter()
831 .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
832 .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
833 .max()
834 .unwrap_or_default();
835 let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
836 let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
837 for (global_idx, key) in account_overrides {
838 if global_idx < static_len {
839 account_keys[global_idx] = key;
840 } else if global_idx < static_len + writable_len {
841 loaded_writable[global_idx - static_len] = key;
842 } else {
843 loaded_readonly[global_idx - static_len - writable_len] = key;
844 }
845 }
846
847 let meta = TransactionStatusMeta {
848 loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
849 loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
850 ..Default::default()
851 };
852 let tx = Transaction {
853 signatures: vec![Signature::default().as_ref().to_vec()],
854 message: Some(Message {
855 header: Some(MessageHeader {
856 num_required_signatures: 1,
857 num_readonly_signed_accounts: 0,
858 num_readonly_unsigned_accounts: 0,
859 }),
860 account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
861 recent_blockhash: vec![0; 32],
862 instructions: vec![CompiledInstruction {
863 program_id_index: program_idx as u32,
864 accounts: ix_accounts,
865 data: create_v2_data(),
866 }],
867 versioned: true,
868 address_table_lookups: Vec::new(),
869 config: None,
870 }),
871 };
872 (meta, Some(tx))
873 }
874
875 fn create_v2_accounts(
876 account_len: usize,
877 program_idx: u8,
878 mint_idx: u8,
879 user_idx: u8,
880 token_program_idx: u8,
881 quote_tail: Option<(u8, u8, u8)>,
882 ) -> Vec<u8> {
883 let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
884 accounts[0] = mint_idx;
885 accounts[5] = user_idx;
886 accounts[7] = token_program_idx;
887 if account_len > 15 {
888 accounts[15] = program_idx;
889 }
890 if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
891 accounts[16] = quote_idx;
892 accounts[17] = quote_vault_idx;
893 accounts[18] = quote_token_program_idx;
894 }
895 accounts
896 }
897
898 fn parse_create_v2_from_grpc(
899 meta: &TransactionStatusMeta,
900 tx: &Option<Transaction>,
901 ) -> crate::core::events::PumpFunCreateTokenEvent {
902 let events = parse_instructions_enhanced(
903 meta,
904 tx,
905 Signature::default(),
906 123,
907 0,
908 Some(456),
909 789,
910 None,
911 );
912 assert_eq!(events.len(), 1);
913 match &events[0] {
914 DexEvent::PumpFunCreate(e) => {
915 assert_eq!(e.ix_name, "create_v2");
916 e.clone()
917 }
918 DexEvent::PumpFunCreateV2(e) => {
919 assert_eq!(e.ix_name, "create_v2");
920 crate::core::events::PumpFunCreateTokenEvent {
921 depth: 0,
922 metadata: e.metadata.clone(),
923 name: e.name.clone(),
924 symbol: e.symbol.clone(),
925 uri: e.uri.clone(),
926 mint: e.mint,
927 bonding_curve: e.bonding_curve,
928 user: e.user,
929 creator: e.creator,
930 timestamp: e.timestamp,
931 virtual_token_reserves: e.virtual_token_reserves,
932 virtual_sol_reserves: e.virtual_sol_reserves,
933 real_token_reserves: e.real_token_reserves,
934 token_total_supply: e.token_total_supply,
935 mint_authority: e.mint_authority,
936 associated_bonding_curve: e.associated_bonding_curve,
937 global: e.global,
938 system_program: e.system_program,
939 token_program: e.token_program,
940 associated_token_program: e.associated_token_program,
941 mayhem_program_id: e.mayhem_program_id,
942 global_params: e.global_params,
943 sol_vault: e.sol_vault,
944 mayhem_state: e.mayhem_state,
945 mayhem_token_vault: e.mayhem_token_vault,
946 event_authority: e.event_authority,
947 program: e.program,
948 quote_mint: e.quote_mint,
949 quote_vault: e.quote_vault,
950 quote_token_program: e.quote_token_program,
951 virtual_quote_reserves: e.virtual_quote_reserves,
952 creator_fee_bps: e.creator_fee_bps,
953 is_holder_reward: e.is_holder_reward,
954 ix_name: e.ix_name.clone(),
955 is_mayhem_mode: e.is_mayhem_mode,
956 is_cashback_enabled: e.is_cashback_enabled,
957 observed_fee_recipient: e.observed_fee_recipient,
958 }
959 }
960 other => panic!("expected PumpFun create_v2 event, got {other:?}"),
961 }
962 }
963
964 #[test]
965 fn test_should_parse_instructions() {
966 assert!(should_parse_instructions(None));
968
969 let filter = EventTypeFilter { include_only: None, exclude_types: None };
971 assert!(should_parse_instructions(Some(&filter)));
972
973 use crate::grpc::types::EventType;
975 let filter = EventTypeFilter {
976 include_only: Some(vec![EventType::PumpFunMigrate]),
977 exclude_types: None,
978 };
979 assert!(should_parse_instructions(Some(&filter)));
980
981 let filter = EventTypeFilter {
983 include_only: Some(vec![EventType::PumpFunTrade]),
984 exclude_types: None,
985 };
986 assert!(should_parse_instructions(Some(&filter)));
987
988 for event_type in [
989 EventType::PumpSwapTrade,
990 EventType::PumpFeesUpdateFeeShares,
991 EventType::RaydiumLaunchlabTrade,
992 EventType::RaydiumCpmmSwap,
993 EventType::RaydiumClmmSwap,
994 EventType::RaydiumAmmV4Swap,
995 EventType::OrcaWhirlpoolSwap,
996 EventType::MeteoraPoolsSwap,
997 EventType::MeteoraDammV2Swap,
998 EventType::MeteoraDammV2InitializePool,
999 EventType::MeteoraDlmmSwap,
1000 ] {
1001 let filter = EventTypeFilter::include_only(vec![event_type]);
1002 assert!(
1003 should_parse_instructions(Some(&filter)),
1004 "instruction parsing should be enabled for {event_type:?}"
1005 );
1006 }
1007
1008 let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
1009 assert!(
1010 should_parse_instructions(Some(&filter)),
1011 "DBC event CPI must parse even with a DBC-only filter"
1012 );
1013
1014 let filter = EventTypeFilter::include_only(vec![
1015 EventType::AccountPumpFunGlobal,
1016 EventType::AccountRaydiumClmmPoolState,
1017 EventType::AccountRaydiumCpmmPoolState,
1018 EventType::AccountOrcaWhirlpool,
1019 ]);
1020 assert!(
1021 !should_parse_instructions(Some(&filter)),
1022 "account-only non-Pump filters should stay on the account update path"
1023 );
1024 }
1025
1026 #[test]
1027 fn test_merge_instruction_events() {
1028 use solana_sdk::signature::Signature;
1029
1030 let metadata = EventMetadata {
1031 signature: Signature::default(),
1032 slot: 100,
1033 tx_index: 1,
1034 block_time_us: 1000,
1035 grpc_recv_us: 2000,
1036 recent_blockhash: None,
1037 };
1038
1039 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1041 metadata: metadata.clone(),
1042 bonding_curve: Pubkey::new_unique(),
1043 ..Default::default()
1044 });
1045
1046 let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1047 metadata: metadata.clone(),
1048 sol_amount: 1000,
1049 token_amount: 2000,
1050 ..Default::default()
1051 });
1052
1053 let events = vec![
1054 IndexedInstructionEvent {
1055 outer_idx: 0,
1056 inner_idx: None,
1057 stack_height: Some(1),
1058 is_event_cpi: false,
1059 event: outer_event,
1060 },
1061 IndexedInstructionEvent {
1062 outer_idx: 0,
1063 inner_idx: Some(0),
1064 stack_height: Some(2),
1065 is_event_cpi: false,
1066 event: inner_event,
1067 },
1068 ];
1069
1070 let result = merge_instruction_events(events);
1071
1072 assert_eq!(result.len(), 1);
1074
1075 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1077 assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
1081 panic!("Expected PumpFunTrade event");
1082 }
1083 }
1084
1085 #[test]
1086 fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
1087 use solana_sdk::signature::Signature;
1088
1089 let metadata = EventMetadata {
1090 signature: Signature::default(),
1091 slot: 100,
1092 tx_index: 1,
1093 block_time_us: 1000,
1094 grpc_recv_us: 2000,
1095 recent_blockhash: None,
1096 };
1097
1098 let bc = Pubkey::new_unique();
1099 let fee = Pubkey::new_unique();
1100
1101 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1102 metadata: metadata.clone(),
1103 bonding_curve: bc,
1104 ..Default::default()
1105 });
1106
1107 let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1108 metadata: metadata.clone(),
1109 sol_amount: 1000,
1110 token_amount: 2000,
1111 is_buy: true,
1112 ..Default::default()
1113 });
1114
1115 let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1117 metadata: metadata.clone(),
1118 fee_recipient: fee,
1119 ..Default::default()
1120 });
1121
1122 let events = vec![
1123 IndexedInstructionEvent {
1124 outer_idx: 0,
1125 inner_idx: None,
1126 stack_height: Some(1),
1127 is_event_cpi: false,
1128 event: outer_event,
1129 },
1130 IndexedInstructionEvent {
1131 outer_idx: 0,
1132 inner_idx: Some(0),
1133 stack_height: Some(2),
1134 is_event_cpi: false,
1135 event: inner_trade,
1136 },
1137 IndexedInstructionEvent {
1138 outer_idx: 0,
1139 inner_idx: Some(1),
1140 stack_height: Some(2),
1141 is_event_cpi: false,
1142 event: inner_fee_only,
1143 },
1144 ];
1145
1146 let result = merge_instruction_events(events);
1147 assert_eq!(result.len(), 1);
1148 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1149 assert_eq!(trade.bonding_curve, bc);
1150 assert_eq!(trade.sol_amount, 1000);
1151 assert_eq!(trade.token_amount, 2000);
1152 assert_eq!(trade.fee_recipient, fee);
1153 } else {
1154 panic!("Expected PumpFunTrade event");
1155 }
1156 }
1157
1158 fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1159 DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1160 metadata: EventMetadata::default(),
1161 token_x_mint: Pubkey::default(),
1162 token_y_mint: Pubkey::default(),
1163 user_token_in: Pubkey::default(),
1164 user_token_out: Pubkey::default(),
1165 min_amount_out: 0,
1166 pool,
1167 from: Pubkey::default(),
1168 start_bin_id: 0,
1169 end_bin_id: 0,
1170 amount_in,
1171 amount_out,
1172 swap_for_y: false,
1173 fee: 0,
1174 protocol_fee: 0,
1175 fee_bps: 0,
1176 host_fee: 0,
1177 ..Default::default()
1178 })
1179 }
1180
1181 fn dlmm_add_liquidity() -> DexEvent {
1182 DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1183 metadata: EventMetadata::default(),
1184 pool: Pubkey::default(),
1185 from: Pubkey::default(),
1186 position: Pubkey::default(),
1187 amounts: [0; 2],
1188 active_bin_id: 0,
1189 })
1190 }
1191
1192 #[test]
1193 fn merge_preserves_unrelated_inner_events() {
1194 let events = vec![
1195 IndexedInstructionEvent {
1196 outer_idx: 0,
1197 inner_idx: None,
1198 stack_height: Some(1),
1199 is_event_cpi: false,
1200 event: dlmm_swap(Pubkey::default(), 0, 0),
1201 },
1202 IndexedInstructionEvent {
1203 outer_idx: 0,
1204 inner_idx: Some(0),
1205 stack_height: Some(2),
1206 is_event_cpi: false,
1207 event: dlmm_add_liquidity(),
1208 },
1209 ];
1210
1211 let result = merge_instruction_events(events);
1212 assert_eq!(result.len(), 2);
1213 assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1214 assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1215 }
1216
1217 #[test]
1218 fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1219 let first_pool = Pubkey::new_unique();
1220 let second_pool = Pubkey::new_unique();
1221 let events = vec![
1222 IndexedInstructionEvent {
1223 outer_idx: 0,
1224 inner_idx: Some(0),
1225 stack_height: Some(2),
1226 is_event_cpi: false,
1227 event: dlmm_swap(first_pool, 1, 0),
1228 },
1229 IndexedInstructionEvent {
1230 outer_idx: 0,
1231 inner_idx: Some(1),
1232 stack_height: Some(3),
1233 is_event_cpi: true,
1234 event: dlmm_swap(first_pool, 10, 9),
1235 },
1236 IndexedInstructionEvent {
1237 outer_idx: 0,
1238 inner_idx: Some(2),
1239 stack_height: Some(2),
1240 is_event_cpi: false,
1241 event: dlmm_swap(second_pool, 2, 0),
1242 },
1243 IndexedInstructionEvent {
1244 outer_idx: 0,
1245 inner_idx: Some(3),
1246 stack_height: Some(3),
1247 is_event_cpi: true,
1248 event: dlmm_swap(second_pool, 20, 18),
1249 },
1250 ];
1251
1252 let result = merge_instruction_events(events);
1253 assert_eq!(result.len(), 2);
1254 let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1255 let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1256 assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1257 assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1258 }
1259
1260 #[test]
1261 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1262 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1263 let static_keys = [
1264 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1265 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1266 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1267 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1268 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1269 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1270 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1271 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1272 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1273 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1274 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1275 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1276 "ComputeBudget111111111111111111111111111111",
1277 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1278 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1279 "SysvarRent111111111111111111111111111111111",
1280 ];
1281 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1282 let loaded_readonly = [
1283 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1284 "So11111111111111111111111111111111111111112",
1285 "11111111111111111111111111111111",
1286 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1287 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1288 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1289 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1290 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1291 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1292 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1293 ];
1294 let meta = TransactionStatusMeta {
1295 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1296 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1297 inner_instructions: vec![InnerInstructions {
1298 index: 2,
1299 instructions: vec![InnerInstruction {
1300 program_id_index: 20,
1301 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1302 data: decode_b58(
1303 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1304 ),
1305 stack_height: Some(2),
1306 }],
1307 }],
1308 ..Default::default()
1309 };
1310 let tx = Some(Transaction {
1311 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1312 message: Some(Message {
1313 header: Some(MessageHeader::default()),
1314 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1315 recent_blockhash: vec![0; 32],
1316 instructions: vec![
1317 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1318 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1319 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1320 ],
1321 versioned: true,
1322 address_table_lookups: Vec::new(),
1323 config: None,
1324 }),
1325 });
1326
1327 let events = parse_instructions_enhanced(
1328 &meta,
1329 &tx,
1330 signature.parse().unwrap(),
1331 427_039_576,
1332 0,
1333 Some(1_781_687_252_000_000),
1334 789,
1335 None,
1336 );
1337
1338 assert_eq!(events.len(), 1, "{signature}");
1339 match &events[0] {
1340 DexEvent::PumpSwapCreatePool(e) => {
1341 assert_eq!(e.index, 0, "{signature}");
1342 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1343 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1344 assert_eq!(
1345 e.coin_creator,
1346 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1347 "{signature}"
1348 );
1349 assert!(!e.is_mayhem_mode, "{signature}");
1350 assert!(e.is_cashback_coin, "{signature}");
1351 assert_eq!(
1352 e.pool,
1353 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1354 "{signature}"
1355 );
1356 assert_eq!(
1357 e.creator,
1358 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1359 "{signature}"
1360 );
1361 assert_eq!(
1362 e.base_mint,
1363 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1364 "{signature}"
1365 );
1366 assert_eq!(
1367 e.quote_mint,
1368 pk("So11111111111111111111111111111111111111112"),
1369 "{signature}"
1370 );
1371 assert_eq!(
1372 e.lp_mint,
1373 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1374 "{signature}"
1375 );
1376 assert_eq!(
1377 e.user_base_token_account,
1378 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1379 "{signature}"
1380 );
1381 assert_eq!(
1382 e.user_quote_token_account,
1383 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1384 "{signature}"
1385 );
1386 }
1387 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1388 }
1389 }
1390
1391 #[test]
1392 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1393 struct Case {
1394 signature: &'static str,
1395 name: &'static str,
1396 static_len: usize,
1397 writable_len: usize,
1398 program_idx: u8,
1399 account_len: usize,
1400 mint_idx: u8,
1401 mint: &'static str,
1402 user_idx: u8,
1403 user: &'static str,
1404 token_program_idx: u8,
1405 quote_idx: u8,
1406 quote_mint: Pubkey,
1407 quote_vault_idx: u8,
1408 quote_vault: &'static str,
1409 quote_token_program_idx: u8,
1410 }
1411 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1412 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1413 let cases = [
1414 Case {
1415 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1416 name: "19-account WSOL quote in ALT",
1417 static_len: 15,
1418 writable_len: 7,
1419 program_idx: 12,
1420 account_len: 19,
1421 mint_idx: 1,
1422 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1423 user_idx: 0,
1424 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1425 token_program_idx: 24,
1426 quote_idx: 27,
1427 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1428 quote_vault_idx: 7,
1429 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1430 quote_token_program_idx: 28,
1431 },
1432 Case {
1433 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1434 name: "19-account WSOL quote in ALT with exact quote buy",
1435 static_len: 20,
1436 writable_len: 7,
1437 program_idx: 15,
1438 account_len: 19,
1439 mint_idx: 1,
1440 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1441 user_idx: 0,
1442 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1443 token_program_idx: 31,
1444 quote_idx: 28,
1445 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1446 quote_vault_idx: 4,
1447 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1448 quote_token_program_idx: 30,
1449 },
1450 Case {
1451 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1452 name: "19-account USDC quote in ALT",
1453 static_len: 19,
1454 writable_len: 6,
1455 program_idx: 16,
1456 account_len: 19,
1457 mint_idx: 1,
1458 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1459 user_idx: 0,
1460 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1461 token_program_idx: 27,
1462 quote_idx: 30,
1463 quote_mint: usdc_mint(),
1464 quote_vault_idx: 6,
1465 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1466 quote_token_program_idx: 31,
1467 },
1468 Case {
1469 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1470 name: "20-account WSOL quote in ALT",
1471 static_len: 15,
1472 writable_len: 7,
1473 program_idx: 12,
1474 account_len: 20,
1475 mint_idx: 1,
1476 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1477 user_idx: 0,
1478 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1479 token_program_idx: 24,
1480 quote_idx: 27,
1481 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1482 quote_vault_idx: 7,
1483 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1484 quote_token_program_idx: 28,
1485 },
1486 Case {
1487 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1488 name: "19-account WSOL quote in ALT with later buy",
1489 static_len: 18,
1490 writable_len: 7,
1491 program_idx: 13,
1492 account_len: 19,
1493 mint_idx: 1,
1494 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1495 user_idx: 0,
1496 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1497 token_program_idx: 27,
1498 quote_idx: 30,
1499 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1500 quote_vault_idx: 7,
1501 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1502 quote_token_program_idx: 31,
1503 },
1504 Case {
1505 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1506 name: "19-account USDC quote in ALT exact quote buy",
1507 static_len: 19,
1508 writable_len: 6,
1509 program_idx: 15,
1510 account_len: 19,
1511 mint_idx: 1,
1512 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1513 user_idx: 0,
1514 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1515 token_program_idx: 28,
1516 quote_idx: 33,
1517 quote_mint: usdc_mint(),
1518 quote_vault_idx: 7,
1519 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1520 quote_token_program_idx: 27,
1521 },
1522 Case {
1523 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1524 name: "20-account WSOL quote in ALT with jit account",
1525 static_len: 15,
1526 writable_len: 7,
1527 program_idx: 12,
1528 account_len: 20,
1529 mint_idx: 1,
1530 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1531 user_idx: 0,
1532 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1533 token_program_idx: 24,
1534 quote_idx: 27,
1535 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1536 quote_vault_idx: 7,
1537 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1538 quote_token_program_idx: 28,
1539 },
1540 ];
1541
1542 for case in cases {
1543 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1544 case.static_len,
1545 case.writable_len,
1546 case.program_idx,
1547 create_v2_accounts(
1548 case.account_len,
1549 case.program_idx,
1550 case.mint_idx,
1551 case.user_idx,
1552 case.token_program_idx,
1553 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1554 ),
1555 vec![
1556 (case.mint_idx as usize, pk(case.mint)),
1557 (case.user_idx as usize, pk(case.user)),
1558 (case.token_program_idx as usize, token_2022_program),
1559 (case.quote_idx as usize, case.quote_mint),
1560 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1561 (case.quote_token_program_idx as usize, spl_token_program),
1562 ],
1563 );
1564 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1565 let idx = global_idx as usize;
1566 if idx < case.static_len {
1567 ("static", idx)
1568 } else if idx < case.static_len + case.writable_len {
1569 ("writable", idx - case.static_len)
1570 } else {
1571 ("readonly", idx - case.static_len - case.writable_len)
1572 }
1573 };
1574 assert_eq!(
1575 meta.loaded_writable_addresses.len(),
1576 case.writable_len,
1577 "{}: {}",
1578 case.name,
1579 case.signature
1580 );
1581 for (global_idx, expected_key) in [
1582 (case.token_program_idx, token_2022_program),
1583 (case.quote_idx, case.quote_mint),
1584 (case.quote_token_program_idx, spl_token_program),
1585 ] {
1586 match loaded_key_location(global_idx) {
1587 ("static", _) => {}
1588 ("writable", offset) => assert_eq!(
1589 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1590 expected_key,
1591 "{}: writable loaded key {global_idx}: {}",
1592 case.name,
1593 case.signature
1594 ),
1595 ("readonly", offset) => assert_eq!(
1596 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1597 expected_key,
1598 "{}: readonly loaded key {global_idx}: {}",
1599 case.name,
1600 case.signature
1601 ),
1602 _ => unreachable!(),
1603 }
1604 }
1605
1606 let create = parse_create_v2_from_grpc(&meta, &tx);
1607
1608 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1609 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1610 assert_eq!(
1611 create.token_program, token_2022_program,
1612 "{}: {}",
1613 case.name, case.signature
1614 );
1615 assert_eq!(
1616 create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1617 "{}: {}",
1618 case.name, case.signature
1619 );
1620 assert_eq!(create.quote_vault, Pubkey::default(), "{}: {}", case.name, case.signature);
1621 assert_eq!(
1622 create.quote_token_program,
1623 Pubkey::default(),
1624 "{}: {}",
1625 case.name,
1626 case.signature
1627 );
1628 }
1629 }
1630
1631 #[test]
1632 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1633 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1634 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1635 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1636 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1637 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1638 16,
1639 5,
1640 12,
1641 create_v2_accounts(16, 12, 1, 0, 24, None),
1642 vec![(1, mint), (0, user), (24, token_program)],
1643 );
1644
1645 let create = parse_create_v2_from_grpc(&meta, &tx);
1646
1647 assert_eq!(create.mint, mint, "{signature}");
1648 assert_eq!(create.user, user, "{signature}");
1649 assert_eq!(create.token_program, token_program, "{signature}");
1650 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1651 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1652 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1653 }
1654
1655 #[test]
1656 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1657 let quote_vault = Pubkey::new_unique();
1658 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1659 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1660 19,
1661 6,
1662 16,
1663 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1664 vec![
1665 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1666 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1667 (6, quote_vault),
1668 (31, quote_token_program),
1669 ],
1670 );
1671
1672 let create = parse_create_v2_from_grpc(&meta, &tx);
1673
1674 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
1675 assert_eq!(create.quote_vault, Pubkey::default());
1676 assert_eq!(create.quote_token_program, Pubkey::default());
1677 }
1678 #[test]
1679 fn grpc_amm_v4_accepts_one_byte_withdraw_pnl_instruction() {
1680 let mut keys: Vec<_> = (0..14).map(|_| Pubkey::new_unique()).collect();
1681 keys.push(crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID);
1682 let tx = Some(Transaction {
1683 message: Some(Message {
1684 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
1685 instructions: vec![CompiledInstruction {
1686 program_id_index: 14,
1687 accounts: (0..14).collect(),
1688 data: vec![crate::instr::raydium_amm::discriminators::WITHDRAW_PNL],
1689 }],
1690 ..Default::default()
1691 }),
1692 ..Default::default()
1693 });
1694 let events = parse_instructions_enhanced(
1695 &TransactionStatusMeta::default(),
1696 &tx,
1697 Signature::default(),
1698 42,
1699 0,
1700 None,
1701 0,
1702 None,
1703 );
1704 assert_eq!(events.len(), 1);
1705 let DexEvent::RaydiumAmmV4WithdrawPnl(e) = &events[0] else { panic!("withdraw pnl") };
1706 assert_eq!(e.amm, keys[1]);
1707 }
1708}
1709
1710#[cfg(test)]
1711mod cpmm_account_decode_boundaries {
1712 use super::*;
1713
1714 #[test]
1715 fn invalid_or_same_family_filtered_cpmm_does_not_resolve_accounts() {
1716 use crate::instr::raydium_cpmm::discriminators::*;
1717 let swaps_only =
1718 EventTypeFilter::include_only(vec![crate::grpc::EventType::RaydiumCpmmSwap]);
1719 let get_key = |_index: usize| -> Option<&Vec<u8>> { panic!("rejected account resolved") };
1720 for (data, count, filter) in [
1721 (COLLECT_CREATOR_FEE.to_vec(), 15, Some(&swaps_only)),
1722 (COLLECT_CREATOR_FEE_PERMISSIONLESS.to_vec(), 65, Some(&swaps_only)),
1723 (SWAP_BASE_IN.to_vec(), 13, None),
1724 (SWAP_BASE_OUT.to_vec(), 65, None),
1725 (DEPOSIT.to_vec(), 13, None),
1726 (COLLECT_CREATOR_FEE.to_vec(), 14, None),
1727 (COLLECT_CREATOR_FEE_PERMISSIONLESS.to_vec(), 15, None),
1728 (vec![255; 8], 65, None),
1729 ] {
1730 assert!(parse_compiled_instruction(
1731 &data,
1732 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1733 Signature::default(),
1734 1,
1735 0,
1736 None,
1737 0,
1738 &vec![0; count],
1739 &get_key,
1740 filter,
1741 )
1742 .is_none());
1743 }
1744 }
1745
1746 #[test]
1747 fn excluded_cpmm_never_decodes_the_instruction_account_array() {
1748 let filter = EventTypeFilter::include_only(vec![crate::grpc::EventType::RaydiumClmmSwap]);
1749 let key = vec![1; 32];
1750 let get_key = |_index: usize| -> Option<&Vec<u8>> { panic!("excluded account decoded") };
1751 for count in [15, 65] {
1752 assert!(parse_compiled_instruction(
1753 &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1754 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1755 Signature::default(),
1756 1,
1757 0,
1758 None,
1759 0,
1760 &vec![0; count],
1761 &get_key,
1762 Some(&filter),
1763 )
1764 .is_none());
1765 let valid = |_index: usize| Some(&key);
1767 assert!(parse_compiled_instruction(
1768 &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1769 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1770 Signature::default(),
1771 1,
1772 0,
1773 None,
1774 0,
1775 &vec![0; count],
1776 &valid,
1777 None,
1778 )
1779 .is_some());
1780 }
1781 }
1782
1783 #[test]
1784 fn stack_and_heap_account_decoders_reject_noncanonical_pubkey_lengths() {
1785 for count in [15, 65] {
1786 for length in [0, 31, 33, 64] {
1787 let key = vec![1; length];
1788 let get_key = |_index: usize| Some(&key);
1789 assert!(parse_compiled_instruction(
1790 &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1791 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1792 Signature::default(),
1793 1,
1794 0,
1795 None,
1796 0,
1797 &vec![0; count],
1798 &get_key,
1799 None,
1800 )
1801 .is_none());
1802 }
1803 }
1804 }
1805}
1806
1807#[cfg(test)]
1808mod pump_occurrence_tests {
1809 use super::*;
1810 #[test]
1811 fn repeated_trades_pair_with_own_cpi() {
1812 let mint = Pubkey::new_unique();
1813 let user = Pubkey::new_unique();
1814 for known in [true, false] {
1815 let event = |amount, executed| {
1816 DexEvent::PumpFunTrade(PumpFunTradeEvent {
1817 mint,
1818 user,
1819 is_buy: true,
1820 ix_name: "buy_v3".into(),
1821 amount: if executed { 0 } else { amount },
1822 token_amount: if executed { amount } else { 0 },
1823 ..Default::default()
1824 })
1825 };
1826 let item = |inner_idx, height, is_cpi, event| IndexedInstructionEvent {
1827 outer_idx: 0,
1828 inner_idx,
1829 stack_height: if known { Some(height) } else { None },
1830 is_event_cpi: is_cpi,
1831 event,
1832 };
1833 let result = merge_instruction_events(vec![
1834 item(None, 1, false, event(100, false)),
1835 item(Some(0), 2, true, event(90, true)),
1836 item(Some(1), 2, false, event(200, false)),
1837 item(Some(2), 3, true, event(180, true)),
1838 ]);
1839 assert_eq!(result.len(), 2);
1840 for (e, (amount, tokens)) in result.iter().zip([(100, 90), (200, 180)]) {
1841 let DexEvent::PumpFunTrade(e) = e else { panic!() };
1842 assert_eq!((e.amount, e.token_amount), (amount, tokens));
1843 }
1844 }
1845 }
1846}
1847
1848#[cfg(test)]
1849mod swap_occurrence_tests {
1850 use super::*;
1851 #[test]
1852 fn repeated_swap_calls_pair_with_their_own_cpi() {
1853 let pool = Pubkey::new_unique();
1854 let user = Pubkey::new_unique();
1855 let accounts = [Pubkey::new_unique(), Pubkey::new_unique()];
1856 for buy in [true, false] {
1857 for known in [true, false] {
1858 let event = |n: usize, execution| {
1859 let account = if execution { Pubkey::default() } else { accounts[n] };
1860 let amount = if execution { 90 * (n as u64 + 1) } else { 0 };
1861 if buy {
1862 DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
1863 pool,
1864 user,
1865 user_base_token_account: account,
1866 quote_amount_in: amount,
1867 ..Default::default()
1868 })
1869 } else {
1870 DexEvent::PumpSwapSell(PumpSwapSellEvent {
1871 pool,
1872 user,
1873 user_base_token_account: account,
1874 quote_amount_out: amount,
1875 ..Default::default()
1876 })
1877 }
1878 };
1879 let item = |inner_idx, height, is_cpi, event| IndexedInstructionEvent {
1880 outer_idx: 0,
1881 inner_idx,
1882 stack_height: if known { Some(height) } else { None },
1883 is_event_cpi: is_cpi,
1884 event,
1885 };
1886 let out = merge_instruction_events(vec![
1887 item(None, 1, false, event(0, false)),
1888 item(Some(0), 2, true, event(0, true)),
1889 item(Some(1), 2, false, event(1, false)),
1890 item(Some(2), 3, true, event(1, true)),
1891 ]);
1892 assert_eq!(out.len(), 2);
1893 for (i, e) in out.iter().enumerate() {
1894 match e {
1895 DexEvent::PumpSwapBuy(e) => assert_eq!(
1896 (e.user_base_token_account, e.quote_amount_in),
1897 (accounts[i], 90 * (i as u64 + 1))
1898 ),
1899 DexEvent::PumpSwapSell(e) => assert_eq!(
1900 (e.user_base_token_account, e.quote_amount_out),
1901 (accounts[i], 90 * (i as u64 + 1))
1902 ),
1903 _ => panic!(),
1904 }
1905 }
1906 }
1907 }
1908 }
1909}