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