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