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