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