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 ) = crate::logs::pump::read_trade_event_extensions(data, &mut offset)?;
302
303 let trade_event = PumpFunTradeEvent {
305 metadata,
306 mint,
307 sol_amount,
308 token_amount,
309 is_buy,
310 is_created_buy,
311 user,
312 timestamp,
313 virtual_sol_reserves,
314 virtual_token_reserves,
315 real_sol_reserves,
316 real_token_reserves,
317 fee_recipient,
318 fee_basis_points,
319 fee,
320 creator,
321 creator_fee_basis_points,
322 creator_fee,
323 track_volume,
324 total_unclaimed_tokens,
325 total_claimed_tokens,
326 current_sol_volume,
327 last_update_timestamp,
328 ix_name: ix_name.clone(),
329 mayhem_mode,
330 cashback_fee_basis_points,
331 cashback,
332 buyback_fee_basis_points,
333 buyback_fee,
334 shareholders,
335 quote_mint,
336 quote_amount,
337 virtual_quote_reserves,
338 real_quote_reserves,
339 is_cashback_coin: cashback_fee_basis_points > 0,
340 ..Default::default() };
342
343 match ix_kind {
345 "buy" => Some(DexEvent::PumpFunBuy(trade_event)),
346 "sell" => Some(DexEvent::PumpFunSell(trade_event)),
347 "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(trade_event)),
348 "buy_exact_quote_in" => Some(DexEvent::PumpFunBuy(trade_event)),
349 _ => Some(DexEvent::PumpFunTrade(trade_event)),
350 }
351 }
352}
353
354#[inline(always)]
362fn parse_create_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
363 #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
364 {
365 parse_create_event_inner_compatible(data, metadata)
366 }
367
368 #[cfg(feature = "parse-zero-copy")]
369 {
370 parse_create_event_inner_zero_copy(data, metadata)
371 }
372}
373
374#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
379#[inline(always)]
380fn parse_create_event_inner_compatible(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
381 parse_create_event_fields(data, metadata)
382}
383
384#[inline(always)]
385#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
386fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
387 let bytes = data.get(offset..offset + 4)?;
388 Some(u32::from_le_bytes(bytes.try_into().ok()?))
389}
390
391#[inline(always)]
392#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
393fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
394 let bytes = data.get(offset..offset + 8)?;
395 Some(u64::from_le_bytes(bytes.try_into().ok()?))
396}
397
398#[inline(always)]
399#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
400fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
401 let bytes = data.get(offset..offset + 8)?;
402 Some(i64::from_le_bytes(bytes.try_into().ok()?))
403}
404
405#[inline(always)]
406#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
407fn read_pubkey(data: &[u8], offset: usize) -> Option<solana_sdk::pubkey::Pubkey> {
408 let bytes = data.get(offset..offset + 32)?;
409 let mut out = [0u8; 32];
410 out.copy_from_slice(bytes);
411 Some(solana_sdk::pubkey::Pubkey::new_from_array(out))
412}
413
414#[inline(always)]
415#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
416fn read_string(data: &[u8], offset: &mut usize) -> Option<String> {
417 let len = read_u32_le(data, *offset)? as usize;
418 *offset = (*offset).checked_add(4)?;
419 let end = (*offset).checked_add(len)?;
420 let bytes = data.get(*offset..end)?;
421 *offset = end;
422 Some(std::str::from_utf8(bytes).ok()?.to_string())
423}
424
425#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
426fn parse_create_event_fields(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
427 let mut offset = 0;
428
429 let name = read_string(data, &mut offset)?;
430 let symbol = read_string(data, &mut offset)?;
431 let uri = read_string(data, &mut offset)?;
432
433 let mint = read_pubkey(data, offset)?;
434 offset += 32;
435 let bonding_curve = read_pubkey(data, offset)?;
436 offset += 32;
437 let user = read_pubkey(data, offset)?;
438 offset += 32;
439 let creator = read_pubkey(data, offset)?;
440 offset += 32;
441 let timestamp = read_i64_le(data, offset)?;
442 offset += 8;
443 let virtual_token_reserves = read_u64_le(data, offset)?;
444 offset += 8;
445 let virtual_sol_reserves = read_u64_le(data, offset)?;
446 offset += 8;
447 let real_token_reserves = read_u64_le(data, offset)?;
448 offset += 8;
449 let token_total_supply = read_u64_le(data, offset)?;
450 offset += 8;
451
452 let token_program = read_pubkey(data, offset).unwrap_or_default();
453 offset += 32;
454 let is_mayhem_mode = data.get(offset).copied().unwrap_or_default() == 1;
455 offset += 1;
456 let is_cashback_enabled = data.get(offset).copied().unwrap_or_default() == 1;
457 offset += 1;
458 let quote_mint = normalize_pumpfun_quote_mint(read_pubkey(data, offset).unwrap_or_default());
459 offset += 32;
460 let virtual_quote_reserves = read_u64_le(data, offset).unwrap_or_default();
461
462 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
463 metadata,
464 name,
465 symbol,
466 uri,
467 mint,
468 bonding_curve,
469 user,
470 creator,
471 timestamp,
472 virtual_token_reserves,
473 virtual_sol_reserves,
474 real_token_reserves,
475 token_total_supply,
476 token_program,
477 is_mayhem_mode,
478 is_cashback_enabled,
479 quote_mint,
480 virtual_quote_reserves,
481 ix_name: "create".to_string(),
482 ..Default::default()
483 }))
484}
485
486#[cfg(feature = "parse-zero-copy")]
490#[inline(always)]
491fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
492 unsafe {
493 let mut offset = 0;
494
495 let (name, name_len) = read_str_unchecked(data, offset)?;
496 offset += name_len;
497
498 let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
499 offset += symbol_len;
500
501 let (uri, uri_len) = read_str_unchecked(data, offset)?;
502 offset += uri_len;
503
504 if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
505 return None;
506 }
507
508 let mint = read_pubkey_unchecked(data, offset);
509 offset += 32;
510
511 let bonding_curve = read_pubkey_unchecked(data, offset);
512 offset += 32;
513
514 let user = read_pubkey_unchecked(data, offset);
515 offset += 32;
516
517 let creator = read_pubkey_unchecked(data, offset);
518 offset += 32;
519
520 let timestamp = read_i64_unchecked(data, offset);
521 offset += 8;
522
523 let virtual_token_reserves = read_u64_unchecked(data, offset);
524 offset += 8;
525
526 let virtual_sol_reserves = read_u64_unchecked(data, offset);
527 offset += 8;
528
529 let real_token_reserves = read_u64_unchecked(data, offset);
530 offset += 8;
531
532 let token_total_supply = read_u64_unchecked(data, offset);
533 offset += 8;
534
535 let token_program = if offset + 32 <= data.len() {
536 read_pubkey_unchecked(data, offset)
537 } else {
538 solana_sdk::pubkey::Pubkey::default()
539 };
540 offset += 32;
541
542 let is_mayhem_mode =
543 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
544 offset += 1;
545
546 let is_cashback_enabled =
548 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
549 offset += 1;
550 let quote_mint = normalize_pumpfun_quote_mint(if offset + 32 <= data.len() {
551 read_pubkey_unchecked(data, offset)
552 } else {
553 solana_sdk::pubkey::Pubkey::default()
554 });
555 offset += 32;
556 let virtual_quote_reserves =
557 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
558
559 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
560 metadata,
561 name: name.to_string(),
562 symbol: symbol.to_string(),
563 uri: uri.to_string(),
564 mint,
565 bonding_curve,
566 user,
567 creator,
568 timestamp,
569 virtual_token_reserves,
570 virtual_sol_reserves,
571 real_token_reserves,
572 token_total_supply,
573 token_program,
574 is_mayhem_mode,
575 is_cashback_enabled,
576 quote_mint,
577 virtual_quote_reserves,
578 ix_name: "create".to_string(),
579 ..Default::default()
580 }))
581 }
582}
583
584#[inline(always)]
592fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
593 #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
594 {
595 parse_migrate_event_inner_borsh(data, metadata)
596 }
597
598 #[cfg(feature = "parse-zero-copy")]
599 {
600 parse_migrate_event_inner_zero_copy(data, metadata)
601 }
602}
603
604#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
608#[inline(always)]
609fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
610 const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32; if data.len() < MIGRATE_EVENT_SIZE {
614 return None;
615 }
616
617 let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
618 event.metadata = metadata;
619 Some(DexEvent::PumpFunMigrate(event))
620}
621
622#[cfg(feature = "parse-zero-copy")]
626#[inline(always)]
627fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
628 unsafe {
629 if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
630 return None;
631 }
632
633 let mut offset = 0;
634
635 let user = read_pubkey_unchecked(data, offset);
636 offset += 32;
637
638 let mint = read_pubkey_unchecked(data, offset);
639 offset += 32;
640
641 let mint_amount = read_u64_unchecked(data, offset);
642 offset += 8;
643
644 let sol_amount = read_u64_unchecked(data, offset);
645 offset += 8;
646
647 let pool_migration_fee = read_u64_unchecked(data, offset);
648 offset += 8;
649
650 let bonding_curve = read_pubkey_unchecked(data, offset);
651 offset += 32;
652
653 let timestamp = read_i64_unchecked(data, offset);
654 offset += 8;
655
656 let pool = read_pubkey_unchecked(data, offset);
657
658 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
659 metadata,
660 user,
661 mint,
662 mint_amount,
663 sol_amount,
664 pool_migration_fee,
665 bonding_curve,
666 timestamp,
667 pool,
668 }))
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use solana_sdk::{pubkey::Pubkey, signature::Signature};
676
677 fn push_u64(out: &mut Vec<u8>, value: u64) {
678 out.extend_from_slice(&value.to_le_bytes());
679 }
680
681 fn push_i64(out: &mut Vec<u8>, value: i64) {
682 out.extend_from_slice(&value.to_le_bytes());
683 }
684
685 fn push_pubkey(out: &mut Vec<u8>, value: Pubkey) {
686 out.extend_from_slice(value.as_ref());
687 }
688
689 fn trade_event_data_without_buyback_tail(ix_name: &str) -> Vec<u8> {
690 let mut data = Vec::new();
691 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());
713 data.extend_from_slice(ix_name.as_bytes());
714 data.push(1); push_u64(&mut data, 130); push_u64(&mut data, 140); data
718 }
719
720 #[test]
721 fn test_discriminator_match() {
722 let disc = discriminators::TRADE_EVENT;
724 assert_eq!(disc.len(), 16);
725 }
726
727 #[test]
728 fn test_parse_trade_event_boundary() {
729 let metadata = EventMetadata {
731 signature: Signature::default(),
732 slot: 0,
733 tx_index: 0,
734 block_time_us: 0,
735 grpc_recv_us: 0,
736 recent_blockhash: None,
737 };
738
739 let short_data = vec![0u8; 10];
740 let result = parse_trade_event_inner(&short_data, metadata, false);
741 assert!(result.is_none());
742 }
743
744 #[test]
745 fn trade_event_parser_accepts_payload_without_latest_tail() {
746 let metadata = EventMetadata {
747 signature: Signature::default(),
748 slot: 10,
749 tx_index: 0,
750 block_time_us: 0,
751 grpc_recv_us: 0,
752 recent_blockhash: None,
753 };
754 let data = trade_event_data_without_buyback_tail("buy_exact_sol_in");
755 let event =
756 parse_pumpfun_inner_instruction(&discriminators::TRADE_EVENT, &data, metadata, true)
757 .expect("legacy tail-compatible trade event");
758
759 match event {
760 DexEvent::PumpFunBuyExactSolIn(t) => {
761 assert_eq!(t.sol_amount, 1_000);
762 assert_eq!(t.token_amount, 2_000);
763 assert_eq!(t.ix_name, "buy_exact_sol_in");
764 assert!(t.track_volume);
765 assert!(t.mayhem_mode);
766 assert_eq!(t.cashback_fee_basis_points, 130);
767 assert_eq!(t.cashback, 140);
768 assert!(t.is_created_buy);
769 assert_eq!(t.buyback_fee_basis_points, 0);
770 assert!(t.shareholders.is_empty());
771 assert_eq!(t.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT);
772 }
773 other => panic!("expected exact buy trade, got {other:?}"),
774 }
775 }
776}