1use crate::core::events::*;
14use memchr::memmem;
15use once_cell::sync::Lazy;
16use solana_sdk::{pubkey::Pubkey, signature::Signature};
17
18#[cfg(feature = "perf-stats")]
19use std::sync::atomic::{AtomicUsize, Ordering};
20
21#[cfg(feature = "perf-stats")]
26pub static PARSE_COUNT: AtomicUsize = AtomicUsize::new(0);
27#[cfg(feature = "perf-stats")]
28pub static PARSE_TIME_NS: AtomicUsize = AtomicUsize::new(0);
29
30pub mod discriminators {
36 pub const BUY: u64 = u64::from_le_bytes([103, 244, 82, 31, 44, 245, 119, 119]); pub const SELL: u64 = u64::from_le_bytes([62, 47, 55, 10, 165, 3, 220, 42]); pub const CREATE_POOL: u64 = u64::from_le_bytes([177, 49, 12, 210, 160, 118, 167, 116]); pub const ADD_LIQUIDITY: u64 = u64::from_le_bytes([120, 248, 61, 83, 31, 142, 107, 144]); pub const REMOVE_LIQUIDITY: u64 = u64::from_le_bytes([22, 9, 133, 26, 160, 44, 71, 192]);
43 }
45
46static BASE64_FINDER: Lazy<memmem::Finder> = Lazy::new(|| memmem::Finder::new(b"Program data: "));
48
49#[inline(always)]
51fn copy_b64_skip_ws_prefix(src: &[u8], out: &mut [u8], max_copy: usize) -> Option<usize> {
52 let cap = max_copy.min(out.len());
53 let mut j = 0usize;
54 for &b in src {
55 if b.is_ascii_whitespace() {
56 continue;
57 }
58 if j >= cap {
59 break;
60 }
61 out[j] = b;
62 j += 1;
63 }
64 if j < 12 {
65 return None;
66 }
67 Some(j)
68}
69
70#[inline(always)]
79fn extract_program_data_zero_copy<'a>(log: &'a str, buf: &'a mut [u8; 2048]) -> Option<&'a [u8]> {
80 let log_bytes = log.as_bytes();
81 let pos = BASE64_FINDER.find(log_bytes)?;
82
83 let data_part = &log[pos + 14..];
84 let trimmed = data_part.trim();
85 let body = trimmed.as_bytes();
86
87 if body.len() > 2700 {
88 return None;
89 }
90
91 use base64_simd::AsOut;
92 const COMPACT_CAP: usize = 2730;
93 let decoded_slice = if body.iter().any(|&b| b.is_ascii_whitespace()) {
94 let mut compact = [0u8; COMPACT_CAP];
95 let n = copy_b64_skip_ws_prefix(body, &mut compact, COMPACT_CAP)?;
96 base64_simd::STANDARD.decode(&compact[..n], buf.as_mut().as_out()).ok()?
97 } else {
98 base64_simd::STANDARD.decode(body, buf.as_mut().as_out()).ok()?
99 };
100
101 Some(decoded_slice)
102}
103
104#[inline(always)]
106fn extract_discriminator_simd(log: &str) -> Option<u64> {
107 let log_bytes = log.as_bytes();
108 let pos = BASE64_FINDER.find(log_bytes)?;
109
110 let data_part = &log[pos + 14..];
111 let body = data_part.trim().as_bytes();
112
113 use base64_simd::AsOut;
114 let mut compact = [0u8; 24];
115 let n = copy_b64_skip_ws_prefix(body, &mut compact, 16)?;
116 let mut dec = [0u8; 12];
117 base64_simd::STANDARD.decode(&compact[..n], dec.as_mut().as_out()).ok()?;
118
119 unsafe {
120 let ptr = dec.as_ptr() as *const u64;
121 Some(ptr.read_unaligned())
122 }
123}
124
125#[inline(always)]
131unsafe fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
132 let ptr = data.as_ptr().add(offset) as *const u64;
133 u64::from_le(ptr.read_unaligned())
134}
135
136#[inline(always)]
138unsafe fn read_i64_unchecked(data: &[u8], offset: usize) -> i64 {
139 let ptr = data.as_ptr().add(offset) as *const i64;
140 i64::from_le(ptr.read_unaligned())
141}
142
143#[inline(always)]
145unsafe fn read_u16_unchecked(data: &[u8], offset: usize) -> u16 {
146 let ptr = data.as_ptr().add(offset) as *const u16;
147 u16::from_le(ptr.read_unaligned())
148}
149
150#[allow(dead_code)]
152#[inline(always)]
153unsafe fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
154 let ptr = data.as_ptr().add(offset) as *const u32;
155 u32::from_le(ptr.read_unaligned())
156}
157
158#[inline(always)]
160unsafe fn read_u8_unchecked(data: &[u8], offset: usize) -> u8 {
161 *data.get_unchecked(offset)
162}
163
164#[inline(always)]
166unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
167 *data.get_unchecked(offset) == 1
168}
169
170#[inline(always)]
174unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> Pubkey {
175 #[cfg(target_arch = "x86_64")]
178 {
179 use std::arch::x86_64::_mm_prefetch;
180 use std::arch::x86_64::_MM_HINT_T0;
181 if offset + 64 < data.len() {
182 _mm_prefetch((data.as_ptr().add(offset + 32)) as *const i8, _MM_HINT_T0);
183 }
184 }
185
186 let ptr = data.as_ptr().add(offset);
187 let mut bytes = [0u8; 32];
188 std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
189 Pubkey::new_from_array(bytes)
190}
191
192#[inline(always)]
200pub fn parse_log(
201 log: &str,
202 signature: Signature,
203 slot: u64,
204 tx_index: u64,
205 block_time_us: Option<i64>,
206 grpc_recv_us: i64,
207) -> Option<DexEvent> {
208 #[cfg(feature = "perf-stats")]
209 let start = std::time::Instant::now();
210
211 let mut buf = [0u8; 2048];
213 let program_data = extract_program_data_zero_copy(log, &mut buf)?;
214
215 if program_data.len() < 8 {
216 return None;
217 }
218
219 let discriminator = unsafe { read_u64_unchecked(program_data, 0) };
221 let data = &program_data[8..];
222
223 let result = match discriminator {
224 discriminators::BUY => {
225 parse_buy_event_optimized(data, signature, slot, tx_index, block_time_us, grpc_recv_us)
226 }
227 discriminators::SELL => {
228 parse_sell_event_optimized(data, signature, slot, tx_index, block_time_us, grpc_recv_us)
229 }
230 discriminators::CREATE_POOL => parse_create_pool_event_optimized(
231 data,
232 signature,
233 slot,
234 tx_index,
235 block_time_us,
236 grpc_recv_us,
237 ),
238 discriminators::ADD_LIQUIDITY => parse_add_liquidity_event_optimized(
239 data,
240 signature,
241 slot,
242 tx_index,
243 block_time_us,
244 grpc_recv_us,
245 ),
246 discriminators::REMOVE_LIQUIDITY => parse_remove_liquidity_event_optimized(
247 data,
248 signature,
249 slot,
250 tx_index,
251 block_time_us,
252 grpc_recv_us,
253 ),
254 _ => None,
255 };
256
257 #[cfg(feature = "perf-stats")]
258 {
259 PARSE_COUNT.fetch_add(1, Ordering::Relaxed);
260 PARSE_TIME_NS.fetch_add(start.elapsed().as_nanos() as usize, Ordering::Relaxed);
261 }
262
263 result
264}
265
266#[inline(always)]
273fn parse_buy_event_optimized(
274 data: &[u8],
275 signature: Signature,
276 slot: u64,
277 tx_index: u64,
278 block_time_us: Option<i64>,
279 grpc_recv_us: i64,
280) -> Option<DexEvent> {
281 const MIN_REQUIRED_LEN: usize = 16 * 8 + 7 * 32 + 1 + 5 * 8 + 4;
283 if data.len() < MIN_REQUIRED_LEN {
284 return None;
285 }
286
287 unsafe {
288 let timestamp = read_i64_unchecked(data, 0);
289 let base_amount_out = read_u64_unchecked(data, 8);
290 let max_quote_amount_in = read_u64_unchecked(data, 16);
291 let user_base_token_reserves = read_u64_unchecked(data, 24);
292 let user_quote_token_reserves = read_u64_unchecked(data, 32);
293 let pool_base_token_reserves = read_u64_unchecked(data, 40);
294 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
295 let quote_amount_in = read_u64_unchecked(data, 56);
296 let lp_fee_basis_points = read_u64_unchecked(data, 64);
297 let lp_fee = read_u64_unchecked(data, 72);
298 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
299 let protocol_fee = read_u64_unchecked(data, 88);
300 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
301 let user_quote_amount_in = read_u64_unchecked(data, 104);
302
303 let pool = read_pubkey_unchecked(data, 112);
304 let user = read_pubkey_unchecked(data, 144);
305 let user_base_token_account = read_pubkey_unchecked(data, 176);
306 let user_quote_token_account = read_pubkey_unchecked(data, 208);
307 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
308 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
309 let coin_creator = read_pubkey_unchecked(data, 304);
310
311 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
312 let coin_creator_fee = read_u64_unchecked(data, 344);
313 let track_volume = read_bool_unchecked(data, 352);
314 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
315 let total_claimed_tokens = read_u64_unchecked(data, 361);
316 let current_sol_volume = read_u64_unchecked(data, 369);
317 let last_update_timestamp = read_i64_unchecked(data, 377);
318
319 let mut offset = 385;
321 let min_base_amount_out = read_u64_unchecked(data, offset);
322 offset += 8;
323
324 let ix_name = if offset + 4 <= data.len() {
326 let len = read_u32_unchecked(data, offset) as usize;
327 offset += 4;
328 if offset + len <= data.len() {
329 let string_bytes = &data[offset..offset + len];
330 let s = std::str::from_utf8_unchecked(string_bytes);
331 offset += len;
332 s.to_string()
333 } else {
334 String::new()
335 }
336 } else {
337 String::new()
338 };
339
340 let cashback_fee_basis_points =
342 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
343 offset += 8;
344 let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
345
346 let metadata = EventMetadata {
347 signature,
348 slot,
349 tx_index,
350 block_time_us: block_time_us.unwrap_or(0),
351 grpc_recv_us,
352 recent_blockhash: None,
353 };
354
355 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
356 metadata,
357 timestamp,
358 base_amount_out,
359 max_quote_amount_in,
360 user_base_token_reserves,
361 user_quote_token_reserves,
362 pool_base_token_reserves,
363 pool_quote_token_reserves,
364 quote_amount_in,
365 lp_fee_basis_points,
366 lp_fee,
367 protocol_fee_basis_points,
368 protocol_fee,
369 quote_amount_in_with_lp_fee,
370 user_quote_amount_in,
371 pool,
372 user,
373 user_base_token_account,
374 user_quote_token_account,
375 protocol_fee_recipient,
376 protocol_fee_recipient_token_account,
377 coin_creator,
378 coin_creator_fee_basis_points,
379 coin_creator_fee,
380 track_volume,
381 total_unclaimed_tokens,
382 total_claimed_tokens,
383 current_sol_volume,
384 last_update_timestamp,
385 min_base_amount_out,
386 ix_name,
387 cashback_fee_basis_points,
388 cashback,
389 ..Default::default()
390 }))
391 }
392}
393
394#[inline(always)]
396fn parse_sell_event_optimized(
397 data: &[u8],
398 signature: Signature,
399 slot: u64,
400 tx_index: u64,
401 block_time_us: Option<i64>,
402 grpc_recv_us: i64,
403) -> Option<DexEvent> {
404 const REQUIRED_LEN: usize = 13 * 8 + 8 + 7 * 32 + 8 + 8;
406 if data.len() < REQUIRED_LEN {
407 return None;
408 }
409
410 unsafe {
411 let timestamp = read_i64_unchecked(data, 0);
412 let base_amount_in = read_u64_unchecked(data, 8);
413 let min_quote_amount_out = read_u64_unchecked(data, 16);
414 let user_base_token_reserves = read_u64_unchecked(data, 24);
415 let user_quote_token_reserves = read_u64_unchecked(data, 32);
416 let pool_base_token_reserves = read_u64_unchecked(data, 40);
417 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
418 let quote_amount_out = read_u64_unchecked(data, 56);
419 let lp_fee_basis_points = read_u64_unchecked(data, 64);
420 let lp_fee = read_u64_unchecked(data, 72);
421 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
422 let protocol_fee = read_u64_unchecked(data, 88);
423 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
424 let user_quote_amount_out = read_u64_unchecked(data, 104);
425
426 let pool = read_pubkey_unchecked(data, 112);
427 let user = read_pubkey_unchecked(data, 144);
428 let user_base_token_account = read_pubkey_unchecked(data, 176);
429 let user_quote_token_account = read_pubkey_unchecked(data, 208);
430 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
431 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
432 let coin_creator = read_pubkey_unchecked(data, 304);
433
434 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
435 let coin_creator_fee = read_u64_unchecked(data, 344);
436 let cashback_fee_basis_points = read_u64_unchecked(data, 352);
438 let cashback = read_u64_unchecked(data, 360);
439
440 let metadata = EventMetadata {
441 signature,
442 slot,
443 tx_index,
444 block_time_us: block_time_us.unwrap_or(0),
445 grpc_recv_us,
446 recent_blockhash: None,
447 };
448
449 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
450 metadata,
451 timestamp,
452 base_amount_in,
453 min_quote_amount_out,
454 user_base_token_reserves,
455 user_quote_token_reserves,
456 pool_base_token_reserves,
457 pool_quote_token_reserves,
458 quote_amount_out,
459 lp_fee_basis_points,
460 lp_fee,
461 protocol_fee_basis_points,
462 protocol_fee,
463 quote_amount_out_without_lp_fee,
464 user_quote_amount_out,
465 pool,
466 user,
467 user_base_token_account,
468 user_quote_token_account,
469 protocol_fee_recipient,
470 protocol_fee_recipient_token_account,
471 coin_creator,
472 coin_creator_fee_basis_points,
473 coin_creator_fee,
474 cashback_fee_basis_points,
475 cashback,
476 ..Default::default()
477 }))
478 }
479}
480
481#[inline(always)]
483fn parse_create_pool_event_optimized(
484 data: &[u8],
485 signature: Signature,
486 slot: u64,
487 tx_index: u64,
488 block_time_us: Option<i64>,
489 grpc_recv_us: i64,
490) -> Option<DexEvent> {
491 const CREATE_POOL_EVENT_LEN: usize = 326;
493 const REQUIRED_LEN: usize = CREATE_POOL_EVENT_LEN;
494 if data.len() < REQUIRED_LEN {
495 return None;
496 }
497
498 unsafe {
499 let timestamp = read_i64_unchecked(data, 0);
500 let index = read_u16_unchecked(data, 8);
501
502 let creator = read_pubkey_unchecked(data, 10);
503 let base_mint = read_pubkey_unchecked(data, 42);
504 let quote_mint = read_pubkey_unchecked(data, 74);
505
506 let base_mint_decimals = read_u8_unchecked(data, 106);
507 let quote_mint_decimals = read_u8_unchecked(data, 107);
508
509 let base_amount_in = read_u64_unchecked(data, 108);
510 let quote_amount_in = read_u64_unchecked(data, 116);
511 let pool_base_amount = read_u64_unchecked(data, 124);
512 let pool_quote_amount = read_u64_unchecked(data, 132);
513 let minimum_liquidity = read_u64_unchecked(data, 140);
514 let initial_liquidity = read_u64_unchecked(data, 148);
515 let lp_token_amount_out = read_u64_unchecked(data, 156);
516
517 let pool_bump = read_u8_unchecked(data, 164);
518
519 let pool = read_pubkey_unchecked(data, 165);
520 let lp_mint = read_pubkey_unchecked(data, 197);
521 let user_base_token_account = read_pubkey_unchecked(data, 229);
522 let user_quote_token_account = read_pubkey_unchecked(data, 261);
523 let coin_creator = read_pubkey_unchecked(data, 293);
524 let is_mayhem_mode = read_bool_unchecked(data, 325);
525
526 let metadata = EventMetadata {
527 signature,
528 slot,
529 tx_index,
530 block_time_us: block_time_us.unwrap_or(0),
531 grpc_recv_us,
532 recent_blockhash: None,
533 };
534
535 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
536 metadata,
537 timestamp,
538 index,
539 creator,
540 base_mint,
541 quote_mint,
542 base_mint_decimals,
543 quote_mint_decimals,
544 base_amount_in,
545 quote_amount_in,
546 pool_base_amount,
547 pool_quote_amount,
548 minimum_liquidity,
549 initial_liquidity,
550 lp_token_amount_out,
551 pool_bump,
552 pool,
553 lp_mint,
554 user_base_token_account,
555 user_quote_token_account,
556 coin_creator,
557 is_mayhem_mode,
558 is_cashback_coin: false,
559 }))
560 }
561}
562
563#[inline(always)]
565fn parse_add_liquidity_event_optimized(
566 data: &[u8],
567 signature: Signature,
568 slot: u64,
569 tx_index: u64,
570 block_time_us: Option<i64>,
571 grpc_recv_us: i64,
572) -> Option<DexEvent> {
573 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
574 if data.len() < REQUIRED_LEN {
575 return None;
576 }
577
578 unsafe {
579 let timestamp = read_i64_unchecked(data, 0);
580 let lp_token_amount_out = read_u64_unchecked(data, 8);
581 let max_base_amount_in = read_u64_unchecked(data, 16);
582 let max_quote_amount_in = read_u64_unchecked(data, 24);
583 let user_base_token_reserves = read_u64_unchecked(data, 32);
584 let user_quote_token_reserves = read_u64_unchecked(data, 40);
585 let pool_base_token_reserves = read_u64_unchecked(data, 48);
586 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
587 let base_amount_in = read_u64_unchecked(data, 64);
588 let quote_amount_in = read_u64_unchecked(data, 72);
589 let lp_mint_supply = read_u64_unchecked(data, 80);
590
591 let pool = read_pubkey_unchecked(data, 88);
592 let user = read_pubkey_unchecked(data, 120);
593 let user_base_token_account = read_pubkey_unchecked(data, 152);
594 let user_quote_token_account = read_pubkey_unchecked(data, 184);
595 let user_pool_token_account = read_pubkey_unchecked(data, 216);
596
597 let metadata = EventMetadata {
598 signature,
599 slot,
600 tx_index,
601 block_time_us: block_time_us.unwrap_or(0),
602 grpc_recv_us,
603 recent_blockhash: None,
604 };
605
606 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
607 metadata,
608 timestamp,
609 lp_token_amount_out,
610 max_base_amount_in,
611 max_quote_amount_in,
612 user_base_token_reserves,
613 user_quote_token_reserves,
614 pool_base_token_reserves,
615 pool_quote_token_reserves,
616 base_amount_in,
617 quote_amount_in,
618 lp_mint_supply,
619 pool,
620 user,
621 user_base_token_account,
622 user_quote_token_account,
623 user_pool_token_account,
624 }))
625 }
626}
627
628#[inline(always)]
630fn parse_remove_liquidity_event_optimized(
631 data: &[u8],
632 signature: Signature,
633 slot: u64,
634 tx_index: u64,
635 block_time_us: Option<i64>,
636 grpc_recv_us: i64,
637) -> Option<DexEvent> {
638 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
639 if data.len() < REQUIRED_LEN {
640 return None;
641 }
642
643 unsafe {
644 let timestamp = read_i64_unchecked(data, 0);
645 let lp_token_amount_in = read_u64_unchecked(data, 8);
646 let min_base_amount_out = read_u64_unchecked(data, 16);
647 let min_quote_amount_out = read_u64_unchecked(data, 24);
648 let user_base_token_reserves = read_u64_unchecked(data, 32);
649 let user_quote_token_reserves = read_u64_unchecked(data, 40);
650 let pool_base_token_reserves = read_u64_unchecked(data, 48);
651 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
652 let base_amount_out = read_u64_unchecked(data, 64);
653 let quote_amount_out = read_u64_unchecked(data, 72);
654 let lp_mint_supply = read_u64_unchecked(data, 80);
655
656 let pool = read_pubkey_unchecked(data, 88);
657 let user = read_pubkey_unchecked(data, 120);
658 let user_base_token_account = read_pubkey_unchecked(data, 152);
659 let user_quote_token_account = read_pubkey_unchecked(data, 184);
660 let user_pool_token_account = read_pubkey_unchecked(data, 216);
661
662 let metadata = EventMetadata {
663 signature,
664 slot,
665 tx_index,
666 block_time_us: block_time_us.unwrap_or(0),
667 grpc_recv_us,
668 recent_blockhash: None,
669 };
670
671 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
672 metadata,
673 timestamp,
674 lp_token_amount_in,
675 min_base_amount_out,
676 min_quote_amount_out,
677 user_base_token_reserves,
678 user_quote_token_reserves,
679 pool_base_token_reserves,
680 pool_quote_token_reserves,
681 base_amount_out,
682 quote_amount_out,
683 lp_mint_supply,
684 pool,
685 user,
686 user_base_token_account,
687 user_quote_token_account,
688 user_pool_token_account,
689 }))
690 }
691}
692
693#[inline(always)]
701pub fn get_event_type_fast(log: &str) -> Option<u64> {
702 extract_discriminator_simd(log)
703}
704
705#[inline(always)]
707pub fn is_event_type(log: &str, discriminator: u64) -> bool {
708 extract_discriminator_simd(log) == Some(discriminator)
709}
710
711#[inline(always)]
718pub fn parse_buy_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
719 const MIN_REQUIRED_LEN: usize = 16 * 8 + 7 * 32 + 1 + 5 * 8 + 4;
721 if data.len() < MIN_REQUIRED_LEN {
722 return None;
723 }
724
725 unsafe {
726 let timestamp = read_i64_unchecked(data, 0);
727 let base_amount_out = read_u64_unchecked(data, 8);
728 let max_quote_amount_in = read_u64_unchecked(data, 16);
729 let user_base_token_reserves = read_u64_unchecked(data, 24);
730 let user_quote_token_reserves = read_u64_unchecked(data, 32);
731 let pool_base_token_reserves = read_u64_unchecked(data, 40);
732 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
733 let quote_amount_in = read_u64_unchecked(data, 56);
734 let lp_fee_basis_points = read_u64_unchecked(data, 64);
735 let lp_fee = read_u64_unchecked(data, 72);
736 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
737 let protocol_fee = read_u64_unchecked(data, 88);
738 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
739 let user_quote_amount_in = read_u64_unchecked(data, 104);
740
741 let pool = read_pubkey_unchecked(data, 112);
742 let user = read_pubkey_unchecked(data, 144);
743 let user_base_token_account = read_pubkey_unchecked(data, 176);
744 let user_quote_token_account = read_pubkey_unchecked(data, 208);
745 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
746 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
747 let coin_creator = read_pubkey_unchecked(data, 304);
748
749 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
750 let coin_creator_fee = read_u64_unchecked(data, 344);
751 let track_volume = read_bool_unchecked(data, 352);
752 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
753 let total_claimed_tokens = read_u64_unchecked(data, 361);
754 let current_sol_volume = read_u64_unchecked(data, 369);
755 let last_update_timestamp = read_i64_unchecked(data, 377);
756
757 let mut offset = 385;
759 let min_base_amount_out = read_u64_unchecked(data, offset);
760 offset += 8;
761
762 let ix_name = if offset + 4 <= data.len() {
764 let len = read_u32_unchecked(data, offset) as usize;
765 offset += 4;
766 if offset + len <= data.len() {
767 let string_bytes = &data[offset..offset + len];
768 let s = std::str::from_utf8_unchecked(string_bytes);
769 s.to_string()
770 } else {
771 String::new()
772 }
773 } else {
774 String::new()
775 };
776
777 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
778 metadata,
779 timestamp,
780 base_amount_out,
781 max_quote_amount_in,
782 user_base_token_reserves,
783 user_quote_token_reserves,
784 pool_base_token_reserves,
785 pool_quote_token_reserves,
786 quote_amount_in,
787 lp_fee_basis_points,
788 lp_fee,
789 protocol_fee_basis_points,
790 protocol_fee,
791 quote_amount_in_with_lp_fee,
792 user_quote_amount_in,
793 pool,
794 user,
795 user_base_token_account,
796 user_quote_token_account,
797 protocol_fee_recipient,
798 protocol_fee_recipient_token_account,
799 coin_creator,
800 coin_creator_fee_basis_points,
801 coin_creator_fee,
802 track_volume,
803 total_unclaimed_tokens,
804 total_claimed_tokens,
805 current_sol_volume,
806 last_update_timestamp,
807 min_base_amount_out,
808 ix_name,
809 ..Default::default()
810 }))
811 }
812}
813
814#[inline(always)]
816pub fn parse_sell_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
817 const REQUIRED_LEN: usize = 13 * 8 + 8 + 7 * 32;
818 const CASHBACK_FEE_BASIS_POINTS_OFFSET: usize = 352;
819 const CASHBACK_OFFSET: usize = 360;
820 const CASHBACK_FIELDS_LEN: usize = 16;
821 if data.len() < REQUIRED_LEN {
822 return None;
823 }
824
825 unsafe {
826 let timestamp = read_i64_unchecked(data, 0);
827 let base_amount_in = read_u64_unchecked(data, 8);
828 let min_quote_amount_out = read_u64_unchecked(data, 16);
829 let user_base_token_reserves = read_u64_unchecked(data, 24);
830 let user_quote_token_reserves = read_u64_unchecked(data, 32);
831 let pool_base_token_reserves = read_u64_unchecked(data, 40);
832 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
833 let quote_amount_out = read_u64_unchecked(data, 56);
834 let lp_fee_basis_points = read_u64_unchecked(data, 64);
835 let lp_fee = read_u64_unchecked(data, 72);
836 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
837 let protocol_fee = read_u64_unchecked(data, 88);
838 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
839 let user_quote_amount_out = read_u64_unchecked(data, 104);
840
841 let pool = read_pubkey_unchecked(data, 112);
842 let user = read_pubkey_unchecked(data, 144);
843 let user_base_token_account = read_pubkey_unchecked(data, 176);
844 let user_quote_token_account = read_pubkey_unchecked(data, 208);
845 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
846 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
847 let coin_creator = read_pubkey_unchecked(data, 304);
848
849 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
850 let coin_creator_fee = read_u64_unchecked(data, 344);
851 let (cashback_fee_basis_points, cashback) =
852 if data.len() >= CASHBACK_FEE_BASIS_POINTS_OFFSET + CASHBACK_FIELDS_LEN {
853 (
854 read_u64_unchecked(data, CASHBACK_FEE_BASIS_POINTS_OFFSET),
855 read_u64_unchecked(data, CASHBACK_OFFSET),
856 )
857 } else {
858 (0, 0)
859 };
860
861 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
862 metadata,
863 timestamp,
864 base_amount_in,
865 min_quote_amount_out,
866 user_base_token_reserves,
867 user_quote_token_reserves,
868 pool_base_token_reserves,
869 pool_quote_token_reserves,
870 quote_amount_out,
871 lp_fee_basis_points,
872 lp_fee,
873 protocol_fee_basis_points,
874 protocol_fee,
875 quote_amount_out_without_lp_fee,
876 user_quote_amount_out,
877 pool,
878 user,
879 user_base_token_account,
880 user_quote_token_account,
881 protocol_fee_recipient,
882 protocol_fee_recipient_token_account,
883 coin_creator,
884 coin_creator_fee_basis_points,
885 coin_creator_fee,
886 cashback_fee_basis_points,
887 cashback,
888 ..Default::default()
889 }))
890 }
891}
892
893#[inline(always)]
895pub fn parse_create_pool_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
896 const CREATE_POOL_EVENT_LEN: usize = 326;
897 const REQUIRED_LEN: usize = CREATE_POOL_EVENT_LEN;
898 if data.len() < REQUIRED_LEN {
899 return None;
900 }
901
902 unsafe {
903 let timestamp = read_i64_unchecked(data, 0);
904 let index = read_u16_unchecked(data, 8);
905
906 let creator = read_pubkey_unchecked(data, 10);
907 let base_mint = read_pubkey_unchecked(data, 42);
908 let quote_mint = read_pubkey_unchecked(data, 74);
909
910 let base_mint_decimals = read_u8_unchecked(data, 106);
911 let quote_mint_decimals = read_u8_unchecked(data, 107);
912
913 let base_amount_in = read_u64_unchecked(data, 108);
914 let quote_amount_in = read_u64_unchecked(data, 116);
915 let pool_base_amount = read_u64_unchecked(data, 124);
916 let pool_quote_amount = read_u64_unchecked(data, 132);
917 let minimum_liquidity = read_u64_unchecked(data, 140);
918 let initial_liquidity = read_u64_unchecked(data, 148);
919 let lp_token_amount_out = read_u64_unchecked(data, 156);
920
921 let pool_bump = read_u8_unchecked(data, 164);
922
923 let pool = read_pubkey_unchecked(data, 165);
924 let lp_mint = read_pubkey_unchecked(data, 197);
925 let user_base_token_account = read_pubkey_unchecked(data, 229);
926 let user_quote_token_account = read_pubkey_unchecked(data, 261);
927 let coin_creator = read_pubkey_unchecked(data, 293);
928 let is_mayhem_mode = data.len() > 325 && read_bool_unchecked(data, 325);
929
930 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
931 metadata,
932 timestamp,
933 index,
934 creator,
935 base_mint,
936 quote_mint,
937 base_mint_decimals,
938 quote_mint_decimals,
939 base_amount_in,
940 quote_amount_in,
941 pool_base_amount,
942 pool_quote_amount,
943 minimum_liquidity,
944 initial_liquidity,
945 lp_token_amount_out,
946 pool_bump,
947 pool,
948 lp_mint,
949 user_base_token_account,
950 user_quote_token_account,
951 coin_creator,
952 is_mayhem_mode,
953 is_cashback_coin: false,
954 }))
955 }
956}
957
958#[inline(always)]
960pub fn parse_add_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
961 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
962 if data.len() < REQUIRED_LEN {
963 return None;
964 }
965
966 unsafe {
967 let timestamp = read_i64_unchecked(data, 0);
968 let lp_token_amount_out = read_u64_unchecked(data, 8);
969 let max_base_amount_in = read_u64_unchecked(data, 16);
970 let max_quote_amount_in = read_u64_unchecked(data, 24);
971 let user_base_token_reserves = read_u64_unchecked(data, 32);
972 let user_quote_token_reserves = read_u64_unchecked(data, 40);
973 let pool_base_token_reserves = read_u64_unchecked(data, 48);
974 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
975 let base_amount_in = read_u64_unchecked(data, 64);
976 let quote_amount_in = read_u64_unchecked(data, 72);
977 let lp_mint_supply = read_u64_unchecked(data, 80);
978
979 let pool = read_pubkey_unchecked(data, 88);
980 let user = read_pubkey_unchecked(data, 120);
981 let user_base_token_account = read_pubkey_unchecked(data, 152);
982 let user_quote_token_account = read_pubkey_unchecked(data, 184);
983 let user_pool_token_account = read_pubkey_unchecked(data, 216);
984
985 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
986 metadata,
987 timestamp,
988 lp_token_amount_out,
989 max_base_amount_in,
990 max_quote_amount_in,
991 user_base_token_reserves,
992 user_quote_token_reserves,
993 pool_base_token_reserves,
994 pool_quote_token_reserves,
995 base_amount_in,
996 quote_amount_in,
997 lp_mint_supply,
998 pool,
999 user,
1000 user_base_token_account,
1001 user_quote_token_account,
1002 user_pool_token_account,
1003 }))
1004 }
1005}
1006
1007#[inline(always)]
1009pub fn parse_remove_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
1010 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
1011 if data.len() < REQUIRED_LEN {
1012 return None;
1013 }
1014
1015 unsafe {
1016 let timestamp = read_i64_unchecked(data, 0);
1017 let lp_token_amount_in = read_u64_unchecked(data, 8);
1018 let min_base_amount_out = read_u64_unchecked(data, 16);
1019 let min_quote_amount_out = read_u64_unchecked(data, 24);
1020 let user_base_token_reserves = read_u64_unchecked(data, 32);
1021 let user_quote_token_reserves = read_u64_unchecked(data, 40);
1022 let pool_base_token_reserves = read_u64_unchecked(data, 48);
1023 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
1024 let base_amount_out = read_u64_unchecked(data, 64);
1025 let quote_amount_out = read_u64_unchecked(data, 72);
1026 let lp_mint_supply = read_u64_unchecked(data, 80);
1027
1028 let pool = read_pubkey_unchecked(data, 88);
1029 let user = read_pubkey_unchecked(data, 120);
1030 let user_base_token_account = read_pubkey_unchecked(data, 152);
1031 let user_quote_token_account = read_pubkey_unchecked(data, 184);
1032 let user_pool_token_account = read_pubkey_unchecked(data, 216);
1033
1034 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
1035 metadata,
1036 timestamp,
1037 lp_token_amount_in,
1038 min_base_amount_out,
1039 min_quote_amount_out,
1040 user_base_token_reserves,
1041 user_quote_token_reserves,
1042 pool_base_token_reserves,
1043 pool_quote_token_reserves,
1044 base_amount_out,
1045 quote_amount_out,
1046 lp_mint_supply,
1047 pool,
1048 user,
1049 user_base_token_account,
1050 user_quote_token_account,
1051 user_pool_token_account,
1052 }))
1053 }
1054}
1055
1056#[cfg(feature = "perf-stats")]
1061pub fn get_perf_stats() -> (usize, usize) {
1062 let count = PARSE_COUNT.load(Ordering::Relaxed);
1063 let total_ns = PARSE_TIME_NS.load(Ordering::Relaxed);
1064 (count, total_ns)
1065}
1066
1067#[cfg(feature = "perf-stats")]
1068pub fn reset_perf_stats() {
1069 PARSE_COUNT.store(0, Ordering::Relaxed);
1070 PARSE_TIME_NS.store(0, Ordering::Relaxed);
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075 use super::*;
1076 use solana_sdk::{pubkey::Pubkey, signature::Signature};
1077
1078 fn metadata() -> EventMetadata {
1079 EventMetadata {
1080 signature: Signature::default(),
1081 slot: 0,
1082 tx_index: 0,
1083 block_time_us: 0,
1084 grpc_recv_us: 0,
1085 recent_blockhash: None,
1086 }
1087 }
1088
1089 fn write_u64(buf: &mut [u8], offset: usize, value: u64) {
1090 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1091 }
1092
1093 fn write_i64(buf: &mut [u8], offset: usize, value: i64) {
1094 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1095 }
1096
1097 fn write_pubkey(buf: &mut [u8], offset: usize, value: Pubkey) {
1098 buf[offset..offset + 32].copy_from_slice(value.as_ref());
1099 }
1100
1101 fn build_sell_payload(include_cashback: bool) -> Vec<u8> {
1102 let len = if include_cashback { 368 } else { 352 };
1103 let mut data = vec![0u8; len];
1104
1105 write_i64(&mut data, 0, 1_713_498_953);
1106 write_u64(&mut data, 8, 11);
1107 write_u64(&mut data, 16, 22);
1108 write_u64(&mut data, 24, 33);
1109 write_u64(&mut data, 32, 44);
1110 write_u64(&mut data, 40, 55);
1111 write_u64(&mut data, 48, 66);
1112 write_u64(&mut data, 56, 77);
1113 write_u64(&mut data, 64, 88);
1114 write_u64(&mut data, 72, 99);
1115 write_u64(&mut data, 80, 111);
1116 write_u64(&mut data, 88, 122);
1117 write_u64(&mut data, 96, 133);
1118 write_u64(&mut data, 104, 144);
1119
1120 write_pubkey(&mut data, 112, Pubkey::new_from_array([1; 32]));
1121 write_pubkey(&mut data, 144, Pubkey::new_from_array([2; 32]));
1122 write_pubkey(&mut data, 176, Pubkey::new_from_array([3; 32]));
1123 write_pubkey(&mut data, 208, Pubkey::new_from_array([4; 32]));
1124 write_pubkey(&mut data, 240, Pubkey::new_from_array([5; 32]));
1125 write_pubkey(&mut data, 272, Pubkey::new_from_array([6; 32]));
1126 write_pubkey(&mut data, 304, Pubkey::new_from_array([7; 32]));
1127
1128 write_u64(&mut data, 336, 155);
1129 write_u64(&mut data, 344, 166);
1130
1131 if include_cashback {
1132 write_u64(&mut data, 352, 177);
1133 write_u64(&mut data, 360, 188);
1134 }
1135
1136 data
1137 }
1138
1139 fn build_create_pool_payload(is_mayhem_mode: bool) -> Vec<u8> {
1140 let mut data = vec![0u8; 326];
1141
1142 write_i64(&mut data, 0, 1_713_498_953);
1143 data[8..10].copy_from_slice(&42u16.to_le_bytes());
1144 write_pubkey(&mut data, 10, Pubkey::new_from_array([1; 32]));
1145 write_pubkey(&mut data, 42, Pubkey::new_from_array([2; 32]));
1146 write_pubkey(&mut data, 74, Pubkey::new_from_array([3; 32]));
1147 data[106] = 6;
1148 data[107] = 9;
1149 write_u64(&mut data, 108, 11);
1150 write_u64(&mut data, 116, 22);
1151 write_u64(&mut data, 124, 33);
1152 write_u64(&mut data, 132, 44);
1153 write_u64(&mut data, 140, 55);
1154 write_u64(&mut data, 148, 66);
1155 write_u64(&mut data, 156, 77);
1156 data[164] = 8;
1157 write_pubkey(&mut data, 165, Pubkey::new_from_array([4; 32]));
1158 write_pubkey(&mut data, 197, Pubkey::new_from_array([5; 32]));
1159 write_pubkey(&mut data, 229, Pubkey::new_from_array([6; 32]));
1160 write_pubkey(&mut data, 261, Pubkey::new_from_array([7; 32]));
1161 write_pubkey(&mut data, 293, Pubkey::new_from_array([8; 32]));
1162 data[325] = u8::from(is_mayhem_mode);
1163
1164 data
1165 }
1166
1167 #[test]
1168 fn test_discriminator_simd() {
1169 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1171 let disc = extract_discriminator_simd(log);
1172 assert!(disc.is_some());
1173 }
1174
1175 #[test]
1176 fn test_parse_performance() {
1177 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1179 let sig = Signature::default();
1180
1181 let start = std::time::Instant::now();
1182 for _ in 0..1000 {
1183 let _ = parse_log(log, sig, 0, 0, Some(0), 0);
1184 }
1185 let elapsed = start.elapsed();
1186
1187 println!("Average parse time: {} ns", elapsed.as_nanos() / 1000);
1188 }
1189
1190 #[test]
1191 fn parse_sell_from_data_preserves_cashback_fields() {
1192 let event = parse_sell_from_data(&build_sell_payload(true), metadata())
1193 .expect("expected pumpswap sell event");
1194
1195 let DexEvent::PumpSwapSell(event) = event else {
1196 panic!("expected PumpSwapSell event");
1197 };
1198
1199 assert_eq!(event.cashback_fee_basis_points, 177);
1200 assert_eq!(event.cashback, 188);
1201 assert_eq!(event.coin_creator_fee_basis_points, 155);
1202 assert_eq!(event.coin_creator_fee, 166);
1203 }
1204
1205 #[test]
1206 fn parse_sell_from_data_keeps_legacy_payload_compatible() {
1207 let event = parse_sell_from_data(&build_sell_payload(false), metadata())
1208 .expect("expected legacy pumpswap sell event");
1209
1210 let DexEvent::PumpSwapSell(event) = event else {
1211 panic!("expected PumpSwapSell event");
1212 };
1213
1214 assert_eq!(event.cashback_fee_basis_points, 0);
1215 assert_eq!(event.cashback, 0);
1216 assert_eq!(event.coin_creator_fee_basis_points, 155);
1217 assert_eq!(event.coin_creator_fee, 166);
1218 }
1219
1220 #[test]
1221 fn parse_buy_from_data_rejects_truncated_min_base_payload() {
1222 assert!(parse_buy_from_data(&vec![0u8; 396], metadata()).is_none());
1223 assert!(parse_buy_from_data(&vec![0u8; 397], metadata()).is_some());
1224 }
1225
1226 #[test]
1227 fn parse_create_pool_from_data_reads_mayhem_mode() {
1228 let event = parse_create_pool_from_data(&build_create_pool_payload(true), metadata())
1229 .expect("expected pumpswap create pool event");
1230
1231 let DexEvent::PumpSwapCreatePool(event) = event else {
1232 panic!("expected PumpSwapCreatePool event");
1233 };
1234
1235 assert_eq!(event.index, 42);
1236 assert!(event.is_mayhem_mode);
1237 assert!(!event.is_cashback_coin);
1238 }
1239}