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_dlmm_event_cpi: bool,
23 event: DexEvent,
24}
25
26#[inline(always)]
27fn is_dlmm_event(event: &DexEvent) -> bool {
28 matches!(
29 event,
30 DexEvent::MeteoraDlmmSwap(_)
31 | DexEvent::MeteoraDlmmAddLiquidity(_)
32 | DexEvent::MeteoraDlmmRemoveLiquidity(_)
33 | DexEvent::MeteoraDlmmInitializePool(_)
34 | DexEvent::MeteoraDlmmInitializeBinArray(_)
35 | DexEvent::MeteoraDlmmCreatePosition(_)
36 | DexEvent::MeteoraDlmmClosePosition(_)
37 | DexEvent::MeteoraDlmmClaimFee(_)
38 )
39}
40
41#[inline]
55pub fn parse_instructions_enhanced(
56 meta: &TransactionStatusMeta,
57 transaction: &Option<Transaction>,
58 sig: Signature,
59 slot: u64,
60 tx_idx: u64,
61 block_us: Option<i64>,
62 grpc_us: i64,
63 filter: Option<&EventTypeFilter>,
64) -> Vec<DexEvent> {
65 let needs_pumpfun = filter.map(EventTypeFilter::includes_pumpfun).unwrap_or(true);
66 let is_created_buy =
67 needs_pumpfun && crate::logs::optimized_matcher::detect_pumpfun_create(&meta.log_messages);
68 let mut events = parse_instructions_enhanced_with_created_buy(
69 meta,
70 transaction,
71 sig,
72 slot,
73 tx_idx,
74 block_us,
75 grpc_us,
76 filter,
77 is_created_buy,
78 );
79 for event in &mut events {
80 crate::core::common_filler::fill_token_balances(event, meta, transaction);
81 }
82 crate::grpc::transaction_meta::fill_recent_blockhash(&mut events, transaction);
83 events
84}
85
86#[inline]
87pub(crate) fn parse_instructions_enhanced_with_created_buy(
88 meta: &TransactionStatusMeta,
89 transaction: &Option<Transaction>,
90 sig: Signature,
91 slot: u64,
92 tx_idx: u64,
93 block_us: Option<i64>,
94 grpc_us: i64,
95 filter: Option<&EventTypeFilter>,
96 is_created_buy: bool,
97) -> Vec<DexEvent> {
98 let Some(tx) = transaction else { return Vec::new() };
99 let Some(msg) = &tx.message else { return Vec::new() };
100
101 if !should_parse_instructions(filter) {
103 return Vec::new();
104 }
105
106 let keys_len = msg.account_keys.len();
108 let writable_len = meta.loaded_writable_addresses.len();
109 let get_key = |i: usize| -> Option<&Vec<u8>> {
110 if i < keys_len {
111 msg.account_keys.get(i)
112 } else if i < keys_len + writable_len {
113 meta.loaded_writable_addresses.get(i - keys_len)
114 } else {
115 meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
116 }
117 };
118
119 let mut result = Vec::with_capacity(8);
120 let mut invokes = crate::core::invoke_context::InvokeContext::default();
121 for (i, ix) in msg.instructions.iter().enumerate() {
123 let pid = get_key(ix.program_id_index as usize)
124 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
125
126 if crate::grpc::program_ids::needs_invoke_context(&pid) {
127 invokes.push(pid, (i as i32, -1));
128 }
129
130 if let Some(event) = parse_outer_instruction(
132 &ix.data,
133 &pid,
134 sig,
135 slot,
136 tx_idx,
137 block_us,
138 grpc_us,
139 &ix.accounts,
140 &get_key,
141 filter,
142 is_created_buy,
143 ) {
144 result.push(IndexedInstructionEvent {
145 outer_idx: i,
146 inner_idx: None,
147 stack_height: Some(1),
148 is_dlmm_event_cpi: false,
149 event,
150 });
151 }
152 }
153
154 for inner in &meta.inner_instructions {
156 let outer_idx = inner.index as usize;
157
158 for (j, inner_ix) in inner.instructions.iter().enumerate() {
159 let pid = get_key(inner_ix.program_id_index as usize)
160 .map_or(Pubkey::default(), |k| read_pubkey_fast(k));
161
162 if crate::grpc::program_ids::needs_invoke_context(&pid) {
163 invokes.push(pid, (outer_idx as i32, j as i32));
164 }
165
166 let event = parse_inner_compiled_instruction_if_supported(
167 &inner_ix.data,
168 &pid,
169 sig,
170 slot,
171 tx_idx,
172 block_us,
173 grpc_us,
174 &inner_ix.accounts,
175 &get_key,
176 filter,
177 )
178 .or_else(|| {
179 parse_inner_instruction(
180 &inner_ix.data,
181 &pid,
182 sig,
183 slot,
184 tx_idx,
185 block_us,
186 grpc_us,
187 filter,
188 is_created_buy,
189 )
190 });
191
192 if let Some(event) = event {
193 result.push(IndexedInstructionEvent {
194 outer_idx,
195 inner_idx: Some(j),
196 stack_height: inner_ix.stack_height,
197 is_dlmm_event_cpi: pid == crate::instr::program_ids::METEORA_DLMM_PROGRAM_ID
198 && crate::instr::all_inner::meteora_dlmm::is_event_cpi(&inner_ix.data),
199 event,
200 });
201 }
202 }
203 }
204
205 let mut final_result = merge_instruction_events(result);
207 enrich_pumpfun_same_tx_post_merge(&mut final_result);
208
209 for event in &mut final_result {
211 crate::core::account_dispatcher::fill_accounts_with_invoke_context(
212 event,
213 meta,
214 transaction,
215 &invokes,
216 );
217 crate::core::common_filler::fill_data_with_invoke_context(
218 event,
219 meta,
220 transaction,
221 &invokes,
222 );
223 }
224
225 final_result
226}
227
228#[inline(always)]
233fn parse_compiled_instruction<'a>(
234 data: &[u8],
235 program_id: &Pubkey,
236 sig: Signature,
237 slot: u64,
238 tx_idx: u64,
239 block_us: Option<i64>,
240 grpc_us: i64,
241 account_indices: &[u8],
242 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
243 filter: Option<&EventTypeFilter>,
244) -> Option<DexEvent> {
245 if data.len() < 8 {
247 return None;
248 }
249
250 const STACK_CAP: usize = 64;
252 if account_indices.len() <= STACK_CAP {
253 let mut stack = [Pubkey::default(); STACK_CAP];
254 let mut n = 0usize;
255 for &idx in account_indices {
256 let k = get_key(idx as usize)?;
257 stack[n] = read_pubkey_fast(k);
258 n += 1;
259 }
260 crate::instr::parse_instruction_unified(
261 data,
262 &stack[..n],
263 sig,
264 slot,
265 tx_idx,
266 block_us,
267 grpc_us,
268 filter,
269 program_id,
270 )
271 } else {
272 let accounts: Vec<Pubkey> = account_indices
273 .iter()
274 .map(|&idx| get_key(idx as usize).map(|k| read_pubkey_fast(k)))
275 .collect::<Option<_>>()?;
276 crate::instr::parse_instruction_unified(
277 data, &accounts, sig, slot, tx_idx, block_us, grpc_us, filter, program_id,
278 )
279 }
280}
281
282#[inline(always)]
283fn is_supported_inner_compiled_instruction(data: &[u8], program_id: &Pubkey) -> bool {
284 crate::instr::normal_instruction_data_may_parse(program_id, data)
285}
286
287#[inline(always)]
288fn parse_inner_compiled_instruction_if_supported<'a>(
289 data: &[u8],
290 program_id: &Pubkey,
291 sig: Signature,
292 slot: u64,
293 tx_idx: u64,
294 block_us: Option<i64>,
295 grpc_us: i64,
296 account_indices: &[u8],
297 get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
298 filter: Option<&EventTypeFilter>,
299) -> Option<DexEvent> {
300 if !is_supported_inner_compiled_instruction(data, program_id) {
301 return None;
302 }
303 parse_compiled_instruction(
304 data,
305 program_id,
306 sig,
307 slot,
308 tx_idx,
309 block_us,
310 grpc_us,
311 account_indices,
312 get_key,
313 filter,
314 )
315}
316
317#[inline(always)]
321fn parse_outer_instruction<'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 _is_created_buy: bool,
333) -> Option<DexEvent> {
334 if !crate::instr::normal_instruction_data_may_parse(program_id, data) {
335 return None;
336 }
337 parse_compiled_instruction(
338 data,
339 program_id,
340 sig,
341 slot,
342 tx_idx,
343 block_us,
344 grpc_us,
345 account_indices,
346 get_key,
347 filter,
348 )
349}
350
351#[inline(always)]
355fn parse_inner_instruction(
356 data: &[u8],
357 program_id: &Pubkey,
358 sig: Signature,
359 slot: u64,
360 tx_idx: u64,
361 block_us: Option<i64>,
362 grpc_us: i64,
363 filter: Option<&EventTypeFilter>,
364 is_created_buy: bool,
365) -> Option<DexEvent> {
366 if data.len() < 16 {
368 return None;
369 }
370
371 let metadata = EventMetadata {
372 signature: sig,
373 slot,
374 tx_index: tx_idx,
375 block_time_us: block_us.unwrap_or(0),
376 grpc_recv_us: grpc_us,
377 recent_blockhash: None, };
379
380 let mut discriminator = [0u8; 16];
382 discriminator.copy_from_slice(&data[..16]);
383 let inner_data = &data[16..];
384
385 use crate::instr::{all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner};
386
387 let event = if *program_id == program_ids::PUMPFUN_PROGRAM_ID {
389 if let Some(f) = filter {
390 if !f.includes_pumpfun() {
391 return None;
392 }
393 }
394 pump_inner::parse_pumpfun_inner_instruction(
395 &discriminator,
396 inner_data,
397 metadata,
398 is_created_buy,
399 )
400 } else if *program_id == program_ids::PUMPSWAP_PROGRAM_ID {
401 if let Some(f) = filter {
402 if !f.includes_pumpswap() {
403 return None;
404 }
405 }
406 pump_amm_inner::parse_pumpswap_inner_instruction(&discriminator, inner_data, metadata)
407 } else if *program_id == program_ids::PUMP_FEES_PROGRAM_ID {
408 if let Some(f) = filter {
409 if !f.includes_pump_fees() {
410 return None;
411 }
412 }
413 all_inner::pump_fees::parse(&discriminator, inner_data, metadata)
414 } else if *program_id == program_ids::RAYDIUM_CLMM_PROGRAM_ID {
415 if let Some(f) = filter {
416 if !f.includes_raydium_clmm() {
417 return None;
418 }
419 }
420 raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
421 &discriminator,
422 inner_data,
423 metadata,
424 )
425 } else if *program_id == program_ids::RAYDIUM_CPMM_PROGRAM_ID {
426 if let Some(f) = filter {
427 if !f.includes_raydium_cpmm() {
428 return None;
429 }
430 }
431 all_inner::raydium_cpmm::parse(&discriminator, inner_data, metadata)
432 } else if *program_id == program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
433 if let Some(f) = filter {
434 if !f.includes_raydium_amm_v4() {
435 return None;
436 }
437 }
438 all_inner::raydium_amm::parse(&discriminator, inner_data, metadata)
439 } else if *program_id == program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
440 if let Some(f) = filter {
441 if !f.includes_orca_whirlpool() {
442 return None;
443 }
444 }
445 all_inner::orca::parse(&discriminator, inner_data, metadata)
446 } else if *program_id == program_ids::METEORA_POOLS_PROGRAM_ID {
447 if let Some(f) = filter {
448 if !f.includes_meteora_pools() {
449 return None;
450 }
451 }
452 all_inner::meteora_amm::parse(&discriminator, inner_data, metadata)
453 } else if *program_id == program_ids::METEORA_DAMM_V2_PROGRAM_ID {
454 if let Some(f) = filter {
455 if !f.includes_meteora_damm_v2() {
456 return None;
457 }
458 }
459 all_inner::meteora_damm::parse(&discriminator, inner_data, metadata)
460 } else if *program_id == program_ids::METEORA_DLMM_PROGRAM_ID {
461 if let Some(f) = filter {
462 if !f.includes_meteora_dlmm() {
463 return None;
464 }
465 }
466 all_inner::meteora_dlmm::parse(&discriminator, inner_data, metadata)
467 } else if *program_id == program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID {
468 if let Some(f) = filter {
469 if !f.includes_raydium_launchlab() {
470 return None;
471 }
472 }
473 all_inner::raydium_launchlab::parse(&discriminator, inner_data, metadata)
474 } else {
475 None
476 };
477
478 if filter.map(|f| event.as_ref().is_some_and(|e| f.should_include_dex_event(e))).unwrap_or(true)
479 {
480 event
481 } else {
482 None
483 }
484}
485
486#[inline(always)]
494fn merge_instruction_events(events: Vec<IndexedInstructionEvent>) -> Vec<DexEvent> {
495 if events.is_empty() {
496 return Vec::new();
497 }
498
499 let mut events = events;
502 events.sort_unstable_by_key(|event| {
503 (event.outer_idx, event.inner_idx.map_or(0, |inner_idx| inner_idx + 1))
504 });
505
506 let mut result = Vec::with_capacity(events.len());
507 let mut outer_target: Option<(usize, usize)> = None;
508 let mut dlmm_targets: [Option<(usize, Option<u32>, usize)>; 8] = [None; 8];
511 let mut dlmm_targets_len = 0usize;
512
513 for indexed in events {
514 let IndexedInstructionEvent {
515 outer_idx,
516 inner_idx,
517 stack_height,
518 is_dlmm_event_cpi,
519 event,
520 } = indexed;
521 match inner_idx {
522 None => {
523 let is_dlmm = is_dlmm_event(&event);
524 let target_idx = result.len();
525 result.push(event);
526 outer_target = Some((outer_idx, target_idx));
527 dlmm_targets_len = 0;
528 if is_dlmm {
529 dlmm_targets[0] = Some((outer_idx, stack_height, target_idx));
530 dlmm_targets_len = 1;
531 }
532 }
533 Some(_) => {
534 if is_dlmm_event_cpi {
535 let target = (0..dlmm_targets_len).rev().find_map(|idx| {
536 let (target_outer, target_height, target_idx) = dlmm_targets[idx]?;
537 let is_direct_child = match (target_height, stack_height) {
538 (Some(parent), Some(child)) => child == parent + 1,
539 _ => true,
540 };
541 (target_outer == outer_idx && is_direct_child).then_some((idx, target_idx))
542 });
543 if let Some((candidate_idx, target_idx)) = target {
544 dlmm_targets_len = candidate_idx + 1;
545 if target_idx < result.len() {
546 let mut unmerged = None;
547 if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
548 continue;
549 }
550 result.push(unmerged.expect("unmerged event remains available"));
551 continue;
552 }
553 }
554 result.push(event);
555 continue;
556 }
557
558 let is_dlmm = is_dlmm_event(&event);
559 let target_idx = if let Some((target_outer, target_idx)) = outer_target {
560 if target_outer == outer_idx {
561 let mut unmerged = None;
562 if try_merge_events(&mut result[target_idx], event, &mut unmerged) {
563 target_idx
564 } else {
565 let target_idx = result.len();
566 result.push(unmerged.expect("unmerged event remains available"));
567 target_idx
568 }
569 } else {
570 let target_idx = result.len();
571 result.push(event);
572 target_idx
573 }
574 } else {
575 let target_idx = result.len();
576 result.push(event);
577 target_idx
578 };
579
580 if is_dlmm {
581 if let Some(height) = stack_height {
582 while dlmm_targets_len > 0 {
583 let Some((candidate_outer, candidate_height, _)) =
584 dlmm_targets[dlmm_targets_len - 1]
585 else {
586 break;
587 };
588 if candidate_outer != outer_idx
589 || candidate_height.is_some_and(|candidate| candidate >= height)
590 {
591 dlmm_targets_len -= 1;
592 } else {
593 break;
594 }
595 }
596 } else {
597 dlmm_targets_len = 0;
598 }
599 if dlmm_targets_len < dlmm_targets.len() {
600 dlmm_targets[dlmm_targets_len] =
601 Some((outer_idx, stack_height, target_idx));
602 dlmm_targets_len += 1;
603 }
604 }
605 }
606 }
607 }
608
609 result
610}
611
612#[inline(always)]
614fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
615 let Some(filter) = filter else { return true };
617
618 if filter.include_only.is_none() {
620 return true;
621 }
622
623 if filter.includes_pumpfun() {
626 return true;
627 }
628
629 if filter.includes_pump_fees() {
630 return true;
631 }
632
633 filter.includes_pumpswap()
634 || filter.includes_raydium_launchlab()
635 || filter.includes_raydium_cpmm()
636 || filter.includes_raydium_clmm()
637 || filter.includes_raydium_amm_v4()
638 || filter.includes_orca_whirlpool()
639 || filter.includes_meteora_pools()
640 || filter.includes_meteora_damm_v2()
641 || filter.includes_meteora_dlmm()
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
648 use yellowstone_grpc_proto::prelude::{
649 CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
650 };
651
652 fn pk(s: &str) -> Pubkey {
653 s.parse().unwrap()
654 }
655
656 fn usdc_mint() -> Pubkey {
657 pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
658 }
659
660 fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
661 key.to_bytes().to_vec()
662 }
663
664 fn decode_b58(s: &str) -> Vec<u8> {
665 bs58::decode(s).into_vec().unwrap()
666 }
667
668 fn str_arg(s: &str, out: &mut Vec<u8>) {
669 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
670 out.extend_from_slice(s.as_bytes());
671 }
672
673 fn create_v2_data() -> Vec<u8> {
674 let mut data = Vec::new();
675 data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
676 str_arg("Alt Coin", &mut data);
677 str_arg("ALT", &mut data);
678 str_arg("https://example.invalid/alt.json", &mut data);
679 data.extend_from_slice(Pubkey::new_unique().as_ref());
680 data.push(1);
681 data.push(1);
682 data
683 }
684
685 fn grpc_pumpfun_create_v2_tx(
686 static_len: usize,
687 writable_len: usize,
688 program_idx: u8,
689 ix_accounts: Vec<u8>,
690 account_overrides: Vec<(usize, Pubkey)>,
691 ) -> (TransactionStatusMeta, Option<Transaction>) {
692 let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
693 account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
694 let readonly_len = account_overrides
695 .iter()
696 .filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
697 .map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
698 .max()
699 .unwrap_or_default();
700 let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
701 let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
702 for (global_idx, key) in account_overrides {
703 if global_idx < static_len {
704 account_keys[global_idx] = key;
705 } else if global_idx < static_len + writable_len {
706 loaded_writable[global_idx - static_len] = key;
707 } else {
708 loaded_readonly[global_idx - static_len - writable_len] = key;
709 }
710 }
711
712 let meta = TransactionStatusMeta {
713 loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
714 loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
715 ..Default::default()
716 };
717 let tx = Transaction {
718 signatures: vec![Signature::default().as_ref().to_vec()],
719 message: Some(Message {
720 header: Some(MessageHeader {
721 num_required_signatures: 1,
722 num_readonly_signed_accounts: 0,
723 num_readonly_unsigned_accounts: 0,
724 }),
725 account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
726 recent_blockhash: vec![0; 32],
727 instructions: vec![CompiledInstruction {
728 program_id_index: program_idx as u32,
729 accounts: ix_accounts,
730 data: create_v2_data(),
731 }],
732 versioned: true,
733 address_table_lookups: Vec::new(),
734 config: None,
735 }),
736 };
737 (meta, Some(tx))
738 }
739
740 fn create_v2_accounts(
741 account_len: usize,
742 program_idx: u8,
743 mint_idx: u8,
744 user_idx: u8,
745 token_program_idx: u8,
746 quote_tail: Option<(u8, u8, u8)>,
747 ) -> Vec<u8> {
748 let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
749 accounts[0] = mint_idx;
750 accounts[5] = user_idx;
751 accounts[7] = token_program_idx;
752 if account_len > 15 {
753 accounts[15] = program_idx;
754 }
755 if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
756 accounts[16] = quote_idx;
757 accounts[17] = quote_vault_idx;
758 accounts[18] = quote_token_program_idx;
759 }
760 accounts
761 }
762
763 fn parse_create_v2_from_grpc(
764 meta: &TransactionStatusMeta,
765 tx: &Option<Transaction>,
766 ) -> crate::core::events::PumpFunCreateTokenEvent {
767 let events = parse_instructions_enhanced(
768 meta,
769 tx,
770 Signature::default(),
771 123,
772 0,
773 Some(456),
774 789,
775 None,
776 );
777 assert_eq!(events.len(), 1);
778 match &events[0] {
779 DexEvent::PumpFunCreate(e) => {
780 assert_eq!(e.ix_name, "create_v2");
781 e.clone()
782 }
783 DexEvent::PumpFunCreateV2(e) => {
784 assert_eq!(e.ix_name, "create_v2");
785 crate::core::events::PumpFunCreateTokenEvent {
786 metadata: e.metadata.clone(),
787 name: e.name.clone(),
788 symbol: e.symbol.clone(),
789 uri: e.uri.clone(),
790 mint: e.mint,
791 bonding_curve: e.bonding_curve,
792 user: e.user,
793 creator: e.creator,
794 timestamp: e.timestamp,
795 virtual_token_reserves: e.virtual_token_reserves,
796 virtual_sol_reserves: e.virtual_sol_reserves,
797 real_token_reserves: e.real_token_reserves,
798 token_total_supply: e.token_total_supply,
799 mint_authority: e.mint_authority,
800 associated_bonding_curve: e.associated_bonding_curve,
801 global: e.global,
802 system_program: e.system_program,
803 token_program: e.token_program,
804 associated_token_program: e.associated_token_program,
805 mayhem_program_id: e.mayhem_program_id,
806 global_params: e.global_params,
807 sol_vault: e.sol_vault,
808 mayhem_state: e.mayhem_state,
809 mayhem_token_vault: e.mayhem_token_vault,
810 event_authority: e.event_authority,
811 program: e.program,
812 quote_mint: e.quote_mint,
813 quote_vault: e.quote_vault,
814 quote_token_program: e.quote_token_program,
815 virtual_quote_reserves: e.virtual_quote_reserves,
816 creator_fee_bps: e.creator_fee_bps,
817 is_holder_reward: e.is_holder_reward,
818 ix_name: e.ix_name.clone(),
819 is_mayhem_mode: e.is_mayhem_mode,
820 is_cashback_enabled: e.is_cashback_enabled,
821 observed_fee_recipient: e.observed_fee_recipient,
822 }
823 }
824 other => panic!("expected PumpFun create_v2 event, got {other:?}"),
825 }
826 }
827
828 #[test]
829 fn test_should_parse_instructions() {
830 assert!(should_parse_instructions(None));
832
833 let filter = EventTypeFilter { include_only: None, exclude_types: None };
835 assert!(should_parse_instructions(Some(&filter)));
836
837 use crate::grpc::types::EventType;
839 let filter = EventTypeFilter {
840 include_only: Some(vec![EventType::PumpFunMigrate]),
841 exclude_types: None,
842 };
843 assert!(should_parse_instructions(Some(&filter)));
844
845 let filter = EventTypeFilter {
847 include_only: Some(vec![EventType::PumpFunTrade]),
848 exclude_types: None,
849 };
850 assert!(should_parse_instructions(Some(&filter)));
851
852 for event_type in [
853 EventType::PumpSwapTrade,
854 EventType::PumpFeesUpdateFeeShares,
855 EventType::RaydiumLaunchlabTrade,
856 EventType::RaydiumCpmmSwap,
857 EventType::RaydiumClmmSwap,
858 EventType::RaydiumAmmV4Swap,
859 EventType::OrcaWhirlpoolSwap,
860 EventType::MeteoraPoolsSwap,
861 EventType::MeteoraDammV2Swap,
862 EventType::MeteoraDammV2InitializePool,
863 EventType::MeteoraDlmmSwap,
864 ] {
865 let filter = EventTypeFilter::include_only(vec![event_type]);
866 assert!(
867 should_parse_instructions(Some(&filter)),
868 "instruction parsing should be enabled for {event_type:?}"
869 );
870 }
871
872 let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
873 assert!(
874 !should_parse_instructions(Some(&filter)),
875 "DBC events are log-only until an instruction parser is implemented"
876 );
877
878 let filter = EventTypeFilter::include_only(vec![
879 EventType::AccountPumpFunGlobal,
880 EventType::AccountRaydiumClmmPoolState,
881 EventType::AccountRaydiumCpmmPoolState,
882 EventType::AccountOrcaWhirlpool,
883 ]);
884 assert!(
885 !should_parse_instructions(Some(&filter)),
886 "account-only non-Pump filters should stay on the account update path"
887 );
888 }
889
890 #[test]
891 fn test_merge_instruction_events() {
892 use solana_sdk::signature::Signature;
893
894 let metadata = EventMetadata {
895 signature: Signature::default(),
896 slot: 100,
897 tx_index: 1,
898 block_time_us: 1000,
899 grpc_recv_us: 2000,
900 recent_blockhash: None,
901 };
902
903 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
905 metadata: metadata.clone(),
906 bonding_curve: Pubkey::new_unique(),
907 ..Default::default()
908 });
909
910 let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
911 metadata: metadata.clone(),
912 sol_amount: 1000,
913 token_amount: 2000,
914 ..Default::default()
915 });
916
917 let events = vec![
918 IndexedInstructionEvent {
919 outer_idx: 0,
920 inner_idx: None,
921 stack_height: Some(1),
922 is_dlmm_event_cpi: false,
923 event: outer_event,
924 },
925 IndexedInstructionEvent {
926 outer_idx: 0,
927 inner_idx: Some(0),
928 stack_height: Some(2),
929 is_dlmm_event_cpi: false,
930 event: inner_event,
931 },
932 ];
933
934 let result = merge_instruction_events(events);
935
936 assert_eq!(result.len(), 1);
938
939 if let DexEvent::PumpFunTrade(trade) = &result[0] {
941 assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
945 panic!("Expected PumpFunTrade event");
946 }
947 }
948
949 #[test]
950 fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
951 use solana_sdk::signature::Signature;
952
953 let metadata = EventMetadata {
954 signature: Signature::default(),
955 slot: 100,
956 tx_index: 1,
957 block_time_us: 1000,
958 grpc_recv_us: 2000,
959 recent_blockhash: None,
960 };
961
962 let bc = Pubkey::new_unique();
963 let fee = Pubkey::new_unique();
964
965 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
966 metadata: metadata.clone(),
967 bonding_curve: bc,
968 ..Default::default()
969 });
970
971 let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
972 metadata: metadata.clone(),
973 sol_amount: 1000,
974 token_amount: 2000,
975 is_buy: true,
976 ..Default::default()
977 });
978
979 let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
981 metadata: metadata.clone(),
982 fee_recipient: fee,
983 ..Default::default()
984 });
985
986 let events = vec![
987 IndexedInstructionEvent {
988 outer_idx: 0,
989 inner_idx: None,
990 stack_height: Some(1),
991 is_dlmm_event_cpi: false,
992 event: outer_event,
993 },
994 IndexedInstructionEvent {
995 outer_idx: 0,
996 inner_idx: Some(0),
997 stack_height: Some(2),
998 is_dlmm_event_cpi: false,
999 event: inner_trade,
1000 },
1001 IndexedInstructionEvent {
1002 outer_idx: 0,
1003 inner_idx: Some(1),
1004 stack_height: Some(2),
1005 is_dlmm_event_cpi: false,
1006 event: inner_fee_only,
1007 },
1008 ];
1009
1010 let result = merge_instruction_events(events);
1011 assert_eq!(result.len(), 1);
1012 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1013 assert_eq!(trade.bonding_curve, bc);
1014 assert_eq!(trade.sol_amount, 1000);
1015 assert_eq!(trade.token_amount, 2000);
1016 assert_eq!(trade.fee_recipient, fee);
1017 } else {
1018 panic!("Expected PumpFunTrade event");
1019 }
1020 }
1021
1022 fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1023 DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1024 metadata: EventMetadata::default(),
1025 token_x_mint: Pubkey::default(),
1026 token_y_mint: Pubkey::default(),
1027 user_token_in: Pubkey::default(),
1028 user_token_out: Pubkey::default(),
1029 min_amount_out: 0,
1030 pool,
1031 from: Pubkey::default(),
1032 start_bin_id: 0,
1033 end_bin_id: 0,
1034 amount_in,
1035 amount_out,
1036 swap_for_y: false,
1037 fee: 0,
1038 protocol_fee: 0,
1039 fee_bps: 0,
1040 host_fee: 0,
1041 ..Default::default()
1042 })
1043 }
1044
1045 fn dlmm_add_liquidity() -> DexEvent {
1046 DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1047 metadata: EventMetadata::default(),
1048 pool: Pubkey::default(),
1049 from: Pubkey::default(),
1050 position: Pubkey::default(),
1051 amounts: [0; 2],
1052 active_bin_id: 0,
1053 })
1054 }
1055
1056 #[test]
1057 fn merge_preserves_unrelated_inner_events() {
1058 let events = vec![
1059 IndexedInstructionEvent {
1060 outer_idx: 0,
1061 inner_idx: None,
1062 stack_height: Some(1),
1063 is_dlmm_event_cpi: false,
1064 event: dlmm_swap(Pubkey::default(), 0, 0),
1065 },
1066 IndexedInstructionEvent {
1067 outer_idx: 0,
1068 inner_idx: Some(0),
1069 stack_height: Some(2),
1070 is_dlmm_event_cpi: false,
1071 event: dlmm_add_liquidity(),
1072 },
1073 ];
1074
1075 let result = merge_instruction_events(events);
1076 assert_eq!(result.len(), 2);
1077 assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1078 assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1079 }
1080
1081 #[test]
1082 fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1083 let first_pool = Pubkey::new_unique();
1084 let second_pool = Pubkey::new_unique();
1085 let events = vec![
1086 IndexedInstructionEvent {
1087 outer_idx: 0,
1088 inner_idx: Some(0),
1089 stack_height: Some(2),
1090 is_dlmm_event_cpi: false,
1091 event: dlmm_swap(first_pool, 1, 0),
1092 },
1093 IndexedInstructionEvent {
1094 outer_idx: 0,
1095 inner_idx: Some(1),
1096 stack_height: Some(3),
1097 is_dlmm_event_cpi: true,
1098 event: dlmm_swap(first_pool, 10, 9),
1099 },
1100 IndexedInstructionEvent {
1101 outer_idx: 0,
1102 inner_idx: Some(2),
1103 stack_height: Some(2),
1104 is_dlmm_event_cpi: false,
1105 event: dlmm_swap(second_pool, 2, 0),
1106 },
1107 IndexedInstructionEvent {
1108 outer_idx: 0,
1109 inner_idx: Some(3),
1110 stack_height: Some(3),
1111 is_dlmm_event_cpi: true,
1112 event: dlmm_swap(second_pool, 20, 18),
1113 },
1114 ];
1115
1116 let result = merge_instruction_events(events);
1117 assert_eq!(result.len(), 2);
1118 let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1119 let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1120 assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1121 assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1122 }
1123
1124 #[test]
1125 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1126 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1127 let static_keys = [
1128 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1129 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1130 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1131 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1132 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1133 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1134 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1135 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1136 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1137 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1138 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1139 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1140 "ComputeBudget111111111111111111111111111111",
1141 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1142 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1143 "SysvarRent111111111111111111111111111111111",
1144 ];
1145 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1146 let loaded_readonly = [
1147 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1148 "So11111111111111111111111111111111111111112",
1149 "11111111111111111111111111111111",
1150 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1151 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1152 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1153 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1154 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1155 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1156 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1157 ];
1158 let meta = TransactionStatusMeta {
1159 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1160 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1161 inner_instructions: vec![InnerInstructions {
1162 index: 2,
1163 instructions: vec![InnerInstruction {
1164 program_id_index: 20,
1165 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1166 data: decode_b58(
1167 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1168 ),
1169 stack_height: Some(2),
1170 }],
1171 }],
1172 ..Default::default()
1173 };
1174 let tx = Some(Transaction {
1175 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1176 message: Some(Message {
1177 header: Some(MessageHeader::default()),
1178 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1179 recent_blockhash: vec![0; 32],
1180 instructions: vec![
1181 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1182 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1183 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1184 ],
1185 versioned: true,
1186 address_table_lookups: Vec::new(),
1187 config: None,
1188 }),
1189 });
1190
1191 let events = parse_instructions_enhanced(
1192 &meta,
1193 &tx,
1194 signature.parse().unwrap(),
1195 427_039_576,
1196 0,
1197 Some(1_781_687_252_000_000),
1198 789,
1199 None,
1200 );
1201
1202 assert_eq!(events.len(), 1, "{signature}");
1203 match &events[0] {
1204 DexEvent::PumpSwapCreatePool(e) => {
1205 assert_eq!(e.index, 0, "{signature}");
1206 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1207 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1208 assert_eq!(
1209 e.coin_creator,
1210 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1211 "{signature}"
1212 );
1213 assert!(!e.is_mayhem_mode, "{signature}");
1214 assert!(e.is_cashback_coin, "{signature}");
1215 assert_eq!(
1216 e.pool,
1217 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1218 "{signature}"
1219 );
1220 assert_eq!(
1221 e.creator,
1222 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1223 "{signature}"
1224 );
1225 assert_eq!(
1226 e.base_mint,
1227 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1228 "{signature}"
1229 );
1230 assert_eq!(
1231 e.quote_mint,
1232 pk("So11111111111111111111111111111111111111112"),
1233 "{signature}"
1234 );
1235 assert_eq!(
1236 e.lp_mint,
1237 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1238 "{signature}"
1239 );
1240 assert_eq!(
1241 e.user_base_token_account,
1242 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1243 "{signature}"
1244 );
1245 assert_eq!(
1246 e.user_quote_token_account,
1247 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1248 "{signature}"
1249 );
1250 }
1251 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1252 }
1253 }
1254
1255 #[test]
1256 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1257 struct Case {
1258 signature: &'static str,
1259 name: &'static str,
1260 static_len: usize,
1261 writable_len: usize,
1262 program_idx: u8,
1263 account_len: usize,
1264 mint_idx: u8,
1265 mint: &'static str,
1266 user_idx: u8,
1267 user: &'static str,
1268 token_program_idx: u8,
1269 quote_idx: u8,
1270 quote_mint: Pubkey,
1271 quote_vault_idx: u8,
1272 quote_vault: &'static str,
1273 quote_token_program_idx: u8,
1274 }
1275 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1276 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1277 let cases = [
1278 Case {
1279 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1280 name: "19-account WSOL quote in ALT",
1281 static_len: 15,
1282 writable_len: 7,
1283 program_idx: 12,
1284 account_len: 19,
1285 mint_idx: 1,
1286 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1287 user_idx: 0,
1288 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1289 token_program_idx: 24,
1290 quote_idx: 27,
1291 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1292 quote_vault_idx: 7,
1293 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1294 quote_token_program_idx: 28,
1295 },
1296 Case {
1297 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1298 name: "19-account WSOL quote in ALT with exact quote buy",
1299 static_len: 20,
1300 writable_len: 7,
1301 program_idx: 15,
1302 account_len: 19,
1303 mint_idx: 1,
1304 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1305 user_idx: 0,
1306 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1307 token_program_idx: 31,
1308 quote_idx: 28,
1309 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1310 quote_vault_idx: 4,
1311 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1312 quote_token_program_idx: 30,
1313 },
1314 Case {
1315 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1316 name: "19-account USDC quote in ALT",
1317 static_len: 19,
1318 writable_len: 6,
1319 program_idx: 16,
1320 account_len: 19,
1321 mint_idx: 1,
1322 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1323 user_idx: 0,
1324 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1325 token_program_idx: 27,
1326 quote_idx: 30,
1327 quote_mint: usdc_mint(),
1328 quote_vault_idx: 6,
1329 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1330 quote_token_program_idx: 31,
1331 },
1332 Case {
1333 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1334 name: "20-account WSOL quote in ALT",
1335 static_len: 15,
1336 writable_len: 7,
1337 program_idx: 12,
1338 account_len: 20,
1339 mint_idx: 1,
1340 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1341 user_idx: 0,
1342 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1343 token_program_idx: 24,
1344 quote_idx: 27,
1345 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1346 quote_vault_idx: 7,
1347 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1348 quote_token_program_idx: 28,
1349 },
1350 Case {
1351 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1352 name: "19-account WSOL quote in ALT with later buy",
1353 static_len: 18,
1354 writable_len: 7,
1355 program_idx: 13,
1356 account_len: 19,
1357 mint_idx: 1,
1358 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1359 user_idx: 0,
1360 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1361 token_program_idx: 27,
1362 quote_idx: 30,
1363 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1364 quote_vault_idx: 7,
1365 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1366 quote_token_program_idx: 31,
1367 },
1368 Case {
1369 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1370 name: "19-account USDC quote in ALT exact quote buy",
1371 static_len: 19,
1372 writable_len: 6,
1373 program_idx: 15,
1374 account_len: 19,
1375 mint_idx: 1,
1376 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1377 user_idx: 0,
1378 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1379 token_program_idx: 28,
1380 quote_idx: 33,
1381 quote_mint: usdc_mint(),
1382 quote_vault_idx: 7,
1383 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1384 quote_token_program_idx: 27,
1385 },
1386 Case {
1387 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1388 name: "20-account WSOL quote in ALT with jit account",
1389 static_len: 15,
1390 writable_len: 7,
1391 program_idx: 12,
1392 account_len: 20,
1393 mint_idx: 1,
1394 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1395 user_idx: 0,
1396 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1397 token_program_idx: 24,
1398 quote_idx: 27,
1399 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1400 quote_vault_idx: 7,
1401 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1402 quote_token_program_idx: 28,
1403 },
1404 ];
1405
1406 for case in cases {
1407 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1408 case.static_len,
1409 case.writable_len,
1410 case.program_idx,
1411 create_v2_accounts(
1412 case.account_len,
1413 case.program_idx,
1414 case.mint_idx,
1415 case.user_idx,
1416 case.token_program_idx,
1417 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1418 ),
1419 vec![
1420 (case.mint_idx as usize, pk(case.mint)),
1421 (case.user_idx as usize, pk(case.user)),
1422 (case.token_program_idx as usize, token_2022_program),
1423 (case.quote_idx as usize, case.quote_mint),
1424 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1425 (case.quote_token_program_idx as usize, spl_token_program),
1426 ],
1427 );
1428 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1429 let idx = global_idx as usize;
1430 if idx < case.static_len {
1431 ("static", idx)
1432 } else if idx < case.static_len + case.writable_len {
1433 ("writable", idx - case.static_len)
1434 } else {
1435 ("readonly", idx - case.static_len - case.writable_len)
1436 }
1437 };
1438 assert_eq!(
1439 meta.loaded_writable_addresses.len(),
1440 case.writable_len,
1441 "{}: {}",
1442 case.name,
1443 case.signature
1444 );
1445 for (global_idx, expected_key) in [
1446 (case.token_program_idx, token_2022_program),
1447 (case.quote_idx, case.quote_mint),
1448 (case.quote_token_program_idx, spl_token_program),
1449 ] {
1450 match loaded_key_location(global_idx) {
1451 ("static", _) => {}
1452 ("writable", offset) => assert_eq!(
1453 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1454 expected_key,
1455 "{}: writable loaded key {global_idx}: {}",
1456 case.name,
1457 case.signature
1458 ),
1459 ("readonly", offset) => assert_eq!(
1460 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1461 expected_key,
1462 "{}: readonly loaded key {global_idx}: {}",
1463 case.name,
1464 case.signature
1465 ),
1466 _ => unreachable!(),
1467 }
1468 }
1469
1470 let create = parse_create_v2_from_grpc(&meta, &tx);
1471
1472 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1473 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1474 assert_eq!(
1475 create.token_program, token_2022_program,
1476 "{}: {}",
1477 case.name, case.signature
1478 );
1479 assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1480 assert_eq!(
1481 create.quote_vault,
1482 pk(case.quote_vault),
1483 "{}: {}",
1484 case.name,
1485 case.signature
1486 );
1487 assert_eq!(
1488 create.quote_token_program, spl_token_program,
1489 "{}: {}",
1490 case.name, case.signature
1491 );
1492 }
1493 }
1494
1495 #[test]
1496 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1497 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1498 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1499 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1500 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1501 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1502 16,
1503 5,
1504 12,
1505 create_v2_accounts(16, 12, 1, 0, 24, None),
1506 vec![(1, mint), (0, user), (24, token_program)],
1507 );
1508
1509 let create = parse_create_v2_from_grpc(&meta, &tx);
1510
1511 assert_eq!(create.mint, mint, "{signature}");
1512 assert_eq!(create.user, user, "{signature}");
1513 assert_eq!(create.token_program, token_program, "{signature}");
1514 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1515 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1516 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1517 }
1518
1519 #[test]
1520 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1521 let quote_vault = Pubkey::new_unique();
1522 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1523 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1524 19,
1525 6,
1526 16,
1527 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1528 vec![
1529 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1530 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1531 (6, quote_vault),
1532 (31, quote_token_program),
1533 ],
1534 );
1535
1536 let create = parse_create_v2_from_grpc(&meta, &tx);
1537
1538 assert_eq!(create.quote_mint, Pubkey::default());
1539 assert_eq!(create.quote_vault, Pubkey::default());
1540 assert_eq!(create.quote_token_program, Pubkey::default());
1541 }
1542}