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
283 let mayhem_mode =
285 if offset + 1 <= data.len() { read_bool_unchecked(data, offset) } else { false };
286 offset += 1;
287 let cashback_fee_basis_points =
288 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
289 offset += 8;
290 let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
291 offset += 8;
292 let (
293 buyback_fee_basis_points,
294 buyback_fee,
295 shareholders,
296 quote_mint,
297 quote_amount,
298 virtual_quote_reserves,
299 real_quote_reserves,
300 ) = crate::logs::pump::read_trade_event_extensions(data, &mut offset)?;
301
302 let trade_event = PumpFunTradeEvent {
304 metadata,
305 mint,
306 sol_amount,
307 token_amount,
308 is_buy,
309 is_created_buy,
310 user,
311 timestamp,
312 virtual_sol_reserves,
313 virtual_token_reserves,
314 real_sol_reserves,
315 real_token_reserves,
316 fee_recipient,
317 fee_basis_points,
318 fee,
319 creator,
320 creator_fee_basis_points,
321 creator_fee,
322 track_volume,
323 total_unclaimed_tokens,
324 total_claimed_tokens,
325 current_sol_volume,
326 last_update_timestamp,
327 ix_name: ix_name.clone(),
328 mayhem_mode,
329 cashback_fee_basis_points,
330 cashback,
331 buyback_fee_basis_points,
332 buyback_fee,
333 shareholders,
334 quote_mint,
335 quote_amount,
336 virtual_quote_reserves,
337 real_quote_reserves,
338 is_cashback_coin: cashback_fee_basis_points > 0,
339 ..Default::default() };
341
342 match ix_name.as_str() {
344 "buy" | "buy_v2" => Some(DexEvent::PumpFunBuy(trade_event)),
345 "sell" | "sell_v2" => Some(DexEvent::PumpFunSell(trade_event)),
346 "buy_exact_sol_in" | "buy_exact_quote_in" | "buy_exact_quote_in_v2" => {
347 Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
348 }
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_borsh(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")))]
378#[inline(always)]
379fn parse_create_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
380 let mut event = borsh::from_slice::<PumpFunCreateTokenEvent>(data).ok()?;
382 event.metadata = metadata;
383 Some(DexEvent::PumpFunCreate(event))
384}
385
386#[cfg(feature = "parse-zero-copy")]
390#[inline(always)]
391fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
392 unsafe {
393 let mut offset = 0;
394
395 let (name, name_len) = read_str_unchecked(data, offset)?;
396 offset += name_len;
397
398 let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
399 offset += symbol_len;
400
401 let (uri, uri_len) = read_str_unchecked(data, offset)?;
402 offset += uri_len;
403
404 if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
405 return None;
406 }
407
408 let mint = read_pubkey_unchecked(data, offset);
409 offset += 32;
410
411 let bonding_curve = read_pubkey_unchecked(data, offset);
412 offset += 32;
413
414 let user = read_pubkey_unchecked(data, offset);
415 offset += 32;
416
417 let creator = read_pubkey_unchecked(data, offset);
418 offset += 32;
419
420 let timestamp = read_i64_unchecked(data, offset);
421 offset += 8;
422
423 let virtual_token_reserves = read_u64_unchecked(data, offset);
424 offset += 8;
425
426 let virtual_sol_reserves = read_u64_unchecked(data, offset);
427 offset += 8;
428
429 let real_token_reserves = read_u64_unchecked(data, offset);
430 offset += 8;
431
432 let token_total_supply = read_u64_unchecked(data, offset);
433 offset += 8;
434
435 let token_program = if offset + 32 <= data.len() {
436 read_pubkey_unchecked(data, offset)
437 } else {
438 solana_sdk::pubkey::Pubkey::default()
439 };
440 offset += 32;
441
442 let is_mayhem_mode =
443 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
444 offset += 1;
445
446 let is_cashback_enabled =
448 if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
449
450 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
451 metadata,
452 name: name.to_string(),
453 symbol: symbol.to_string(),
454 uri: uri.to_string(),
455 mint,
456 bonding_curve,
457 user,
458 creator,
459 timestamp,
460 virtual_token_reserves,
461 virtual_sol_reserves,
462 real_token_reserves,
463 token_total_supply,
464 token_program,
465 is_mayhem_mode,
466 is_cashback_enabled,
467 }))
468 }
469}
470
471#[inline(always)]
479fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
480 #[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
481 {
482 parse_migrate_event_inner_borsh(data, metadata)
483 }
484
485 #[cfg(feature = "parse-zero-copy")]
486 {
487 parse_migrate_event_inner_zero_copy(data, metadata)
488 }
489}
490
491#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
495#[inline(always)]
496fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
497 const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32; if data.len() < MIGRATE_EVENT_SIZE {
501 return None;
502 }
503
504 let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
505 event.metadata = metadata;
506 Some(DexEvent::PumpFunMigrate(event))
507}
508
509#[cfg(feature = "parse-zero-copy")]
513#[inline(always)]
514fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
515 unsafe {
516 if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
517 return None;
518 }
519
520 let mut offset = 0;
521
522 let user = read_pubkey_unchecked(data, offset);
523 offset += 32;
524
525 let mint = read_pubkey_unchecked(data, offset);
526 offset += 32;
527
528 let mint_amount = read_u64_unchecked(data, offset);
529 offset += 8;
530
531 let sol_amount = read_u64_unchecked(data, offset);
532 offset += 8;
533
534 let pool_migration_fee = read_u64_unchecked(data, offset);
535 offset += 8;
536
537 let bonding_curve = read_pubkey_unchecked(data, offset);
538 offset += 32;
539
540 let timestamp = read_i64_unchecked(data, offset);
541 offset += 8;
542
543 let pool = read_pubkey_unchecked(data, offset);
544
545 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
546 metadata,
547 user,
548 mint,
549 mint_amount,
550 sol_amount,
551 pool_migration_fee,
552 bonding_curve,
553 timestamp,
554 pool,
555 }))
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use solana_sdk::{pubkey::Pubkey, signature::Signature};
563
564 fn push_u64(out: &mut Vec<u8>, value: u64) {
565 out.extend_from_slice(&value.to_le_bytes());
566 }
567
568 fn push_i64(out: &mut Vec<u8>, value: i64) {
569 out.extend_from_slice(&value.to_le_bytes());
570 }
571
572 fn push_pubkey(out: &mut Vec<u8>, value: Pubkey) {
573 out.extend_from_slice(value.as_ref());
574 }
575
576 fn trade_event_data_without_buyback_tail(ix_name: &str) -> Vec<u8> {
577 let mut data = Vec::new();
578 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());
600 data.extend_from_slice(ix_name.as_bytes());
601 data.push(1); push_u64(&mut data, 130); push_u64(&mut data, 140); data
605 }
606
607 #[test]
608 fn test_discriminator_match() {
609 let disc = discriminators::TRADE_EVENT;
611 assert_eq!(disc.len(), 16);
612 }
613
614 #[test]
615 fn test_parse_trade_event_boundary() {
616 let metadata = EventMetadata {
618 signature: Signature::default(),
619 slot: 0,
620 tx_index: 0,
621 block_time_us: 0,
622 grpc_recv_us: 0,
623 recent_blockhash: None,
624 };
625
626 let short_data = vec![0u8; 10];
627 let result = parse_trade_event_inner(&short_data, metadata, false);
628 assert!(result.is_none());
629 }
630
631 #[test]
632 fn trade_event_parser_accepts_payload_without_latest_tail() {
633 let metadata = EventMetadata {
634 signature: Signature::default(),
635 slot: 10,
636 tx_index: 0,
637 block_time_us: 0,
638 grpc_recv_us: 0,
639 recent_blockhash: None,
640 };
641 let data = trade_event_data_without_buyback_tail("buy_exact_sol_in");
642 let event =
643 parse_pumpfun_inner_instruction(&discriminators::TRADE_EVENT, &data, metadata, true)
644 .expect("legacy tail-compatible trade event");
645
646 match event {
647 DexEvent::PumpFunBuyExactSolIn(t) => {
648 assert_eq!(t.sol_amount, 1_000);
649 assert_eq!(t.token_amount, 2_000);
650 assert_eq!(t.ix_name, "buy_exact_sol_in");
651 assert!(t.track_volume);
652 assert!(t.mayhem_mode);
653 assert_eq!(t.cashback_fee_basis_points, 130);
654 assert_eq!(t.cashback, 140);
655 assert!(t.is_created_buy);
656 assert_eq!(t.buyback_fee_basis_points, 0);
657 assert!(t.shareholders.is_empty());
658 assert_eq!(t.quote_mint, Pubkey::default());
659 }
660 other => panic!("expected exact buy trade, got {other:?}"),
661 }
662 }
663}