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
696 let mint = acc[0];
697 let bonding_curve = acc[2];
698 let user = acc[5];
699 let (quote_mint, quote_vault, quote_token_program) =
700 create_v2_quote_accounts_from_accounts(accounts);
701
702 let metadata =
703 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
704
705 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
706 metadata,
707 name,
708 symbol,
709 uri,
710 mint,
711 bonding_curve,
712 user,
713 creator,
714 mint_authority: acc[1],
715 associated_bonding_curve: acc[3],
716 global: acc[4],
717 system_program: acc[6],
718 token_program: acc[7],
719 associated_token_program: acc[8],
720 mayhem_program_id: acc[9],
721 global_params: acc[10],
722 sol_vault: acc[11],
723 mayhem_state: acc[12],
724 mayhem_token_vault: acc[13],
725 event_authority: acc[14],
726 program: acc[15],
727 is_mayhem_mode,
728 is_cashback_enabled,
729 quote_mint,
730 quote_vault,
731 quote_token_program,
732 ix_name: "create_v2".to_string(),
733 ..Default::default()
734 }))
735}
736
737#[allow(unused_variables)]
739fn parse_migrate_log_instruction(
740 data: &[u8],
741 accounts: &[Pubkey],
742 signature: Signature,
743 slot: u64,
744 tx_index: u64,
745 block_time_us: Option<i64>,
746 rpc_recv_us: i64,
747) -> Option<DexEvent> {
748 let mut offset = 0;
749
750 let user = read_pubkey(data, offset)?;
752 offset += 32;
753
754 let mint = read_pubkey(data, offset)?;
756 offset += 32;
757
758 let mint_amount = read_u64_le(data, offset)?;
760 offset += 8;
761
762 let sol_amount = read_u64_le(data, offset)?;
764 offset += 8;
765
766 let pool_migration_fee = read_u64_le(data, offset)?;
768 offset += 8;
769
770 let bonding_curve = read_pubkey(data, offset)?;
772 offset += 32;
773
774 let timestamp = read_u64_le(data, offset)? as i64;
776 offset += 8;
777
778 let pool = read_pubkey(data, offset)?;
780
781 let metadata =
782 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
783
784 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
785 metadata,
786 user,
787 mint,
788 mint_amount,
789 sol_amount,
790 pool_migration_fee,
791 bonding_curve,
792 timestamp,
793 pool,
794 }))
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800
801 fn instruction_data(discriminator: [u8; 8], first: u64, second: u64) -> Vec<u8> {
802 let mut data = Vec::with_capacity(24);
803 data.extend_from_slice(&discriminator);
804 data.extend_from_slice(&first.to_le_bytes());
805 data.extend_from_slice(&second.to_le_bytes());
806 data
807 }
808
809 fn accounts(n: usize) -> Vec<Pubkey> {
810 (0..n).map(|_| Pubkey::new_unique()).collect()
811 }
812
813 fn str_arg(s: &str, out: &mut Vec<u8>) {
814 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
815 out.extend_from_slice(s.as_bytes());
816 }
817
818 fn create_v2_data() -> Vec<u8> {
819 let mut data = Vec::new();
820 data.extend_from_slice(&discriminators::CREATE_V2);
821 str_arg("Token", &mut data);
822 str_arg("TOK", &mut data);
823 str_arg("https://example.invalid/token.json", &mut data);
824 data.extend_from_slice(Pubkey::new_unique().as_ref());
825 data.push(1);
826 data.push(1);
827 data
828 }
829
830 #[test]
831 fn pumpfun_create_v2_instruction_emits_canonical_create() {
832 let acc = accounts(16);
833 let event =
834 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
835 .expect("event");
836
837 match event {
838 DexEvent::PumpFunCreate(c) => {
839 assert_eq!(c.name, "Token");
840 assert_eq!(c.symbol, "TOK");
841 assert_eq!(c.mint, acc[0]);
842 assert_eq!(c.mint_authority, acc[1]);
843 assert_eq!(c.bonding_curve, acc[2]);
844 assert_eq!(c.associated_bonding_curve, acc[3]);
845 assert_eq!(c.user, acc[5]);
846 assert_eq!(c.token_program, acc[7]);
847 assert_eq!(c.mayhem_program_id, acc[9]);
848 assert_eq!(c.program, acc[15]);
849 assert_eq!(c.ix_name, "create_v2");
850 assert!(c.is_mayhem_mode);
851 assert!(c.is_cashback_enabled);
852 assert_eq!(c.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
853 }
854 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
855 }
856 }
857
858 #[test]
859 fn pumpfun_create_v2_instruction_uses_appended_quote_mint_only_for_19_accounts() {
860 let mut acc = accounts(19);
861 acc[16] = PUMPFUN_WSOL_QUOTE_MINT;
862 let event =
863 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
864 .expect("event");
865
866 match event {
867 DexEvent::PumpFunCreate(c) => {
868 assert_eq!(c.quote_mint, PUMPFUN_WSOL_QUOTE_MINT);
869 }
870 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
871 }
872 }
873
874 #[test]
875 fn pumpfun_create_v2_instruction_does_not_emit_partial_quote_tail() {
876 let mut acc = accounts(19);
877 acc[16] = PUMPFUN_WSOL_QUOTE_MINT;
878 acc[17] = Pubkey::default();
879 let event =
880 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
881 .expect("event");
882
883 match event {
884 DexEvent::PumpFunCreate(c) => {
885 assert_eq!(c.quote_mint, Pubkey::default());
886 assert_eq!(c.quote_vault, Pubkey::default());
887 assert_eq!(c.quote_token_program, Pubkey::default());
888 }
889 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
890 }
891 }
892
893 #[test]
894 fn pumpfun_create_v2_instruction_rejects_program_id_as_quote_mint() {
895 let mut acc = accounts(19);
896 acc[16] = program_ids::PUMPFUN_PROGRAM_ID;
897 acc[17] = Pubkey::new_unique();
898 acc[18] = Pubkey::new_unique();
899 let event =
900 parse_instruction(&create_v2_data(), &acc, Signature::default(), 1, 0, None, 99)
901 .expect("event");
902
903 match event {
904 DexEvent::PumpFunCreate(c) => {
905 assert_eq!(c.quote_mint, Pubkey::default());
906 assert_eq!(c.quote_vault, Pubkey::default());
907 assert_eq!(c.quote_token_program, Pubkey::default());
908 }
909 other => panic!("expected canonical PumpFunCreate, got {other:?}"),
910 }
911 }
912
913 #[test]
914 fn pumpfun_buy_instruction_exposes_raw_args() {
915 let data = instruction_data(discriminators::BUY, 123, 456);
916 let acc = accounts(18);
917 let event =
918 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
919
920 match event {
921 DexEvent::PumpFunBuy(t) => {
922 assert_eq!(t.amount, 123);
923 assert_eq!(t.max_sol_cost, 456);
924 assert_eq!(t.min_sol_output, 0);
925 assert_eq!(t.spendable_sol_in, 0);
926 assert_eq!(t.min_tokens_out, 0);
927 assert_eq!(t.token_amount, 123);
928 assert_eq!(t.sol_amount, 456);
929 assert_eq!(t.bonding_curve_v2, acc[16]);
930 assert_eq!(t.buyback_fee_recipient, acc[17]);
931 assert_eq!(t.ix_name, "buy");
932 }
933 other => panic!("expected PumpFunBuy, got {other:?}"),
934 }
935 }
936
937 #[test]
938 fn pumpfun_legacy_trade_rejects_short_account_lists() {
939 let buy_data = instruction_data(discriminators::BUY, 123, 456);
940 assert!(parse_instruction(&buy_data, &accounts(15), Signature::default(), 1, 0, None, 99)
941 .is_none());
942
943 let sell_data = instruction_data(discriminators::SELL, 321, 654);
944 assert!(parse_instruction(&sell_data, &accounts(13), Signature::default(), 1, 0, None, 99)
945 .is_none());
946 }
947
948 #[test]
949 fn pumpfun_sell_instruction_exposes_raw_args() {
950 let data = instruction_data(discriminators::SELL, 321, 654);
951 let acc = accounts(16);
952 let event =
953 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
954
955 match event {
956 DexEvent::PumpFunSell(t) => {
957 assert_eq!(t.amount, 321);
958 assert_eq!(t.max_sol_cost, 0);
959 assert_eq!(t.min_sol_output, 654);
960 assert_eq!(t.spendable_sol_in, 0);
961 assert_eq!(t.min_tokens_out, 0);
962 assert_eq!(t.token_amount, 321);
963 assert_eq!(t.sol_amount, 654);
964 assert_eq!(t.user_volume_accumulator, Pubkey::default());
965 assert_eq!(t.bonding_curve_v2, acc[14]);
966 assert_eq!(t.buyback_fee_recipient, acc[15]);
967 assert_eq!(t.ix_name, "sell");
968 }
969 other => panic!("expected PumpFunSell, got {other:?}"),
970 }
971 }
972
973 #[test]
974 fn pumpfun_cashback_sell_uses_17_account_layout() {
975 let data = instruction_data(discriminators::SELL, 321, 654);
976 let acc = accounts(17);
977 let event =
978 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
979
980 match event {
981 DexEvent::PumpFunSell(t) => {
982 assert_eq!(t.user_volume_accumulator, acc[14]);
983 assert_eq!(t.bonding_curve_v2, acc[15]);
984 assert_eq!(t.buyback_fee_recipient, acc[16]);
985 }
986 other => panic!("expected PumpFunSell, got {other:?}"),
987 }
988 }
989
990 #[test]
991 fn pumpfun_buy_exact_sol_in_exposes_exact_args() {
992 let data = instruction_data(discriminators::BUY_EXACT_SOL_IN, 1_111, 2_222);
993 let acc = accounts(18);
994 let event =
995 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
996
997 match event {
998 DexEvent::PumpFunBuyExactSolIn(t) => {
999 assert_eq!(t.spendable_sol_in, 1_111);
1000 assert_eq!(t.spendable_quote_in, 0);
1001 assert_eq!(t.min_tokens_out, 2_222);
1002 assert_eq!(t.sol_amount, 1_111);
1003 assert_eq!(t.token_amount, 2_222);
1004 assert_eq!(t.global, acc[0]);
1005 assert_eq!(t.associated_user, acc[5]);
1006 assert_eq!(t.event_authority, acc[10]);
1007 assert_eq!(t.fee_program, acc[15]);
1008 assert_eq!(t.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
1009 assert_eq!(t.bonding_curve_v2, acc[16]);
1010 assert_eq!(t.buyback_fee_recipient, acc[17]);
1011 assert_eq!(t.ix_name, "buy_exact_sol_in");
1012 }
1013 other => panic!("expected PumpFunBuyExactSolIn, got {other:?}"),
1014 }
1015 }
1016
1017 #[test]
1018 fn pumpfun_v2_instruction_args_use_v2_account_layout() {
1019 let data = instruction_data(discriminators::BUY_V2, 777, 888);
1020 let acc = accounts(27);
1021 let event =
1022 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
1023
1024 match event {
1025 DexEvent::PumpFunBuy(t) => {
1026 assert_eq!(t.amount, 777);
1027 assert_eq!(t.max_sol_cost, 888);
1028 assert_eq!(t.mint, acc[1]);
1029 assert_eq!(t.quote_mint, acc[2]);
1030 assert_eq!(t.bonding_curve, acc[10]);
1031 assert_eq!(t.associated_bonding_curve, acc[11]);
1032 assert_eq!(t.associated_quote_bonding_curve, acc[12]);
1033 assert_eq!(t.user, acc[13]);
1034 assert_eq!(t.associated_quote_user, acc[15]);
1035 assert_eq!(t.quote_token_program, acc[4]);
1036 assert_eq!(t.associated_token_program, acc[5]);
1037 assert_eq!(t.associated_quote_fee_recipient, acc[7]);
1038 assert_eq!(t.buyback_fee_recipient, acc[8]);
1039 assert_eq!(t.associated_quote_buyback_fee_recipient, acc[9]);
1040 assert_eq!(t.associated_creator_vault, acc[17]);
1041 assert_eq!(t.sharing_config, acc[18]);
1042 assert_eq!(t.global_volume_accumulator, acc[19]);
1043 assert_eq!(t.associated_user_volume_accumulator, acc[21]);
1044 assert_eq!(t.ix_name, "buy_v2");
1045 }
1046 other => panic!("expected PumpFunBuy, got {other:?}"),
1047 }
1048 }
1049
1050 #[test]
1051 fn pumpfun_buy_exact_quote_in_v2_uses_quote_amount_fields() {
1052 let data = instruction_data(discriminators::BUY_EXACT_QUOTE_IN_V2, 777, 888);
1053 let acc = accounts(27);
1054 let event =
1055 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
1056
1057 match event {
1058 DexEvent::PumpFunBuy(t) => {
1059 assert_eq!(t.ix_name, "buy_exact_quote_in_v2");
1060 assert_eq!(t.amount, 888);
1061 assert_eq!(t.max_sol_cost, 0);
1062 assert_eq!(t.quote_amount, 777);
1063 assert_eq!(t.spendable_quote_in, 777);
1064 assert_eq!(t.min_tokens_out, 888);
1065 assert_eq!(t.quote_mint, acc[2]);
1066 }
1067 other => panic!("expected PumpFunBuy, got {other:?}"),
1068 }
1069 }
1070}