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