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