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 })
1042 }
1043
1044 fn dlmm_add_liquidity() -> DexEvent {
1045 DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1046 metadata: EventMetadata::default(),
1047 pool: Pubkey::default(),
1048 from: Pubkey::default(),
1049 position: Pubkey::default(),
1050 amounts: [0; 2],
1051 active_bin_id: 0,
1052 })
1053 }
1054
1055 #[test]
1056 fn merge_preserves_unrelated_inner_events() {
1057 let events = vec![
1058 IndexedInstructionEvent {
1059 outer_idx: 0,
1060 inner_idx: None,
1061 stack_height: Some(1),
1062 is_dlmm_event_cpi: false,
1063 event: dlmm_swap(Pubkey::default(), 0, 0),
1064 },
1065 IndexedInstructionEvent {
1066 outer_idx: 0,
1067 inner_idx: Some(0),
1068 stack_height: Some(2),
1069 is_dlmm_event_cpi: false,
1070 event: dlmm_add_liquidity(),
1071 },
1072 ];
1073
1074 let result = merge_instruction_events(events);
1075 assert_eq!(result.len(), 2);
1076 assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1077 assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1078 }
1079
1080 #[test]
1081 fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1082 let first_pool = Pubkey::new_unique();
1083 let second_pool = Pubkey::new_unique();
1084 let events = vec![
1085 IndexedInstructionEvent {
1086 outer_idx: 0,
1087 inner_idx: Some(0),
1088 stack_height: Some(2),
1089 is_dlmm_event_cpi: false,
1090 event: dlmm_swap(first_pool, 1, 0),
1091 },
1092 IndexedInstructionEvent {
1093 outer_idx: 0,
1094 inner_idx: Some(1),
1095 stack_height: Some(3),
1096 is_dlmm_event_cpi: true,
1097 event: dlmm_swap(first_pool, 10, 9),
1098 },
1099 IndexedInstructionEvent {
1100 outer_idx: 0,
1101 inner_idx: Some(2),
1102 stack_height: Some(2),
1103 is_dlmm_event_cpi: false,
1104 event: dlmm_swap(second_pool, 2, 0),
1105 },
1106 IndexedInstructionEvent {
1107 outer_idx: 0,
1108 inner_idx: Some(3),
1109 stack_height: Some(3),
1110 is_dlmm_event_cpi: true,
1111 event: dlmm_swap(second_pool, 20, 18),
1112 },
1113 ];
1114
1115 let result = merge_instruction_events(events);
1116 assert_eq!(result.len(), 2);
1117 let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1118 let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1119 assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1120 assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1121 }
1122
1123 #[test]
1124 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1125 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1126 let static_keys = [
1127 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1128 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1129 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1130 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1131 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1132 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1133 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1134 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1135 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1136 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1137 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1138 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1139 "ComputeBudget111111111111111111111111111111",
1140 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1141 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1142 "SysvarRent111111111111111111111111111111111",
1143 ];
1144 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1145 let loaded_readonly = [
1146 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1147 "So11111111111111111111111111111111111111112",
1148 "11111111111111111111111111111111",
1149 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1150 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1151 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1152 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1153 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1154 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1155 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1156 ];
1157 let meta = TransactionStatusMeta {
1158 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1159 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1160 inner_instructions: vec![InnerInstructions {
1161 index: 2,
1162 instructions: vec![InnerInstruction {
1163 program_id_index: 20,
1164 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1165 data: decode_b58(
1166 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1167 ),
1168 stack_height: Some(2),
1169 }],
1170 }],
1171 ..Default::default()
1172 };
1173 let tx = Some(Transaction {
1174 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1175 message: Some(Message {
1176 header: Some(MessageHeader::default()),
1177 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1178 recent_blockhash: vec![0; 32],
1179 instructions: vec![
1180 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1181 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1182 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1183 ],
1184 versioned: true,
1185 address_table_lookups: Vec::new(),
1186 config: None,
1187 }),
1188 });
1189
1190 let events = parse_instructions_enhanced(
1191 &meta,
1192 &tx,
1193 signature.parse().unwrap(),
1194 427_039_576,
1195 0,
1196 Some(1_781_687_252_000_000),
1197 789,
1198 None,
1199 );
1200
1201 assert_eq!(events.len(), 1, "{signature}");
1202 match &events[0] {
1203 DexEvent::PumpSwapCreatePool(e) => {
1204 assert_eq!(e.index, 0, "{signature}");
1205 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1206 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1207 assert_eq!(
1208 e.coin_creator,
1209 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1210 "{signature}"
1211 );
1212 assert!(!e.is_mayhem_mode, "{signature}");
1213 assert!(e.is_cashback_coin, "{signature}");
1214 assert_eq!(
1215 e.pool,
1216 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1217 "{signature}"
1218 );
1219 assert_eq!(
1220 e.creator,
1221 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1222 "{signature}"
1223 );
1224 assert_eq!(
1225 e.base_mint,
1226 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1227 "{signature}"
1228 );
1229 assert_eq!(
1230 e.quote_mint,
1231 pk("So11111111111111111111111111111111111111112"),
1232 "{signature}"
1233 );
1234 assert_eq!(
1235 e.lp_mint,
1236 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1237 "{signature}"
1238 );
1239 assert_eq!(
1240 e.user_base_token_account,
1241 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1242 "{signature}"
1243 );
1244 assert_eq!(
1245 e.user_quote_token_account,
1246 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1247 "{signature}"
1248 );
1249 }
1250 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1251 }
1252 }
1253
1254 #[test]
1255 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1256 struct Case {
1257 signature: &'static str,
1258 name: &'static str,
1259 static_len: usize,
1260 writable_len: usize,
1261 program_idx: u8,
1262 account_len: usize,
1263 mint_idx: u8,
1264 mint: &'static str,
1265 user_idx: u8,
1266 user: &'static str,
1267 token_program_idx: u8,
1268 quote_idx: u8,
1269 quote_mint: Pubkey,
1270 quote_vault_idx: u8,
1271 quote_vault: &'static str,
1272 quote_token_program_idx: u8,
1273 }
1274 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1275 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1276 let cases = [
1277 Case {
1278 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1279 name: "19-account WSOL quote in ALT",
1280 static_len: 15,
1281 writable_len: 7,
1282 program_idx: 12,
1283 account_len: 19,
1284 mint_idx: 1,
1285 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1286 user_idx: 0,
1287 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1288 token_program_idx: 24,
1289 quote_idx: 27,
1290 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1291 quote_vault_idx: 7,
1292 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1293 quote_token_program_idx: 28,
1294 },
1295 Case {
1296 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1297 name: "19-account WSOL quote in ALT with exact quote buy",
1298 static_len: 20,
1299 writable_len: 7,
1300 program_idx: 15,
1301 account_len: 19,
1302 mint_idx: 1,
1303 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1304 user_idx: 0,
1305 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1306 token_program_idx: 31,
1307 quote_idx: 28,
1308 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1309 quote_vault_idx: 4,
1310 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1311 quote_token_program_idx: 30,
1312 },
1313 Case {
1314 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1315 name: "19-account USDC quote in ALT",
1316 static_len: 19,
1317 writable_len: 6,
1318 program_idx: 16,
1319 account_len: 19,
1320 mint_idx: 1,
1321 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1322 user_idx: 0,
1323 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1324 token_program_idx: 27,
1325 quote_idx: 30,
1326 quote_mint: usdc_mint(),
1327 quote_vault_idx: 6,
1328 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1329 quote_token_program_idx: 31,
1330 },
1331 Case {
1332 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1333 name: "20-account WSOL quote in ALT",
1334 static_len: 15,
1335 writable_len: 7,
1336 program_idx: 12,
1337 account_len: 20,
1338 mint_idx: 1,
1339 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1340 user_idx: 0,
1341 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1342 token_program_idx: 24,
1343 quote_idx: 27,
1344 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1345 quote_vault_idx: 7,
1346 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1347 quote_token_program_idx: 28,
1348 },
1349 Case {
1350 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1351 name: "19-account WSOL quote in ALT with later buy",
1352 static_len: 18,
1353 writable_len: 7,
1354 program_idx: 13,
1355 account_len: 19,
1356 mint_idx: 1,
1357 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1358 user_idx: 0,
1359 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1360 token_program_idx: 27,
1361 quote_idx: 30,
1362 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1363 quote_vault_idx: 7,
1364 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1365 quote_token_program_idx: 31,
1366 },
1367 Case {
1368 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1369 name: "19-account USDC quote in ALT exact quote buy",
1370 static_len: 19,
1371 writable_len: 6,
1372 program_idx: 15,
1373 account_len: 19,
1374 mint_idx: 1,
1375 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1376 user_idx: 0,
1377 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1378 token_program_idx: 28,
1379 quote_idx: 33,
1380 quote_mint: usdc_mint(),
1381 quote_vault_idx: 7,
1382 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1383 quote_token_program_idx: 27,
1384 },
1385 Case {
1386 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1387 name: "20-account WSOL quote in ALT with jit account",
1388 static_len: 15,
1389 writable_len: 7,
1390 program_idx: 12,
1391 account_len: 20,
1392 mint_idx: 1,
1393 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1394 user_idx: 0,
1395 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1396 token_program_idx: 24,
1397 quote_idx: 27,
1398 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1399 quote_vault_idx: 7,
1400 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1401 quote_token_program_idx: 28,
1402 },
1403 ];
1404
1405 for case in cases {
1406 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1407 case.static_len,
1408 case.writable_len,
1409 case.program_idx,
1410 create_v2_accounts(
1411 case.account_len,
1412 case.program_idx,
1413 case.mint_idx,
1414 case.user_idx,
1415 case.token_program_idx,
1416 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1417 ),
1418 vec![
1419 (case.mint_idx as usize, pk(case.mint)),
1420 (case.user_idx as usize, pk(case.user)),
1421 (case.token_program_idx as usize, token_2022_program),
1422 (case.quote_idx as usize, case.quote_mint),
1423 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1424 (case.quote_token_program_idx as usize, spl_token_program),
1425 ],
1426 );
1427 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1428 let idx = global_idx as usize;
1429 if idx < case.static_len {
1430 ("static", idx)
1431 } else if idx < case.static_len + case.writable_len {
1432 ("writable", idx - case.static_len)
1433 } else {
1434 ("readonly", idx - case.static_len - case.writable_len)
1435 }
1436 };
1437 assert_eq!(
1438 meta.loaded_writable_addresses.len(),
1439 case.writable_len,
1440 "{}: {}",
1441 case.name,
1442 case.signature
1443 );
1444 for (global_idx, expected_key) in [
1445 (case.token_program_idx, token_2022_program),
1446 (case.quote_idx, case.quote_mint),
1447 (case.quote_token_program_idx, spl_token_program),
1448 ] {
1449 match loaded_key_location(global_idx) {
1450 ("static", _) => {}
1451 ("writable", offset) => assert_eq!(
1452 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1453 expected_key,
1454 "{}: writable loaded key {global_idx}: {}",
1455 case.name,
1456 case.signature
1457 ),
1458 ("readonly", offset) => assert_eq!(
1459 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1460 expected_key,
1461 "{}: readonly loaded key {global_idx}: {}",
1462 case.name,
1463 case.signature
1464 ),
1465 _ => unreachable!(),
1466 }
1467 }
1468
1469 let create = parse_create_v2_from_grpc(&meta, &tx);
1470
1471 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1472 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1473 assert_eq!(
1474 create.token_program, token_2022_program,
1475 "{}: {}",
1476 case.name, case.signature
1477 );
1478 assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1479 assert_eq!(
1480 create.quote_vault,
1481 pk(case.quote_vault),
1482 "{}: {}",
1483 case.name,
1484 case.signature
1485 );
1486 assert_eq!(
1487 create.quote_token_program, spl_token_program,
1488 "{}: {}",
1489 case.name, case.signature
1490 );
1491 }
1492 }
1493
1494 #[test]
1495 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1496 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1497 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1498 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1499 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1500 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1501 16,
1502 5,
1503 12,
1504 create_v2_accounts(16, 12, 1, 0, 24, None),
1505 vec![(1, mint), (0, user), (24, token_program)],
1506 );
1507
1508 let create = parse_create_v2_from_grpc(&meta, &tx);
1509
1510 assert_eq!(create.mint, mint, "{signature}");
1511 assert_eq!(create.user, user, "{signature}");
1512 assert_eq!(create.token_program, token_program, "{signature}");
1513 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1514 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1515 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1516 }
1517
1518 #[test]
1519 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1520 let quote_vault = Pubkey::new_unique();
1521 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1522 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1523 19,
1524 6,
1525 16,
1526 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1527 vec![
1528 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1529 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1530 (6, quote_vault),
1531 (31, quote_token_program),
1532 ],
1533 );
1534
1535 let create = parse_create_v2_from_grpc(&meta, &tx);
1536
1537 assert_eq!(create.quote_mint, Pubkey::default());
1538 assert_eq!(create.quote_vault, Pubkey::default());
1539 assert_eq!(create.quote_token_program, Pubkey::default());
1540 }
1541}