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 = 14 * 8 + 7 * 32 + 1 + 5 * 8 + 4;
284 if data.len() < MIN_REQUIRED_LEN {
285 return None;
286 }
287
288 unsafe {
289 let timestamp = read_i64_unchecked(data, 0);
290 let base_amount_out = read_u64_unchecked(data, 8);
291 let max_quote_amount_in = read_u64_unchecked(data, 16);
292 let user_base_token_reserves = read_u64_unchecked(data, 24);
293 let user_quote_token_reserves = read_u64_unchecked(data, 32);
294 let pool_base_token_reserves = read_u64_unchecked(data, 40);
295 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
296 let quote_amount_in = read_u64_unchecked(data, 56);
297 let lp_fee_basis_points = read_u64_unchecked(data, 64);
298 let lp_fee = read_u64_unchecked(data, 72);
299 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
300 let protocol_fee = read_u64_unchecked(data, 88);
301 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
302 let user_quote_amount_in = read_u64_unchecked(data, 104);
303
304 let pool = read_pubkey_unchecked(data, 112);
305 let user = read_pubkey_unchecked(data, 144);
306 let user_base_token_account = read_pubkey_unchecked(data, 176);
307 let user_quote_token_account = read_pubkey_unchecked(data, 208);
308 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
309 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
310 let coin_creator = read_pubkey_unchecked(data, 304);
311
312 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
313 let coin_creator_fee = read_u64_unchecked(data, 344);
314 let track_volume = read_bool_unchecked(data, 352);
315 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
316 let total_claimed_tokens = read_u64_unchecked(data, 361);
317 let current_sol_volume = read_u64_unchecked(data, 369);
318 let last_update_timestamp = read_i64_unchecked(data, 377);
319
320 let mut offset = 385;
322 let min_base_amount_out = read_u64_unchecked(data, offset);
323 offset += 8;
324
325 let ix_name = if offset + 4 <= data.len() {
327 let len = read_u32_unchecked(data, offset) as usize;
328 offset += 4;
329 if offset + len <= data.len() {
330 let string_bytes = &data[offset..offset + len];
331 let s = std::str::from_utf8_unchecked(string_bytes);
332 offset += len;
333 s.to_string()
334 } else {
335 String::new()
336 }
337 } else {
338 String::new()
339 };
340
341 let cashback_fee_basis_points =
343 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
344 offset += 8;
345 let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
346
347 let metadata = EventMetadata {
348 signature,
349 slot,
350 tx_index,
351 block_time_us: block_time_us.unwrap_or(0),
352 grpc_recv_us,
353 recent_blockhash: None,
354 };
355
356 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
357 metadata,
358 timestamp,
359 base_amount_out,
360 max_quote_amount_in,
361 user_base_token_reserves,
362 user_quote_token_reserves,
363 pool_base_token_reserves,
364 pool_quote_token_reserves,
365 quote_amount_in,
366 lp_fee_basis_points,
367 lp_fee,
368 protocol_fee_basis_points,
369 protocol_fee,
370 quote_amount_in_with_lp_fee,
371 user_quote_amount_in,
372 pool,
373 user,
374 user_base_token_account,
375 user_quote_token_account,
376 protocol_fee_recipient,
377 protocol_fee_recipient_token_account,
378 coin_creator,
379 coin_creator_fee_basis_points,
380 coin_creator_fee,
381 track_volume,
382 total_unclaimed_tokens,
383 total_claimed_tokens,
384 current_sol_volume,
385 last_update_timestamp,
386 min_base_amount_out,
387 ix_name,
388 cashback_fee_basis_points,
389 cashback,
390 ..Default::default()
391 }))
392 }
393}
394
395#[inline(always)]
397fn parse_sell_event_optimized(
398 data: &[u8],
399 signature: Signature,
400 slot: u64,
401 tx_index: u64,
402 block_time_us: Option<i64>,
403 grpc_recv_us: i64,
404) -> Option<DexEvent> {
405 const REQUIRED_LEN: usize = 13 * 8 + 8 + 7 * 32 + 8 + 8;
407 if data.len() < REQUIRED_LEN {
408 return None;
409 }
410
411 unsafe {
412 let timestamp = read_i64_unchecked(data, 0);
413 let base_amount_in = read_u64_unchecked(data, 8);
414 let min_quote_amount_out = read_u64_unchecked(data, 16);
415 let user_base_token_reserves = read_u64_unchecked(data, 24);
416 let user_quote_token_reserves = read_u64_unchecked(data, 32);
417 let pool_base_token_reserves = read_u64_unchecked(data, 40);
418 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
419 let quote_amount_out = read_u64_unchecked(data, 56);
420 let lp_fee_basis_points = read_u64_unchecked(data, 64);
421 let lp_fee = read_u64_unchecked(data, 72);
422 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
423 let protocol_fee = read_u64_unchecked(data, 88);
424 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
425 let user_quote_amount_out = read_u64_unchecked(data, 104);
426
427 let pool = read_pubkey_unchecked(data, 112);
428 let user = read_pubkey_unchecked(data, 144);
429 let user_base_token_account = read_pubkey_unchecked(data, 176);
430 let user_quote_token_account = read_pubkey_unchecked(data, 208);
431 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
432 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
433 let coin_creator = read_pubkey_unchecked(data, 304);
434
435 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
436 let coin_creator_fee = read_u64_unchecked(data, 344);
437 let cashback_fee_basis_points = read_u64_unchecked(data, 352);
439 let cashback = read_u64_unchecked(data, 360);
440
441 let metadata = EventMetadata {
442 signature,
443 slot,
444 tx_index,
445 block_time_us: block_time_us.unwrap_or(0),
446 grpc_recv_us,
447 recent_blockhash: None,
448 };
449
450 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
451 metadata,
452 timestamp,
453 base_amount_in,
454 min_quote_amount_out,
455 user_base_token_reserves,
456 user_quote_token_reserves,
457 pool_base_token_reserves,
458 pool_quote_token_reserves,
459 quote_amount_out,
460 lp_fee_basis_points,
461 lp_fee,
462 protocol_fee_basis_points,
463 protocol_fee,
464 quote_amount_out_without_lp_fee,
465 user_quote_amount_out,
466 pool,
467 user,
468 user_base_token_account,
469 user_quote_token_account,
470 protocol_fee_recipient,
471 protocol_fee_recipient_token_account,
472 coin_creator,
473 coin_creator_fee_basis_points,
474 coin_creator_fee,
475 cashback_fee_basis_points,
476 cashback,
477 ..Default::default()
478 }))
479 }
480}
481
482#[inline(always)]
484fn parse_create_pool_event_optimized(
485 data: &[u8],
486 signature: Signature,
487 slot: u64,
488 tx_index: u64,
489 block_time_us: Option<i64>,
490 grpc_recv_us: i64,
491) -> Option<DexEvent> {
492 const REQUIRED_LEN: usize = 8 + 2 + 32 * 6 + 2 + 8 * 7 + 1 + 1;
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 = 14 * 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 REQUIRED_LEN: usize = 8 + 2 + 32 * 6 + 2 + 8 * 7 + 1;
896 if data.len() < REQUIRED_LEN {
897 return None;
898 }
899
900 unsafe {
901 let timestamp = read_i64_unchecked(data, 0);
902 let index = read_u16_unchecked(data, 8);
903
904 let creator = read_pubkey_unchecked(data, 10);
905 let base_mint = read_pubkey_unchecked(data, 42);
906 let quote_mint = read_pubkey_unchecked(data, 74);
907
908 let base_mint_decimals = read_u8_unchecked(data, 106);
909 let quote_mint_decimals = read_u8_unchecked(data, 107);
910
911 let base_amount_in = read_u64_unchecked(data, 108);
912 let quote_amount_in = read_u64_unchecked(data, 116);
913 let pool_base_amount = read_u64_unchecked(data, 124);
914 let pool_quote_amount = read_u64_unchecked(data, 132);
915 let minimum_liquidity = read_u64_unchecked(data, 140);
916 let initial_liquidity = read_u64_unchecked(data, 148);
917 let lp_token_amount_out = read_u64_unchecked(data, 156);
918
919 let pool_bump = read_u8_unchecked(data, 164);
920
921 let pool = read_pubkey_unchecked(data, 165);
922 let lp_mint = read_pubkey_unchecked(data, 197);
923 let user_base_token_account = read_pubkey_unchecked(data, 229);
924 let user_quote_token_account = read_pubkey_unchecked(data, 261);
925 let coin_creator = read_pubkey_unchecked(data, 293);
926 let is_mayhem_mode = data.len() > 325 && read_bool_unchecked(data, 325);
927
928 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
929 metadata,
930 timestamp,
931 index,
932 creator,
933 base_mint,
934 quote_mint,
935 base_mint_decimals,
936 quote_mint_decimals,
937 base_amount_in,
938 quote_amount_in,
939 pool_base_amount,
940 pool_quote_amount,
941 minimum_liquidity,
942 initial_liquidity,
943 lp_token_amount_out,
944 pool_bump,
945 pool,
946 lp_mint,
947 user_base_token_account,
948 user_quote_token_account,
949 coin_creator,
950 is_mayhem_mode,
951 }))
952 }
953}
954
955#[inline(always)]
957pub fn parse_add_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
958 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
959 if data.len() < REQUIRED_LEN {
960 return None;
961 }
962
963 unsafe {
964 let timestamp = read_i64_unchecked(data, 0);
965 let lp_token_amount_out = read_u64_unchecked(data, 8);
966 let max_base_amount_in = read_u64_unchecked(data, 16);
967 let max_quote_amount_in = read_u64_unchecked(data, 24);
968 let user_base_token_reserves = read_u64_unchecked(data, 32);
969 let user_quote_token_reserves = read_u64_unchecked(data, 40);
970 let pool_base_token_reserves = read_u64_unchecked(data, 48);
971 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
972 let base_amount_in = read_u64_unchecked(data, 64);
973 let quote_amount_in = read_u64_unchecked(data, 72);
974 let lp_mint_supply = read_u64_unchecked(data, 80);
975
976 let pool = read_pubkey_unchecked(data, 88);
977 let user = read_pubkey_unchecked(data, 120);
978 let user_base_token_account = read_pubkey_unchecked(data, 152);
979 let user_quote_token_account = read_pubkey_unchecked(data, 184);
980 let user_pool_token_account = read_pubkey_unchecked(data, 216);
981
982 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
983 metadata,
984 timestamp,
985 lp_token_amount_out,
986 max_base_amount_in,
987 max_quote_amount_in,
988 user_base_token_reserves,
989 user_quote_token_reserves,
990 pool_base_token_reserves,
991 pool_quote_token_reserves,
992 base_amount_in,
993 quote_amount_in,
994 lp_mint_supply,
995 pool,
996 user,
997 user_base_token_account,
998 user_quote_token_account,
999 user_pool_token_account,
1000 }))
1001 }
1002}
1003
1004#[inline(always)]
1006pub fn parse_remove_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
1007 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
1008 if data.len() < REQUIRED_LEN {
1009 return None;
1010 }
1011
1012 unsafe {
1013 let timestamp = read_i64_unchecked(data, 0);
1014 let lp_token_amount_in = read_u64_unchecked(data, 8);
1015 let min_base_amount_out = read_u64_unchecked(data, 16);
1016 let min_quote_amount_out = read_u64_unchecked(data, 24);
1017 let user_base_token_reserves = read_u64_unchecked(data, 32);
1018 let user_quote_token_reserves = read_u64_unchecked(data, 40);
1019 let pool_base_token_reserves = read_u64_unchecked(data, 48);
1020 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
1021 let base_amount_out = read_u64_unchecked(data, 64);
1022 let quote_amount_out = read_u64_unchecked(data, 72);
1023 let lp_mint_supply = read_u64_unchecked(data, 80);
1024
1025 let pool = read_pubkey_unchecked(data, 88);
1026 let user = read_pubkey_unchecked(data, 120);
1027 let user_base_token_account = read_pubkey_unchecked(data, 152);
1028 let user_quote_token_account = read_pubkey_unchecked(data, 184);
1029 let user_pool_token_account = read_pubkey_unchecked(data, 216);
1030
1031 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
1032 metadata,
1033 timestamp,
1034 lp_token_amount_in,
1035 min_base_amount_out,
1036 min_quote_amount_out,
1037 user_base_token_reserves,
1038 user_quote_token_reserves,
1039 pool_base_token_reserves,
1040 pool_quote_token_reserves,
1041 base_amount_out,
1042 quote_amount_out,
1043 lp_mint_supply,
1044 pool,
1045 user,
1046 user_base_token_account,
1047 user_quote_token_account,
1048 user_pool_token_account,
1049 }))
1050 }
1051}
1052
1053#[cfg(feature = "perf-stats")]
1058pub fn get_perf_stats() -> (usize, usize) {
1059 let count = PARSE_COUNT.load(Ordering::Relaxed);
1060 let total_ns = PARSE_TIME_NS.load(Ordering::Relaxed);
1061 (count, total_ns)
1062}
1063
1064#[cfg(feature = "perf-stats")]
1065pub fn reset_perf_stats() {
1066 PARSE_COUNT.store(0, Ordering::Relaxed);
1067 PARSE_TIME_NS.store(0, Ordering::Relaxed);
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073 use solana_sdk::{pubkey::Pubkey, signature::Signature};
1074
1075 fn metadata() -> EventMetadata {
1076 EventMetadata {
1077 signature: Signature::default(),
1078 slot: 0,
1079 tx_index: 0,
1080 block_time_us: 0,
1081 grpc_recv_us: 0,
1082 recent_blockhash: None,
1083 }
1084 }
1085
1086 fn write_u64(buf: &mut [u8], offset: usize, value: u64) {
1087 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1088 }
1089
1090 fn write_i64(buf: &mut [u8], offset: usize, value: i64) {
1091 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1092 }
1093
1094 fn write_pubkey(buf: &mut [u8], offset: usize, value: Pubkey) {
1095 buf[offset..offset + 32].copy_from_slice(value.as_ref());
1096 }
1097
1098 fn build_sell_payload(include_cashback: bool) -> Vec<u8> {
1099 let len = if include_cashback { 368 } else { 352 };
1100 let mut data = vec![0u8; len];
1101
1102 write_i64(&mut data, 0, 1_713_498_953);
1103 write_u64(&mut data, 8, 11);
1104 write_u64(&mut data, 16, 22);
1105 write_u64(&mut data, 24, 33);
1106 write_u64(&mut data, 32, 44);
1107 write_u64(&mut data, 40, 55);
1108 write_u64(&mut data, 48, 66);
1109 write_u64(&mut data, 56, 77);
1110 write_u64(&mut data, 64, 88);
1111 write_u64(&mut data, 72, 99);
1112 write_u64(&mut data, 80, 111);
1113 write_u64(&mut data, 88, 122);
1114 write_u64(&mut data, 96, 133);
1115 write_u64(&mut data, 104, 144);
1116
1117 write_pubkey(&mut data, 112, Pubkey::new_from_array([1; 32]));
1118 write_pubkey(&mut data, 144, Pubkey::new_from_array([2; 32]));
1119 write_pubkey(&mut data, 176, Pubkey::new_from_array([3; 32]));
1120 write_pubkey(&mut data, 208, Pubkey::new_from_array([4; 32]));
1121 write_pubkey(&mut data, 240, Pubkey::new_from_array([5; 32]));
1122 write_pubkey(&mut data, 272, Pubkey::new_from_array([6; 32]));
1123 write_pubkey(&mut data, 304, Pubkey::new_from_array([7; 32]));
1124
1125 write_u64(&mut data, 336, 155);
1126 write_u64(&mut data, 344, 166);
1127
1128 if include_cashback {
1129 write_u64(&mut data, 352, 177);
1130 write_u64(&mut data, 360, 188);
1131 }
1132
1133 data
1134 }
1135
1136 #[test]
1137 fn test_discriminator_simd() {
1138 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1140 let disc = extract_discriminator_simd(log);
1141 assert!(disc.is_some());
1142 }
1143
1144 #[test]
1145 fn test_parse_performance() {
1146 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1148 let sig = Signature::default();
1149
1150 let start = std::time::Instant::now();
1151 for _ in 0..1000 {
1152 let _ = parse_log(log, sig, 0, 0, Some(0), 0);
1153 }
1154 let elapsed = start.elapsed();
1155
1156 println!("Average parse time: {} ns", elapsed.as_nanos() / 1000);
1157 }
1158
1159 #[test]
1160 fn parse_sell_from_data_preserves_cashback_fields() {
1161 let event = parse_sell_from_data(&build_sell_payload(true), metadata())
1162 .expect("expected pumpswap sell event");
1163
1164 let DexEvent::PumpSwapSell(event) = event else {
1165 panic!("expected PumpSwapSell event");
1166 };
1167
1168 assert_eq!(event.cashback_fee_basis_points, 177);
1169 assert_eq!(event.cashback, 188);
1170 assert_eq!(event.coin_creator_fee_basis_points, 155);
1171 assert_eq!(event.coin_creator_fee, 166);
1172 }
1173
1174 #[test]
1175 fn parse_sell_from_data_keeps_legacy_payload_compatible() {
1176 let event = parse_sell_from_data(&build_sell_payload(false), metadata())
1177 .expect("expected legacy pumpswap sell event");
1178
1179 let DexEvent::PumpSwapSell(event) = event else {
1180 panic!("expected PumpSwapSell event");
1181 };
1182
1183 assert_eq!(event.cashback_fee_basis_points, 0);
1184 assert_eq!(event.cashback, 0);
1185 assert_eq!(event.coin_creator_fee_basis_points, 155);
1186 assert_eq!(event.coin_creator_fee, 166);
1187 }
1188}