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 if accounts.len() < 7 {
196 return None;
197 }
198
199 let (first_arg, second_arg) = if data.len() >= 16 {
201 (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
202 } else {
203 (0, 0)
204 };
205 let (token_amount, sol_amount, amount, max_sol_cost) = if exact_quote_in {
206 (second_arg, first_arg, 0, 0)
207 } else {
208 (first_arg, second_arg, first_arg, second_arg)
209 };
210
211 let mint = get_account(accounts, 2)?;
212 let metadata =
213 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
214
215 let trade_event = PumpFunTradeEvent {
216 metadata,
217 mint,
218 is_buy: true,
219 bonding_curve: get_account(accounts, 3).unwrap_or_default(),
220 user: get_account(accounts, 6).unwrap_or_default(),
221 sol_amount,
222 token_amount,
223 amount,
224 max_sol_cost,
225 fee_recipient: get_account(accounts, 1).unwrap_or_default(),
226 associated_bonding_curve: get_account(accounts, 4).unwrap_or_default(),
227 token_program: get_account(accounts, 8).unwrap_or_default(),
228 creator_vault: get_account(accounts, 9).unwrap_or_default(),
229 ix_name: ix_name.to_string(),
230 ..Default::default()
231 };
232
233 if exact_quote_in {
234 Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
235 } else {
236 Some(DexEvent::PumpFunBuy(trade_event))
237 }
238}
239
240fn parse_sell_instruction(
249 data: &[u8],
250 accounts: &[Pubkey],
251 signature: Signature,
252 slot: u64,
253 tx_index: u64,
254 block_time_us: Option<i64>,
255 grpc_recv_us: i64,
256 ix_name: &'static str,
257 v2_accounts: bool,
258) -> Option<DexEvent> {
259 let min_accounts = if v2_accounts { 26 } else { 7 };
260 if accounts.len() < min_accounts {
261 return None;
262 }
263
264 let (amount, min_sol_output) = if data.len() >= 16 {
266 (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
267 } else {
268 (0, 0)
269 };
270 let token_amount = amount;
271 let sol_amount = min_sol_output;
272
273 let (
274 mint_idx,
275 bonding_curve_idx,
276 associated_bonding_curve_idx,
277 user_idx,
278 fee_recipient_idx,
279 token_program_idx,
280 creator_vault_idx,
281 ) = if v2_accounts { (1, 10, 11, 13, 6, 3, 16) } else { (2, 3, 4, 6, 1, 9, 8) };
282 let mint = get_account(accounts, mint_idx)?;
283 let metadata =
284 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
285
286 Some(DexEvent::PumpFunSell(PumpFunTradeEvent {
287 metadata,
288 mint,
289 is_buy: false,
290 bonding_curve: get_account(accounts, bonding_curve_idx).unwrap_or_default(),
291 user: get_account(accounts, user_idx).unwrap_or_default(),
292 sol_amount,
293 token_amount,
294 amount,
295 min_sol_output,
296 fee_recipient: get_account(accounts, fee_recipient_idx).unwrap_or_default(),
297 associated_bonding_curve: get_account(accounts, associated_bonding_curve_idx)
298 .unwrap_or_default(),
299 token_program: get_account(accounts, token_program_idx).unwrap_or_default(),
300 creator_vault: get_account(accounts, creator_vault_idx).unwrap_or_default(),
301 ix_name: ix_name.to_string(),
302 ..Default::default()
303 }))
304}
305
306fn parse_buy_v2_instruction(
307 data: &[u8],
308 accounts: &[Pubkey],
309 signature: Signature,
310 slot: u64,
311 tx_index: u64,
312 block_time_us: Option<i64>,
313 grpc_recv_us: i64,
314 ix_name: &'static str,
315 exact_quote_in: bool,
316) -> Option<DexEvent> {
317 const MIN_ACC: usize = 27;
318 if accounts.len() < MIN_ACC {
319 return None;
320 }
321
322 let (first_arg, second_arg) = if data.len() >= 16 {
324 (read_u64_le(data, 0).unwrap_or(0), read_u64_le(data, 8).unwrap_or(0))
325 } else {
326 (0, 0)
327 };
328 let (token_amount, sol_amount, amount, max_sol_cost) = if exact_quote_in {
329 (second_arg, first_arg, 0, 0)
330 } else {
331 (first_arg, second_arg, first_arg, second_arg)
332 };
333
334 let metadata =
335 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
336 let trade_event = PumpFunTradeEvent {
337 metadata,
338 mint: accounts[1],
339 is_buy: true,
340 bonding_curve: accounts[10],
341 associated_bonding_curve: accounts[11],
342 user: accounts[13],
343 sol_amount,
344 token_amount,
345 amount,
346 max_sol_cost,
347 fee_recipient: accounts[6],
348 token_program: accounts[3],
349 creator_vault: accounts[16],
350 ix_name: ix_name.to_string(),
351 ..Default::default()
352 };
353
354 if exact_quote_in {
355 Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
356 } else {
357 Some(DexEvent::PumpFunBuy(trade_event))
358 }
359}
360
361fn parse_sell_v2_instruction(
362 data: &[u8],
363 accounts: &[Pubkey],
364 signature: Signature,
365 slot: u64,
366 tx_index: u64,
367 block_time_us: Option<i64>,
368 grpc_recv_us: i64,
369 ix_name: &'static str,
370) -> Option<DexEvent> {
371 parse_sell_instruction(
372 data,
373 accounts,
374 signature,
375 slot,
376 tx_index,
377 block_time_us,
378 grpc_recv_us,
379 ix_name,
380 true,
381 )
382}
383
384fn parse_create_instruction(
390 data: &[u8],
391 accounts: &[Pubkey],
392 signature: Signature,
393 slot: u64,
394 tx_index: u64,
395 block_time_us: Option<i64>,
396 grpc_recv_us: i64,
397) -> Option<DexEvent> {
398 if accounts.len() < 8 {
399 return None;
400 }
401
402 let mut offset = 0;
403
404 let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
407 offset += len;
408 s.to_string()
409 } else {
410 String::new()
411 };
412
413 let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
414 offset += len;
415 s.to_string()
416 } else {
417 String::new()
418 };
419
420 let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
421 offset += len;
422 s.to_string()
423 } else {
424 String::new()
425 };
426
427 if data.len() < offset + 32 + 32 + 32 + 32 {
429 return None;
430 }
431
432 let mint = read_pubkey(data, offset).unwrap_or_default();
433 offset += 32;
434
435 let bonding_curve = read_pubkey(data, offset).unwrap_or_default();
436 offset += 32;
437
438 let user = read_pubkey(data, offset).unwrap_or_default();
439 offset += 32;
440
441 let creator = read_pubkey(data, offset).unwrap_or_default();
442
443 let metadata =
444 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
445
446 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
447 metadata,
448 name,
449 symbol,
450 uri,
451 mint,
452 bonding_curve,
453 user,
454 creator,
455 ..Default::default()
456 }))
457}
458
459fn parse_create_v2_instruction(
468 data: &[u8],
469 accounts: &[Pubkey],
470 signature: Signature,
471 slot: u64,
472 tx_index: u64,
473 block_time_us: Option<i64>,
474 grpc_recv_us: i64,
475) -> Option<DexEvent> {
476 const CREATE_V2_MIN_ACCOUNTS: usize = 16;
477 if accounts.len() < CREATE_V2_MIN_ACCOUNTS {
478 return None;
479 }
480 let acc = &accounts[0..CREATE_V2_MIN_ACCOUNTS];
481
482 let mut offset = 0usize;
484 let name = if let Some((s, len)) = read_str_unchecked(data, offset) {
485 offset += len;
486 s.to_string()
487 } else {
488 String::new()
489 };
490 let symbol = if let Some((s, len)) = read_str_unchecked(data, offset) {
491 offset += len;
492 s.to_string()
493 } else {
494 String::new()
495 };
496 let uri = if let Some((s, len)) = read_str_unchecked(data, offset) {
497 offset += len;
498 s.to_string()
499 } else {
500 String::new()
501 };
502 if data.len() < offset + 32 + 1 {
503 return None;
504 }
505 let creator = read_pubkey(data, offset)?;
506 offset += 32;
507 let is_mayhem_mode = read_bool(data, offset)?;
508 offset += 1;
509 let is_cashback_enabled = read_option_bool_idl(data, offset).unwrap_or(false);
510
511 let mint = acc[0];
512 let bonding_curve = acc[2];
513 let user = acc[5];
514
515 let metadata =
516 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), grpc_recv_us);
517
518 Some(DexEvent::PumpFunCreateV2(PumpFunCreateV2TokenEvent {
519 metadata,
520 name,
521 symbol,
522 uri,
523 mint,
524 bonding_curve,
525 user,
526 creator,
527 mint_authority: acc[1],
528 associated_bonding_curve: acc[3],
529 global: acc[4],
530 system_program: acc[6],
531 token_program: acc[7],
532 associated_token_program: acc[8],
533 mayhem_program_id: acc[9],
534 global_params: acc[10],
535 sol_vault: acc[11],
536 mayhem_state: acc[12],
537 mayhem_token_vault: acc[13],
538 event_authority: acc[14],
539 program: acc[15],
540 is_mayhem_mode,
541 is_cashback_enabled,
542 ..Default::default()
543 }))
544}
545
546#[allow(unused_variables)]
548fn parse_migrate_log_instruction(
549 data: &[u8],
550 accounts: &[Pubkey],
551 signature: Signature,
552 slot: u64,
553 tx_index: u64,
554 block_time_us: Option<i64>,
555 rpc_recv_us: i64,
556) -> Option<DexEvent> {
557 let mut offset = 0;
558
559 let user = read_pubkey(data, offset)?;
561 offset += 32;
562
563 let mint = read_pubkey(data, offset)?;
565 offset += 32;
566
567 let mint_amount = read_u64_le(data, offset)?;
569 offset += 8;
570
571 let sol_amount = read_u64_le(data, offset)?;
573 offset += 8;
574
575 let pool_migration_fee = read_u64_le(data, offset)?;
577 offset += 8;
578
579 let bonding_curve = read_pubkey(data, offset)?;
581 offset += 32;
582
583 let timestamp = read_u64_le(data, offset)? as i64;
585 offset += 8;
586
587 let pool = read_pubkey(data, offset)?;
589
590 let metadata =
591 create_metadata(signature, slot, tx_index, block_time_us.unwrap_or_default(), rpc_recv_us);
592
593 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
594 metadata,
595 user,
596 mint,
597 mint_amount,
598 sol_amount,
599 pool_migration_fee,
600 bonding_curve,
601 timestamp,
602 pool,
603 }))
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 fn instruction_data(discriminator: [u8; 8], first: u64, second: u64) -> Vec<u8> {
611 let mut data = Vec::with_capacity(24);
612 data.extend_from_slice(&discriminator);
613 data.extend_from_slice(&first.to_le_bytes());
614 data.extend_from_slice(&second.to_le_bytes());
615 data
616 }
617
618 fn accounts(n: usize) -> Vec<Pubkey> {
619 (0..n).map(|_| Pubkey::new_unique()).collect()
620 }
621
622 #[test]
623 fn pumpfun_buy_instruction_exposes_raw_args() {
624 let data = instruction_data(discriminators::BUY, 123, 456);
625 let acc = accounts(15);
626 let event =
627 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
628
629 match event {
630 DexEvent::PumpFunBuy(t) => {
631 assert_eq!(t.amount, 123);
632 assert_eq!(t.max_sol_cost, 456);
633 assert_eq!(t.min_sol_output, 0);
634 assert_eq!(t.token_amount, 123);
635 assert_eq!(t.sol_amount, 456);
636 assert_eq!(t.ix_name, "buy");
637 }
638 other => panic!("expected PumpFunBuy, got {other:?}"),
639 }
640 }
641
642 #[test]
643 fn pumpfun_sell_instruction_exposes_raw_args() {
644 let data = instruction_data(discriminators::SELL, 321, 654);
645 let acc = accounts(14);
646 let event =
647 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
648
649 match event {
650 DexEvent::PumpFunSell(t) => {
651 assert_eq!(t.amount, 321);
652 assert_eq!(t.max_sol_cost, 0);
653 assert_eq!(t.min_sol_output, 654);
654 assert_eq!(t.token_amount, 321);
655 assert_eq!(t.sol_amount, 654);
656 assert_eq!(t.ix_name, "sell");
657 }
658 other => panic!("expected PumpFunSell, got {other:?}"),
659 }
660 }
661
662 #[test]
663 fn pumpfun_v2_instruction_args_use_v2_account_layout() {
664 let data = instruction_data(discriminators::BUY_V2, 777, 888);
665 let acc = accounts(27);
666 let event =
667 parse_instruction(&data, &acc, Signature::default(), 1, 0, None, 99).expect("event");
668
669 match event {
670 DexEvent::PumpFunBuy(t) => {
671 assert_eq!(t.amount, 777);
672 assert_eq!(t.max_sol_cost, 888);
673 assert_eq!(t.mint, acc[1]);
674 assert_eq!(t.bonding_curve, acc[10]);
675 assert_eq!(t.associated_bonding_curve, acc[11]);
676 assert_eq!(t.user, acc[13]);
677 assert_eq!(t.ix_name, "buy_v2");
678 }
679 other => panic!("expected PumpFunBuy, got {other:?}"),
680 }
681 }
682}