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#[derive(Default)]
144struct PumpSwapTradeTail {
145 creator_fee_unclaimed: u64,
146 cashback_fee_basis_points: u64,
147 cashback: u64,
148 buyback_fee_basis_points: u64,
149 buyback_fee: u64,
150 virtual_quote_reserves: i128,
151 can_boost: bool,
152 base_supply: u64,
153 holder_rewards_bps: u64,
154 holder_rewards: u64,
155}
156
157#[inline(always)]
158fn read_u64_le_at(data: &[u8], offset: usize) -> Option<u64> {
159 let bytes = data.get(offset..offset.checked_add(8)?)?;
160 Some(u64::from_le_bytes(bytes.try_into().ok()?))
161}
162
163#[inline(always)]
164fn read_i128_le_at(data: &[u8], offset: usize) -> Option<i128> {
165 let bytes = data.get(offset..offset.checked_add(16)?)?;
166 Some(i128::from_le_bytes(bytes.try_into().ok()?))
167}
168
169#[inline(always)]
170fn read_borsh_string(data: &[u8], offset: usize) -> Option<(String, usize)> {
171 let content_offset = offset.checked_add(4)?;
172 let len = u32::from_le_bytes(data.get(offset..content_offset)?.try_into().ok()?) as usize;
173 let end = content_offset.checked_add(len)?;
174 let value = std::str::from_utf8(data.get(content_offset..end)?).ok()?.to_owned();
175 Some((value, end))
176}
177
178#[inline(always)]
180fn parse_trade_tail(data: &[u8]) -> Option<PumpSwapTradeTail> {
181 const CASHBACK_LEN: usize = 16;
182 const BUYBACK_LEN: usize = 32;
183 const BOOST_LEN: usize = 57;
184 const HOLDER_REWARDS_LEN: usize = 73;
185
186 if data.is_empty() {
187 return Some(PumpSwapTradeTail::default());
188 }
189 if data.len() < CASHBACK_LEN {
190 return None;
191 }
192
193 let mut tail = PumpSwapTradeTail {
194 cashback_fee_basis_points: read_u64_le_at(data, 0)?,
195 cashback: read_u64_le_at(data, 8)?,
196 ..Default::default()
197 };
198 if data.len() == CASHBACK_LEN {
199 return Some(tail);
200 }
201 if data.len() < BUYBACK_LEN {
202 return None;
203 }
204
205 tail.buyback_fee_basis_points = read_u64_le_at(data, 16)?;
206 tail.buyback_fee = read_u64_le_at(data, 24)?;
207 if data.len() == BUYBACK_LEN {
208 return Some(tail);
209 }
210 if data.len() < BOOST_LEN {
211 return None;
212 }
213
214 tail.virtual_quote_reserves = read_i128_le_at(data, 32)?;
215 tail.can_boost = match data[48] {
216 0 => false,
217 1 => true,
218 _ => return None,
219 };
220 tail.base_supply = read_u64_le_at(data, 49)?;
221 if data.len() != BOOST_LEN && data.len() < HOLDER_REWARDS_LEN {
222 return None;
223 }
224 if data.len() >= HOLDER_REWARDS_LEN {
225 tail.holder_rewards_bps = read_u64_le_at(data, 57)?;
226 tail.holder_rewards = read_u64_le_at(data, 65)?;
227 }
228 if data.len() > 73 && data.len() < 81 {
229 return None;
230 }
231 tail.creator_fee_unclaimed = read_u64_le_at(data, 73).unwrap_or(0);
232 Some(tail)
233}
234
235#[inline(always)]
237unsafe fn read_u16_unchecked(data: &[u8], offset: usize) -> u16 {
238 let ptr = data.as_ptr().add(offset) as *const u16;
239 u16::from_le(ptr.read_unaligned())
240}
241
242#[inline(always)]
244unsafe fn read_u8_unchecked(data: &[u8], offset: usize) -> u8 {
245 *data.get_unchecked(offset)
246}
247
248#[inline(always)]
250unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
251 *data.get_unchecked(offset) == 1
252}
253
254#[inline(always)]
258unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> Pubkey {
259 #[cfg(target_arch = "x86_64")]
262 {
263 use std::arch::x86_64::_mm_prefetch;
264 use std::arch::x86_64::_MM_HINT_T0;
265 if offset + 64 < data.len() {
266 _mm_prefetch((data.as_ptr().add(offset + 32)) as *const i8, _MM_HINT_T0);
267 }
268 }
269
270 let ptr = data.as_ptr().add(offset);
271 let mut bytes = [0u8; 32];
272 std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
273 Pubkey::new_from_array(bytes)
274}
275
276#[inline(always)]
284pub fn parse_log(
285 log: &str,
286 signature: Signature,
287 slot: u64,
288 tx_index: u64,
289 block_time_us: Option<i64>,
290 grpc_recv_us: i64,
291) -> Option<DexEvent> {
292 #[cfg(feature = "perf-stats")]
293 let start = std::time::Instant::now();
294
295 let mut buf = [0u8; 2048];
297 let program_data = extract_program_data_zero_copy(log, &mut buf)?;
298
299 if program_data.len() < 8 {
300 return None;
301 }
302
303 let discriminator = unsafe { read_u64_unchecked(program_data, 0) };
305 let data = &program_data[8..];
306
307 let result = match discriminator {
308 discriminators::BUY => parse_buy_from_data(
309 data,
310 EventMetadata {
311 signature,
312 slot,
313 tx_index,
314 block_time_us: block_time_us.unwrap_or(0),
315 grpc_recv_us,
316 recent_blockhash: None,
317 },
318 ),
319 discriminators::SELL => parse_sell_from_data(
320 data,
321 EventMetadata {
322 signature,
323 slot,
324 tx_index,
325 block_time_us: block_time_us.unwrap_or(0),
326 grpc_recv_us,
327 recent_blockhash: None,
328 },
329 ),
330 discriminators::CREATE_POOL => parse_create_pool_event_optimized(
331 data,
332 signature,
333 slot,
334 tx_index,
335 block_time_us,
336 grpc_recv_us,
337 ),
338 discriminators::ADD_LIQUIDITY => parse_add_liquidity_event_optimized(
339 data,
340 signature,
341 slot,
342 tx_index,
343 block_time_us,
344 grpc_recv_us,
345 ),
346 discriminators::REMOVE_LIQUIDITY => parse_remove_liquidity_event_optimized(
347 data,
348 signature,
349 slot,
350 tx_index,
351 block_time_us,
352 grpc_recv_us,
353 ),
354 _ => None,
355 };
356
357 #[cfg(feature = "perf-stats")]
358 {
359 PARSE_COUNT.fetch_add(1, Ordering::Relaxed);
360 PARSE_TIME_NS.fetch_add(start.elapsed().as_nanos() as usize, Ordering::Relaxed);
361 }
362
363 result
364}
365
366#[inline(always)]
368fn parse_create_pool_event_optimized(
369 data: &[u8],
370 signature: Signature,
371 slot: u64,
372 tx_index: u64,
373 block_time_us: Option<i64>,
374 grpc_recv_us: i64,
375) -> Option<DexEvent> {
376 parse_create_pool_from_data(
377 data,
378 EventMetadata {
379 signature,
380 slot,
381 tx_index,
382 block_time_us: block_time_us.unwrap_or(0),
383 grpc_recv_us,
384 recent_blockhash: None,
385 },
386 )
387}
388
389#[inline(always)]
391fn parse_add_liquidity_event_optimized(
392 data: &[u8],
393 signature: Signature,
394 slot: u64,
395 tx_index: u64,
396 block_time_us: Option<i64>,
397 grpc_recv_us: i64,
398) -> Option<DexEvent> {
399 parse_add_liquidity_from_data(
400 data,
401 EventMetadata {
402 signature,
403 slot,
404 tx_index,
405 block_time_us: block_time_us.unwrap_or(0),
406 grpc_recv_us,
407 recent_blockhash: None,
408 },
409 )
410}
411
412#[inline(always)]
414fn parse_remove_liquidity_event_optimized(
415 data: &[u8],
416 signature: Signature,
417 slot: u64,
418 tx_index: u64,
419 block_time_us: Option<i64>,
420 grpc_recv_us: i64,
421) -> Option<DexEvent> {
422 parse_remove_liquidity_from_data(
423 data,
424 EventMetadata {
425 signature,
426 slot,
427 tx_index,
428 block_time_us: block_time_us.unwrap_or(0),
429 grpc_recv_us,
430 recent_blockhash: None,
431 },
432 )
433}
434
435#[inline(always)]
443pub fn get_event_type_fast(log: &str) -> Option<u64> {
444 extract_discriminator_simd(log)
445}
446
447#[inline(always)]
449pub fn is_event_type(log: &str, discriminator: u64) -> bool {
450 extract_discriminator_simd(log) == Some(discriminator)
451}
452
453#[inline(always)]
460pub fn parse_buy_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
461 const LEGACY_LEN: usize = 16 * 8 + 7 * 32 + 1 + 4 * 8;
464 const MIN_REQUIRED_LEN: usize = LEGACY_LEN + 8 + 4;
465 if data.len() != LEGACY_LEN && data.len() < MIN_REQUIRED_LEN {
466 return None;
467 }
468 let track_volume = match data[352] {
469 0 => false,
470 1 => true,
471 _ => return None,
472 };
473
474 unsafe {
475 let timestamp = read_i64_unchecked(data, 0);
476 let base_amount_out = read_u64_unchecked(data, 8);
477 let max_quote_amount_in = read_u64_unchecked(data, 16);
478 let user_base_token_reserves = read_u64_unchecked(data, 24);
479 let user_quote_token_reserves = read_u64_unchecked(data, 32);
480 let pool_base_token_reserves = read_u64_unchecked(data, 40);
481 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
482 let quote_amount_in = read_u64_unchecked(data, 56);
483 let lp_fee_basis_points = read_u64_unchecked(data, 64);
484 let lp_fee = read_u64_unchecked(data, 72);
485 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
486 let protocol_fee = read_u64_unchecked(data, 88);
487 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
488 let user_quote_amount_in = read_u64_unchecked(data, 104);
489
490 let pool = read_pubkey_unchecked(data, 112);
491 let user = read_pubkey_unchecked(data, 144);
492 let user_base_token_account = read_pubkey_unchecked(data, 176);
493 let user_quote_token_account = read_pubkey_unchecked(data, 208);
494 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
495 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
496 let coin_creator = read_pubkey_unchecked(data, 304);
497
498 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
499 let coin_creator_fee = read_u64_unchecked(data, 344);
500 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
501 let total_claimed_tokens = read_u64_unchecked(data, 361);
502 let current_sol_volume = read_u64_unchecked(data, 369);
503 let last_update_timestamp = read_i64_unchecked(data, 377);
504
505 let (min_base_amount_out, ix_name, tail) = if data.len() == LEGACY_LEN {
506 (0, String::new(), PumpSwapTradeTail::default())
507 } else {
508 let min_base_amount_out = read_u64_unchecked(data, LEGACY_LEN);
509 let (ix_name, tail_offset) = read_borsh_string(data, LEGACY_LEN + 8)?;
510 let tail = parse_trade_tail(&data[tail_offset..])?;
511 (min_base_amount_out, ix_name, tail)
512 };
513
514 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
515 metadata,
516 timestamp,
517 base_amount_out,
518 max_quote_amount_in,
519 user_base_token_reserves,
520 user_quote_token_reserves,
521 pool_base_token_reserves,
522 pool_quote_token_reserves,
523 quote_amount_in,
524 lp_fee_basis_points,
525 lp_fee,
526 protocol_fee_basis_points,
527 protocol_fee,
528 quote_amount_in_with_lp_fee,
529 user_quote_amount_in,
530 pool,
531 user,
532 user_base_token_account,
533 user_quote_token_account,
534 protocol_fee_recipient,
535 protocol_fee_recipient_token_account,
536 coin_creator,
537 coin_creator_fee_basis_points,
538 coin_creator_fee,
539 track_volume,
540 total_unclaimed_tokens,
541 total_claimed_tokens,
542 current_sol_volume,
543 last_update_timestamp,
544 min_base_amount_out,
545 ix_name,
546 cashback_fee_basis_points: tail.cashback_fee_basis_points,
547 cashback: tail.cashback,
548 buyback_fee_basis_points: tail.buyback_fee_basis_points,
549 buyback_fee: tail.buyback_fee,
550 virtual_quote_reserves: tail.virtual_quote_reserves,
551 can_boost: tail.can_boost,
552 base_supply: tail.base_supply,
553 holder_rewards_bps: tail.holder_rewards_bps,
554 holder_rewards: tail.holder_rewards,
555 creator_fee_unclaimed: tail.creator_fee_unclaimed,
556 ..Default::default()
557 }))
558 }
559}
560
561#[inline(always)]
563pub fn parse_sell_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
564 const REQUIRED_LEN: usize = 14 * 8 + 7 * 32 + 2 * 8;
566 if data.len() < REQUIRED_LEN {
567 return None;
568 }
569
570 let tail = parse_trade_tail(&data[REQUIRED_LEN..])?;
571
572 unsafe {
573 let timestamp = read_i64_unchecked(data, 0);
574 let base_amount_in = read_u64_unchecked(data, 8);
575 let min_quote_amount_out = read_u64_unchecked(data, 16);
576 let user_base_token_reserves = read_u64_unchecked(data, 24);
577 let user_quote_token_reserves = read_u64_unchecked(data, 32);
578 let pool_base_token_reserves = read_u64_unchecked(data, 40);
579 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
580 let quote_amount_out = read_u64_unchecked(data, 56);
581 let lp_fee_basis_points = read_u64_unchecked(data, 64);
582 let lp_fee = read_u64_unchecked(data, 72);
583 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
584 let protocol_fee = read_u64_unchecked(data, 88);
585 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
586 let user_quote_amount_out = read_u64_unchecked(data, 104);
587
588 let pool = read_pubkey_unchecked(data, 112);
589 let user = read_pubkey_unchecked(data, 144);
590 let user_base_token_account = read_pubkey_unchecked(data, 176);
591 let user_quote_token_account = read_pubkey_unchecked(data, 208);
592 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
593 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
594 let coin_creator = read_pubkey_unchecked(data, 304);
595
596 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
597 let coin_creator_fee = read_u64_unchecked(data, 344);
598
599 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
600 metadata,
601 timestamp,
602 base_amount_in,
603 min_quote_amount_out,
604 user_base_token_reserves,
605 user_quote_token_reserves,
606 pool_base_token_reserves,
607 pool_quote_token_reserves,
608 quote_amount_out,
609 lp_fee_basis_points,
610 lp_fee,
611 protocol_fee_basis_points,
612 protocol_fee,
613 quote_amount_out_without_lp_fee,
614 user_quote_amount_out,
615 pool,
616 user,
617 user_base_token_account,
618 user_quote_token_account,
619 protocol_fee_recipient,
620 protocol_fee_recipient_token_account,
621 coin_creator,
622 coin_creator_fee_basis_points,
623 coin_creator_fee,
624 cashback_fee_basis_points: tail.cashback_fee_basis_points,
625 cashback: tail.cashback,
626 buyback_fee_basis_points: tail.buyback_fee_basis_points,
627 buyback_fee: tail.buyback_fee,
628 virtual_quote_reserves: tail.virtual_quote_reserves,
629 can_boost: tail.can_boost,
630 base_supply: tail.base_supply,
631 holder_rewards_bps: tail.holder_rewards_bps,
632 holder_rewards: tail.holder_rewards,
633 creator_fee_unclaimed: tail.creator_fee_unclaimed,
634 ..Default::default()
635 }))
636 }
637}
638
639#[inline(always)]
641fn read_borsh_bool_at(data: &[u8], offset: usize) -> Option<bool> {
642 match *data.get(offset)? {
643 0 => Some(false),
644 1 => Some(true),
645 _ => None,
646 }
647}
648
649pub fn parse_create_pool_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
650 const CREATE_POOL_EVENT_LEN: usize = 326;
651 const CREATOR_FEE_EVENT_LEN: usize = 335;
652 const REQUIRED_LEN: usize = CREATE_POOL_EVENT_LEN;
653 if data.len() < REQUIRED_LEN
654 || (data.len() != CREATE_POOL_EVENT_LEN && data.len() < CREATOR_FEE_EVENT_LEN)
655 {
656 return None;
657 }
658
659 unsafe {
660 let timestamp = read_i64_unchecked(data, 0);
661 let index = read_u16_unchecked(data, 8);
662
663 let creator = read_pubkey_unchecked(data, 10);
664 let base_mint = read_pubkey_unchecked(data, 42);
665 let quote_mint = read_pubkey_unchecked(data, 74);
666
667 let base_mint_decimals = read_u8_unchecked(data, 106);
668 let quote_mint_decimals = read_u8_unchecked(data, 107);
669
670 let base_amount_in = read_u64_unchecked(data, 108);
671 let quote_amount_in = read_u64_unchecked(data, 116);
672 let pool_base_amount = read_u64_unchecked(data, 124);
673 let pool_quote_amount = read_u64_unchecked(data, 132);
674 let minimum_liquidity = read_u64_unchecked(data, 140);
675 let initial_liquidity = read_u64_unchecked(data, 148);
676 let lp_token_amount_out = read_u64_unchecked(data, 156);
677
678 let pool_bump = read_u8_unchecked(data, 164);
679
680 let pool = read_pubkey_unchecked(data, 165);
681 let lp_mint = read_pubkey_unchecked(data, 197);
682 let user_base_token_account = read_pubkey_unchecked(data, 229);
683 let user_quote_token_account = read_pubkey_unchecked(data, 261);
684 let coin_creator = read_pubkey_unchecked(data, 293);
685 let is_mayhem_mode = read_borsh_bool_at(data, 325)?;
686 let creator_fee_bps = if data.len() >= 334 { read_u64_unchecked(data, 326) } else { 0 };
687 let can_edit_creator_fee =
688 if data.len() > 334 { read_borsh_bool_at(data, 334)? } else { false };
689 let is_holder_reward =
690 if data.len() > 335 { read_borsh_bool_at(data, 335)? } else { false };
691
692 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
693 metadata,
694 timestamp,
695 index,
696 creator,
697 base_mint,
698 quote_mint,
699 base_mint_decimals,
700 quote_mint_decimals,
701 base_amount_in,
702 quote_amount_in,
703 pool_base_amount,
704 pool_quote_amount,
705 minimum_liquidity,
706 initial_liquidity,
707 lp_token_amount_out,
708 pool_bump,
709 pool,
710 lp_mint,
711 user_base_token_account,
712 user_quote_token_account,
713 coin_creator,
714 is_mayhem_mode,
715 is_cashback_coin: false,
716 creator_fee_bps,
717 can_edit_creator_fee,
718 is_holder_reward,
719 }))
720 }
721}
722
723#[inline(always)]
725pub fn parse_add_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
726 const REQUIRED_LEN: usize = 11 * 8 + 5 * 32;
727 if data.len() < REQUIRED_LEN {
728 return None;
729 }
730
731 unsafe {
732 let timestamp = read_i64_unchecked(data, 0);
733 let lp_token_amount_out = read_u64_unchecked(data, 8);
734 let max_base_amount_in = read_u64_unchecked(data, 16);
735 let max_quote_amount_in = read_u64_unchecked(data, 24);
736 let user_base_token_reserves = read_u64_unchecked(data, 32);
737 let user_quote_token_reserves = read_u64_unchecked(data, 40);
738 let pool_base_token_reserves = read_u64_unchecked(data, 48);
739 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
740 let base_amount_in = read_u64_unchecked(data, 64);
741 let quote_amount_in = read_u64_unchecked(data, 72);
742 let lp_mint_supply = read_u64_unchecked(data, 80);
743
744 let pool = read_pubkey_unchecked(data, 88);
745 let user = read_pubkey_unchecked(data, 120);
746 let user_base_token_account = read_pubkey_unchecked(data, 152);
747 let user_quote_token_account = read_pubkey_unchecked(data, 184);
748 let user_pool_token_account = read_pubkey_unchecked(data, 216);
749
750 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
751 metadata,
752 timestamp,
753 lp_token_amount_out,
754 max_base_amount_in,
755 max_quote_amount_in,
756 user_base_token_reserves,
757 user_quote_token_reserves,
758 pool_base_token_reserves,
759 pool_quote_token_reserves,
760 base_amount_in,
761 quote_amount_in,
762 lp_mint_supply,
763 pool,
764 user,
765 user_base_token_account,
766 user_quote_token_account,
767 user_pool_token_account,
768 }))
769 }
770}
771
772#[inline(always)]
774pub fn parse_remove_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
775 const REQUIRED_LEN: usize = 11 * 8 + 5 * 32;
776 if data.len() < REQUIRED_LEN {
777 return None;
778 }
779
780 unsafe {
781 let timestamp = read_i64_unchecked(data, 0);
782 let lp_token_amount_in = read_u64_unchecked(data, 8);
783 let min_base_amount_out = read_u64_unchecked(data, 16);
784 let min_quote_amount_out = read_u64_unchecked(data, 24);
785 let user_base_token_reserves = read_u64_unchecked(data, 32);
786 let user_quote_token_reserves = read_u64_unchecked(data, 40);
787 let pool_base_token_reserves = read_u64_unchecked(data, 48);
788 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
789 let base_amount_out = read_u64_unchecked(data, 64);
790 let quote_amount_out = read_u64_unchecked(data, 72);
791 let lp_mint_supply = read_u64_unchecked(data, 80);
792
793 let pool = read_pubkey_unchecked(data, 88);
794 let user = read_pubkey_unchecked(data, 120);
795 let user_base_token_account = read_pubkey_unchecked(data, 152);
796 let user_quote_token_account = read_pubkey_unchecked(data, 184);
797 let user_pool_token_account = read_pubkey_unchecked(data, 216);
798
799 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
800 metadata,
801 timestamp,
802 lp_token_amount_in,
803 min_base_amount_out,
804 min_quote_amount_out,
805 user_base_token_reserves,
806 user_quote_token_reserves,
807 pool_base_token_reserves,
808 pool_quote_token_reserves,
809 base_amount_out,
810 quote_amount_out,
811 lp_mint_supply,
812 pool,
813 user,
814 user_base_token_account,
815 user_quote_token_account,
816 user_pool_token_account,
817 }))
818 }
819}
820
821#[cfg(feature = "perf-stats")]
826pub fn get_perf_stats() -> (usize, usize) {
827 let count = PARSE_COUNT.load(Ordering::Relaxed);
828 let total_ns = PARSE_TIME_NS.load(Ordering::Relaxed);
829 (count, total_ns)
830}
831
832#[cfg(feature = "perf-stats")]
833pub fn reset_perf_stats() {
834 PARSE_COUNT.store(0, Ordering::Relaxed);
835 PARSE_TIME_NS.store(0, Ordering::Relaxed);
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use base64::{engine::general_purpose::STANDARD, Engine as _};
842 use solana_sdk::{pubkey::Pubkey, signature::Signature};
843
844 fn metadata() -> EventMetadata {
845 EventMetadata {
846 signature: Signature::default(),
847 slot: 0,
848 tx_index: 0,
849 block_time_us: 0,
850 grpc_recv_us: 0,
851 recent_blockhash: None,
852 }
853 }
854
855 fn write_u64(buf: &mut [u8], offset: usize, value: u64) {
856 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
857 }
858
859 fn write_i64(buf: &mut [u8], offset: usize, value: i64) {
860 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
861 }
862
863 fn write_pubkey(buf: &mut [u8], offset: usize, value: Pubkey) {
864 buf[offset..offset + 32].copy_from_slice(value.as_ref());
865 }
866
867 fn append_current_trade_tail(data: &mut Vec<u8>) {
868 data.extend_from_slice(&177u64.to_le_bytes()); data.extend_from_slice(&188u64.to_le_bytes()); data.extend_from_slice(&199u64.to_le_bytes()); data.extend_from_slice(&211u64.to_le_bytes()); data.extend_from_slice(&(-987_654_321i128).to_le_bytes());
873 data.push(1); data.extend_from_slice(&222u64.to_le_bytes()); data.extend_from_slice(&233u64.to_le_bytes()); data.extend_from_slice(&244u64.to_le_bytes()); }
878
879 fn append_buyback_trade_tail(data: &mut Vec<u8>) {
880 data.extend_from_slice(&177u64.to_le_bytes()); data.extend_from_slice(&188u64.to_le_bytes()); data.extend_from_slice(&199u64.to_le_bytes()); data.extend_from_slice(&211u64.to_le_bytes()); }
885
886 fn build_buy_payload(include_current_tail: bool) -> Vec<u8> {
887 let mut data = vec![0u8; 393];
888 write_i64(&mut data, 0, 1_713_498_953);
889 write_u64(&mut data, 8, 11);
890 write_u64(&mut data, 385, 22);
891 data.extend_from_slice(&3u32.to_le_bytes());
892 data.extend_from_slice(b"buy");
893 if include_current_tail {
894 append_current_trade_tail(&mut data);
895 }
896 data
897 }
898
899 fn build_sell_payload(include_cashback: bool) -> Vec<u8> {
900 let len = if include_cashback { 368 } else { 352 };
901 let mut data = vec![0u8; len];
902
903 write_i64(&mut data, 0, 1_713_498_953);
904 write_u64(&mut data, 8, 11);
905 write_u64(&mut data, 16, 22);
906 write_u64(&mut data, 24, 33);
907 write_u64(&mut data, 32, 44);
908 write_u64(&mut data, 40, 55);
909 write_u64(&mut data, 48, 66);
910 write_u64(&mut data, 56, 77);
911 write_u64(&mut data, 64, 88);
912 write_u64(&mut data, 72, 99);
913 write_u64(&mut data, 80, 111);
914 write_u64(&mut data, 88, 122);
915 write_u64(&mut data, 96, 133);
916 write_u64(&mut data, 104, 144);
917
918 write_pubkey(&mut data, 112, Pubkey::new_from_array([1; 32]));
919 write_pubkey(&mut data, 144, Pubkey::new_from_array([2; 32]));
920 write_pubkey(&mut data, 176, Pubkey::new_from_array([3; 32]));
921 write_pubkey(&mut data, 208, Pubkey::new_from_array([4; 32]));
922 write_pubkey(&mut data, 240, Pubkey::new_from_array([5; 32]));
923 write_pubkey(&mut data, 272, Pubkey::new_from_array([6; 32]));
924 write_pubkey(&mut data, 304, Pubkey::new_from_array([7; 32]));
925
926 write_u64(&mut data, 336, 155);
927 write_u64(&mut data, 344, 166);
928
929 if include_cashback {
930 write_u64(&mut data, 352, 177);
931 write_u64(&mut data, 360, 188);
932 }
933
934 data
935 }
936
937 fn build_create_pool_payload(is_mayhem_mode: bool) -> Vec<u8> {
938 let mut data = vec![0u8; 326];
939
940 write_i64(&mut data, 0, 1_713_498_953);
941 data[8..10].copy_from_slice(&42u16.to_le_bytes());
942 write_pubkey(&mut data, 10, Pubkey::new_from_array([1; 32]));
943 write_pubkey(&mut data, 42, Pubkey::new_from_array([2; 32]));
944 write_pubkey(&mut data, 74, Pubkey::new_from_array([3; 32]));
945 data[106] = 6;
946 data[107] = 9;
947 write_u64(&mut data, 108, 11);
948 write_u64(&mut data, 116, 22);
949 write_u64(&mut data, 124, 33);
950 write_u64(&mut data, 132, 44);
951 write_u64(&mut data, 140, 55);
952 write_u64(&mut data, 148, 66);
953 write_u64(&mut data, 156, 77);
954 data[164] = 8;
955 write_pubkey(&mut data, 165, Pubkey::new_from_array([4; 32]));
956 write_pubkey(&mut data, 197, Pubkey::new_from_array([5; 32]));
957 write_pubkey(&mut data, 229, Pubkey::new_from_array([6; 32]));
958 write_pubkey(&mut data, 261, Pubkey::new_from_array([7; 32]));
959 write_pubkey(&mut data, 293, Pubkey::new_from_array([8; 32]));
960 data[325] = u8::from(is_mayhem_mode);
961 data.extend_from_slice(&250u64.to_le_bytes());
962 data.push(1);
963 data.push(1);
964
965 data
966 }
967
968 #[test]
969 fn test_discriminator_simd() {
970 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
972 let disc = extract_discriminator_simd(log);
973 assert!(disc.is_some());
974 }
975
976 #[test]
977 fn test_parse_performance() {
978 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
980 let sig = Signature::default();
981
982 let start = std::time::Instant::now();
983 for _ in 0..1000 {
984 let _ = parse_log(log, sig, 0, 0, Some(0), 0);
985 }
986 let elapsed = start.elapsed();
987
988 println!("Average parse time: {} ns", elapsed.as_nanos() / 1000);
989 }
990
991 #[test]
992 fn parse_sell_from_data_preserves_cashback_fields() {
993 let event = parse_sell_from_data(&build_sell_payload(true), metadata())
994 .expect("expected pumpswap sell event");
995
996 let DexEvent::PumpSwapSell(event) = event else {
997 panic!("expected PumpSwapSell event");
998 };
999
1000 assert_eq!(event.cashback_fee_basis_points, 177);
1001 assert_eq!(event.cashback, 188);
1002 assert_eq!(event.coin_creator_fee_basis_points, 155);
1003 assert_eq!(event.coin_creator_fee, 166);
1004 }
1005
1006 #[test]
1007 fn parse_current_buy_from_data_reads_virtual_reserves() {
1008 let event = parse_buy_from_data(&build_buy_payload(true), metadata())
1009 .expect("expected current pumpswap buy event");
1010
1011 let DexEvent::PumpSwapBuy(event) = event else {
1012 panic!("expected PumpSwapBuy event");
1013 };
1014
1015 assert_eq!(event.min_base_amount_out, 22);
1016 assert_eq!(event.ix_name, "buy");
1017 assert_eq!(event.cashback_fee_basis_points, 177);
1018 assert_eq!(event.cashback, 188);
1019 assert_eq!(event.buyback_fee_basis_points, 199);
1020 assert_eq!(event.buyback_fee, 211);
1021 assert_eq!(event.virtual_quote_reserves, -987_654_321);
1022 assert!(event.can_boost);
1023 assert_eq!(event.holder_rewards_bps, 233);
1024 assert_eq!(event.holder_rewards, 244);
1025 assert_eq!(event.base_supply, 222);
1026 assert_eq!(event.holder_rewards_bps, 233);
1027 assert_eq!(event.holder_rewards, 244);
1028 }
1029
1030 #[test]
1031 fn parse_log_uses_current_buy_layout() {
1032 let mut program_data = discriminators::BUY.to_le_bytes().to_vec();
1033 program_data.extend_from_slice(&build_buy_payload(true));
1034 let log = format!("Program data: {}", STANDARD.encode(program_data));
1035
1036 let event = parse_log(&log, Signature::default(), 7, 8, Some(9), 10)
1037 .expect("expected current pumpswap buy log");
1038 let DexEvent::PumpSwapBuy(event) = event else {
1039 panic!("expected PumpSwapBuy event");
1040 };
1041
1042 assert_eq!(event.metadata.slot, 7);
1043 assert_eq!(event.virtual_quote_reserves, -987_654_321);
1044 assert!(event.can_boost);
1045 }
1046
1047 #[test]
1048 fn parse_log_uses_current_sell_layout() {
1049 let mut payload = build_sell_payload(false);
1050 append_current_trade_tail(&mut payload);
1051 let mut program_data = discriminators::SELL.to_le_bytes().to_vec();
1052 program_data.extend_from_slice(&payload);
1053 let log = format!("Program data: {}", STANDARD.encode(program_data));
1054
1055 let event = parse_log(&log, Signature::default(), 17, 18, Some(19), 20)
1056 .expect("expected current pumpswap sell log");
1057 let DexEvent::PumpSwapSell(event) = event else {
1058 panic!("expected PumpSwapSell event");
1059 };
1060
1061 assert_eq!(event.metadata.slot, 17);
1062 assert_eq!(event.virtual_quote_reserves, -987_654_321);
1063 assert!(event.can_boost);
1064 assert_eq!(event.base_supply, 222);
1065 }
1066
1067 #[test]
1068 fn parse_current_sell_from_data_reads_virtual_reserves() {
1069 let mut data = build_sell_payload(false);
1070 append_current_trade_tail(&mut data);
1071 let event =
1072 parse_sell_from_data(&data, metadata()).expect("expected current pumpswap sell event");
1073
1074 let DexEvent::PumpSwapSell(event) = event else {
1075 panic!("expected PumpSwapSell event");
1076 };
1077
1078 assert_eq!(event.cashback_fee_basis_points, 177);
1079 assert_eq!(event.cashback, 188);
1080 assert_eq!(event.buyback_fee_basis_points, 199);
1081 assert_eq!(event.buyback_fee, 211);
1082 assert_eq!(event.virtual_quote_reserves, -987_654_321);
1083 assert!(event.can_boost);
1084 assert_eq!(event.base_supply, 222);
1085 }
1086
1087 #[test]
1088 fn trade_parsers_preserve_i128_extremes() {
1089 let mut buy = build_buy_payload(true);
1090 buy[432..448].copy_from_slice(&i128::MIN.to_le_bytes());
1091 let DexEvent::PumpSwapBuy(buy) =
1092 parse_buy_from_data(&buy, metadata()).expect("expected current buy")
1093 else {
1094 panic!("expected PumpSwapBuy event");
1095 };
1096 assert_eq!(buy.virtual_quote_reserves, i128::MIN);
1097
1098 let mut sell = build_sell_payload(false);
1099 append_current_trade_tail(&mut sell);
1100 sell[384..400].copy_from_slice(&i128::MAX.to_le_bytes());
1101 let DexEvent::PumpSwapSell(sell) =
1102 parse_sell_from_data(&sell, metadata()).expect("expected current sell")
1103 else {
1104 panic!("expected PumpSwapSell event");
1105 };
1106 assert_eq!(sell.virtual_quote_reserves, i128::MAX);
1107 }
1108
1109 #[test]
1110 fn parse_sell_from_data_keeps_legacy_payload_compatible() {
1111 let event = parse_sell_from_data(&build_sell_payload(false), metadata())
1112 .expect("expected legacy pumpswap sell event");
1113
1114 let DexEvent::PumpSwapSell(event) = event else {
1115 panic!("expected PumpSwapSell event");
1116 };
1117
1118 assert_eq!(event.cashback_fee_basis_points, 0);
1119 assert_eq!(event.cashback, 0);
1120 assert_eq!(event.coin_creator_fee_basis_points, 155);
1121 assert_eq!(event.coin_creator_fee, 166);
1122 assert_eq!(event.virtual_quote_reserves, 0);
1123 assert!(!event.can_boost);
1124 assert_eq!(event.base_supply, 0);
1125 }
1126
1127 #[test]
1128 fn parse_buyback_layouts_without_boost_fields() {
1129 let mut buy = build_buy_payload(false);
1130 append_buyback_trade_tail(&mut buy);
1131 let DexEvent::PumpSwapBuy(buy) =
1132 parse_buy_from_data(&buy, metadata()).expect("expected buyback buy event")
1133 else {
1134 panic!("expected PumpSwapBuy event");
1135 };
1136 assert_eq!(buy.cashback_fee_basis_points, 177);
1137 assert_eq!(buy.buyback_fee_basis_points, 199);
1138 assert_eq!(buy.buyback_fee, 211);
1139 assert_eq!(buy.virtual_quote_reserves, 0);
1140
1141 let mut sell = build_sell_payload(false);
1142 append_buyback_trade_tail(&mut sell);
1143 let DexEvent::PumpSwapSell(sell) =
1144 parse_sell_from_data(&sell, metadata()).expect("expected buyback sell event")
1145 else {
1146 panic!("expected PumpSwapSell event");
1147 };
1148 assert_eq!(sell.cashback_fee_basis_points, 177);
1149 assert_eq!(sell.buyback_fee_basis_points, 199);
1150 assert_eq!(sell.buyback_fee, 211);
1151 assert_eq!(sell.virtual_quote_reserves, 0);
1152 }
1153
1154 #[test]
1155 fn trade_tail_accepts_only_complete_layouts() {
1156 for len in 0..=80 {
1157 let tail = vec![0u8; len];
1158 let expected = matches!(len, 0 | 16 | 32 | 57 | 73 | 81..);
1159 assert_eq!(parse_trade_tail(&tail).is_some(), expected, "tail length {len}");
1160 }
1161
1162 for invalid_bool in 2..=u8::MAX {
1163 let mut tail = vec![0u8; 57];
1164 tail[48] = invalid_bool;
1165 assert!(parse_trade_tail(&tail).is_none(), "bool value {invalid_bool}");
1166 }
1167 }
1168
1169 #[test]
1170 fn parse_buy_from_data_rejects_truncated_min_base_payload() {
1171 assert!(parse_buy_from_data(&vec![0u8; 385], metadata()).is_some());
1172 for len in 386..397 {
1173 assert!(parse_buy_from_data(&vec![0u8; len], metadata()).is_none());
1174 }
1175 assert!(parse_buy_from_data(&vec![0u8; 397], metadata()).is_some());
1176 }
1177
1178 #[test]
1179 fn parse_buy_from_data_rejects_malformed_string_and_partial_tails() {
1180 let mut invalid_track_volume = build_buy_payload(false);
1181 invalid_track_volume[352] = 2;
1182 assert!(parse_buy_from_data(&invalid_track_volume, metadata()).is_none());
1183
1184 let mut invalid_utf8 = build_buy_payload(false);
1185 invalid_utf8[397] = 0xff;
1186 assert!(parse_buy_from_data(&invalid_utf8, metadata()).is_none());
1187
1188 let legacy = build_buy_payload(false);
1189 for partial_len in [1, 15, 17, 31, 33, 56] {
1190 let mut partial = legacy.clone();
1191 partial.resize(legacy.len() + partial_len, 0);
1192 assert!(parse_buy_from_data(&partial, metadata()).is_none());
1193 }
1194
1195 let mut oversized_name = vec![0u8; 397];
1196 oversized_name[393..397].copy_from_slice(&u32::MAX.to_le_bytes());
1197 assert!(parse_buy_from_data(&oversized_name, metadata()).is_none());
1198 }
1199
1200 #[test]
1201 fn parse_sell_from_data_rejects_partial_or_invalid_boost_tails() {
1202 for truncated_len in 336..352 {
1203 assert!(parse_sell_from_data(&vec![0u8; truncated_len], metadata()).is_none());
1204 }
1205
1206 let legacy = build_sell_payload(false);
1207 for partial_len in [1, 15, 17, 31, 33, 56] {
1208 let mut partial = legacy.clone();
1209 partial.resize(legacy.len() + partial_len, 0);
1210 assert!(parse_sell_from_data(&partial, metadata()).is_none());
1211 }
1212
1213 let mut invalid_bool = legacy;
1214 append_current_trade_tail(&mut invalid_bool);
1215 invalid_bool[352 + 48] = 2;
1216 assert!(parse_sell_from_data(&invalid_bool, metadata()).is_none());
1217 }
1218
1219 #[test]
1220 fn parse_create_pool_from_data_reads_current_fields() {
1221 let event = parse_create_pool_from_data(&build_create_pool_payload(true), metadata())
1222 .expect("expected pumpswap create pool event");
1223
1224 let DexEvent::PumpSwapCreatePool(event) = event else {
1225 panic!("expected PumpSwapCreatePool event");
1226 };
1227
1228 assert_eq!(event.index, 42);
1229 assert!(event.is_mayhem_mode);
1230 assert!(!event.is_cashback_coin);
1231 assert_eq!(event.creator_fee_bps, 250);
1232 assert!(event.can_edit_creator_fee);
1233 assert!(event.is_holder_reward);
1234 }
1235
1236 #[test]
1237 fn parse_create_pool_accepts_only_complete_layouts() {
1238 for len in 326..=336 {
1239 let expected = len == 326 || len >= 335;
1240 assert_eq!(
1241 parse_create_pool_from_data(&vec![0u8; len], metadata()).is_some(),
1242 expected,
1243 "create pool length {len}"
1244 );
1245 }
1246 }
1247}