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