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 ix_name: e.ix_name.clone(),
817 is_mayhem_mode: e.is_mayhem_mode,
818 is_cashback_enabled: e.is_cashback_enabled,
819 observed_fee_recipient: e.observed_fee_recipient,
820 }
821 }
822 other => panic!("expected PumpFun create_v2 event, got {other:?}"),
823 }
824 }
825
826 #[test]
827 fn test_should_parse_instructions() {
828 assert!(should_parse_instructions(None));
830
831 let filter = EventTypeFilter { include_only: None, exclude_types: None };
833 assert!(should_parse_instructions(Some(&filter)));
834
835 use crate::grpc::types::EventType;
837 let filter = EventTypeFilter {
838 include_only: Some(vec![EventType::PumpFunMigrate]),
839 exclude_types: None,
840 };
841 assert!(should_parse_instructions(Some(&filter)));
842
843 let filter = EventTypeFilter {
845 include_only: Some(vec![EventType::PumpFunTrade]),
846 exclude_types: None,
847 };
848 assert!(should_parse_instructions(Some(&filter)));
849
850 for event_type in [
851 EventType::PumpSwapTrade,
852 EventType::PumpFeesUpdateFeeShares,
853 EventType::RaydiumLaunchlabTrade,
854 EventType::RaydiumCpmmSwap,
855 EventType::RaydiumClmmSwap,
856 EventType::RaydiumAmmV4Swap,
857 EventType::OrcaWhirlpoolSwap,
858 EventType::MeteoraPoolsSwap,
859 EventType::MeteoraDammV2Swap,
860 EventType::MeteoraDammV2InitializePool,
861 EventType::MeteoraDlmmSwap,
862 ] {
863 let filter = EventTypeFilter::include_only(vec![event_type]);
864 assert!(
865 should_parse_instructions(Some(&filter)),
866 "instruction parsing should be enabled for {event_type:?}"
867 );
868 }
869
870 let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
871 assert!(
872 !should_parse_instructions(Some(&filter)),
873 "DBC events are log-only until an instruction parser is implemented"
874 );
875
876 let filter = EventTypeFilter::include_only(vec![
877 EventType::AccountPumpFunGlobal,
878 EventType::AccountRaydiumClmmPoolState,
879 EventType::AccountRaydiumCpmmPoolState,
880 EventType::AccountOrcaWhirlpool,
881 ]);
882 assert!(
883 !should_parse_instructions(Some(&filter)),
884 "account-only non-Pump filters should stay on the account update path"
885 );
886 }
887
888 #[test]
889 fn test_merge_instruction_events() {
890 use solana_sdk::signature::Signature;
891
892 let metadata = EventMetadata {
893 signature: Signature::default(),
894 slot: 100,
895 tx_index: 1,
896 block_time_us: 1000,
897 grpc_recv_us: 2000,
898 recent_blockhash: None,
899 };
900
901 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
903 metadata: metadata.clone(),
904 bonding_curve: Pubkey::new_unique(),
905 ..Default::default()
906 });
907
908 let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
909 metadata: metadata.clone(),
910 sol_amount: 1000,
911 token_amount: 2000,
912 ..Default::default()
913 });
914
915 let events = vec![
916 IndexedInstructionEvent {
917 outer_idx: 0,
918 inner_idx: None,
919 stack_height: Some(1),
920 is_dlmm_event_cpi: false,
921 event: outer_event,
922 },
923 IndexedInstructionEvent {
924 outer_idx: 0,
925 inner_idx: Some(0),
926 stack_height: Some(2),
927 is_dlmm_event_cpi: false,
928 event: inner_event,
929 },
930 ];
931
932 let result = merge_instruction_events(events);
933
934 assert_eq!(result.len(), 1);
936
937 if let DexEvent::PumpFunTrade(trade) = &result[0] {
939 assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
943 panic!("Expected PumpFunTrade event");
944 }
945 }
946
947 #[test]
948 fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
949 use solana_sdk::signature::Signature;
950
951 let metadata = EventMetadata {
952 signature: Signature::default(),
953 slot: 100,
954 tx_index: 1,
955 block_time_us: 1000,
956 grpc_recv_us: 2000,
957 recent_blockhash: None,
958 };
959
960 let bc = Pubkey::new_unique();
961 let fee = Pubkey::new_unique();
962
963 let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
964 metadata: metadata.clone(),
965 bonding_curve: bc,
966 ..Default::default()
967 });
968
969 let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
970 metadata: metadata.clone(),
971 sol_amount: 1000,
972 token_amount: 2000,
973 is_buy: true,
974 ..Default::default()
975 });
976
977 let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
979 metadata: metadata.clone(),
980 fee_recipient: fee,
981 ..Default::default()
982 });
983
984 let events = vec![
985 IndexedInstructionEvent {
986 outer_idx: 0,
987 inner_idx: None,
988 stack_height: Some(1),
989 is_dlmm_event_cpi: false,
990 event: outer_event,
991 },
992 IndexedInstructionEvent {
993 outer_idx: 0,
994 inner_idx: Some(0),
995 stack_height: Some(2),
996 is_dlmm_event_cpi: false,
997 event: inner_trade,
998 },
999 IndexedInstructionEvent {
1000 outer_idx: 0,
1001 inner_idx: Some(1),
1002 stack_height: Some(2),
1003 is_dlmm_event_cpi: false,
1004 event: inner_fee_only,
1005 },
1006 ];
1007
1008 let result = merge_instruction_events(events);
1009 assert_eq!(result.len(), 1);
1010 if let DexEvent::PumpFunTrade(trade) = &result[0] {
1011 assert_eq!(trade.bonding_curve, bc);
1012 assert_eq!(trade.sol_amount, 1000);
1013 assert_eq!(trade.token_amount, 2000);
1014 assert_eq!(trade.fee_recipient, fee);
1015 } else {
1016 panic!("Expected PumpFunTrade event");
1017 }
1018 }
1019
1020 fn dlmm_swap(pool: Pubkey, amount_in: u64, amount_out: u64) -> DexEvent {
1021 DexEvent::MeteoraDlmmSwap(MeteoraDlmmSwapEvent {
1022 metadata: EventMetadata::default(),
1023 token_x_mint: Pubkey::default(),
1024 token_y_mint: Pubkey::default(),
1025 user_token_in: Pubkey::default(),
1026 user_token_out: Pubkey::default(),
1027 min_amount_out: 0,
1028 pool,
1029 from: Pubkey::default(),
1030 start_bin_id: 0,
1031 end_bin_id: 0,
1032 amount_in,
1033 amount_out,
1034 swap_for_y: false,
1035 fee: 0,
1036 protocol_fee: 0,
1037 fee_bps: 0,
1038 host_fee: 0,
1039 })
1040 }
1041
1042 fn dlmm_add_liquidity() -> DexEvent {
1043 DexEvent::MeteoraDlmmAddLiquidity(MeteoraDlmmAddLiquidityEvent {
1044 metadata: EventMetadata::default(),
1045 pool: Pubkey::default(),
1046 from: Pubkey::default(),
1047 position: Pubkey::default(),
1048 amounts: [0; 2],
1049 active_bin_id: 0,
1050 })
1051 }
1052
1053 #[test]
1054 fn merge_preserves_unrelated_inner_events() {
1055 let events = vec![
1056 IndexedInstructionEvent {
1057 outer_idx: 0,
1058 inner_idx: None,
1059 stack_height: Some(1),
1060 is_dlmm_event_cpi: false,
1061 event: dlmm_swap(Pubkey::default(), 0, 0),
1062 },
1063 IndexedInstructionEvent {
1064 outer_idx: 0,
1065 inner_idx: Some(0),
1066 stack_height: Some(2),
1067 is_dlmm_event_cpi: false,
1068 event: dlmm_add_liquidity(),
1069 },
1070 ];
1071
1072 let result = merge_instruction_events(events);
1073 assert_eq!(result.len(), 2);
1074 assert!(matches!(result[0], DexEvent::MeteoraDlmmSwap(_)));
1075 assert!(matches!(result[1], DexEvent::MeteoraDlmmAddLiquidity(_)));
1076 }
1077
1078 #[test]
1079 fn merge_dlmm_inner_instruction_with_direct_event_cpi() {
1080 let first_pool = Pubkey::new_unique();
1081 let second_pool = Pubkey::new_unique();
1082 let events = vec![
1083 IndexedInstructionEvent {
1084 outer_idx: 0,
1085 inner_idx: Some(0),
1086 stack_height: Some(2),
1087 is_dlmm_event_cpi: false,
1088 event: dlmm_swap(first_pool, 1, 0),
1089 },
1090 IndexedInstructionEvent {
1091 outer_idx: 0,
1092 inner_idx: Some(1),
1093 stack_height: Some(3),
1094 is_dlmm_event_cpi: true,
1095 event: dlmm_swap(first_pool, 10, 9),
1096 },
1097 IndexedInstructionEvent {
1098 outer_idx: 0,
1099 inner_idx: Some(2),
1100 stack_height: Some(2),
1101 is_dlmm_event_cpi: false,
1102 event: dlmm_swap(second_pool, 2, 0),
1103 },
1104 IndexedInstructionEvent {
1105 outer_idx: 0,
1106 inner_idx: Some(3),
1107 stack_height: Some(3),
1108 is_dlmm_event_cpi: true,
1109 event: dlmm_swap(second_pool, 20, 18),
1110 },
1111 ];
1112
1113 let result = merge_instruction_events(events);
1114 assert_eq!(result.len(), 2);
1115 let DexEvent::MeteoraDlmmSwap(first) = &result[0] else { panic!("swap") };
1116 let DexEvent::MeteoraDlmmSwap(second) = &result[1] else { panic!("swap") };
1117 assert_eq!((first.pool, first.amount_in, first.amount_out), (first_pool, 10, 9));
1118 assert_eq!((second.pool, second.amount_in, second.amount_out), (second_pool, 20, 18));
1119 }
1120
1121 #[test]
1122 fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
1123 let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
1124 let static_keys = [
1125 "9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
1126 "CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
1127 "6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
1128 "F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
1129 "HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
1130 "4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
1131 "2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
1132 "HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
1133 "GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
1134 "H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
1135 "56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
1136 "GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
1137 "ComputeBudget111111111111111111111111111111",
1138 "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
1139 "9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
1140 "SysvarRent111111111111111111111111111111111",
1141 ];
1142 let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
1143 let loaded_readonly = [
1144 "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
1145 "So11111111111111111111111111111111111111112",
1146 "11111111111111111111111111111111",
1147 "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
1148 "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
1149 "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
1150 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1151 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
1152 "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
1153 "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
1154 ];
1155 let meta = TransactionStatusMeta {
1156 loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1157 loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1158 inner_instructions: vec![InnerInstructions {
1159 index: 2,
1160 instructions: vec![InnerInstruction {
1161 program_id_index: 20,
1162 accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
1163 data: decode_b58(
1164 "iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
1165 ),
1166 stack_height: Some(2),
1167 }],
1168 }],
1169 ..Default::default()
1170 };
1171 let tx = Some(Transaction {
1172 signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
1173 message: Some(Message {
1174 header: Some(MessageHeader::default()),
1175 account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
1176 recent_blockhash: vec![0; 32],
1177 instructions: vec![
1178 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1179 CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
1180 CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
1181 ],
1182 versioned: true,
1183 address_table_lookups: Vec::new(),
1184 config: None,
1185 }),
1186 });
1187
1188 let events = parse_instructions_enhanced(
1189 &meta,
1190 &tx,
1191 signature.parse().unwrap(),
1192 427_039_576,
1193 0,
1194 Some(1_781_687_252_000_000),
1195 789,
1196 None,
1197 );
1198
1199 assert_eq!(events.len(), 1, "{signature}");
1200 match &events[0] {
1201 DexEvent::PumpSwapCreatePool(e) => {
1202 assert_eq!(e.index, 0, "{signature}");
1203 assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
1204 assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
1205 assert_eq!(
1206 e.coin_creator,
1207 pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
1208 "{signature}"
1209 );
1210 assert!(!e.is_mayhem_mode, "{signature}");
1211 assert!(e.is_cashback_coin, "{signature}");
1212 assert_eq!(
1213 e.pool,
1214 pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
1215 "{signature}"
1216 );
1217 assert_eq!(
1218 e.creator,
1219 pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
1220 "{signature}"
1221 );
1222 assert_eq!(
1223 e.base_mint,
1224 pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
1225 "{signature}"
1226 );
1227 assert_eq!(
1228 e.quote_mint,
1229 pk("So11111111111111111111111111111111111111112"),
1230 "{signature}"
1231 );
1232 assert_eq!(
1233 e.lp_mint,
1234 pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
1235 "{signature}"
1236 );
1237 assert_eq!(
1238 e.user_base_token_account,
1239 pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
1240 "{signature}"
1241 );
1242 assert_eq!(
1243 e.user_quote_token_account,
1244 pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
1245 "{signature}"
1246 );
1247 }
1248 other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
1249 }
1250 }
1251
1252 #[test]
1253 fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
1254 struct Case {
1255 signature: &'static str,
1256 name: &'static str,
1257 static_len: usize,
1258 writable_len: usize,
1259 program_idx: u8,
1260 account_len: usize,
1261 mint_idx: u8,
1262 mint: &'static str,
1263 user_idx: u8,
1264 user: &'static str,
1265 token_program_idx: u8,
1266 quote_idx: u8,
1267 quote_mint: Pubkey,
1268 quote_vault_idx: u8,
1269 quote_vault: &'static str,
1270 quote_token_program_idx: u8,
1271 }
1272 let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1273 let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1274 let cases = [
1275 Case {
1276 signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
1277 name: "19-account WSOL quote in ALT",
1278 static_len: 15,
1279 writable_len: 7,
1280 program_idx: 12,
1281 account_len: 19,
1282 mint_idx: 1,
1283 mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
1284 user_idx: 0,
1285 user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
1286 token_program_idx: 24,
1287 quote_idx: 27,
1288 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1289 quote_vault_idx: 7,
1290 quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
1291 quote_token_program_idx: 28,
1292 },
1293 Case {
1294 signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
1295 name: "19-account WSOL quote in ALT with exact quote buy",
1296 static_len: 20,
1297 writable_len: 7,
1298 program_idx: 15,
1299 account_len: 19,
1300 mint_idx: 1,
1301 mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
1302 user_idx: 0,
1303 user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
1304 token_program_idx: 31,
1305 quote_idx: 28,
1306 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1307 quote_vault_idx: 4,
1308 quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
1309 quote_token_program_idx: 30,
1310 },
1311 Case {
1312 signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
1313 name: "19-account USDC quote in ALT",
1314 static_len: 19,
1315 writable_len: 6,
1316 program_idx: 16,
1317 account_len: 19,
1318 mint_idx: 1,
1319 mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
1320 user_idx: 0,
1321 user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
1322 token_program_idx: 27,
1323 quote_idx: 30,
1324 quote_mint: usdc_mint(),
1325 quote_vault_idx: 6,
1326 quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
1327 quote_token_program_idx: 31,
1328 },
1329 Case {
1330 signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
1331 name: "20-account WSOL quote in ALT",
1332 static_len: 15,
1333 writable_len: 7,
1334 program_idx: 12,
1335 account_len: 20,
1336 mint_idx: 1,
1337 mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
1338 user_idx: 0,
1339 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1340 token_program_idx: 24,
1341 quote_idx: 27,
1342 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1343 quote_vault_idx: 7,
1344 quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
1345 quote_token_program_idx: 28,
1346 },
1347 Case {
1348 signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
1349 name: "19-account WSOL quote in ALT with later buy",
1350 static_len: 18,
1351 writable_len: 7,
1352 program_idx: 13,
1353 account_len: 19,
1354 mint_idx: 1,
1355 mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
1356 user_idx: 0,
1357 user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
1358 token_program_idx: 27,
1359 quote_idx: 30,
1360 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1361 quote_vault_idx: 7,
1362 quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
1363 quote_token_program_idx: 31,
1364 },
1365 Case {
1366 signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
1367 name: "19-account USDC quote in ALT exact quote buy",
1368 static_len: 19,
1369 writable_len: 6,
1370 program_idx: 15,
1371 account_len: 19,
1372 mint_idx: 1,
1373 mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
1374 user_idx: 0,
1375 user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
1376 token_program_idx: 28,
1377 quote_idx: 33,
1378 quote_mint: usdc_mint(),
1379 quote_vault_idx: 7,
1380 quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
1381 quote_token_program_idx: 27,
1382 },
1383 Case {
1384 signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
1385 name: "20-account WSOL quote in ALT with jit account",
1386 static_len: 15,
1387 writable_len: 7,
1388 program_idx: 12,
1389 account_len: 20,
1390 mint_idx: 1,
1391 mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
1392 user_idx: 0,
1393 user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
1394 token_program_idx: 24,
1395 quote_idx: 27,
1396 quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
1397 quote_vault_idx: 7,
1398 quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
1399 quote_token_program_idx: 28,
1400 },
1401 ];
1402
1403 for case in cases {
1404 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1405 case.static_len,
1406 case.writable_len,
1407 case.program_idx,
1408 create_v2_accounts(
1409 case.account_len,
1410 case.program_idx,
1411 case.mint_idx,
1412 case.user_idx,
1413 case.token_program_idx,
1414 Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
1415 ),
1416 vec![
1417 (case.mint_idx as usize, pk(case.mint)),
1418 (case.user_idx as usize, pk(case.user)),
1419 (case.token_program_idx as usize, token_2022_program),
1420 (case.quote_idx as usize, case.quote_mint),
1421 (case.quote_vault_idx as usize, pk(case.quote_vault)),
1422 (case.quote_token_program_idx as usize, spl_token_program),
1423 ],
1424 );
1425 let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
1426 let idx = global_idx as usize;
1427 if idx < case.static_len {
1428 ("static", idx)
1429 } else if idx < case.static_len + case.writable_len {
1430 ("writable", idx - case.static_len)
1431 } else {
1432 ("readonly", idx - case.static_len - case.writable_len)
1433 }
1434 };
1435 assert_eq!(
1436 meta.loaded_writable_addresses.len(),
1437 case.writable_len,
1438 "{}: {}",
1439 case.name,
1440 case.signature
1441 );
1442 for (global_idx, expected_key) in [
1443 (case.token_program_idx, token_2022_program),
1444 (case.quote_idx, case.quote_mint),
1445 (case.quote_token_program_idx, spl_token_program),
1446 ] {
1447 match loaded_key_location(global_idx) {
1448 ("static", _) => {}
1449 ("writable", offset) => assert_eq!(
1450 read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
1451 expected_key,
1452 "{}: writable loaded key {global_idx}: {}",
1453 case.name,
1454 case.signature
1455 ),
1456 ("readonly", offset) => assert_eq!(
1457 read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
1458 expected_key,
1459 "{}: readonly loaded key {global_idx}: {}",
1460 case.name,
1461 case.signature
1462 ),
1463 _ => unreachable!(),
1464 }
1465 }
1466
1467 let create = parse_create_v2_from_grpc(&meta, &tx);
1468
1469 assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
1470 assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
1471 assert_eq!(
1472 create.token_program, token_2022_program,
1473 "{}: {}",
1474 case.name, case.signature
1475 );
1476 assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
1477 assert_eq!(
1478 create.quote_vault,
1479 pk(case.quote_vault),
1480 "{}: {}",
1481 case.name,
1482 case.signature
1483 );
1484 assert_eq!(
1485 create.quote_token_program, spl_token_program,
1486 "{}: {}",
1487 case.name, case.signature
1488 );
1489 }
1490 }
1491
1492 #[test]
1493 fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
1494 let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
1495 let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
1496 let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
1497 let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
1498 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1499 16,
1500 5,
1501 12,
1502 create_v2_accounts(16, 12, 1, 0, 24, None),
1503 vec![(1, mint), (0, user), (24, token_program)],
1504 );
1505
1506 let create = parse_create_v2_from_grpc(&meta, &tx);
1507
1508 assert_eq!(create.mint, mint, "{signature}");
1509 assert_eq!(create.user, user, "{signature}");
1510 assert_eq!(create.token_program, token_program, "{signature}");
1511 assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
1512 assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
1513 assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
1514 }
1515
1516 #[test]
1517 fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
1518 let quote_vault = Pubkey::new_unique();
1519 let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
1520 let (meta, tx) = grpc_pumpfun_create_v2_tx(
1521 19,
1522 6,
1523 16,
1524 create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
1525 vec![
1526 (27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
1527 (30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
1528 (6, quote_vault),
1529 (31, quote_token_program),
1530 ],
1531 );
1532
1533 let create = parse_create_v2_from_grpc(&meta, &tx);
1534
1535 assert_eq!(create.quote_mint, Pubkey::default());
1536 assert_eq!(create.quote_vault, Pubkey::default());
1537 assert_eq!(create.quote_token_program, Pubkey::default());
1538 }
1539}