1use crate::core::events::*;
31
32pub mod discriminators {
38 pub const TRADE_EVENT: [u8; 16] = [
41 189, 219, 127, 211, 78, 230, 97, 238, 155, 167, 108, 32, 122, 76, 173, 64, ];
44
45 pub const CREATE_TOKEN_EVENT: [u8; 16] =
47 [27, 114, 169, 77, 222, 235, 99, 118, 155, 167, 108, 32, 122, 76, 173, 64];
48
49 pub const COMPLETE_PUMP_AMM_MIGRATION_EVENT: [u8; 16] =
51 [189, 233, 93, 185, 92, 148, 234, 148, 155, 167, 108, 32, 122, 76, 173, 64];
52}
53
54#[cfg(feature = "parse-zero-copy")]
59#[inline(always)]
60unsafe fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
61 let ptr = data.as_ptr().add(offset) as *const u64;
62 u64::from_le(ptr.read_unaligned())
63}
64
65#[cfg(feature = "parse-zero-copy")]
66#[inline(always)]
67unsafe fn read_i64_unchecked(data: &[u8], offset: usize) -> i64 {
68 let ptr = data.as_ptr().add(offset) as *const i64;
69 i64::from_le(ptr.read_unaligned())
70}
71
72#[cfg(feature = "parse-zero-copy")]
73#[inline(always)]
74unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
75 *data.get_unchecked(offset) == 1
76}
77
78#[cfg(feature = "parse-zero-copy")]
79#[inline(always)]
80unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> solana_sdk::pubkey::Pubkey {
81 use solana_sdk::pubkey::Pubkey;
82 let ptr = data.as_ptr().add(offset);
83 let mut bytes = [0u8; 32];
84 std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
85 Pubkey::new_from_array(bytes)
86}
87
88#[cfg(feature = "parse-zero-copy")]
89#[inline(always)]
90unsafe fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
91 if data.len() < offset + 4 {
92 return None;
93 }
94
95 let len = read_u32_unchecked(data, offset) as usize;
96 if data.len() < offset + 4 + len {
97 return None;
98 }
99
100 let string_bytes = &data[offset + 4..offset + 4 + len];
101 let s = std::str::from_utf8_unchecked(string_bytes);
102 Some((s, 4 + len))
103}
104
105#[cfg(feature = "parse-zero-copy")]
106#[inline(always)]
107unsafe fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
108 let ptr = data.as_ptr().add(offset) as *const u32;
109 u32::from_le(ptr.read_unaligned())
110}
111
112#[inline]
129pub fn parse_pumpfun_inner_instruction(
130 discriminator: &[u8; 16],
131 data: &[u8],
132 metadata: EventMetadata,
133 is_created_buy: bool,
134) -> Option<DexEvent> {
135 match *discriminator {
136 discriminators::TRADE_EVENT => parse_trade_event_inner(data, metadata, is_created_buy),
137 discriminators::CREATE_TOKEN_EVENT => parse_create_event_inner(data, metadata),
138 discriminators::COMPLETE_PUMP_AMM_MIGRATION_EVENT => {
139 parse_migrate_event_inner(data, metadata)
140 }
141 _ => None,
142 }
143}
144
145#[inline(always)]
153fn parse_trade_event_inner(
154 data: &[u8],
155 metadata: EventMetadata,
156 is_created_buy: bool,
157) -> Option<DexEvent> {
158 #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
159 {
160 parse_trade_event_inner_borsh(data, metadata, is_created_buy)
161 }
162
163 #[cfg(feature = "parse-zero-copy")]
164 {
165 parse_trade_event_inner_zero_copy(data, metadata, is_created_buy)
166 }
167}
168
169#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
173#[inline(always)]
174fn parse_trade_event_inner_borsh(
175 data: &[u8],
176 metadata: EventMetadata,
177 is_created_buy: bool,
178) -> Option<DexEvent> {
179 crate::logs::pump::parse_trade_from_data(data, metadata, is_created_buy)
183}
184
185#[cfg(feature = "parse-zero-copy")]
189#[inline(always)]
190fn parse_trade_event_inner_zero_copy(
191 data: &[u8],
192 metadata: EventMetadata,
193 is_created_buy: bool,
194) -> Option<DexEvent> {
195 unsafe {
196 if data.len() < 32 + 8 + 8 + 1 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 8 + 8 + 32 + 8 + 8 {
198 return None;
199 }
200
201 let mut offset = 0;
202
203 let mint = read_pubkey_unchecked(data, offset);
204 offset += 32;
205
206 let sol_amount = read_u64_unchecked(data, offset);
207 offset += 8;
208
209 let token_amount = read_u64_unchecked(data, offset);
210 offset += 8;
211
212 let is_buy = read_bool_unchecked(data, offset);
213 offset += 1;
214
215 let user = read_pubkey_unchecked(data, offset);
216 offset += 32;
217
218 let timestamp = read_i64_unchecked(data, offset);
219 offset += 8;
220
221 let virtual_sol_reserves = read_u64_unchecked(data, offset);
222 offset += 8;
223
224 let virtual_token_reserves = read_u64_unchecked(data, offset);
225 offset += 8;
226
227 let real_sol_reserves = read_u64_unchecked(data, offset);
228 offset += 8;
229
230 let real_token_reserves = read_u64_unchecked(data, offset);
231 offset += 8;
232
233 let fee_recipient = read_pubkey_unchecked(data, offset);
234 offset += 32;
235
236 let fee_basis_points = read_u64_unchecked(data, offset);
237 offset += 8;
238
239 let fee = read_u64_unchecked(data, offset);
240 offset += 8;
241
242 let creator = read_pubkey_unchecked(data, offset);
243 offset += 32;
244
245 let creator_fee_basis_points = read_u64_unchecked(data, offset);
246 offset += 8;
247
248 let creator_fee = read_u64_unchecked(data, offset);
249 offset += 8;
250
251 let track_volume =
253 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
254 offset += 1;
255
256 let total_unclaimed_tokens =
257 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
258 offset += 8;
259
260 let total_claimed_tokens =
261 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
262 offset += 8;
263
264 let current_sol_volume =
265 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
266 offset += 8;
267
268 let last_update_timestamp =
269 if offset + 8 <= data.len() { read_i64_unchecked(data, offset) } else { 0 };
270 offset += 8;
271
272 let (ix_name, ix_name_len) = if offset + 4 <= data.len() {
273 if let Some((s, consumed)) = read_str_unchecked(data, offset) {
274 (s.to_string(), consumed)
275 } else {
276 (String::new(), 0)
277 }
278 } else {
279 (String::new(), 0)
280 };
281 offset += ix_name_len;
282 let ix_kind = crate::logs::pump::normalize_pumpfun_ix_name(&ix_name);
283
284 let mayhem_mode =
286 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
287 offset += 1;
288 let cashback_fee_basis_points =
289 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
290 offset += 8;
291 let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
292 offset += 8;
293 let (
294 buyback_fee_basis_points,
295 buyback_fee,
296 shareholders,
297 quote_mint,
298 quote_amount,
299 virtual_quote_reserves,
300 real_quote_reserves,
301 holder_rewards_bps,
302 holder_rewards,
303 ) = crate::logs::pump::read_trade_event_extensions(data, &mut offset)?;
304
305 let trade_event = PumpFunTradeEvent {
307 metadata,
308 mint,
309 sol_amount,
310 token_amount,
311 is_buy,
312 is_created_buy,
313 user,
314 timestamp,
315 virtual_sol_reserves,
316 virtual_token_reserves,
317 real_sol_reserves,
318 real_token_reserves,
319 fee_recipient,
320 fee_basis_points,
321 fee,
322 creator,
323 creator_fee_basis_points,
324 creator_fee,
325 track_volume,
326 total_unclaimed_tokens,
327 total_claimed_tokens,
328 current_sol_volume,
329 last_update_timestamp,
330 ix_name: ix_name.clone(),
331 mayhem_mode,
332 cashback_fee_basis_points,
333 cashback,
334 buyback_fee_basis_points,
335 buyback_fee,
336 shareholders,
337 quote_mint,
338 quote_amount,
339 virtual_quote_reserves,
340 real_quote_reserves,
341 holder_rewards_bps,
342 holder_rewards,
343 is_cashback_coin: cashback_fee_basis_points > 0,
344 ..Default::default() };
346
347 match ix_kind {
349 "buy" => Some(DexEvent::PumpFunBuy(trade_event)),
350 "sell" => Some(DexEvent::PumpFunSell(trade_event)),
351 "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(trade_event)),
352 "buy_exact_quote_in" => Some(DexEvent::PumpFunBuy(trade_event)),
353 _ => Some(DexEvent::PumpFunTrade(trade_event)),
354 }
355 }
356}
357
358#[inline(always)]
366fn parse_create_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
367 #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
368 {
369 parse_create_event_inner_compatible(data, metadata)
370 }
371
372 #[cfg(feature = "parse-zero-copy")]
373 {
374 parse_create_event_inner_zero_copy(data, metadata)
375 }
376}
377
378#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
383#[inline(always)]
384fn parse_create_event_inner_compatible(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
385 parse_create_event_fields(data, metadata)
386}
387
388#[inline(always)]
389#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
390fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
391 let bytes = data.get(offset..offset + 4)?;
392 Some(u32::from_le_bytes(bytes.try_into().ok()?))
393}
394
395#[inline(always)]
396#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
397fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
398 let bytes = data.get(offset..offset + 8)?;
399 Some(u64::from_le_bytes(bytes.try_into().ok()?))
400}
401
402#[inline(always)]
403#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
404fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
405 let bytes = data.get(offset..offset + 8)?;
406 Some(i64::from_le_bytes(bytes.try_into().ok()?))
407}
408
409#[inline(always)]
410#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
411fn read_pubkey(data: &[u8], offset: usize) -> Option<solana_sdk::pubkey::Pubkey> {
412 let bytes = data.get(offset..offset + 32)?;
413 let mut out = [0u8; 32];
414 out.copy_from_slice(bytes);
415 Some(solana_sdk::pubkey::Pubkey::new_from_array(out))
416}
417
418#[inline(always)]
419#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
420fn read_string(data: &[u8], offset: &mut usize) -> Option<String> {
421 let len = read_u32_le(data, *offset)? as usize;
422 *offset = (*offset).checked_add(4)?;
423 let end = (*offset).checked_add(len)?;
424 let bytes = data.get(*offset..end)?;
425 *offset = end;
426 Some(std::str::from_utf8(bytes).ok()?.to_string())
427}
428
429#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
430fn parse_create_event_fields(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
431 let mut offset = 0;
432
433 let name = read_string(data, &mut offset)?;
434 let symbol = read_string(data, &mut offset)?;
435 let uri = read_string(data, &mut offset)?;
436
437 let mint = read_pubkey(data, offset)?;
438 offset += 32;
439 let bonding_curve = read_pubkey(data, offset)?;
440 offset += 32;
441 let user = read_pubkey(data, offset)?;
442 offset += 32;
443 let creator = read_pubkey(data, offset)?;
444 offset += 32;
445 let timestamp = read_i64_le(data, offset)?;
446 offset += 8;
447 let virtual_token_reserves = read_u64_le(data, offset)?;
448 offset += 8;
449 let virtual_sol_reserves = read_u64_le(data, offset)?;
450 offset += 8;
451 let real_token_reserves = read_u64_le(data, offset)?;
452 offset += 8;
453 let token_total_supply = read_u64_le(data, offset)?;
454 offset += 8;
455
456 let token_program = read_pubkey(data, offset).unwrap_or_default();
457 offset += 32;
458 let is_mayhem_mode = data.get(offset).copied().unwrap_or_default() == 1;
459 offset += 1;
460 let is_cashback_enabled = data.get(offset).copied().unwrap_or_default() == 1;
461 offset += 1;
462 let quote_mint = normalize_pumpfun_quote_mint(read_pubkey(data, offset).unwrap_or_default());
463 offset += 32;
464 let virtual_quote_reserves = read_u64_le(data, offset).unwrap_or_default();
465 offset += 8;
466 let creator_fee_bps = read_u64_le(data, offset).unwrap_or_default();
467 offset += 8;
468 let is_holder_reward = data.get(offset).copied().unwrap_or_default() == 1;
469
470 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
471 metadata,
472 name,
473 symbol,
474 uri,
475 mint,
476 bonding_curve,
477 user,
478 creator,
479 timestamp,
480 virtual_token_reserves,
481 virtual_sol_reserves,
482 real_token_reserves,
483 token_total_supply,
484 token_program,
485 is_mayhem_mode,
486 is_cashback_enabled,
487 quote_mint,
488 virtual_quote_reserves,
489 creator_fee_bps,
490 is_holder_reward,
491 ix_name: "create".to_string(),
492 ..Default::default()
493 }))
494}
495
496#[cfg(feature = "parse-zero-copy")]
500#[inline(always)]
501fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
502 unsafe {
503 let mut offset = 0;
504
505 let (name, name_len) = read_str_unchecked(data, offset)?;
506 offset += name_len;
507
508 let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
509 offset += symbol_len;
510
511 let (uri, uri_len) = read_str_unchecked(data, offset)?;
512 offset += uri_len;
513
514 if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
515 return None;
516 }
517
518 let mint = read_pubkey_unchecked(data, offset);
519 offset += 32;
520
521 let bonding_curve = read_pubkey_unchecked(data, offset);
522 offset += 32;
523
524 let user = read_pubkey_unchecked(data, offset);
525 offset += 32;
526
527 let creator = read_pubkey_unchecked(data, offset);
528 offset += 32;
529
530 let timestamp = read_i64_unchecked(data, offset);
531 offset += 8;
532
533 let virtual_token_reserves = read_u64_unchecked(data, offset);
534 offset += 8;
535
536 let virtual_sol_reserves = read_u64_unchecked(data, offset);
537 offset += 8;
538
539 let real_token_reserves = read_u64_unchecked(data, offset);
540 offset += 8;
541
542 let token_total_supply = read_u64_unchecked(data, offset);
543 offset += 8;
544
545 let token_program = if offset + 32 <= data.len() {
546 read_pubkey_unchecked(data, offset)
547 } else {
548 solana_sdk::pubkey::Pubkey::default()
549 };
550 offset += 32;
551
552 let is_mayhem_mode =
553 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
554 offset += 1;
555
556 let is_cashback_enabled =
558 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
559 offset += 1;
560 let quote_mint = normalize_pumpfun_quote_mint(if offset + 32 <= data.len() {
561 read_pubkey_unchecked(data, offset)
562 } else {
563 solana_sdk::pubkey::Pubkey::default()
564 });
565 offset += 32;
566 let virtual_quote_reserves =
567 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
568 offset += 8;
569 let creator_fee_bps =
570 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
571 offset += 8;
572 let is_holder_reward =
573 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
574
575 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
576 metadata,
577 name: name.to_string(),
578 symbol: symbol.to_string(),
579 uri: uri.to_string(),
580 mint,
581 bonding_curve,
582 user,
583 creator,
584 timestamp,
585 virtual_token_reserves,
586 virtual_sol_reserves,
587 real_token_reserves,
588 token_total_supply,
589 token_program,
590 is_mayhem_mode,
591 is_cashback_enabled,
592 quote_mint,
593 virtual_quote_reserves,
594 creator_fee_bps,
595 is_holder_reward,
596 ix_name: "create".to_string(),
597 ..Default::default()
598 }))
599 }
600}
601
602#[inline(always)]
610fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
611 #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
612 {
613 parse_migrate_event_inner_borsh(data, metadata)
614 }
615
616 #[cfg(feature = "parse-zero-copy")]
617 {
618 parse_migrate_event_inner_zero_copy(data, metadata)
619 }
620}
621
622#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
626#[inline(always)]
627fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
628 const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32; if data.len() < MIGRATE_EVENT_SIZE {
632 return None;
633 }
634
635 let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
636 event.metadata = metadata;
637 Some(DexEvent::PumpFunMigrate(event))
638}
639
640#[cfg(feature = "parse-zero-copy")]
644#[inline(always)]
645fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
646 unsafe {
647 if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
648 return None;
649 }
650
651 let mut offset = 0;
652
653 let user = read_pubkey_unchecked(data, offset);
654 offset += 32;
655
656 let mint = read_pubkey_unchecked(data, offset);
657 offset += 32;
658
659 let mint_amount = read_u64_unchecked(data, offset);
660 offset += 8;
661
662 let sol_amount = read_u64_unchecked(data, offset);
663 offset += 8;
664
665 let pool_migration_fee = read_u64_unchecked(data, offset);
666 offset += 8;
667
668 let bonding_curve = read_pubkey_unchecked(data, offset);
669 offset += 32;
670
671 let timestamp = read_i64_unchecked(data, offset);
672 offset += 8;
673
674 let pool = read_pubkey_unchecked(data, offset);
675
676 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
677 metadata,
678 user,
679 mint,
680 mint_amount,
681 sol_amount,
682 pool_migration_fee,
683 bonding_curve,
684 timestamp,
685 pool,
686 }))
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use solana_sdk::{pubkey::Pubkey, signature::Signature};
694
695 fn push_u64(out: &mut Vec<u8>, value: u64) {
696 out.extend_from_slice(&value.to_le_bytes());
697 }
698
699 fn push_i64(out: &mut Vec<u8>, value: i64) {
700 out.extend_from_slice(&value.to_le_bytes());
701 }
702
703 fn push_pubkey(out: &mut Vec<u8>, value: Pubkey) {
704 out.extend_from_slice(value.as_ref());
705 }
706
707 fn trade_event_data_without_buyback_tail(ix_name: &str) -> Vec<u8> {
708 let mut data = Vec::new();
709 push_pubkey(&mut data, Pubkey::new_unique()); push_u64(&mut data, 1_000); push_u64(&mut data, 2_000); data.push(1); push_pubkey(&mut data, Pubkey::new_unique()); push_i64(&mut data, 123); push_u64(&mut data, 10); push_u64(&mut data, 20); push_u64(&mut data, 30); push_u64(&mut data, 40); push_pubkey(&mut data, Pubkey::new_unique()); push_u64(&mut data, 50); push_u64(&mut data, 60); push_pubkey(&mut data, Pubkey::new_unique()); push_u64(&mut data, 70); push_u64(&mut data, 80); data.push(1); push_u64(&mut data, 90); push_u64(&mut data, 100); push_u64(&mut data, 110); push_i64(&mut data, 120); data.extend_from_slice(&(ix_name.len() as u32).to_le_bytes());
731 data.extend_from_slice(ix_name.as_bytes());
732 data.push(1); push_u64(&mut data, 130); push_u64(&mut data, 140); data
736 }
737
738 #[test]
739 fn test_discriminator_match() {
740 let disc = discriminators::TRADE_EVENT;
742 assert_eq!(disc.len(), 16);
743 }
744
745 #[test]
746 fn test_parse_trade_event_boundary() {
747 let metadata = EventMetadata {
749 signature: Signature::default(),
750 slot: 0,
751 tx_index: 0,
752 block_time_us: 0,
753 grpc_recv_us: 0,
754 recent_blockhash: None,
755 };
756
757 let short_data = vec![0u8; 10];
758 let result = parse_trade_event_inner(&short_data, metadata, false);
759 assert!(result.is_none());
760 }
761
762 #[test]
763 fn trade_event_parser_accepts_payload_without_latest_tail() {
764 let metadata = EventMetadata {
765 signature: Signature::default(),
766 slot: 10,
767 tx_index: 0,
768 block_time_us: 0,
769 grpc_recv_us: 0,
770 recent_blockhash: None,
771 };
772 let data = trade_event_data_without_buyback_tail("buy_exact_sol_in");
773 let event =
774 parse_pumpfun_inner_instruction(&discriminators::TRADE_EVENT, &data, metadata, true)
775 .expect("legacy tail-compatible trade event");
776
777 match event {
778 DexEvent::PumpFunBuyExactSolIn(t) => {
779 assert_eq!(t.sol_amount, 1_000);
780 assert_eq!(t.token_amount, 2_000);
781 assert_eq!(t.ix_name, "buy_exact_sol_in");
782 assert!(t.track_volume);
783 assert!(t.mayhem_mode);
784 assert_eq!(t.cashback_fee_basis_points, 130);
785 assert_eq!(t.cashback, 140);
786 assert!(t.is_created_buy);
787 assert_eq!(t.buyback_fee_basis_points, 0);
788 assert!(t.shareholders.is_empty());
789 assert_eq!(t.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
790 }
791 other => panic!("expected exact buy trade, got {other:?}"),
792 }
793 }
794}