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 }))
559 }
560}
561
562#[inline(always)]
564fn parse_add_liquidity_event_optimized(
565 data: &[u8],
566 signature: Signature,
567 slot: u64,
568 tx_index: u64,
569 block_time_us: Option<i64>,
570 grpc_recv_us: i64,
571) -> Option<DexEvent> {
572 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
573 if data.len() < REQUIRED_LEN {
574 return None;
575 }
576
577 unsafe {
578 let timestamp = read_i64_unchecked(data, 0);
579 let lp_token_amount_out = read_u64_unchecked(data, 8);
580 let max_base_amount_in = read_u64_unchecked(data, 16);
581 let max_quote_amount_in = read_u64_unchecked(data, 24);
582 let user_base_token_reserves = read_u64_unchecked(data, 32);
583 let user_quote_token_reserves = read_u64_unchecked(data, 40);
584 let pool_base_token_reserves = read_u64_unchecked(data, 48);
585 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
586 let base_amount_in = read_u64_unchecked(data, 64);
587 let quote_amount_in = read_u64_unchecked(data, 72);
588 let lp_mint_supply = read_u64_unchecked(data, 80);
589
590 let pool = read_pubkey_unchecked(data, 88);
591 let user = read_pubkey_unchecked(data, 120);
592 let user_base_token_account = read_pubkey_unchecked(data, 152);
593 let user_quote_token_account = read_pubkey_unchecked(data, 184);
594 let user_pool_token_account = read_pubkey_unchecked(data, 216);
595
596 let metadata = EventMetadata {
597 signature,
598 slot,
599 tx_index,
600 block_time_us: block_time_us.unwrap_or(0),
601 grpc_recv_us,
602 recent_blockhash: None,
603 };
604
605 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
606 metadata,
607 timestamp,
608 lp_token_amount_out,
609 max_base_amount_in,
610 max_quote_amount_in,
611 user_base_token_reserves,
612 user_quote_token_reserves,
613 pool_base_token_reserves,
614 pool_quote_token_reserves,
615 base_amount_in,
616 quote_amount_in,
617 lp_mint_supply,
618 pool,
619 user,
620 user_base_token_account,
621 user_quote_token_account,
622 user_pool_token_account,
623 }))
624 }
625}
626
627#[inline(always)]
629fn parse_remove_liquidity_event_optimized(
630 data: &[u8],
631 signature: Signature,
632 slot: u64,
633 tx_index: u64,
634 block_time_us: Option<i64>,
635 grpc_recv_us: i64,
636) -> Option<DexEvent> {
637 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
638 if data.len() < REQUIRED_LEN {
639 return None;
640 }
641
642 unsafe {
643 let timestamp = read_i64_unchecked(data, 0);
644 let lp_token_amount_in = read_u64_unchecked(data, 8);
645 let min_base_amount_out = read_u64_unchecked(data, 16);
646 let min_quote_amount_out = read_u64_unchecked(data, 24);
647 let user_base_token_reserves = read_u64_unchecked(data, 32);
648 let user_quote_token_reserves = read_u64_unchecked(data, 40);
649 let pool_base_token_reserves = read_u64_unchecked(data, 48);
650 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
651 let base_amount_out = read_u64_unchecked(data, 64);
652 let quote_amount_out = read_u64_unchecked(data, 72);
653 let lp_mint_supply = read_u64_unchecked(data, 80);
654
655 let pool = read_pubkey_unchecked(data, 88);
656 let user = read_pubkey_unchecked(data, 120);
657 let user_base_token_account = read_pubkey_unchecked(data, 152);
658 let user_quote_token_account = read_pubkey_unchecked(data, 184);
659 let user_pool_token_account = read_pubkey_unchecked(data, 216);
660
661 let metadata = EventMetadata {
662 signature,
663 slot,
664 tx_index,
665 block_time_us: block_time_us.unwrap_or(0),
666 grpc_recv_us,
667 recent_blockhash: None,
668 };
669
670 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
671 metadata,
672 timestamp,
673 lp_token_amount_in,
674 min_base_amount_out,
675 min_quote_amount_out,
676 user_base_token_reserves,
677 user_quote_token_reserves,
678 pool_base_token_reserves,
679 pool_quote_token_reserves,
680 base_amount_out,
681 quote_amount_out,
682 lp_mint_supply,
683 pool,
684 user,
685 user_base_token_account,
686 user_quote_token_account,
687 user_pool_token_account,
688 }))
689 }
690}
691
692#[inline(always)]
700pub fn get_event_type_fast(log: &str) -> Option<u64> {
701 extract_discriminator_simd(log)
702}
703
704#[inline(always)]
706pub fn is_event_type(log: &str, discriminator: u64) -> bool {
707 extract_discriminator_simd(log) == Some(discriminator)
708}
709
710#[inline(always)]
717pub fn parse_buy_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
718 const MIN_REQUIRED_LEN: usize = 16 * 8 + 7 * 32 + 1 + 5 * 8 + 4;
720 if data.len() < MIN_REQUIRED_LEN {
721 return None;
722 }
723
724 unsafe {
725 let timestamp = read_i64_unchecked(data, 0);
726 let base_amount_out = read_u64_unchecked(data, 8);
727 let max_quote_amount_in = read_u64_unchecked(data, 16);
728 let user_base_token_reserves = read_u64_unchecked(data, 24);
729 let user_quote_token_reserves = read_u64_unchecked(data, 32);
730 let pool_base_token_reserves = read_u64_unchecked(data, 40);
731 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
732 let quote_amount_in = read_u64_unchecked(data, 56);
733 let lp_fee_basis_points = read_u64_unchecked(data, 64);
734 let lp_fee = read_u64_unchecked(data, 72);
735 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
736 let protocol_fee = read_u64_unchecked(data, 88);
737 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
738 let user_quote_amount_in = read_u64_unchecked(data, 104);
739
740 let pool = read_pubkey_unchecked(data, 112);
741 let user = read_pubkey_unchecked(data, 144);
742 let user_base_token_account = read_pubkey_unchecked(data, 176);
743 let user_quote_token_account = read_pubkey_unchecked(data, 208);
744 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
745 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
746 let coin_creator = read_pubkey_unchecked(data, 304);
747
748 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
749 let coin_creator_fee = read_u64_unchecked(data, 344);
750 let track_volume = read_bool_unchecked(data, 352);
751 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
752 let total_claimed_tokens = read_u64_unchecked(data, 361);
753 let current_sol_volume = read_u64_unchecked(data, 369);
754 let last_update_timestamp = read_i64_unchecked(data, 377);
755
756 let mut offset = 385;
758 let min_base_amount_out = read_u64_unchecked(data, offset);
759 offset += 8;
760
761 let ix_name = if offset + 4 <= data.len() {
763 let len = read_u32_unchecked(data, offset) as usize;
764 offset += 4;
765 if offset + len <= data.len() {
766 let string_bytes = &data[offset..offset + len];
767 let s = std::str::from_utf8_unchecked(string_bytes);
768 s.to_string()
769 } else {
770 String::new()
771 }
772 } else {
773 String::new()
774 };
775
776 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
777 metadata,
778 timestamp,
779 base_amount_out,
780 max_quote_amount_in,
781 user_base_token_reserves,
782 user_quote_token_reserves,
783 pool_base_token_reserves,
784 pool_quote_token_reserves,
785 quote_amount_in,
786 lp_fee_basis_points,
787 lp_fee,
788 protocol_fee_basis_points,
789 protocol_fee,
790 quote_amount_in_with_lp_fee,
791 user_quote_amount_in,
792 pool,
793 user,
794 user_base_token_account,
795 user_quote_token_account,
796 protocol_fee_recipient,
797 protocol_fee_recipient_token_account,
798 coin_creator,
799 coin_creator_fee_basis_points,
800 coin_creator_fee,
801 track_volume,
802 total_unclaimed_tokens,
803 total_claimed_tokens,
804 current_sol_volume,
805 last_update_timestamp,
806 min_base_amount_out,
807 ix_name,
808 ..Default::default()
809 }))
810 }
811}
812
813#[inline(always)]
815pub fn parse_sell_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
816 const REQUIRED_LEN: usize = 13 * 8 + 8 + 7 * 32;
817 const CASHBACK_FEE_BASIS_POINTS_OFFSET: usize = 352;
818 const CASHBACK_OFFSET: usize = 360;
819 const CASHBACK_FIELDS_LEN: usize = 16;
820 if data.len() < REQUIRED_LEN {
821 return None;
822 }
823
824 unsafe {
825 let timestamp = read_i64_unchecked(data, 0);
826 let base_amount_in = read_u64_unchecked(data, 8);
827 let min_quote_amount_out = read_u64_unchecked(data, 16);
828 let user_base_token_reserves = read_u64_unchecked(data, 24);
829 let user_quote_token_reserves = read_u64_unchecked(data, 32);
830 let pool_base_token_reserves = read_u64_unchecked(data, 40);
831 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
832 let quote_amount_out = read_u64_unchecked(data, 56);
833 let lp_fee_basis_points = read_u64_unchecked(data, 64);
834 let lp_fee = read_u64_unchecked(data, 72);
835 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
836 let protocol_fee = read_u64_unchecked(data, 88);
837 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
838 let user_quote_amount_out = read_u64_unchecked(data, 104);
839
840 let pool = read_pubkey_unchecked(data, 112);
841 let user = read_pubkey_unchecked(data, 144);
842 let user_base_token_account = read_pubkey_unchecked(data, 176);
843 let user_quote_token_account = read_pubkey_unchecked(data, 208);
844 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
845 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
846 let coin_creator = read_pubkey_unchecked(data, 304);
847
848 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
849 let coin_creator_fee = read_u64_unchecked(data, 344);
850 let (cashback_fee_basis_points, cashback) =
851 if data.len() >= CASHBACK_FEE_BASIS_POINTS_OFFSET + CASHBACK_FIELDS_LEN {
852 (
853 read_u64_unchecked(data, CASHBACK_FEE_BASIS_POINTS_OFFSET),
854 read_u64_unchecked(data, CASHBACK_OFFSET),
855 )
856 } else {
857 (0, 0)
858 };
859
860 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
861 metadata,
862 timestamp,
863 base_amount_in,
864 min_quote_amount_out,
865 user_base_token_reserves,
866 user_quote_token_reserves,
867 pool_base_token_reserves,
868 pool_quote_token_reserves,
869 quote_amount_out,
870 lp_fee_basis_points,
871 lp_fee,
872 protocol_fee_basis_points,
873 protocol_fee,
874 quote_amount_out_without_lp_fee,
875 user_quote_amount_out,
876 pool,
877 user,
878 user_base_token_account,
879 user_quote_token_account,
880 protocol_fee_recipient,
881 protocol_fee_recipient_token_account,
882 coin_creator,
883 coin_creator_fee_basis_points,
884 coin_creator_fee,
885 cashback_fee_basis_points,
886 cashback,
887 ..Default::default()
888 }))
889 }
890}
891
892#[inline(always)]
894pub fn parse_create_pool_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
895 const CREATE_POOL_EVENT_LEN: usize = 326;
896 const REQUIRED_LEN: usize = CREATE_POOL_EVENT_LEN;
897 if data.len() < REQUIRED_LEN {
898 return None;
899 }
900
901 unsafe {
902 let timestamp = read_i64_unchecked(data, 0);
903 let index = read_u16_unchecked(data, 8);
904
905 let creator = read_pubkey_unchecked(data, 10);
906 let base_mint = read_pubkey_unchecked(data, 42);
907 let quote_mint = read_pubkey_unchecked(data, 74);
908
909 let base_mint_decimals = read_u8_unchecked(data, 106);
910 let quote_mint_decimals = read_u8_unchecked(data, 107);
911
912 let base_amount_in = read_u64_unchecked(data, 108);
913 let quote_amount_in = read_u64_unchecked(data, 116);
914 let pool_base_amount = read_u64_unchecked(data, 124);
915 let pool_quote_amount = read_u64_unchecked(data, 132);
916 let minimum_liquidity = read_u64_unchecked(data, 140);
917 let initial_liquidity = read_u64_unchecked(data, 148);
918 let lp_token_amount_out = read_u64_unchecked(data, 156);
919
920 let pool_bump = read_u8_unchecked(data, 164);
921
922 let pool = read_pubkey_unchecked(data, 165);
923 let lp_mint = read_pubkey_unchecked(data, 197);
924 let user_base_token_account = read_pubkey_unchecked(data, 229);
925 let user_quote_token_account = read_pubkey_unchecked(data, 261);
926 let coin_creator = read_pubkey_unchecked(data, 293);
927 let is_mayhem_mode = data.len() > 325 && read_bool_unchecked(data, 325);
928
929 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
930 metadata,
931 timestamp,
932 index,
933 creator,
934 base_mint,
935 quote_mint,
936 base_mint_decimals,
937 quote_mint_decimals,
938 base_amount_in,
939 quote_amount_in,
940 pool_base_amount,
941 pool_quote_amount,
942 minimum_liquidity,
943 initial_liquidity,
944 lp_token_amount_out,
945 pool_bump,
946 pool,
947 lp_mint,
948 user_base_token_account,
949 user_quote_token_account,
950 coin_creator,
951 is_mayhem_mode,
952 }))
953 }
954}
955
956#[inline(always)]
958pub fn parse_add_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
959 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
960 if data.len() < REQUIRED_LEN {
961 return None;
962 }
963
964 unsafe {
965 let timestamp = read_i64_unchecked(data, 0);
966 let lp_token_amount_out = read_u64_unchecked(data, 8);
967 let max_base_amount_in = read_u64_unchecked(data, 16);
968 let max_quote_amount_in = read_u64_unchecked(data, 24);
969 let user_base_token_reserves = read_u64_unchecked(data, 32);
970 let user_quote_token_reserves = read_u64_unchecked(data, 40);
971 let pool_base_token_reserves = read_u64_unchecked(data, 48);
972 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
973 let base_amount_in = read_u64_unchecked(data, 64);
974 let quote_amount_in = read_u64_unchecked(data, 72);
975 let lp_mint_supply = read_u64_unchecked(data, 80);
976
977 let pool = read_pubkey_unchecked(data, 88);
978 let user = read_pubkey_unchecked(data, 120);
979 let user_base_token_account = read_pubkey_unchecked(data, 152);
980 let user_quote_token_account = read_pubkey_unchecked(data, 184);
981 let user_pool_token_account = read_pubkey_unchecked(data, 216);
982
983 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
984 metadata,
985 timestamp,
986 lp_token_amount_out,
987 max_base_amount_in,
988 max_quote_amount_in,
989 user_base_token_reserves,
990 user_quote_token_reserves,
991 pool_base_token_reserves,
992 pool_quote_token_reserves,
993 base_amount_in,
994 quote_amount_in,
995 lp_mint_supply,
996 pool,
997 user,
998 user_base_token_account,
999 user_quote_token_account,
1000 user_pool_token_account,
1001 }))
1002 }
1003}
1004
1005#[inline(always)]
1007pub fn parse_remove_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
1008 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
1009 if data.len() < REQUIRED_LEN {
1010 return None;
1011 }
1012
1013 unsafe {
1014 let timestamp = read_i64_unchecked(data, 0);
1015 let lp_token_amount_in = read_u64_unchecked(data, 8);
1016 let min_base_amount_out = read_u64_unchecked(data, 16);
1017 let min_quote_amount_out = read_u64_unchecked(data, 24);
1018 let user_base_token_reserves = read_u64_unchecked(data, 32);
1019 let user_quote_token_reserves = read_u64_unchecked(data, 40);
1020 let pool_base_token_reserves = read_u64_unchecked(data, 48);
1021 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
1022 let base_amount_out = read_u64_unchecked(data, 64);
1023 let quote_amount_out = read_u64_unchecked(data, 72);
1024 let lp_mint_supply = read_u64_unchecked(data, 80);
1025
1026 let pool = read_pubkey_unchecked(data, 88);
1027 let user = read_pubkey_unchecked(data, 120);
1028 let user_base_token_account = read_pubkey_unchecked(data, 152);
1029 let user_quote_token_account = read_pubkey_unchecked(data, 184);
1030 let user_pool_token_account = read_pubkey_unchecked(data, 216);
1031
1032 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
1033 metadata,
1034 timestamp,
1035 lp_token_amount_in,
1036 min_base_amount_out,
1037 min_quote_amount_out,
1038 user_base_token_reserves,
1039 user_quote_token_reserves,
1040 pool_base_token_reserves,
1041 pool_quote_token_reserves,
1042 base_amount_out,
1043 quote_amount_out,
1044 lp_mint_supply,
1045 pool,
1046 user,
1047 user_base_token_account,
1048 user_quote_token_account,
1049 user_pool_token_account,
1050 }))
1051 }
1052}
1053
1054#[cfg(feature = "perf-stats")]
1059pub fn get_perf_stats() -> (usize, usize) {
1060 let count = PARSE_COUNT.load(Ordering::Relaxed);
1061 let total_ns = PARSE_TIME_NS.load(Ordering::Relaxed);
1062 (count, total_ns)
1063}
1064
1065#[cfg(feature = "perf-stats")]
1066pub fn reset_perf_stats() {
1067 PARSE_COUNT.store(0, Ordering::Relaxed);
1068 PARSE_TIME_NS.store(0, Ordering::Relaxed);
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074 use solana_sdk::{pubkey::Pubkey, signature::Signature};
1075
1076 fn metadata() -> EventMetadata {
1077 EventMetadata {
1078 signature: Signature::default(),
1079 slot: 0,
1080 tx_index: 0,
1081 block_time_us: 0,
1082 grpc_recv_us: 0,
1083 recent_blockhash: None,
1084 }
1085 }
1086
1087 fn write_u64(buf: &mut [u8], offset: usize, value: u64) {
1088 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1089 }
1090
1091 fn write_i64(buf: &mut [u8], offset: usize, value: i64) {
1092 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1093 }
1094
1095 fn write_pubkey(buf: &mut [u8], offset: usize, value: Pubkey) {
1096 buf[offset..offset + 32].copy_from_slice(value.as_ref());
1097 }
1098
1099 fn build_sell_payload(include_cashback: bool) -> Vec<u8> {
1100 let len = if include_cashback { 368 } else { 352 };
1101 let mut data = vec![0u8; len];
1102
1103 write_i64(&mut data, 0, 1_713_498_953);
1104 write_u64(&mut data, 8, 11);
1105 write_u64(&mut data, 16, 22);
1106 write_u64(&mut data, 24, 33);
1107 write_u64(&mut data, 32, 44);
1108 write_u64(&mut data, 40, 55);
1109 write_u64(&mut data, 48, 66);
1110 write_u64(&mut data, 56, 77);
1111 write_u64(&mut data, 64, 88);
1112 write_u64(&mut data, 72, 99);
1113 write_u64(&mut data, 80, 111);
1114 write_u64(&mut data, 88, 122);
1115 write_u64(&mut data, 96, 133);
1116 write_u64(&mut data, 104, 144);
1117
1118 write_pubkey(&mut data, 112, Pubkey::new_from_array([1; 32]));
1119 write_pubkey(&mut data, 144, Pubkey::new_from_array([2; 32]));
1120 write_pubkey(&mut data, 176, Pubkey::new_from_array([3; 32]));
1121 write_pubkey(&mut data, 208, Pubkey::new_from_array([4; 32]));
1122 write_pubkey(&mut data, 240, Pubkey::new_from_array([5; 32]));
1123 write_pubkey(&mut data, 272, Pubkey::new_from_array([6; 32]));
1124 write_pubkey(&mut data, 304, Pubkey::new_from_array([7; 32]));
1125
1126 write_u64(&mut data, 336, 155);
1127 write_u64(&mut data, 344, 166);
1128
1129 if include_cashback {
1130 write_u64(&mut data, 352, 177);
1131 write_u64(&mut data, 360, 188);
1132 }
1133
1134 data
1135 }
1136
1137 fn build_create_pool_payload(is_mayhem_mode: bool) -> Vec<u8> {
1138 let mut data = vec![0u8; 326];
1139
1140 write_i64(&mut data, 0, 1_713_498_953);
1141 data[8..10].copy_from_slice(&42u16.to_le_bytes());
1142 write_pubkey(&mut data, 10, Pubkey::new_from_array([1; 32]));
1143 write_pubkey(&mut data, 42, Pubkey::new_from_array([2; 32]));
1144 write_pubkey(&mut data, 74, Pubkey::new_from_array([3; 32]));
1145 data[106] = 6;
1146 data[107] = 9;
1147 write_u64(&mut data, 108, 11);
1148 write_u64(&mut data, 116, 22);
1149 write_u64(&mut data, 124, 33);
1150 write_u64(&mut data, 132, 44);
1151 write_u64(&mut data, 140, 55);
1152 write_u64(&mut data, 148, 66);
1153 write_u64(&mut data, 156, 77);
1154 data[164] = 8;
1155 write_pubkey(&mut data, 165, Pubkey::new_from_array([4; 32]));
1156 write_pubkey(&mut data, 197, Pubkey::new_from_array([5; 32]));
1157 write_pubkey(&mut data, 229, Pubkey::new_from_array([6; 32]));
1158 write_pubkey(&mut data, 261, Pubkey::new_from_array([7; 32]));
1159 write_pubkey(&mut data, 293, Pubkey::new_from_array([8; 32]));
1160 data[325] = u8::from(is_mayhem_mode);
1161
1162 data
1163 }
1164
1165 #[test]
1166 fn test_discriminator_simd() {
1167 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1169 let disc = extract_discriminator_simd(log);
1170 assert!(disc.is_some());
1171 }
1172
1173 #[test]
1174 fn test_parse_performance() {
1175 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1177 let sig = Signature::default();
1178
1179 let start = std::time::Instant::now();
1180 for _ in 0..1000 {
1181 let _ = parse_log(log, sig, 0, 0, Some(0), 0);
1182 }
1183 let elapsed = start.elapsed();
1184
1185 println!("Average parse time: {} ns", elapsed.as_nanos() / 1000);
1186 }
1187
1188 #[test]
1189 fn parse_sell_from_data_preserves_cashback_fields() {
1190 let event = parse_sell_from_data(&build_sell_payload(true), metadata())
1191 .expect("expected pumpswap sell event");
1192
1193 let DexEvent::PumpSwapSell(event) = event else {
1194 panic!("expected PumpSwapSell event");
1195 };
1196
1197 assert_eq!(event.cashback_fee_basis_points, 177);
1198 assert_eq!(event.cashback, 188);
1199 assert_eq!(event.coin_creator_fee_basis_points, 155);
1200 assert_eq!(event.coin_creator_fee, 166);
1201 }
1202
1203 #[test]
1204 fn parse_sell_from_data_keeps_legacy_payload_compatible() {
1205 let event = parse_sell_from_data(&build_sell_payload(false), metadata())
1206 .expect("expected legacy pumpswap sell event");
1207
1208 let DexEvent::PumpSwapSell(event) = event else {
1209 panic!("expected PumpSwapSell event");
1210 };
1211
1212 assert_eq!(event.cashback_fee_basis_points, 0);
1213 assert_eq!(event.cashback, 0);
1214 assert_eq!(event.coin_creator_fee_basis_points, 155);
1215 assert_eq!(event.coin_creator_fee, 166);
1216 }
1217
1218 #[test]
1219 fn parse_buy_from_data_rejects_truncated_min_base_payload() {
1220 assert!(parse_buy_from_data(&vec![0u8; 396], metadata()).is_none());
1221 assert!(parse_buy_from_data(&vec![0u8; 397], metadata()).is_some());
1222 }
1223
1224 #[test]
1225 fn parse_create_pool_from_data_reads_mayhem_mode() {
1226 let event = parse_create_pool_from_data(&build_create_pool_payload(true), metadata())
1227 .expect("expected pumpswap create pool event");
1228
1229 let DexEvent::PumpSwapCreatePool(event) = event else {
1230 panic!("expected PumpSwapCreatePool event");
1231 };
1232
1233 assert_eq!(event.index, 42);
1234 assert!(event.is_mayhem_mode);
1235 }
1236}