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