1use super::program_ids;
6use super::utils::*;
7use crate::core::events::*;
8use solana_sdk::{pubkey::Pubkey, signature::Signature};
9
10pub mod discriminators {
12 pub const BUY: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
14 pub const SELL: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
16 pub const CREATE: [u8; 8] = [24, 30, 200, 40, 5, 28, 7, 119];
18 pub const CREATE_V2: [u8; 8] = [214, 144, 76, 236, 95, 139, 49, 180];
20 pub const BUY_EXACT_SOL_IN: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
22 pub const MIGRATE_EVENT_LOG: [u8; 8] = [189, 233, 93, 185, 92, 148, 234, 148];
24 pub const MIGRATE_BONDING_CURVE_CREATOR: [u8; 8] = [87, 124, 52, 191, 52, 38, 214, 232];
26 pub const BUY_V2: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
28 pub const SELL_V2: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
30 pub const BUY_EXACT_QUOTE_IN_V2: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
32}
33
34pub const PROGRAM_ID_PUBKEY: Pubkey = program_ids::PUMPFUN_PROGRAM_ID;
36
37#[inline(always)]
38fn create_v2_quote_accounts_from_accounts(accounts: &[Pubkey]) -> (Pubkey, Pubkey, Pubkey) {
39 if accounts.len() < 19 {
40 return (PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, Pubkey::default(), Pubkey::default());
41 }
42 let quote_mint = get_account(accounts, 16).unwrap_or_default();
43 let quote_vault = get_account(accounts, 17).unwrap_or_default();
44 let quote_token_program = get_account(accounts, 18).unwrap_or_default();
45 if quote_mint == Pubkey::default()
46 || quote_mint == program_ids::PUMPFUN_PROGRAM_ID
47 || quote_vault == Pubkey::default()
48 || quote_token_program == Pubkey::default()
49 {
50 return (Pubkey::default(), Pubkey::default(), Pubkey::default());
51 }
52 (normalize_pumpfun_quote_mint(quote_mint), quote_vault, quote_token_program)
53}
54
55pub fn parse_instruction(
60 instruction_data: &[u8],
61 accounts: &[Pubkey],
62 signature: Signature,
63 slot: u64,
64 tx_index: u64,
65 block_time_us: Option<i64>,
66 grpc_recv_us: i64,
67) -> Option<DexEvent> {
68 if instruction_data.len() < 8 {
69 return None;
70 }
71 let outer_disc: [u8; 8] = instruction_data[0..8].try_into().ok()?;
72 let data = &instruction_data[8..];
73
74 if outer_disc == discriminators::CREATE_V2 {
76 return parse_create_v2_instruction(
77 data,
78 accounts,
79 signature,
80 slot,
81 tx_index,
82 block_time_us,
83 grpc_recv_us,
84 );
85 }
86 if outer_disc == discriminators::CREATE {
87 return parse_create_instruction(
88 data,
89 accounts,
90 signature,
91 slot,
92 tx_index,
93 block_time_us,
94 grpc_recv_us,
95 );
96 }
97 if outer_disc == discriminators::BUY {
98 return parse_buy_instruction(
99 data,
100 accounts,
101 signature,
102 slot,
103 tx_index,
104 block_time_us,
105 grpc_recv_us,
106 "buy",
107 false,
108 );
109 }
110 if outer_disc == discriminators::BUY_EXACT_SOL_IN {
111 return parse_buy_instruction(
112 data,
113 accounts,
114 signature,
115 slot,
116 tx_index,
117 block_time_us,
118 grpc_recv_us,
119 "buy_exact_sol_in",
120 true,
121 );
122 }
123 if outer_disc == discriminators::SELL {
124 return parse_sell_instruction(
125 data,
126 accounts,
127 signature,
128 slot,
129 tx_index,
130 block_time_us,
131 grpc_recv_us,
132 "sell",
133 false,
134 );
135 }
136 if outer_disc == discriminators::BUY_V2 {
137 return parse_buy_v2_instruction(
138 data,
139 accounts,
140 signature,
141 slot,
142 tx_index,
143 block_time_us,
144 grpc_recv_us,
145 "buy_v2",
146 false,
147 );
148 }
149 if outer_disc == discriminators::BUY_EXACT_QUOTE_IN_V2 {
150 return parse_buy_v2_instruction(
151 data,
152 accounts,
153 signature,
154 slot,
155 tx_index,
156 block_time_us,
157 grpc_recv_us,
158 "buy_exact_quote_in_v2",
159 true,
160 );
161 }
162 if outer_disc == discriminators::SELL_V2 {
163 return parse_sell_v2_instruction(
164 data,
165 accounts,
166 signature,
167 slot,
168 tx_index,
169 block_time_us,
170 grpc_recv_us,
171 "sell_v2",
172 );
173 }
174
175 if instruction_data.len() >= 16 {
177 let cpi_disc: [u8; 8] = instruction_data[8..16].try_into().ok()?;
178 if cpi_disc == discriminators::MIGRATE_EVENT_LOG {
179 return parse_migrate_log_instruction(
180 &instruction_data[16..],
181 accounts,
182 signature,
183 slot,
184 tx_index,
185 block_time_us,
186 grpc_recv_us,
187 );
188 }
189 }
190 None
191}
192
193fn parse_buy_instruction(
203 data: &[u8],
204 accounts: &[Pubkey],
205 signature: Signature,
206 slot: u64,
207 tx_index: u64,
208 block_time_us: Option<i64>,
209 grpc_recv_us: i64,
210 ix_name: &'static str,
211 exact_quote_in: bool,
212) -> Option<DexEvent> {
213 const LEGACY_BUY_ACCOUNTS: usize = 16;
214 if accounts.len() < LEGACY_BUY_ACCOUNTS {
215 return None;
216 }
217
218 let (first_arg, second_arg) = if data.len() >= 16 {
220 (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
221 } else {
222 (0, 0)
223 };
224 let track_volume = data.get(16).copied().map(|b| b != 0).unwrap_or(false);
225 let (
226 token_amount,
227 sol_amount,
228 amount,
229 max_sol_cost,
230 spendable_sol_in,
231 spendable_quote_in,
232 min_tokens_out,
233 ) = if exact_quote_in {
234 (second_arg, first_arg, second_arg, first_arg, first_arg, 0, second_arg)
235 } else {
236 (first_arg, second_arg, first_arg, second_arg, 0, 0, 0)
237 };
238 let bonding_curve_v2 = get_account(accounts, 16).unwrap_or_default();
239 let buyback_fee_recipient = get_account(accounts, 17).unwrap_or_default();
240 let account =
241 if buyback_fee_recipient != Pubkey::default() { Some(buyback_fee_recipient) } else { None };
242 let fee_program = get_account(accounts, 15).unwrap_or_default();
243 let mint = get_account(accounts, 2)?;
244 let metadata =
245 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
246
247 let trade_event = PumpFunTradeEvent {
248 metadata,
249 mint,
250 quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
251 is_buy: true,
252 global: get_account(accounts, 0).unwrap_or_default(),
253 fee_recipient: get_account(accounts, 1).unwrap_or_default(),
254 bonding_curve: get_account(accounts, 3).unwrap_or_default(),
255 bonding_curve_v2,
256 associated_bonding_curve: get_account(accounts, 4).unwrap_or_default(),
257 associated_user: get_account(accounts, 5).unwrap_or_default(),
258 user: get_account(accounts, 6).unwrap_or_default(),
259 system_program: get_account(accounts, 7).unwrap_or_default(),
260 token_program: get_account(accounts, 8).unwrap_or_default(),
261 creator_vault: get_account(accounts, 9).unwrap_or_default(),
262 event_authority: get_account(accounts, 10).unwrap_or_default(),
263 program: get_account(accounts, 11).unwrap_or_default(),
264 global_volume_accumulator: get_account(accounts, 12).unwrap_or_default(),
265 user_volume_accumulator: get_account(accounts, 13).unwrap_or_default(),
266 fee_config: get_account(accounts, 14).unwrap_or_default(),
267 fee_program,
268 buyback_fee_recipient,
269 account,
270 sol_amount,
271 token_amount,
272 amount,
273 max_sol_cost,
274 spendable_sol_in,
275 spendable_quote_in,
276 min_tokens_out,
277 track_volume,
278 ix_name: ix_name.to_string(),
279 ..Default::default()
280 };
281
282 if exact_quote_in {
283 Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
284 } else {
285 Some(DexEvent::PumpFunBuy(trade_event))
286 }
287}
288
289fn parse_sell_instruction(
299 data: &[u8],
300 accounts: &[Pubkey],
301 signature: Signature,
302 slot: u64,
303 tx_index: u64,
304 block_time_us: Option<i64>,
305 grpc_recv_us: i64,
306 ix_name: &'static str,
307 v2_accounts: bool,
308) -> Option<DexEvent> {
309 let min_accounts = if v2_accounts { 26 } else { 14 };
310 if accounts.len() < min_accounts {
311 return None;
312 }
313
314 let (amount, min_sol_output) = if data.len() >= 16 {
316 (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
317 } else {
318 (0, 0)
319 };
320 let token_amount = amount;
321 let sol_amount = min_sol_output;
322
323 let (
324 global_idx,
325 mint_idx,
326 bonding_curve_idx,
327 associated_bonding_curve_idx,
328 associated_user_idx,
329 user_idx,
330 system_program_idx,
331 fee_recipient_idx,
332 token_program_idx,
333 creator_vault_idx,
334 event_authority_idx,
335 program_idx,
336 user_volume_accumulator_idx,
337 fee_config_idx,
338 fee_program_idx,
339 ) = if v2_accounts {
340 (0, 1, 10, 11, 14, 13, 23, 6, 3, 16, 24, 25, 19, 21, 22)
341 } else {
342 (0, 2, 3, 4, 5, 6, 7, 1, 9, 8, 10, 11, usize::MAX, 12, 13)
343 };
344 let mint = get_account(accounts, mint_idx)?;
345 let (legacy_user_volume_accumulator, legacy_bonding_curve_v2, legacy_buyback_fee_recipient) =
346 if v2_accounts {
347 (Pubkey::default(), Pubkey::default(), Pubkey::default())
348 } else if accounts.len() >= 17 {
349 (
350 get_account(accounts, 14).unwrap_or_default(),
351 get_account(accounts, 15).unwrap_or_default(),
352 get_account(accounts, 16).unwrap_or_default(),
353 )
354 } else if accounts.len() >= 16 {
355 (
356 Pubkey::default(),
357 get_account(accounts, 14).unwrap_or_default(),
358 get_account(accounts, 15).unwrap_or_default(),
359 )
360 } else {
361 (Pubkey::default(), get_account(accounts, 14).unwrap_or_default(), Pubkey::default())
362 };
363 let account = if legacy_buyback_fee_recipient != Pubkey::default() {
364 Some(legacy_buyback_fee_recipient)
365 } else {
366 None
367 };
368 let metadata =
369 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
370
371 Some(DexEvent::PumpFunSell(PumpFunTradeEvent {
372 metadata,
373 mint,
374 quote_mint: normalize_pumpfun_quote_mint(if v2_accounts {
375 get_account(accounts, 2).unwrap_or_default()
376 } else {
377 Pubkey::default()
378 }),
379 is_buy: false,
380 global: get_account(accounts, global_idx).unwrap_or_default(),
381 bonding_curve: get_account(accounts, bonding_curve_idx).unwrap_or_default(),
382 bonding_curve_v2: legacy_bonding_curve_v2,
383 associated_bonding_curve: get_account(accounts, associated_bonding_curve_idx)
384 .unwrap_or_default(),
385 associated_user: get_account(accounts, associated_user_idx).unwrap_or_default(),
386 user: get_account(accounts, user_idx).unwrap_or_default(),
387 system_program: get_account(accounts, system_program_idx).unwrap_or_default(),
388 fee_recipient: get_account(accounts, fee_recipient_idx).unwrap_or_default(),
389 token_program: get_account(accounts, token_program_idx).unwrap_or_default(),
390 quote_token_program: if v2_accounts {
391 get_account(accounts, 4).unwrap_or_default()
392 } else {
393 Pubkey::default()
394 },
395 associated_token_program: if v2_accounts {
396 get_account(accounts, 5).unwrap_or_default()
397 } else {
398 Pubkey::default()
399 },
400 creator_vault: get_account(accounts, creator_vault_idx).unwrap_or_default(),
401 associated_quote_fee_recipient: if v2_accounts {
402 get_account(accounts, 7).unwrap_or_default()
403 } else {
404 Pubkey::default()
405 },
406 associated_quote_buyback_fee_recipient: if v2_accounts {
407 get_account(accounts, 9).unwrap_or_default()
408 } else {
409 Pubkey::default()
410 },
411 associated_quote_bonding_curve: if v2_accounts {
412 get_account(accounts, 12).unwrap_or_default()
413 } else {
414 Pubkey::default()
415 },
416 associated_quote_user: if v2_accounts {
417 get_account(accounts, 15).unwrap_or_default()
418 } else {
419 Pubkey::default()
420 },
421 associated_creator_vault: if v2_accounts {
422 get_account(accounts, 17).unwrap_or_default()
423 } else {
424 Pubkey::default()
425 },
426 sharing_config: if v2_accounts {
427 get_account(accounts, 18).unwrap_or_default()
428 } else {
429 Pubkey::default()
430 },
431 event_authority: get_account(accounts, event_authority_idx).unwrap_or_default(),
432 program: get_account(accounts, program_idx).unwrap_or_default(),
433 user_volume_accumulator: if v2_accounts {
434 get_account(accounts, user_volume_accumulator_idx).unwrap_or_default()
435 } else {
436 legacy_user_volume_accumulator
437 },
438 associated_user_volume_accumulator: if v2_accounts {
439 get_account(accounts, 20).unwrap_or_default()
440 } else {
441 Pubkey::default()
442 },
443 fee_config: get_account(accounts, fee_config_idx).unwrap_or_default(),
444 fee_program: get_account(accounts, fee_program_idx).unwrap_or_default(),
445 buyback_fee_recipient: if v2_accounts {
446 get_account(accounts, 8).unwrap_or_default()
447 } else {
448 legacy_buyback_fee_recipient
449 },
450 account,
451 sol_amount,
452 token_amount,
453 amount,
454 min_sol_output,
455 ix_name: ix_name.to_string(),
456 ..Default::default()
457 }))
458}
459
460fn parse_buy_v2_instruction(
461 data: &[u8],
462 accounts: &[Pubkey],
463 signature: Signature,
464 slot: u64,
465 tx_index: u64,
466 block_time_us: Option<i64>,
467 grpc_recv_us: i64,
468 ix_name: &'static str,
469 exact_quote_in: bool,
470) -> Option<DexEvent> {
471 const MIN_ACC: usize = 27;
472 if accounts.len() < MIN_ACC {
473 return None;
474 }
475
476 let (first_arg, second_arg) = if data.len() >= 16 {
478 (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
479 } else {
480 (0, 0)
481 };
482 let (
483 token_amount,
484 sol_amount,
485 amount,
486 max_sol_cost,
487 quote_amount,
488 spendable_quote_in,
489 min_tokens_out,
490 ) = if exact_quote_in {
491 (second_arg, first_arg, second_arg, 0, first_arg, first_arg, second_arg)
492 } else {
493 (first_arg, second_arg, first_arg, second_arg, 0, 0, 0)
494 };
495
496 let metadata =
497 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
498 let trade_event = PumpFunTradeEvent {
499 metadata,
500 mint: accounts[1],
501 quote_mint: normalize_pumpfun_quote_mint(accounts[2]),
502 is_buy: true,
503 global: accounts[0],
504 bonding_curve: accounts[10],
505 associated_bonding_curve: accounts[11],
506 associated_user: accounts[14],
507 user: accounts[13],
508 system_program: accounts[24],
509 quote_token_program: accounts[4],
510 associated_token_program: accounts[5],
511 sol_amount,
512 token_amount,
513 amount,
514 max_sol_cost,
515 quote_amount,
516 spendable_sol_in: 0,
517 spendable_quote_in,
518 min_tokens_out,
519 fee_recipient: accounts[6],
520 token_program: accounts[3],
521 creator_vault: accounts[16],
522 associated_quote_fee_recipient: accounts[7],
523 buyback_fee_recipient: accounts[8],
524 associated_quote_buyback_fee_recipient: accounts[9],
525 associated_quote_bonding_curve: accounts[12],
526 associated_quote_user: accounts[15],
527 associated_creator_vault: accounts[17],
528 sharing_config: accounts[18],
529 event_authority: accounts[25],
530 program: accounts[26],
531 global_volume_accumulator: accounts[19],
532 user_volume_accumulator: accounts[20],
533 associated_user_volume_accumulator: accounts[21],
534 fee_config: accounts[22],
535 fee_program: accounts[23],
536 ix_name: ix_name.to_string(),
537 ..Default::default()
538 };
539
540 Some(DexEvent::PumpFunBuy(trade_event))
541}
542
543fn parse_sell_v2_instruction(
544 data: &[u8],
545 accounts: &[Pubkey],
546 signature: Signature,
547 slot: u64,
548 tx_index: u64,
549 block_time_us: Option<i64>,
550 grpc_recv_us: i64,
551 ix_name: &'static str,
552) -> Option<DexEvent> {
553 parse_sell_instruction(
554 data,
555 accounts,
556 signature,
557 slot,
558 tx_index,
559 block_time_us,
560 grpc_recv_us,
561 ix_name,
562 true,
563 )
564}
565
566fn parse_create_instruction(
572 data: &[u8],
573 accounts: &[Pubkey],
574 signature: Signature,
575 slot: u64,
576 tx_index: u64,
577 block_time_us: Option<i64>,
578 grpc_recv_us: i64,
579) -> Option<DexEvent> {
580 if accounts.len() < 8 {
581 return None;
582 }
583
584 let mut offset = 0;
585
586 let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
589 offset += len;
590 s.to_string()
591 } else {
592 String::new()
593 };
594
595 let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
596 offset += len;
597 s.to_string()
598 } else {
599 String::new()
600 };
601
602 let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
603 offset += len;
604 s.to_string()
605 } else {
606 String::new()
607 };
608
609 if data.len() < offset + 32 + 32 + 32 + 32 {
611 return None;
612 }
613
614 let mint = read_pubkey(data, offset).unwrap_or_default();
615 offset += 32;
616
617 let bonding_curve = read_pubkey(data, offset).unwrap_or_default();
618 offset += 32;
619
620 let user = read_pubkey(data, offset).unwrap_or_default();
621 offset += 32;
622
623 let creator = read_pubkey(data, offset).unwrap_or_default();
624
625 let metadata =
626 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
627
628 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
629 metadata,
630 name,
631 symbol,
632 uri,
633 mint,
634 bonding_curve,
635 user,
636 creator,
637 quote_mint: PUMPFUN_SOLSCAN_SOL_QUOTE_MINT,
638 ix_name: "create".to_string(),
639 ..Default::default()
640 }))
641}
642
643fn parse_create_v2_instruction(
653 data: &[u8],
654 accounts: &[Pubkey],
655 signature: Signature,
656 slot: u64,
657 tx_index: u64,
658 block_time_us: Option<i64>,
659 grpc_recv_us: i64,
660) -> Option<DexEvent> {
661 const CREATE_V2_MIN_ACCOUNTS: usize = 16;
662 if accounts.len() < CREATE_V2_MIN_ACCOUNTS {
663 return None;
664 }
665 let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
666
667 let mut offset = 0usize;
669 let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
670 offset += len;
671 s.to_string()
672 } else {
673 String::new()
674 };
675 let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
676 offset += len;
677 s.to_string()
678 } else {
679 String::new()
680 };
681 let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
682 offset += len;
683 s.to_string()
684 } else {
685 String::new()
686 };
687 if data.len() < offset + 32 + 1 {
688 return None;
689 }
690 let creator = read_pubkey(data, offset)?;
691 offset += 32;
692 let is_mayhem_mode = read_bool(data, offset)?;
693 offset += 1;
694 let is_cashback_enabled = read_option_bool_idl(data, offset).unwrap_or(false);
695 if offset < data.len() {
696 offset += 1;
697 }
698 let creator_fee_bps = read_option_u64_idl(data, offset).unwrap_or_default();
699 if offset + 8 <= data.len() {
700 offset += 8;
701 }
702 let is_holder_reward = read_option_bool_idl(data, offset).unwrap_or_default();
703
704 let mint = acc[0];
705 let bonding_curve = acc[2];
706 let user = acc[5];
707 let (quote_mint, quote_vault, quote_token_program) =
708 create_v2_quote_accounts_from_accounts(accounts);
709
710 let metadata =
711 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
712
713 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
714 metadata,
715 name,
716 symbol,
717 uri,
718 mint,
719 bonding_curve,
720 user,
721 creator,
722 mint_authority: acc[1],
723 associated_bonding_curve: acc[3],
724 global: acc[4],
725 system_program: acc[6],
726 token_program: acc[7],
727 associated_token_program: acc[8],
728 mayhem_program_id: acc[9],
729 global_params: acc[10],
730 sol_vault: acc[11],
731 mayhem_state: acc[12],
732 mayhem_token_vault: acc[13],
733 event_authority: acc[14],
734 program: acc[15],
735 is_mayhem_mode,
736 is_cashback_enabled,
737 creator_fee_bps,
738 is_holder_reward,
739 quote_mint,
740 quote_vault,
741 quote_token_program,
742 ix_name: "create_v2".to_string(),
743 ..Default::default()
744 }))
745}
746
747#[allow(unused_variables)]
749fn parse_migrate_log_instruction(
750 data: &[u8],
751 accounts: &[Pubkey],
752 signature: Signature,
753 slot: u64,
754 tx_index: u64,
755 block_time_us: Option<i64>,
756 rpc_recv_us: i64,
757) -> Option<DexEvent> {
758 let mut offset = 0;
759
760 let user = read_pubkey(data, offset)?;
762 offset += 32;
763
764 let mint = read_pubkey(data, offset)?;
766 offset += 32;
767
768 let mint_amount = read_u64_le(data, offset)?;
770 offset += 8;
771
772 let sol_amount = read_u64_le(data, offset)?;
774 offset += 8;
775
776 let pool_migration_fee = read_u64_le(data, offset)?;
778 offset += 8;
779
780 let bonding_curve = read_pubkey(data, offset)?;
782 offset += 32;
783
784 let timestamp = read_u64_le(data, offset)? as i64;
786 offset += 8;
787
788 let pool = read_pubkey(data, offset)?;
790
791 let metadata =
792 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
793
794 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
795 metadata,
796 user,
797 mint,
798 mint_amount,
799 sol_amount,
800 pool_migration_fee,
801 bonding_curve,
802 timestamp,
803 pool,
804 }))
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 fn instruction_data(discriminator: [u8; 8], first: u64, second: u64) -> Vec<u8> {
812 let mut data = Vec::with_capacity(24);
813 data.extend_from_slice(&discriminator);
814 data.extend_from_slice(&first.to_le_bytes());
815 data.extend_from_slice(&second.to_le_bytes());
816 data
817 }
818
819 fn accounts(n: usize) -> Vec<Pubkey> {
820 (0..n).map(|_| Pubkey::new_unique()).collect()
821 }
822
823 fn str_arg(s: &str, out: &mut Vec<u8>) {
824 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
825 out.extend_from_slice(s.as_bytes());
826 }
827
828 fn create_v2_data() -> Vec<u8> {
829 let mut data = Vec::new();
830 data.extend_from_slice(&discriminators::CREATE_V2);
831 str_arg("Token", &mut data);
832 str_arg("TOK", &mut data);
833 str_arg("https://example.invalid/token.json", &mut data);
834 data.extend_from_slice(Pubkey::new_unique().as_ref());
835 data.push(1);
836 data.push(1);
837 data
838 }
839
840 #[test]
841 fn pumpfun_create_v2_instruction_emits_canonical_create() {
842 let acc = accounts(16);
843 let event =
844 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
845 .expect("event");
846
847 match event {
848 DexEvent::PumpFunCreate(c) => {
849 assert_eq!(c.name, "Token");
850 assert_eq!(c.symbol, "TOK");
851 assert_eq!(c.mint, acc[0]);
852 assert_eq!(c.mint_authority, acc[1]);
853 assert_eq!(c.bonding_curve, acc[2]);
854 assert_eq!(c.associated_bonding_curve, acc[3]);
855 assert_eq!(c.user, acc[5]);
856 assert_eq!(c.token_program, acc[7]);
857 assert_eq!(c.mayhem_program_id, acc[9]);
858 assert_eq!(c.program, acc[15]);
859 assert_eq!(c.ix_name, "create_v2");
860 assert!(c.is_mayhem_mode);
861 assert!(c.is_cashback_enabled);
862 assert_eq!(c.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
863 }
864 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
865 }
866 }
867
868 #[test]
869 fn pumpfun_create_v2_instruction_uses_appended_quote_mint_only_for_19_accounts() {
870 let mut acc = accounts(19);
871 acc[16] = PUMPFUN_WSOL_QUOTE_MINT;
872 let event =
873 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
874 .expect("event");
875
876 match event {
877 DexEvent::PumpFunCreate(c) => {
878 assert_eq!(c.quote_mint, PUMPFUN_WSOL_QUOTE_MINT);
879 }
880 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
881 }
882 }
883
884 #[test]
885 fn pumpfun_create_v2_instruction_does_not_emit_partial_quote_tail() {
886 let mut acc = accounts(19);
887 acc[16] = PUMPFUN_WSOL_QUOTE_MINT;
888 acc[17] = Pubkey::default();
889 let event =
890 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
891 .expect("event");
892
893 match event {
894 DexEvent::PumpFunCreate(c) => {
895 assert_eq!(c.quote_mint, Pubkey::default());
896 assert_eq!(c.quote_vault, Pubkey::default());
897 assert_eq!(c.quote_token_program, Pubkey::default());
898 }
899 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
900 }
901 }
902
903 #[test]
904 fn pumpfun_create_v2_instruction_rejects_program_id_as_quote_mint() {
905 let mut acc = accounts(19);
906 acc[16] = program_ids::PUMPFUN_PROGRAM_ID;
907 acc[17] = Pubkey::new_unique();
908 acc[18] = Pubkey::new_unique();
909 let event =
910 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
911 .expect("event");
912
913 match event {
914 DexEvent::PumpFunCreate(c) => {
915 assert_eq!(c.quote_mint, Pubkey::default());
916 assert_eq!(c.quote_vault, Pubkey::default());
917 assert_eq!(c.quote_token_program, Pubkey::default());
918 }
919 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
920 }
921 }
922
923 #[test]
924 fn pumpfun_buy_instruction_exposes_raw_args() {
925 let data = instruction_data(discriminators::BUY, 123, 456);
926 let acc = accounts(18);
927 let event =
928 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
929
930 match event {
931 DexEvent::PumpFunBuy(t) => {
932 assert_eq!(t.amount, 123);
933 assert_eq!(t.max_sol_cost, 456);
934 assert_eq!(t.min_sol_output, 0);
935 assert_eq!(t.spendable_sol_in, 0);
936 assert_eq!(t.min_tokens_out, 0);
937 assert_eq!(t.token_amount, 123);
938 assert_eq!(t.sol_amount, 456);
939 assert_eq!(t.bonding_curve_v2, acc[16]);
940 assert_eq!(t.buyback_fee_recipient, acc[17]);
941 assert_eq!(t.ix_name, "buy");
942 }
943 other => panic!("expected PumpFunBuy, got {other:?}"),
944 }
945 }
946
947 #[test]
948 fn pumpfun_legacy_trade_rejects_short_account_lists() {
949 let buy_data = instruction_data(discriminators::BUY, 123, 456);
950 assert!(parse_instruction(&buy_data, &accounts(15), Signature::default(), 1, 0, None, 99)
951 .is_none());
952
953 let sell_data = instruction_data(discriminators::SELL, 321, 654);
954 assert!(parse_instruction(&sell_data, &accounts(13), Signature::default(), 1, 0, None, 99)
955 .is_none());
956 }
957
958 #[test]
959 fn pumpfun_sell_instruction_exposes_raw_args() {
960 let data = instruction_data(discriminators::SELL, 321, 654);
961 let acc = accounts(16);
962 let event =
963 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
964
965 match event {
966 DexEvent::PumpFunSell(t) => {
967 assert_eq!(t.amount, 321);
968 assert_eq!(t.max_sol_cost, 0);
969 assert_eq!(t.min_sol_output, 654);
970 assert_eq!(t.spendable_sol_in, 0);
971 assert_eq!(t.min_tokens_out, 0);
972 assert_eq!(t.token_amount, 321);
973 assert_eq!(t.sol_amount, 654);
974 assert_eq!(t.user_volume_accumulator, Pubkey::default());
975 assert_eq!(t.bonding_curve_v2, acc[14]);
976 assert_eq!(t.buyback_fee_recipient, acc[15]);
977 assert_eq!(t.ix_name, "sell");
978 }
979 other => panic!("expected PumpFunSell, got {other:?}"),
980 }
981 }
982
983 #[test]
984 fn pumpfun_cashback_sell_uses_17_account_layout() {
985 let data = instruction_data(discriminators::SELL, 321, 654);
986 let acc = accounts(17);
987 let event =
988 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
989
990 match event {
991 DexEvent::PumpFunSell(t) => {
992 assert_eq!(t.user_volume_accumulator, acc[14]);
993 assert_eq!(t.bonding_curve_v2, acc[15]);
994 assert_eq!(t.buyback_fee_recipient, acc[16]);
995 }
996 other => panic!("expected PumpFunSell, got {other:?}"),
997 }
998 }
999
1000 #[test]
1001 fn pumpfun_buy_exact_sol_in_exposes_exact_args() {
1002 let data = instruction_data(discriminators::BUY_EXACT_SOL_IN, 1_111, 2_222);
1003 let acc = accounts(18);
1004 let event =
1005 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
1006
1007 match event {
1008 DexEvent::PumpFunBuyExactSolIn(t) => {
1009 assert_eq!(t.spendable_sol_in, 1_111);
1010 assert_eq!(t.spendable_quote_in, 0);
1011 assert_eq!(t.min_tokens_out, 2_222);
1012 assert_eq!(t.sol_amount, 1_111);
1013 assert_eq!(t.token_amount, 2_222);
1014 assert_eq!(t.global, acc[0]);
1015 assert_eq!(t.associated_user, acc[5]);
1016 assert_eq!(t.event_authority, acc[10]);
1017 assert_eq!(t.fee_program, acc[15]);
1018 assert_eq!(t.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
1019 assert_eq!(t.bonding_curve_v2, acc[16]);
1020 assert_eq!(t.buyback_fee_recipient, acc[17]);
1021 assert_eq!(t.ix_name, "buy_exact_sol_in");
1022 }
1023 other => panic!("expected PumpFunBuyExactSolIn, got {other:?}"),
1024 }
1025 }
1026
1027 #[test]
1028 fn pumpfun_v2_instruction_args_use_v2_account_layout() {
1029 let data = instruction_data(discriminators::BUY_V2, 777, 888);
1030 let acc = accounts(27);
1031 let event =
1032 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
1033
1034 match event {
1035 DexEvent::PumpFunBuy(t) => {
1036 assert_eq!(t.amount, 777);
1037 assert_eq!(t.max_sol_cost, 888);
1038 assert_eq!(t.mint, acc[1]);
1039 assert_eq!(t.quote_mint, acc[2]);
1040 assert_eq!(t.bonding_curve, acc[10]);
1041 assert_eq!(t.associated_bonding_curve, acc[11]);
1042 assert_eq!(t.associated_quote_bonding_curve, acc[12]);
1043 assert_eq!(t.user, acc[13]);
1044 assert_eq!(t.associated_quote_user, acc[15]);
1045 assert_eq!(t.quote_token_program, acc[4]);
1046 assert_eq!(t.associated_token_program, acc[5]);
1047 assert_eq!(t.associated_quote_fee_recipient, acc[7]);
1048 assert_eq!(t.buyback_fee_recipient, acc[8]);
1049 assert_eq!(t.associated_quote_buyback_fee_recipient, acc[9]);
1050 assert_eq!(t.associated_creator_vault, acc[17]);
1051 assert_eq!(t.sharing_config, acc[18]);
1052 assert_eq!(t.global_volume_accumulator, acc[19]);
1053 assert_eq!(t.associated_user_volume_accumulator, acc[21]);
1054 assert_eq!(t.ix_name, "buy_v2");
1055 }
1056 other => panic!("expected PumpFunBuy, got {other:?}"),
1057 }
1058 }
1059
1060 #[test]
1061 fn pumpfun_buy_exact_quote_in_v2_uses_quote_amount_fields() {
1062 let data = instruction_data(discriminators::BUY_EXACT_QUOTE_IN_V2, 777, 888);
1063 let acc = accounts(27);
1064 let event =
1065 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
1066
1067 match event {
1068 DexEvent::PumpFunBuy(t) => {
1069 assert_eq!(t.ix_name, "buy_exact_quote_in_v2");
1070 assert_eq!(t.amount, 888);
1071 assert_eq!(t.max_sol_cost, 0);
1072 assert_eq!(t.quote_amount, 777);
1073 assert_eq!(t.spendable_quote_in, 777);
1074 assert_eq!(t.min_tokens_out, 888);
1075 assert_eq!(t.quote_mint, acc[2]);
1076 }
1077 other => panic!("expected PumpFunBuy, got {other:?}"),
1078 }
1079 }
1080}