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_transaction_context(&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 alt_addresses: e.alt_addresses.clone(),
913 transaction_version: e.transaction_version,
914 depth: 0,
915 metadata: e.metadata.clone(),
916 name: e.name.clone(),
917 symbol: e.symbol.clone(),
918 uri: e.uri.clone(),
919 mint: e.mint,
920 bonding_curve: e.bonding_curve,
921 user: e.user,
922 creator: e.creator,
923 timestamp: e.timestamp,
924 virtual_token_reserves: e.virtual_token_reserves,
925 virtual_sol_reserves: e.virtual_sol_reserves,
926 real_token_reserves: e.real_token_reserves,
927 token_total_supply: e.token_total_supply,
928 mint_authority: e.mint_authority,
929 associated_bonding_curve: e.associated_bonding_curve,
930 global: e.global,
931 system_program: e.system_program,
932 token_program: e.token_program,
933 associated_token_program: e.associated_token_program,
934 mayhem_program_id: e.mayhem_program_id,
935 global_params: e.global_params,
936 sol_vault: e.sol_vault,
937 mayhem_state: e.mayhem_state,
938 mayhem_token_vault: e.mayhem_token_vault,
939 event_authority: e.event_authority,
940 program: e.program,
941 quote_mint: e.quote_mint,
942 quote_vault: e.quote_vault,
943 quote_token_program: e.quote_token_program,
944 virtual_quote_reserves: e.virtual_quote_reserves,
945 creator_fee_bps: e.creator_fee_bps,
946 is_holder_reward: e.is_holder_reward,
947 ix_name: e.ix_name.clone(),
948 is_mayhem_mode: e.is_mayhem_mode,
949 is_cashback_enabled: e.is_cashback_enabled,
950 observed_fee_recipient: e.observed_fee_recipient,
951 }
952 }
953 other => panic!("expected PumpFun create_v2 event, got {other:?}"),
954 }
955 }
956
957 #[test]
958 fn test_should_parse_instructions() {
959 assert!(should_parse_instructions(None));
961
962 let filter = EventTypeFilter { include_only: None, exclude_types: None };
964 assert!(should_parse_instructions(Some(&filter)));
965
966 use crate::grpc::types::EventType;
968 let filter = EventTypeFilter {
969 include_only: Some(vec![EventType::PumpFunMigrate]),
970 exclude_types: None,
971 };
972 assert!(should_parse_instructions(Some(&filter)));
973
974 let filter = EventTypeFilter {
976 include_only: Some(vec![EventType::PumpFunTrade]),
977 exclude_types: None,
978 };
979 assert!(should_parse_instructions(Some(&filter)));
980
981 for event_type in [
982 EventType::PumpSwapTrade,
983 EventType::PumpFeesUpdateFeeShares,
984 EventType::RaydiumLaunchlabTrade,
985 EventType::RaydiumCpmmSwap,
986 EventType::RaydiumClmmSwap,
987 EventType::RaydiumAmmV4Swap,
988 EventType::OrcaWhirlpoolSwap,
989 EventType::MeteoraPoolsSwap,
990 EventType::MeteoraDammV2Swap,
991 EventType::MeteoraDammV2InitializePool,
992 EventType::MeteoraDlmmSwap,
993 ] {
994 let filter = EventTypeFilter::include_only(vec![event_type]);
995 assert!(
996 should_parse_instructions(Some(&filter)),
997 "instruction parsing should be enabled for {event_type:?}"
998 );
999 }
1000
1001 let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
1002 assert!(
1003 should_parse_instructions(Some(&filter)),
1004 "DBC event CPI must parse even with a DBC-only filter"
1005 );
1006
1007 let filter = EventTypeFilter::include_only(vec![
1008 EventType::AccountPumpFunGlobal,
1009 EventType::AccountRaydiumClmmPoolState,
1010 EventType::AccountRaydiumCpmmPoolState,
1011 EventType::AccountOrcaWhirlpool,
1012 ]);
1013 assert!(
1014 !should_parse_instructions(Some(&filter)),
1015 "account-only non-Pump filters should stay on the account update path"
1016 );
1017 }
1018
1019 #[test]
1020 fn test_merge_instruction_events() {
1021 use solana_sdk::signature::Signature;
1022
1023 let metadata = EventMetadata {
1024 signature: Signature::default(),
1025 slot: 100,
1026 tx_index: 1,
1027 block_time_us: 1000,
1028 grpc_recv_us: 2000,
1029 recent_blockhash: None,
1030 };
1031
1032 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1034 metadata: metadata.clone(),
1035 bonding_curve: Pubkey::new_unique(),
1036 ..Default::default()
1037 });
1038
1039 let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1040 metadata: metadata.clone(),
1041 sol_amount: 1000,
1042 token_amount: 2000,
1043 ..Default::default()
1044 });
1045
1046 let events = vec![
1047 IndexedInstructionEvent {
1048 outer_idx: 0,
1049 inner_idx: None,
1050 stack_height: Some(1),
1051 is_event_cpi: false,
1052 event: outer_event,
1053 },
1054 IndexedInstructionEvent {
1055 outer_idx: 0,
1056 inner_idx: Some(0),
1057 stack_height: Some(2),
1058 is_event_cpi: false,
1059 event: inner_event,
1060 },
1061 ];
1062
1063 let result = merge_instruction_events(events);
1064
1065 assert_eq!(result.len(), 1);
1067
1068 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1070 assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
1074 panic!("Expected PumpFunTrade event");
1075 }
1076 }
1077
1078 #[test]
1079 fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
1080 use solana_sdk::signature::Signature;
1081
1082 let metadata = EventMetadata {
1083 signature: Signature::default(),
1084 slot: 100,
1085 tx_index: 1,
1086 block_time_us: 1000,
1087 grpc_recv_us: 2000,
1088 recent_blockhash: None,
1089 };
1090
1091 let bc = Pubkey::new_unique();
1092 let fee = Pubkey::new_unique();
1093
1094 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1095 metadata: metadata.clone(),
1096 bonding_curve: bc,
1097 ..Default::default()
1098 });
1099
1100 let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1101 metadata: metadata.clone(),
1102 sol_amount: 1000,
1103 token_amount: 2000,
1104 is_buy: true,
1105 ..Default::default()
1106 });
1107
1108 let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
1110 metadata: metadata.clone(),
1111 fee_recipient: fee,
1112 ..Default::default()
1113 });
1114
1115 let events = vec![
1116 IndexedInstructionEvent {
1117 outer_idx: 0,
1118 inner_idx: None,
1119 stack_height: Some(1),
1120 is_event_cpi: false,
1121 event: outer_event,
1122 },
1123 IndexedInstructionEvent {
1124 outer_idx: 0,
1125 inner_idx: Some(0),
1126 stack_height: Some(2),
1127 is_event_cpi: false,
1128 event: inner_trade,
1129 },
1130 IndexedInstructionEvent {
1131 outer_idx: 0,
1132 inner_idx: Some(1),
1133 stack_height: Some(2),
1134 is_event_cpi: false,
1135 event: inner_fee_only,
1136 },
1137 ];
1138
1139 let result = merge_instruction_events(events);
1140 assert_eq!(result.len(), 1);
1141 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1142 assert_eq!(trade.bonding_curve, bc);
1143 assert_eq!(trade.sol_amount, 1000);
1144 assert_eq!(trade.token_amount, 2000);
1145 assert_eq!(trade.fee_recipient, fee);
1146 } else {
1147 panic!("Expected PumpFunTrade event");
1148 }
1149 }
1150
1151 fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1152 DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1153 metadata: EventMetadata::default(),
1154 token_x_mint: Pubkey::default(),
1155 token_y_mint: Pubkey::default(),
1156 user_token_in: Pubkey::default(),
1157 user_token_out: Pubkey::default(),
1158 min_amount_out: 0,
1159 pool,
1160 from: Pubkey::default(),
1161 start_bin_id: 0,
1162 end_bin_id: 0,
1163 amount_in,
1164 amount_out,
1165 swap_for_y: false,
1166 fee: 0,
1167 protocol_fee: 0,
1168 fee_bps: 0,
1169 host_fee: 0,
1170 ..Default::default()
1171 })
1172 }
1173
1174 fn dlmm_add_liquidity() -> DexEvent {
1175 DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1176 metadata: EventMetadata::default(),
1177 pool: Pubkey::default(),
1178 from: Pubkey::default(),
1179 position: Pubkey::default(),
1180 amounts: [0; 2],
1181 active_bin_id: 0,
1182 })
1183 }
1184
1185 #[test]
1186 fn merge_preserves_unrelated_inner_events() {
1187 let events = vec![
1188 IndexedInstructionEvent {
1189 outer_idx: 0,
1190 inner_idx: None,
1191 stack_height: Some(1),
1192 is_event_cpi: false,
1193 event: dlmm_swap(Pubkey::default(), 0, 0),
1194 },
1195 IndexedInstructionEvent {
1196 outer_idx: 0,
1197 inner_idx: Some(0),
1198 stack_height: Some(2),
1199 is_event_cpi: false,
1200 event: dlmm_add_liquidity(),
1201 },
1202 ];
1203
1204 let result = merge_instruction_events(events);
1205 assert_eq!(result.len(), 2);
1206 assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1207 assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1208 }
1209
1210 #[test]
1211 fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1212 let first_pool = Pubkey::new_unique();
1213 let second_pool = Pubkey::new_unique();
1214 let events = vec![
1215 IndexedInstructionEvent {
1216 outer_idx: 0,
1217 inner_idx: Some(0),
1218 stack_height: Some(2),
1219 is_event_cpi: false,
1220 event: dlmm_swap(first_pool, 1, 0),
1221 },
1222 IndexedInstructionEvent {
1223 outer_idx: 0,
1224 inner_idx: Some(1),
1225 stack_height: Some(3),
1226 is_event_cpi: true,
1227 event: dlmm_swap(first_pool, 10, 9),
1228 },
1229 IndexedInstructionEvent {
1230 outer_idx: 0,
1231 inner_idx: Some(2),
1232 stack_height: Some(2),
1233 is_event_cpi: false,
1234 event: dlmm_swap(second_pool, 2, 0),
1235 },
1236 IndexedInstructionEvent {
1237 outer_idx: 0,
1238 inner_idx: Some(3),
1239 stack_height: Some(3),
1240 is_event_cpi: true,
1241 event: dlmm_swap(second_pool, 20, 18),
1242 },
1243 ];
1244
1245 let result = merge_instruction_events(events);
1246 assert_eq!(result.len(), 2);
1247 let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1248 let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1249 assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1250 assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1251 }
1252
1253 #[test]
1254 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1255 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1256 let static_keys = [
1257 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1258 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1259 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1260 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1261 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1262 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1263 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1264 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1265 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1266 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1267 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1268 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1269 "ComputeBudget111111111111111111111111111111",
1270 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1271 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1272 "SysvarRent111111111111111111111111111111111",
1273 ];
1274 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1275 let loaded_readonly = [
1276 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1277 "So11111111111111111111111111111111111111112",
1278 "11111111111111111111111111111111",
1279 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1280 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1281 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1282 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1283 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1284 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1285 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1286 ];
1287 let meta = TransactionStatusMeta {
1288 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1289 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1290 inner_instructions: vec![InnerInstructions {
1291 index: 2,
1292 instructions: vec![InnerInstruction {
1293 program_id_index: 20,
1294 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1295 data: decode_b58(
1296 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1297 ),
1298 stack_height: Some(2),
1299 }],
1300 }],
1301 ..Default::default()
1302 };
1303 let tx = Some(Transaction {
1304 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1305 message: Some(Message {
1306 header: Some(MessageHeader::default()),
1307 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1308 recent_blockhash: vec![0; 32],
1309 instructions: vec![
1310 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1311 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1312 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1313 ],
1314 versioned: true,
1315 address_table_lookups: Vec::new(),
1316 config: None,
1317 }),
1318 });
1319
1320 let events = parse_instructions_enhanced(
1321 &meta,
1322 &tx,
1323 signature.parse().unwrap(),
1324 427_039_576,
1325 0,
1326 Some(1_781_687_252_000_000),
1327 789,
1328 None,
1329 );
1330
1331 assert_eq!(events.len(), 1, "{signature}");
1332 match &events[0] {
1333 DexEvent::PumpSwapCreatePool(e) => {
1334 assert_eq!(e.index, 0, "{signature}");
1335 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1336 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1337 assert_eq!(
1338 e.coin_creator,
1339 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1340 "{signature}"
1341 );
1342 assert!(!e.is_mayhem_mode, "{signature}");
1343 assert!(e.is_cashback_coin, "{signature}");
1344 assert_eq!(
1345 e.pool,
1346 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1347 "{signature}"
1348 );
1349 assert_eq!(
1350 e.creator,
1351 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1352 "{signature}"
1353 );
1354 assert_eq!(
1355 e.base_mint,
1356 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1357 "{signature}"
1358 );
1359 assert_eq!(
1360 e.quote_mint,
1361 pk("So11111111111111111111111111111111111111112"),
1362 "{signature}"
1363 );
1364 assert_eq!(
1365 e.lp_mint,
1366 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1367 "{signature}"
1368 );
1369 assert_eq!(
1370 e.user_base_token_account,
1371 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1372 "{signature}"
1373 );
1374 assert_eq!(
1375 e.user_quote_token_account,
1376 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1377 "{signature}"
1378 );
1379 }
1380 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1381 }
1382 }
1383
1384 #[test]
1385 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1386 struct Case {
1387 signature: &'static str,
1388 name: &'static str,
1389 static_len: usize,
1390 writable_len: usize,
1391 program_idx: u8,
1392 account_len: usize,
1393 mint_idx: u8,
1394 mint: &'static str,
1395 user_idx: u8,
1396 user: &'static str,
1397 token_program_idx: u8,
1398 quote_idx: u8,
1399 quote_mint: Pubkey,
1400 quote_vault_idx: u8,
1401 quote_vault: &'static str,
1402 quote_token_program_idx: u8,
1403 }
1404 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1405 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1406 let cases = [
1407 Case {
1408 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1409 name: "19-account WSOL quote in ALT",
1410 static_len: 15,
1411 writable_len: 7,
1412 program_idx: 12,
1413 account_len: 19,
1414 mint_idx: 1,
1415 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1416 user_idx: 0,
1417 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1418 token_program_idx: 24,
1419 quote_idx: 27,
1420 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1421 quote_vault_idx: 7,
1422 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1423 quote_token_program_idx: 28,
1424 },
1425 Case {
1426 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1427 name: "19-account WSOL quote in ALT with exact quote buy",
1428 static_len: 20,
1429 writable_len: 7,
1430 program_idx: 15,
1431 account_len: 19,
1432 mint_idx: 1,
1433 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1434 user_idx: 0,
1435 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1436 token_program_idx: 31,
1437 quote_idx: 28,
1438 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1439 quote_vault_idx: 4,
1440 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1441 quote_token_program_idx: 30,
1442 },
1443 Case {
1444 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1445 name: "19-account USDC quote in ALT",
1446 static_len: 19,
1447 writable_len: 6,
1448 program_idx: 16,
1449 account_len: 19,
1450 mint_idx: 1,
1451 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1452 user_idx: 0,
1453 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1454 token_program_idx: 27,
1455 quote_idx: 30,
1456 quote_mint: usdc_mint(),
1457 quote_vault_idx: 6,
1458 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1459 quote_token_program_idx: 31,
1460 },
1461 Case {
1462 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1463 name: "20-account WSOL quote in ALT",
1464 static_len: 15,
1465 writable_len: 7,
1466 program_idx: 12,
1467 account_len: 20,
1468 mint_idx: 1,
1469 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1470 user_idx: 0,
1471 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1472 token_program_idx: 24,
1473 quote_idx: 27,
1474 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1475 quote_vault_idx: 7,
1476 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1477 quote_token_program_idx: 28,
1478 },
1479 Case {
1480 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1481 name: "19-account WSOL quote in ALT with later buy",
1482 static_len: 18,
1483 writable_len: 7,
1484 program_idx: 13,
1485 account_len: 19,
1486 mint_idx: 1,
1487 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1488 user_idx: 0,
1489 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1490 token_program_idx: 27,
1491 quote_idx: 30,
1492 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1493 quote_vault_idx: 7,
1494 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1495 quote_token_program_idx: 31,
1496 },
1497 Case {
1498 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1499 name: "19-account USDC quote in ALT exact quote buy",
1500 static_len: 19,
1501 writable_len: 6,
1502 program_idx: 15,
1503 account_len: 19,
1504 mint_idx: 1,
1505 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1506 user_idx: 0,
1507 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1508 token_program_idx: 28,
1509 quote_idx: 33,
1510 quote_mint: usdc_mint(),
1511 quote_vault_idx: 7,
1512 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1513 quote_token_program_idx: 27,
1514 },
1515 Case {
1516 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1517 name: "20-account WSOL quote in ALT with jit account",
1518 static_len: 15,
1519 writable_len: 7,
1520 program_idx: 12,
1521 account_len: 20,
1522 mint_idx: 1,
1523 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1524 user_idx: 0,
1525 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1526 token_program_idx: 24,
1527 quote_idx: 27,
1528 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1529 quote_vault_idx: 7,
1530 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1531 quote_token_program_idx: 28,
1532 },
1533 ];
1534
1535 for case in cases {
1536 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1537 case.static_len,
1538 case.writable_len,
1539 case.program_idx,
1540 create_v2_accounts(
1541 case.account_len,
1542 case.program_idx,
1543 case.mint_idx,
1544 case.user_idx,
1545 case.token_program_idx,
1546 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1547 ),
1548 vec![
1549 (case.mint_idx as usize, pk(case.mint)),
1550 (case.user_idx as usize, pk(case.user)),
1551 (case.token_program_idx as usize, token_2022_program),
1552 (case.quote_idx as usize, case.quote_mint),
1553 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1554 (case.quote_token_program_idx as usize, spl_token_program),
1555 ],
1556 );
1557 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1558 let idx = global_idx as usize;
1559 if idx < case.static_len {
1560 ("static", idx)
1561 } else if idx < case.static_len + case.writable_len {
1562 ("writable", idx - case.static_len)
1563 } else {
1564 ("readonly", idx - case.static_len - case.writable_len)
1565 }
1566 };
1567 assert_eq!(
1568 meta.loaded_writable_addresses.len(),
1569 case.writable_len,
1570 "{}: {}",
1571 case.name,
1572 case.signature
1573 );
1574 for (global_idx, expected_key) in [
1575 (case.token_program_idx, token_2022_program),
1576 (case.quote_idx, case.quote_mint),
1577 (case.quote_token_program_idx, spl_token_program),
1578 ] {
1579 match loaded_key_location(global_idx) {
1580 ("static", _) => {}
1581 ("writable", offset) => assert_eq!(
1582 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1583 expected_key,
1584 "{}: writable loaded key {global_idx}: {}",
1585 case.name,
1586 case.signature
1587 ),
1588 ("readonly", offset) => assert_eq!(
1589 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1590 expected_key,
1591 "{}: readonly loaded key {global_idx}: {}",
1592 case.name,
1593 case.signature
1594 ),
1595 _ => unreachable!(),
1596 }
1597 }
1598
1599 let create = parse_create_v2_from_grpc(&meta, &tx);
1600
1601 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1602 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1603 assert_eq!(
1604 create.token_program, token_2022_program,
1605 "{}: {}",
1606 case.name, case.signature
1607 );
1608 assert_eq!(
1609 create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
1610 "{}: {}",
1611 case.name, case.signature
1612 );
1613 assert_eq!(create.quote_vault, Pubkey::default(), "{}: {}", case.name, case.signature);
1614 assert_eq!(
1615 create.quote_token_program,
1616 Pubkey::default(),
1617 "{}: {}",
1618 case.name,
1619 case.signature
1620 );
1621 }
1622 }
1623
1624 #[test]
1625 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1626 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1627 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1628 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1629 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1630 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1631 16,
1632 5,
1633 12,
1634 create_v2_accounts(16, 12, 1, 0, 24, None),
1635 vec![(1, mint), (0, user), (24, token_program)],
1636 );
1637
1638 let create = parse_create_v2_from_grpc(&meta, &tx);
1639
1640 assert_eq!(create.mint, mint, "{signature}");
1641 assert_eq!(create.user, user, "{signature}");
1642 assert_eq!(create.token_program, token_program, "{signature}");
1643 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1644 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1645 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1646 }
1647
1648 #[test]
1649 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1650 let quote_vault = Pubkey::new_unique();
1651 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1652 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1653 19,
1654 6,
1655 16,
1656 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1657 vec![
1658 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1659 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1660 (6, quote_vault),
1661 (31, quote_token_program),
1662 ],
1663 );
1664
1665 let create = parse_create_v2_from_grpc(&meta, &tx);
1666
1667 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
1668 assert_eq!(create.quote_vault, Pubkey::default());
1669 assert_eq!(create.quote_token_program, Pubkey::default());
1670 }
1671 #[test]
1672 fn grpc_amm_v4_accepts_one_byte_withdraw_pnl_instruction() {
1673 let mut keys: Vec<_> = (0..14).map(|_| Pubkey::new_unique()).collect();
1674 keys.push(crate::instr::program_ids::RAYDIUM_AMM_V4_PROGRAM_ID);
1675 let tx = Some(Transaction {
1676 message: Some(Message {
1677 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
1678 instructions: vec![CompiledInstruction {
1679 program_id_index: 14,
1680 accounts: (0..14).collect(),
1681 data: vec![crate::instr::raydium_amm::discriminators::WITHDRAW_PNL],
1682 }],
1683 ..Default::default()
1684 }),
1685 ..Default::default()
1686 });
1687 let events = parse_instructions_enhanced(
1688 &TransactionStatusMeta::default(),
1689 &tx,
1690 Signature::default(),
1691 42,
1692 0,
1693 None,
1694 0,
1695 None,
1696 );
1697 assert_eq!(events.len(), 1);
1698 let DexEvent::RaydiumAmmV4WithdrawPnl(e) = &events[0] else { panic!("withdraw pnl") };
1699 assert_eq!(e.amm, keys[1]);
1700 }
1701}
1702
1703#[cfg(test)]
1704mod cpmm_account_decode_boundaries {
1705 use super::*;
1706
1707 #[test]
1708 fn invalid_or_same_family_filtered_cpmm_does_not_resolve_accounts() {
1709 use crate::instr::raydium_cpmm::discriminators::*;
1710 let swaps_only =
1711 EventTypeFilter::include_only(vec![crate::grpc::EventType::RaydiumCpmmSwap]);
1712 let get_key = |_index: usize| -> Option<&Vec<u8>> { panic!("rejected account resolved") };
1713 for (data, count, filter) in [
1714 (COLLECT_CREATOR_FEE.to_vec(), 15, Some(&swaps_only)),
1715 (COLLECT_CREATOR_FEE_PERMISSIONLESS.to_vec(), 65, Some(&swaps_only)),
1716 (SWAP_BASE_IN.to_vec(), 13, None),
1717 (SWAP_BASE_OUT.to_vec(), 65, None),
1718 (DEPOSIT.to_vec(), 13, None),
1719 (COLLECT_CREATOR_FEE.to_vec(), 14, None),
1720 (COLLECT_CREATOR_FEE_PERMISSIONLESS.to_vec(), 15, None),
1721 (vec![255; 8], 65, None),
1722 ] {
1723 assert!(parse_compiled_instruction(
1724 &data,
1725 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1726 Signature::default(),
1727 1,
1728 0,
1729 None,
1730 0,
1731 &vec![0; count],
1732 &get_key,
1733 filter,
1734 )
1735 .is_none());
1736 }
1737 }
1738
1739 #[test]
1740 fn excluded_cpmm_never_decodes_the_instruction_account_array() {
1741 let filter = EventTypeFilter::include_only(vec![crate::grpc::EventType::RaydiumClmmSwap]);
1742 let key = vec![1; 32];
1743 let get_key = |_index: usize| -> Option<&Vec<u8>> { panic!("excluded account decoded") };
1744 for count in [15, 65] {
1745 assert!(parse_compiled_instruction(
1746 &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1747 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1748 Signature::default(),
1749 1,
1750 0,
1751 None,
1752 0,
1753 &vec![0; count],
1754 &get_key,
1755 Some(&filter),
1756 )
1757 .is_none());
1758 let valid = |_index: usize| Some(&key);
1760 assert!(parse_compiled_instruction(
1761 &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1762 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1763 Signature::default(),
1764 1,
1765 0,
1766 None,
1767 0,
1768 &vec![0; count],
1769 &valid,
1770 None,
1771 )
1772 .is_some());
1773 }
1774 }
1775
1776 #[test]
1777 fn stack_and_heap_account_decoders_reject_noncanonical_pubkey_lengths() {
1778 for count in [15, 65] {
1779 for length in [0, 31, 33, 64] {
1780 let key = vec![1; length];
1781 let get_key = |_index: usize| Some(&key);
1782 assert!(parse_compiled_instruction(
1783 &crate::instr::raydium_cpmm::discriminators::COLLECT_CREATOR_FEE,
1784 &crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID,
1785 Signature::default(),
1786 1,
1787 0,
1788 None,
1789 0,
1790 &vec![0; count],
1791 &get_key,
1792 None,
1793 )
1794 .is_none());
1795 }
1796 }
1797 }
1798}
1799
1800#[cfg(test)]
1801mod pump_occurrence_tests {
1802 use super::*;
1803 #[test]
1804 fn repeated_trades_pair_with_own_cpi() {
1805 let mint = Pubkey::new_unique();
1806 let user = Pubkey::new_unique();
1807 for known in [true, false] {
1808 let event = |amount, executed| {
1809 DexEvent::PumpFunTrade(PumpFunTradeEvent {
1810 mint,
1811 user,
1812 is_buy: true,
1813 ix_name: "buy_v3".into(),
1814 amount: if executed { 0 } else { amount },
1815 token_amount: if executed { amount } else { 0 },
1816 ..Default::default()
1817 })
1818 };
1819 let item = |inner_idx, height, is_cpi, event| IndexedInstructionEvent {
1820 outer_idx: 0,
1821 inner_idx,
1822 stack_height: if known { Some(height) } else { None },
1823 is_event_cpi: is_cpi,
1824 event,
1825 };
1826 let result = merge_instruction_events(vec![
1827 item(None, 1, false, event(100, false)),
1828 item(Some(0), 2, true, event(90, true)),
1829 item(Some(1), 2, false, event(200, false)),
1830 item(Some(2), 3, true, event(180, true)),
1831 ]);
1832 assert_eq!(result.len(), 2);
1833 for (e, (amount, tokens)) in result.iter().zip([(100, 90), (200, 180)]) {
1834 let DexEvent::PumpFunTrade(e) = e else { panic!() };
1835 assert_eq!((e.amount, e.token_amount), (amount, tokens));
1836 }
1837 }
1838 }
1839}
1840
1841#[cfg(test)]
1842mod swap_occurrence_tests {
1843 use super::*;
1844 #[test]
1845 fn repeated_swap_calls_pair_with_their_own_cpi() {
1846 let pool = Pubkey::new_unique();
1847 let user = Pubkey::new_unique();
1848 let accounts = [Pubkey::new_unique(), Pubkey::new_unique()];
1849 for buy in [true, false] {
1850 for known in [true, false] {
1851 let event = |n: usize, execution| {
1852 let account = if execution { Pubkey::default() } else { accounts[n] };
1853 let amount = if execution { 90 * (n as u64 + 1) } else { 0 };
1854 if buy {
1855 DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
1856 pool,
1857 user,
1858 user_base_token_account: account,
1859 quote_amount_in: amount,
1860 ..Default::default()
1861 })
1862 } else {
1863 DexEvent::PumpSwapSell(PumpSwapSellEvent {
1864 pool,
1865 user,
1866 user_base_token_account: account,
1867 quote_amount_out: amount,
1868 ..Default::default()
1869 })
1870 }
1871 };
1872 let item = |inner_idx, height, is_cpi, event| IndexedInstructionEvent {
1873 outer_idx: 0,
1874 inner_idx,
1875 stack_height: if known { Some(height) } else { None },
1876 is_event_cpi: is_cpi,
1877 event,
1878 };
1879 let out = merge_instruction_events(vec![
1880 item(None, 1, false, event(0, false)),
1881 item(Some(0), 2, true, event(0, true)),
1882 item(Some(1), 2, false, event(1, false)),
1883 item(Some(2), 3, true, event(1, true)),
1884 ]);
1885 assert_eq!(out.len(), 2);
1886 for (i, e) in out.iter().enumerate() {
1887 match e {
1888 DexEvent::PumpSwapBuy(e) => assert_eq!(
1889 (e.user_base_token_account, e.quote_amount_in),
1890 (accounts[i], 90 * (i as u64 + 1))
1891 ),
1892 DexEvent::PumpSwapSell(e) => assert_eq!(
1893 (e.user_base_token_account, e.quote_amount_out),
1894 (accounts[i], 90 * (i as u64 + 1))
1895 ),
1896 _ => panic!(),
1897 }
1898 }
1899 }
1900 }
1901 }
1902}