1use crate::core::{
9 events::*, merger::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 std::collections::HashMap;
16use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
17
18#[inline]
32pub fn parse_instructions_enhanced(
33 meta: &TransactionStatusMeta,
34 transaction: &Option<Transaction>,
35 sig: Signature,
36 slot: u64,
37 tx_idx: u64,
38 block_us: Option<i64>,
39 grpc_us: i64,
40 filter: Option<&EventTypeFilter>,
41) -> Vec<DexEvent> {
42 let Some(tx) = transaction else { return Vec::new() };
43 let Some(msg) = &tx.message else { return Vec::new() };
44
45 let recent_blockhash = if msg.recent_blockhash.is_empty() {
46 None
47 } else {
48 Some(bs58::encode(&msg.recent_blockhash).into_string())
49 };
50
51 if !should_parse_instructions(filter) {
53 return Vec::new();
54 }
55
56 let is_created_buy = crate::logs::optimized_matcher::detect_pumpfun_create(&meta.log_messages);
58
59 let keys_len = msg.account_keys.len();
61 let writable_len = meta.loaded_writable_addresses.len();
62 let get_key = |i: usize| -> Option<&Vec<u8>> {
63 if i < keys_len {
64 msg.account_keys.get(i)
65 } else if i < keys_len + writable_len {
66 meta.loaded_writable_addresses.get(i - keys_len)
67 } else {
68 meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
69 }
70 };
71
72 let mut result = Vec::with_capacity(8);
73 let mut invokes: HashMap<Pubkey, Vec<(i32, i32)>> = HashMap::with_capacity(8);
74
75 for (i, ix) in msg.instructions.iter().enumerate() {
77 let pid = get_key(ix.program_id_index as usize)
78 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
79
80 invokes.entry(pid).or_default().push((i as i32, -1));
81
82 if let Some(event) = parse_outer_instruction(
84 &ix.data,
85 &pid,
86 sig,
87 slot,
88 tx_idx,
89 block_us,
90 grpc_us,
91 &ix.accounts,
92 &get_key,
93 filter,
94 is_created_buy,
95 ) {
96 result.push((i, None, event)); }
98 }
99
100 for inner in &meta.inner_instructions {
102 let outer_idx = inner.index as usize;
103
104 for (j, inner_ix) in inner.instructions.iter().enumerate() {
105 let pid = get_key(inner_ix.program_id_index as usize)
106 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
107
108 invokes.entry(pid).or_default().push((outer_idx as i32, j as i32));
109
110 let event = parse_inner_compiled_instruction_if_supported(
111 &inner_ix.data,
112 &pid,
113 sig,
114 slot,
115 tx_idx,
116 block_us,
117 grpc_us,
118 &inner_ix.accounts,
119 &get_key,
120 filter,
121 )
122 .or_else(|| {
123 parse_inner_instruction(
124 &inner_ix.data,
125 &pid,
126 sig,
127 slot,
128 tx_idx,
129 block_us,
130 grpc_us,
131 filter,
132 is_created_buy,
133 )
134 });
135
136 if let Some(event) = event {
137 result.push((outer_idx, Some(j), event)); }
139 }
140 }
141
142 let mut merged = merge_instruction_events(result);
144 enrich_pumpfun_same_tx_post_merge(&mut merged);
145
146 for e in merged.iter_mut() {
147 if let Some(m) = e.metadata_mut() {
148 m.recent_blockhash = recent_blockhash.clone();
149 }
150 }
151
152 let mut final_result = Vec::with_capacity(merged.len());
154 for mut event in merged {
155 crate::core::account_dispatcher::fill_accounts_with_owned_keys(
156 &mut event,
157 meta,
158 transaction,
159 &invokes,
160 );
161 crate::core::common_filler::fill_data(&mut event, meta, transaction, &invokes);
162 final_result.push(event);
163 }
164
165 final_result
166}
167
168#[inline(always)]
173fn parse_compiled_instruction<'a>(
174 data: &[u8],
175 program_id: &Pubkey,
176 sig: Signature,
177 slot: u64,
178 tx_idx: u64,
179 block_us: Option<i64>,
180 grpc_us: i64,
181 account_indices: &[u8],
182 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
183 filter: Option<&EventTypeFilter>,
184) -> Option<DexEvent> {
185 if data.len() < 8 {
187 return None;
188 }
189
190 const STACK_CAP: usize = 64;
192 if account_indices.len() <= STACK_CAP {
193 let mut stack = [Pubkey::default(); STACK_CAP];
194 let mut n = 0usize;
195 for &idx in account_indices {
196 let k = get_key(idx as usize)?;
197 stack[n] = read_pubkey_fast(k);
198 n += 1;
199 }
200 crate::instr::parse_instruction_unified(
201 data,
202 &stack[..n],
203 sig,
204 slot,
205 tx_idx,
206 block_us,
207 grpc_us,
208 filter,
209 program_id,
210 )
211 } else {
212 let accounts: Vec<Pubkey> = account_indices
213 .iter()
214 .map(|&idx| get_key(idx as usize).map(|k| read_pubkey_fast(k)))
215 .collect::<Option<_>>()?;
216 crate::instr::parse_instruction_unified(
217 data, &accounts, sig, slot, tx_idx, block_us, grpc_us, filter, program_id,
218 )
219 }
220}
221
222#[inline(always)]
223fn is_supported_inner_compiled_instruction(data: &[u8], program_id: &Pubkey) -> bool {
224 crate::instr::normal_instruction_data_may_parse(program_id, data)
225}
226
227#[inline(always)]
228fn parse_inner_compiled_instruction_if_supported<'a>(
229 data: &[u8],
230 program_id: &Pubkey,
231 sig: Signature,
232 slot: u64,
233 tx_idx: u64,
234 block_us: Option<i64>,
235 grpc_us: i64,
236 account_indices: &[u8],
237 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
238 filter: Option<&EventTypeFilter>,
239) -> Option<DexEvent> {
240 if !is_supported_inner_compiled_instruction(data, program_id) {
241 return None;
242 }
243 parse_compiled_instruction(
244 data,
245 program_id,
246 sig,
247 slot,
248 tx_idx,
249 block_us,
250 grpc_us,
251 account_indices,
252 get_key,
253 filter,
254 )
255}
256
257#[inline(always)]
261fn parse_outer_instruction<'a>(
262 data: &[u8],
263 program_id: &Pubkey,
264 sig: Signature,
265 slot: u64,
266 tx_idx: u64,
267 block_us: Option<i64>,
268 grpc_us: i64,
269 account_indices: &[u8],
270 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
271 filter: Option<&EventTypeFilter>,
272 _is_created_buy: bool,
273) -> Option<DexEvent> {
274 parse_compiled_instruction(
275 data,
276 program_id,
277 sig,
278 slot,
279 tx_idx,
280 block_us,
281 grpc_us,
282 account_indices,
283 get_key,
284 filter,
285 )
286}
287
288#[inline(always)]
292fn parse_inner_instruction(
293 data: &[u8],
294 program_id: &Pubkey,
295 sig: Signature,
296 slot: u64,
297 tx_idx: u64,
298 block_us: Option<i64>,
299 grpc_us: i64,
300 filter: Option<&EventTypeFilter>,
301 is_created_buy: bool,
302) -> Option<DexEvent> {
303 if data.len() < 16 {
305 return None;
306 }
307
308 let metadata = EventMetadata {
309 signature: sig,
310 slot,
311 tx_index: tx_idx,
312 block_time_us: block_us.unwrap_or(0),
313 grpc_recv_us: grpc_us,
314 recent_blockhash: None, };
316
317 let mut discriminator = [0u8; 16];
319 discriminator.copy_from_slice(&data[..16]);
320 let inner_data = &data[16..];
321
322 use crate::instr::{all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner};
323
324 let event = if *program_id == program_ids::PUMPFUN_PROGRAM_ID {
326 if let Some(f) = filter {
327 if !f.includes_pumpfun() {
328 return None;
329 }
330 }
331 pump_inner::parse_pumpfun_inner_instruction(
332 &discriminator,
333 inner_data,
334 metadata,
335 is_created_buy,
336 )
337 } else if *program_id == program_ids::PUMPSWAP_PROGRAM_ID {
338 if let Some(f) = filter {
339 if !f.includes_pumpswap() {
340 return None;
341 }
342 }
343 pump_amm_inner::parse_pumpswap_inner_instruction(&discriminator, inner_data, metadata)
344 } else if *program_id == program_ids::PUMP_FEES_PROGRAM_ID {
345 if let Some(f) = filter {
346 if !f.includes_pump_fees() {
347 return None;
348 }
349 }
350 all_inner::pump_fees::parse(&discriminator, inner_data, metadata)
351 } else if *program_id == program_ids::RAYDIUM_CLMM_PROGRAM_ID {
352 if let Some(f) = filter {
353 if !f.includes_raydium_clmm() {
354 return None;
355 }
356 }
357 raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
358 &discriminator,
359 inner_data,
360 metadata,
361 )
362 } else if *program_id == program_ids::RAYDIUM_CPMM_PROGRAM_ID {
363 if let Some(f) = filter {
364 if !f.includes_raydium_cpmm() {
365 return None;
366 }
367 }
368 all_inner::raydium_cpmm::parse(&discriminator, inner_data, metadata)
369 } else if *program_id == program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
370 if let Some(f) = filter {
371 if !f.includes_raydium_amm_v4() {
372 return None;
373 }
374 }
375 all_inner::raydium_amm::parse(&discriminator, inner_data, metadata)
376 } else if *program_id == program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
377 if let Some(f) = filter {
378 if !f.includes_orca_whirlpool() {
379 return None;
380 }
381 }
382 all_inner::orca::parse(&discriminator, inner_data, metadata)
383 } else if *program_id == program_ids::METEORA_POOLS_PROGRAM_ID {
384 if let Some(f) = filter {
385 if !f.includes_meteora_pools() {
386 return None;
387 }
388 }
389 all_inner::meteora_amm::parse(&discriminator, inner_data, metadata)
390 } else if *program_id == program_ids::METEORA_DAMM_V2_PROGRAM_ID {
391 if let Some(f) = filter {
392 if !f.includes_meteora_damm_v2() {
393 return None;
394 }
395 }
396 all_inner::meteora_damm::parse(&discriminator, inner_data, metadata)
397 } else if *program_id == program_ids::METEORA_DLMM_PROGRAM_ID {
398 if let Some(f) = filter {
399 if !f.includes_meteora_dlmm() {
400 return None;
401 }
402 }
403 all_inner::meteora_dlmm::parse(&discriminator, inner_data, metadata)
404 } else if *program_id == program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID {
405 if let Some(f) = filter {
406 if !f.includes_raydium_launchlab() {
407 return None;
408 }
409 }
410 all_inner::raydium_launchlab::parse(&discriminator, inner_data, metadata)
411 } else {
412 None
413 };
414
415 if filter.map(|f| event.as_ref().is_some_and(|e| f.should_include_dex_event(e))).unwrap_or(true)
416 {
417 event
418 } else {
419 None
420 }
421}
422
423#[inline(always)]
431fn merge_instruction_events(events: Vec<(usize, Option<usize>, DexEvent)>) -> Vec<DexEvent> {
432 if events.is_empty() {
433 return Vec::new();
434 }
435
436 let mut events = events;
439 events.sort_by_key(|(outer, inner, _)| (*outer, inner.map_or(0, |i| i + 1)));
440
441 let mut result = Vec::with_capacity(events.len());
442 let mut pending_outer: Option<(usize, DexEvent)> = None;
443
444 for (outer_idx, inner_idx, event) in events {
445 match inner_idx {
446 None => {
447 if let Some((_, outer_event)) = pending_outer.take() {
450 result.push(outer_event);
451 }
452 pending_outer = Some((outer_idx, event));
454 }
455 Some(_) => {
456 if let Some((pending_outer_idx, mut outer_event)) = pending_outer.take() {
458 if pending_outer_idx == outer_idx {
459 merge_events(&mut outer_event, event);
461 pending_outer = Some((outer_idx, outer_event));
462 } else {
463 result.push(outer_event);
465 result.push(event);
466 }
467 } else {
468 result.push(event);
470 }
471 }
472 }
473 }
474
475 if let Some((_, outer_event)) = pending_outer {
477 result.push(outer_event);
478 }
479
480 result
481}
482
483#[inline(always)]
485fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
486 let Some(filter) = filter else { return true };
488
489 if filter.include_only.is_none() {
491 return true;
492 }
493
494 if filter.includes_pumpfun() {
497 return true;
498 }
499
500 if filter.includes_pump_fees() {
501 return true;
502 }
503
504 filter.includes_pumpswap()
505 || filter.includes_raydium_launchlab()
506 || filter.includes_raydium_cpmm()
507 || filter.includes_raydium_clmm()
508 || filter.includes_raydium_amm_v4()
509 || filter.includes_orca_whirlpool()
510 || filter.includes_meteora_pools()
511 || filter.includes_meteora_damm_v2()
512 || filter.includes_meteora_dlmm()
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
519 use yellowstone_grpc_proto::prelude::{
520 CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
521 };
522
523 fn pk(s: &str) -> Pubkey {
524 s.parse().unwrap()
525 }
526
527 fn usdc_mint() -> Pubkey {
528 pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
529 }
530
531 fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
532 key.to_bytes().to_vec()
533 }
534
535 fn decode_b58(s: &str) -> Vec<u8> {
536 bs58::decode(s).into_vec().unwrap()
537 }
538
539 fn str_arg(s: &str, out: &mut Vec<u8>) {
540 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
541 out.extend_from_slice(s.as_bytes());
542 }
543
544 fn create_v2_data() -> Vec<u8> {
545 let mut data = Vec::new();
546 data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
547 str_arg("Alt Coin", &mut data);
548 str_arg("ALT", &mut data);
549 str_arg("https://example.invalid/alt.json", &mut data);
550 data.extend_from_slice(Pubkey::new_unique().as_ref());
551 data.push(1);
552 data.push(1);
553 data
554 }
555
556 fn grpc_pumpfun_create_v2_tx(
557 static_len: usize,
558 writable_len: usize,
559 program_idx: u8,
560 ix_accounts: Vec<u8>,
561 account_overrides: Vec<(usize, Pubkey)>,
562 ) -> (TransactionStatusMeta, Option<Transaction>) {
563 let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
564 account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
565 let readonly_len = account_overrides
566 .iter()
567 .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
568 .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
569 .max()
570 .unwrap_or_default();
571 let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
572 let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
573 for (global_idx, key) in account_overrides {
574 if global_idx < static_len {
575 account_keys[global_idx] = key;
576 } else if global_idx < static_len + writable_len {
577 loaded_writable[global_idx - static_len] = key;
578 } else {
579 loaded_readonly[global_idx - static_len - writable_len] = key;
580 }
581 }
582
583 let meta = TransactionStatusMeta {
584 loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
585 loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
586 ..Default::default()
587 };
588 let tx = Transaction {
589 signatures: vec![Signature::default().as_ref().to_vec()],
590 message: Some(Message {
591 header: Some(MessageHeader {
592 num_required_signatures: 1,
593 num_readonly_signed_accounts: 0,
594 num_readonly_unsigned_accounts: 0,
595 }),
596 account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
597 recent_blockhash: vec![0; 32],
598 instructions: vec![CompiledInstruction {
599 program_id_index: program_idx as u32,
600 accounts: ix_accounts,
601 data: create_v2_data(),
602 }],
603 versioned: true,
604 address_table_lookups: Vec::new(),
605 }),
606 };
607 (meta, Some(tx))
608 }
609
610 fn create_v2_accounts(
611 account_len: usize,
612 program_idx: u8,
613 mint_idx: u8,
614 user_idx: u8,
615 token_program_idx: u8,
616 quote_tail: Option<(u8, u8, u8)>,
617 ) -> Vec<u8> {
618 let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
619 accounts[0] = mint_idx;
620 accounts[5] = user_idx;
621 accounts[7] = token_program_idx;
622 if account_len > 15 {
623 accounts[15] = program_idx;
624 }
625 if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
626 accounts[16] = quote_idx;
627 accounts[17] = quote_vault_idx;
628 accounts[18] = quote_token_program_idx;
629 }
630 accounts
631 }
632
633 fn parse_create_v2_from_grpc(
634 meta: &TransactionStatusMeta,
635 tx: &Option<Transaction>,
636 ) -> crate::core::events::PumpFunCreateTokenEvent {
637 let events = parse_instructions_enhanced(
638 meta,
639 tx,
640 Signature::default(),
641 123,
642 0,
643 Some(456),
644 789,
645 None,
646 );
647 assert_eq!(events.len(), 1);
648 match &events[0] {
649 DexEvent::PumpFunCreate(e) => {
650 assert_eq!(e.ix_name, "create_v2");
651 e.clone()
652 }
653 DexEvent::PumpFunCreateV2(e) => {
654 assert_eq!(e.ix_name, "create_v2");
655 crate::core::events::PumpFunCreateTokenEvent {
656 metadata: e.metadata.clone(),
657 name: e.name.clone(),
658 symbol: e.symbol.clone(),
659 uri: e.uri.clone(),
660 mint: e.mint,
661 bonding_curve: e.bonding_curve,
662 user: e.user,
663 creator: e.creator,
664 timestamp: e.timestamp,
665 virtual_token_reserves: e.virtual_token_reserves,
666 virtual_sol_reserves: e.virtual_sol_reserves,
667 real_token_reserves: e.real_token_reserves,
668 token_total_supply: e.token_total_supply,
669 mint_authority: e.mint_authority,
670 associated_bonding_curve: e.associated_bonding_curve,
671 global: e.global,
672 system_program: e.system_program,
673 token_program: e.token_program,
674 associated_token_program: e.associated_token_program,
675 mayhem_program_id: e.mayhem_program_id,
676 global_params: e.global_params,
677 sol_vault: e.sol_vault,
678 mayhem_state: e.mayhem_state,
679 mayhem_token_vault: e.mayhem_token_vault,
680 event_authority: e.event_authority,
681 program: e.program,
682 quote_mint: e.quote_mint,
683 quote_vault: e.quote_vault,
684 quote_token_program: e.quote_token_program,
685 virtual_quote_reserves: e.virtual_quote_reserves,
686 ix_name: e.ix_name.clone(),
687 is_mayhem_mode: e.is_mayhem_mode,
688 is_cashback_enabled: e.is_cashback_enabled,
689 observed_fee_recipient: e.observed_fee_recipient,
690 }
691 }
692 other => panic!("expected PumpFun create_v2 event, got {other:?}"),
693 }
694 }
695
696 #[test]
697 fn test_should_parse_instructions() {
698 assert!(should_parse_instructions(None));
700
701 let filter = EventTypeFilter { include_only: None, exclude_types: None };
703 assert!(should_parse_instructions(Some(&filter)));
704
705 use crate::grpc::types::EventType;
707 let filter = EventTypeFilter {
708 include_only: Some(vec![EventType::PumpFunMigrate]),
709 exclude_types: None,
710 };
711 assert!(should_parse_instructions(Some(&filter)));
712
713 let filter = EventTypeFilter {
715 include_only: Some(vec![EventType::PumpFunTrade]),
716 exclude_types: None,
717 };
718 assert!(should_parse_instructions(Some(&filter)));
719
720 for event_type in [
721 EventType::PumpSwapTrade,
722 EventType::PumpFeesUpdateFeeShares,
723 EventType::RaydiumLaunchlabTrade,
724 EventType::RaydiumCpmmSwap,
725 EventType::RaydiumClmmSwap,
726 EventType::RaydiumAmmV4Swap,
727 EventType::OrcaWhirlpoolSwap,
728 EventType::MeteoraPoolsSwap,
729 EventType::MeteoraDammV2Swap,
730 EventType::MeteoraDammV2InitializePool,
731 EventType::MeteoraDlmmSwap,
732 ] {
733 let filter = EventTypeFilter::include_only(vec![event_type]);
734 assert!(
735 should_parse_instructions(Some(&filter)),
736 "instruction parsing should be enabled for {event_type:?}"
737 );
738 }
739
740 let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
741 assert!(
742 !should_parse_instructions(Some(&filter)),
743 "DBC events are log-only until an instruction parser is implemented"
744 );
745
746 let filter = EventTypeFilter::include_only(vec![
747 EventType::AccountPumpFunGlobal,
748 EventType::AccountRaydiumClmmPoolState,
749 EventType::AccountRaydiumCpmmPoolState,
750 EventType::AccountOrcaWhirlpool,
751 ]);
752 assert!(
753 !should_parse_instructions(Some(&filter)),
754 "account-only non-Pump filters should stay on the account update path"
755 );
756 }
757
758 #[test]
759 fn test_merge_instruction_events() {
760 use solana_sdk::signature::Signature;
761
762 let metadata = EventMetadata {
763 signature: Signature::default(),
764 slot: 100,
765 tx_index: 1,
766 block_time_us: 1000,
767 grpc_recv_us: 2000,
768 recent_blockhash: None,
769 };
770
771 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
773 metadata: metadata.clone(),
774 bonding_curve: Pubkey::new_unique(),
775 ..Default::default()
776 });
777
778 let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
779 metadata: metadata.clone(),
780 sol_amount: 1000,
781 token_amount: 2000,
782 ..Default::default()
783 });
784
785 let events = vec![
786 (0, None, outer_event), (0, Some(0), inner_event), ];
789
790 let result = merge_instruction_events(events);
791
792 assert_eq!(result.len(), 1);
794
795 if let DexEvent::PumpFunTrade(trade) = &result[0] {
797 assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
801 panic!("Expected PumpFunTrade event");
802 }
803 }
804
805 #[test]
806 fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
807 use solana_sdk::signature::Signature;
808
809 let metadata = EventMetadata {
810 signature: Signature::default(),
811 slot: 100,
812 tx_index: 1,
813 block_time_us: 1000,
814 grpc_recv_us: 2000,
815 recent_blockhash: None,
816 };
817
818 let bc = Pubkey::new_unique();
819 let fee = Pubkey::new_unique();
820
821 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
822 metadata: metadata.clone(),
823 bonding_curve: bc,
824 ..Default::default()
825 });
826
827 let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
828 metadata: metadata.clone(),
829 sol_amount: 1000,
830 token_amount: 2000,
831 is_buy: true,
832 ..Default::default()
833 });
834
835 let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
837 metadata: metadata.clone(),
838 fee_recipient: fee,
839 ..Default::default()
840 });
841
842 let events =
843 vec![(0, None, outer_event), (0, Some(0), inner_trade), (0, Some(1), inner_fee_only)];
844
845 let result = merge_instruction_events(events);
846 assert_eq!(result.len(), 1);
847 if let DexEvent::PumpFunTrade(trade) = &result[0] {
848 assert_eq!(trade.bonding_curve, bc);
849 assert_eq!(trade.sol_amount, 1000);
850 assert_eq!(trade.token_amount, 2000);
851 assert_eq!(trade.fee_recipient, fee);
852 } else {
853 panic!("Expected PumpFunTrade event");
854 }
855 }
856
857 #[test]
858 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
859 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
860 let static_keys = [
861 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
862 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
863 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
864 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
865 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
866 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
867 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
868 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
869 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
870 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
871 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
872 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
873 "ComputeBudget111111111111111111111111111111",
874 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
875 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
876 "SysvarRent111111111111111111111111111111111",
877 ];
878 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
879 let loaded_readonly = [
880 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
881 "So11111111111111111111111111111111111111112",
882 "11111111111111111111111111111111",
883 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
884 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
885 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
886 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
887 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
888 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
889 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
890 ];
891 let meta = TransactionStatusMeta {
892 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
893 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
894 inner_instructions: vec![InnerInstructions {
895 index: 2,
896 instructions: vec![InnerInstruction {
897 program_id_index: 20,
898 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
899 data: decode_b58(
900 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
901 ),
902 stack_height: Some(2),
903 }],
904 }],
905 ..Default::default()
906 };
907 let tx = Some(Transaction {
908 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
909 message: Some(Message {
910 header: Some(MessageHeader::default()),
911 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
912 recent_blockhash: vec![0; 32],
913 instructions: vec![
914 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
915 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
916 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
917 ],
918 versioned: true,
919 address_table_lookups: Vec::new(),
920 }),
921 });
922
923 let events = parse_instructions_enhanced(
924 &meta,
925 &tx,
926 signature.parse().unwrap(),
927 427_039_576,
928 0,
929 Some(1_781_687_252_000_000),
930 789,
931 None,
932 );
933
934 assert_eq!(events.len(), 1, "{signature}");
935 match &events[0] {
936 DexEvent::PumpSwapCreatePool(e) => {
937 assert_eq!(e.index, 0, "{signature}");
938 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
939 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
940 assert_eq!(
941 e.coin_creator,
942 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
943 "{signature}"
944 );
945 assert!(!e.is_mayhem_mode, "{signature}");
946 assert!(e.is_cashback_coin, "{signature}");
947 assert_eq!(
948 e.pool,
949 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
950 "{signature}"
951 );
952 assert_eq!(
953 e.creator,
954 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
955 "{signature}"
956 );
957 assert_eq!(
958 e.base_mint,
959 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
960 "{signature}"
961 );
962 assert_eq!(
963 e.quote_mint,
964 pk("So11111111111111111111111111111111111111112"),
965 "{signature}"
966 );
967 assert_eq!(
968 e.lp_mint,
969 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
970 "{signature}"
971 );
972 assert_eq!(
973 e.user_base_token_account,
974 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
975 "{signature}"
976 );
977 assert_eq!(
978 e.user_quote_token_account,
979 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
980 "{signature}"
981 );
982 }
983 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
984 }
985 }
986
987 #[test]
988 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
989 struct Case {
990 signature: &'static str,
991 name: &'static str,
992 static_len: usize,
993 writable_len: usize,
994 program_idx: u8,
995 account_len: usize,
996 mint_idx: u8,
997 mint: &'static str,
998 user_idx: u8,
999 user: &'static str,
1000 token_program_idx: u8,
1001 quote_idx: u8,
1002 quote_mint: Pubkey,
1003 quote_vault_idx: u8,
1004 quote_vault: &'static str,
1005 quote_token_program_idx: u8,
1006 }
1007 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1008 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1009 let cases = [
1010 Case {
1011 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1012 name: "19-account WSOL quote in ALT",
1013 static_len: 15,
1014 writable_len: 7,
1015 program_idx: 12,
1016 account_len: 19,
1017 mint_idx: 1,
1018 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1019 user_idx: 0,
1020 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1021 token_program_idx: 24,
1022 quote_idx: 27,
1023 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1024 quote_vault_idx: 7,
1025 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1026 quote_token_program_idx: 28,
1027 },
1028 Case {
1029 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1030 name: "19-account WSOL quote in ALT with exact quote buy",
1031 static_len: 20,
1032 writable_len: 7,
1033 program_idx: 15,
1034 account_len: 19,
1035 mint_idx: 1,
1036 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1037 user_idx: 0,
1038 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1039 token_program_idx: 31,
1040 quote_idx: 28,
1041 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1042 quote_vault_idx: 4,
1043 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1044 quote_token_program_idx: 30,
1045 },
1046 Case {
1047 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1048 name: "19-account USDC quote in ALT",
1049 static_len: 19,
1050 writable_len: 6,
1051 program_idx: 16,
1052 account_len: 19,
1053 mint_idx: 1,
1054 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1055 user_idx: 0,
1056 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1057 token_program_idx: 27,
1058 quote_idx: 30,
1059 quote_mint: usdc_mint(),
1060 quote_vault_idx: 6,
1061 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1062 quote_token_program_idx: 31,
1063 },
1064 Case {
1065 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1066 name: "20-account WSOL quote in ALT",
1067 static_len: 15,
1068 writable_len: 7,
1069 program_idx: 12,
1070 account_len: 20,
1071 mint_idx: 1,
1072 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1073 user_idx: 0,
1074 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1075 token_program_idx: 24,
1076 quote_idx: 27,
1077 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1078 quote_vault_idx: 7,
1079 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1080 quote_token_program_idx: 28,
1081 },
1082 Case {
1083 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1084 name: "19-account WSOL quote in ALT with later buy",
1085 static_len: 18,
1086 writable_len: 7,
1087 program_idx: 13,
1088 account_len: 19,
1089 mint_idx: 1,
1090 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1091 user_idx: 0,
1092 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1093 token_program_idx: 27,
1094 quote_idx: 30,
1095 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1096 quote_vault_idx: 7,
1097 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1098 quote_token_program_idx: 31,
1099 },
1100 Case {
1101 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1102 name: "19-account USDC quote in ALT exact quote buy",
1103 static_len: 19,
1104 writable_len: 6,
1105 program_idx: 15,
1106 account_len: 19,
1107 mint_idx: 1,
1108 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1109 user_idx: 0,
1110 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1111 token_program_idx: 28,
1112 quote_idx: 33,
1113 quote_mint: usdc_mint(),
1114 quote_vault_idx: 7,
1115 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1116 quote_token_program_idx: 27,
1117 },
1118 Case {
1119 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1120 name: "20-account WSOL quote in ALT with jit account",
1121 static_len: 15,
1122 writable_len: 7,
1123 program_idx: 12,
1124 account_len: 20,
1125 mint_idx: 1,
1126 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1127 user_idx: 0,
1128 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1129 token_program_idx: 24,
1130 quote_idx: 27,
1131 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1132 quote_vault_idx: 7,
1133 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1134 quote_token_program_idx: 28,
1135 },
1136 ];
1137
1138 for case in cases {
1139 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1140 case.static_len,
1141 case.writable_len,
1142 case.program_idx,
1143 create_v2_accounts(
1144 case.account_len,
1145 case.program_idx,
1146 case.mint_idx,
1147 case.user_idx,
1148 case.token_program_idx,
1149 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1150 ),
1151 vec![
1152 (case.mint_idx as usize, pk(case.mint)),
1153 (case.user_idx as usize, pk(case.user)),
1154 (case.token_program_idx as usize, token_2022_program),
1155 (case.quote_idx as usize, case.quote_mint),
1156 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1157 (case.quote_token_program_idx as usize, spl_token_program),
1158 ],
1159 );
1160 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1161 let idx = global_idx as usize;
1162 if idx < case.static_len {
1163 ("static", idx)
1164 } else if idx < case.static_len + case.writable_len {
1165 ("writable", idx - case.static_len)
1166 } else {
1167 ("readonly", idx - case.static_len - case.writable_len)
1168 }
1169 };
1170 assert_eq!(
1171 meta.loaded_writable_addresses.len(),
1172 case.writable_len,
1173 "{}: {}",
1174 case.name,
1175 case.signature
1176 );
1177 for (global_idx, expected_key) in [
1178 (case.token_program_idx, token_2022_program),
1179 (case.quote_idx, case.quote_mint),
1180 (case.quote_token_program_idx, spl_token_program),
1181 ] {
1182 match loaded_key_location(global_idx) {
1183 ("static", _) => {}
1184 ("writable", offset) => assert_eq!(
1185 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1186 expected_key,
1187 "{}: writable loaded key {global_idx}: {}",
1188 case.name,
1189 case.signature
1190 ),
1191 ("readonly", offset) => assert_eq!(
1192 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1193 expected_key,
1194 "{}: readonly loaded key {global_idx}: {}",
1195 case.name,
1196 case.signature
1197 ),
1198 _ => unreachable!(),
1199 }
1200 }
1201
1202 let create = parse_create_v2_from_grpc(&meta, &tx);
1203
1204 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1205 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1206 assert_eq!(
1207 create.token_program, token_2022_program,
1208 "{}: {}",
1209 case.name, case.signature
1210 );
1211 assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1212 assert_eq!(
1213 create.quote_vault,
1214 pk(case.quote_vault),
1215 "{}: {}",
1216 case.name,
1217 case.signature
1218 );
1219 assert_eq!(
1220 create.quote_token_program, spl_token_program,
1221 "{}: {}",
1222 case.name, case.signature
1223 );
1224 }
1225 }
1226
1227 #[test]
1228 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1229 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1230 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1231 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1232 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1233 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1234 16,
1235 5,
1236 12,
1237 create_v2_accounts(16, 12, 1, 0, 24, None),
1238 vec![(1, mint), (0, user), (24, token_program)],
1239 );
1240
1241 let create = parse_create_v2_from_grpc(&meta, &tx);
1242
1243 assert_eq!(create.mint, mint, "{signature}");
1244 assert_eq!(create.user, user, "{signature}");
1245 assert_eq!(create.token_program, token_program, "{signature}");
1246 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1247 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1248 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1249 }
1250
1251 #[test]
1252 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1253 let quote_vault = Pubkey::new_unique();
1254 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1255 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1256 19,
1257 6,
1258 16,
1259 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1260 vec![
1261 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1262 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1263 (6, quote_vault),
1264 (31, quote_token_program),
1265 ],
1266 );
1267
1268 let create = parse_create_v2_from_grpc(&meta, &tx);
1269
1270 assert_eq!(create.quote_mint, Pubkey::default());
1271 assert_eq!(create.quote_vault, Pubkey::default());
1272 assert_eq!(create.quote_token_program, Pubkey::default());
1273 }
1274}