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)]
137unsafe fn read_i128_unchecked(data: &[u8], offset: usize) -> i128 {
138 let ptr = data.as_ptr().add(offset) as *const i128;
139 i128::from_le(ptr.read_unaligned())
140}
141
142#[inline(always)]
144unsafe fn read_i64_unchecked(data: &[u8], offset: usize) -> i64 {
145 let ptr = data.as_ptr().add(offset) as *const i64;
146 i64::from_le(ptr.read_unaligned())
147}
148
149#[inline(always)]
151unsafe fn read_u16_unchecked(data: &[u8], offset: usize) -> u16 {
152 let ptr = data.as_ptr().add(offset) as *const u16;
153 u16::from_le(ptr.read_unaligned())
154}
155
156#[allow(dead_code)]
158#[inline(always)]
159unsafe fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
160 let ptr = data.as_ptr().add(offset) as *const u32;
161 u32::from_le(ptr.read_unaligned())
162}
163
164#[inline(always)]
166unsafe fn read_u8_unchecked(data: &[u8], offset: usize) -> u8 {
167 *data.get_unchecked(offset)
168}
169
170#[inline(always)]
172unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
173 *data.get_unchecked(offset) == 1
174}
175
176#[inline(always)]
180unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> Pubkey {
181 #[cfg(target_arch = "x86_64")]
184 {
185 use std::arch::x86_64::_mm_prefetch;
186 use std::arch::x86_64::_MM_HINT_T0;
187 if offset + 64 < data.len() {
188 _mm_prefetch((data.as_ptr().add(offset + 32)) as *const i8, _MM_HINT_T0);
189 }
190 }
191
192 let ptr = data.as_ptr().add(offset);
193 let mut bytes = [0u8; 32];
194 std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
195 Pubkey::new_from_array(bytes)
196}
197
198const BUY_LEGACY_LEN: usize = 385;
199const BUY_MIN_AND_STRING_PREFIX_LEN: usize = 397;
200const BUY_CASHBACK_TAIL_LEN: usize = 16;
201const BUY_CURRENT_TAIL_LEN: usize = 57;
202const SELL_LEGACY_LEN: usize = 352;
203const SELL_CASHBACK_LEN: usize = 368;
204const SELL_CURRENT_LEN: usize = 409;
205
206#[inline]
207pub(crate) fn validate_buy_payload_layout(data: &[u8]) -> Option<()> {
208 if data.len() == BUY_LEGACY_LEN {
209 return Some(());
210 }
211 if data.len() < BUY_MIN_AND_STRING_PREFIX_LEN {
212 return None;
213 }
214
215 let name_len = u32::from_le_bytes(data.get(393..397)?.try_into().ok()?) as usize;
216 let name_end = BUY_MIN_AND_STRING_PREFIX_LEN.checked_add(name_len)?;
217 let name = data.get(BUY_MIN_AND_STRING_PREFIX_LEN..name_end)?;
218 std::str::from_utf8(name).ok()?;
219
220 match data.len() - name_end {
221 0 | BUY_CASHBACK_TAIL_LEN => Some(()),
222 BUY_CURRENT_TAIL_LEN.. => Some(()),
223 _ => None,
224 }
225}
226
227#[inline]
228pub(crate) fn validate_sell_payload_layout(data: &[u8]) -> Option<()> {
229 match data.len() {
230 SELL_LEGACY_LEN | SELL_CASHBACK_LEN => Some(()),
231 SELL_CURRENT_LEN.. => Some(()),
232 _ => None,
233 }
234}
235
236#[inline(always)]
244pub fn parse_log(
245 log: &str,
246 signature: Signature,
247 slot: u64,
248 tx_index: u64,
249 block_time_us: Option<i64>,
250 grpc_recv_us: i64,
251) -> Option<DexEvent> {
252 #[cfg(feature = "perf-stats")]
253 let start = std::time::Instant::now();
254
255 let mut buf = [0u8; 2048];
257 let program_data = extract_program_data_zero_copy(log, &mut buf)?;
258
259 if program_data.len() < 8 {
260 return None;
261 }
262
263 let discriminator = unsafe { read_u64_unchecked(program_data, 0) };
265 let data = &program_data[8..];
266
267 let result = match discriminator {
268 discriminators::BUY => {
269 parse_buy_event_optimized(data, signature, slot, tx_index, block_time_us, grpc_recv_us)
270 }
271 discriminators::SELL => {
272 parse_sell_event_optimized(data, signature, slot, tx_index, block_time_us, grpc_recv_us)
273 }
274 discriminators::CREATE_POOL => parse_create_pool_event_optimized(
275 data,
276 signature,
277 slot,
278 tx_index,
279 block_time_us,
280 grpc_recv_us,
281 ),
282 discriminators::ADD_LIQUIDITY => parse_add_liquidity_event_optimized(
283 data,
284 signature,
285 slot,
286 tx_index,
287 block_time_us,
288 grpc_recv_us,
289 ),
290 discriminators::REMOVE_LIQUIDITY => parse_remove_liquidity_event_optimized(
291 data,
292 signature,
293 slot,
294 tx_index,
295 block_time_us,
296 grpc_recv_us,
297 ),
298 _ => None,
299 };
300
301 #[cfg(feature = "perf-stats")]
302 {
303 PARSE_COUNT.fetch_add(1, Ordering::Relaxed);
304 PARSE_TIME_NS.fetch_add(start.elapsed().as_nanos() as usize, Ordering::Relaxed);
305 }
306
307 result
308}
309
310#[inline(always)]
317fn parse_buy_event_optimized(
318 data: &[u8],
319 signature: Signature,
320 slot: u64,
321 tx_index: u64,
322 block_time_us: Option<i64>,
323 grpc_recv_us: i64,
324) -> Option<DexEvent> {
325 validate_buy_payload_layout(data)?;
326
327 unsafe {
328 let timestamp = read_i64_unchecked(data, 0);
329 let base_amount_out = read_u64_unchecked(data, 8);
330 let max_quote_amount_in = read_u64_unchecked(data, 16);
331 let user_base_token_reserves = read_u64_unchecked(data, 24);
332 let user_quote_token_reserves = read_u64_unchecked(data, 32);
333 let pool_base_token_reserves = read_u64_unchecked(data, 40);
334 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
335 let quote_amount_in = read_u64_unchecked(data, 56);
336 let lp_fee_basis_points = read_u64_unchecked(data, 64);
337 let lp_fee = read_u64_unchecked(data, 72);
338 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
339 let protocol_fee = read_u64_unchecked(data, 88);
340 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
341 let user_quote_amount_in = read_u64_unchecked(data, 104);
342
343 let pool = read_pubkey_unchecked(data, 112);
344 let user = read_pubkey_unchecked(data, 144);
345 let user_base_token_account = read_pubkey_unchecked(data, 176);
346 let user_quote_token_account = read_pubkey_unchecked(data, 208);
347 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
348 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
349 let coin_creator = read_pubkey_unchecked(data, 304);
350
351 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
352 let coin_creator_fee = read_u64_unchecked(data, 344);
353 let track_volume = read_bool_unchecked(data, 352);
354 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
355 let total_claimed_tokens = read_u64_unchecked(data, 361);
356 let current_sol_volume = read_u64_unchecked(data, 369);
357 let last_update_timestamp = read_i64_unchecked(data, 377);
358
359 let mut offset = BUY_LEGACY_LEN;
361 let min_base_amount_out = if data.len() == BUY_LEGACY_LEN {
362 0
363 } else {
364 let value = read_u64_unchecked(data, offset);
365 offset += 8;
366 value
367 };
368
369 let ix_name = if offset + 4 <= data.len() {
371 let len = read_u32_unchecked(data, offset) as usize;
372 offset += 4;
373 if offset + len <= data.len() {
374 let string_bytes = &data[offset..offset + len];
375 let s = std::str::from_utf8(string_bytes).ok()?;
376 offset += len;
377 s.to_string()
378 } else {
379 String::new()
380 }
381 } else {
382 String::new()
383 };
384
385 let cashback_fee_basis_points =
387 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
388 offset += 8;
389 let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
390 offset += 8;
391 let buyback_fee_basis_points =
392 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
393 offset += 8;
394 let buyback_fee =
395 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
396 offset += 8;
397 let virtual_quote_reserves =
398 if offset + 16 <= data.len() { read_i128_unchecked(data, offset) } else { 0 };
399 offset += 16;
400 let can_boost = if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
401 offset += 1;
402 let base_supply =
403 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
404
405 let metadata = EventMetadata {
406 signature,
407 slot,
408 tx_index,
409 block_time_us: block_time_us.unwrap_or(0),
410 grpc_recv_us,
411 recent_blockhash: None,
412 };
413
414 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
415 metadata,
416 timestamp,
417 base_amount_out,
418 max_quote_amount_in,
419 user_base_token_reserves,
420 user_quote_token_reserves,
421 pool_base_token_reserves,
422 pool_quote_token_reserves,
423 quote_amount_in,
424 lp_fee_basis_points,
425 lp_fee,
426 protocol_fee_basis_points,
427 protocol_fee,
428 quote_amount_in_with_lp_fee,
429 user_quote_amount_in,
430 pool,
431 user,
432 user_base_token_account,
433 user_quote_token_account,
434 protocol_fee_recipient,
435 protocol_fee_recipient_token_account,
436 coin_creator,
437 coin_creator_fee_basis_points,
438 coin_creator_fee,
439 track_volume,
440 total_unclaimed_tokens,
441 total_claimed_tokens,
442 current_sol_volume,
443 last_update_timestamp,
444 min_base_amount_out,
445 ix_name,
446 cashback_fee_basis_points,
447 cashback,
448 buyback_fee_basis_points,
449 buyback_fee,
450 virtual_quote_reserves,
451 can_boost,
452 base_supply,
453 ..Default::default()
454 }))
455 }
456}
457
458#[inline(always)]
460fn parse_sell_event_optimized(
461 data: &[u8],
462 signature: Signature,
463 slot: u64,
464 tx_index: u64,
465 block_time_us: Option<i64>,
466 grpc_recv_us: i64,
467) -> Option<DexEvent> {
468 validate_sell_payload_layout(data)?;
469
470 unsafe {
471 let timestamp = read_i64_unchecked(data, 0);
472 let base_amount_in = read_u64_unchecked(data, 8);
473 let min_quote_amount_out = read_u64_unchecked(data, 16);
474 let user_base_token_reserves = read_u64_unchecked(data, 24);
475 let user_quote_token_reserves = read_u64_unchecked(data, 32);
476 let pool_base_token_reserves = read_u64_unchecked(data, 40);
477 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
478 let quote_amount_out = read_u64_unchecked(data, 56);
479 let lp_fee_basis_points = read_u64_unchecked(data, 64);
480 let lp_fee = read_u64_unchecked(data, 72);
481 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
482 let protocol_fee = read_u64_unchecked(data, 88);
483 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
484 let user_quote_amount_out = read_u64_unchecked(data, 104);
485
486 let pool = read_pubkey_unchecked(data, 112);
487 let user = read_pubkey_unchecked(data, 144);
488 let user_base_token_account = read_pubkey_unchecked(data, 176);
489 let user_quote_token_account = read_pubkey_unchecked(data, 208);
490 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
491 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
492 let coin_creator = read_pubkey_unchecked(data, 304);
493
494 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
495 let coin_creator_fee = read_u64_unchecked(data, 344);
496 let cashback_fee_basis_points =
498 if data.len() >= 360 { read_u64_unchecked(data, 352) } else { 0 };
499 let cashback = if data.len() >= 368 { read_u64_unchecked(data, 360) } else { 0 };
500 let buyback_fee_basis_points =
501 if data.len() >= 376 { read_u64_unchecked(data, 368) } else { 0 };
502 let buyback_fee = if data.len() >= 384 { read_u64_unchecked(data, 376) } else { 0 };
503 let virtual_quote_reserves =
504 if data.len() >= 400 { read_i128_unchecked(data, 384) } else { 0 };
505 let can_boost = if data.len() >= 401 { read_bool_unchecked(data, 400) } else { false };
506 let base_supply = if data.len() >= 409 { read_u64_unchecked(data, 401) } else { 0 };
507
508 let metadata = EventMetadata {
509 signature,
510 slot,
511 tx_index,
512 block_time_us: block_time_us.unwrap_or(0),
513 grpc_recv_us,
514 recent_blockhash: None,
515 };
516
517 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
518 metadata,
519 timestamp,
520 base_amount_in,
521 min_quote_amount_out,
522 user_base_token_reserves,
523 user_quote_token_reserves,
524 pool_base_token_reserves,
525 pool_quote_token_reserves,
526 quote_amount_out,
527 lp_fee_basis_points,
528 lp_fee,
529 protocol_fee_basis_points,
530 protocol_fee,
531 quote_amount_out_without_lp_fee,
532 user_quote_amount_out,
533 pool,
534 user,
535 user_base_token_account,
536 user_quote_token_account,
537 protocol_fee_recipient,
538 protocol_fee_recipient_token_account,
539 coin_creator,
540 coin_creator_fee_basis_points,
541 coin_creator_fee,
542 cashback_fee_basis_points,
543 cashback,
544 buyback_fee_basis_points,
545 buyback_fee,
546 virtual_quote_reserves,
547 can_boost,
548 base_supply,
549 ..Default::default()
550 }))
551 }
552}
553
554#[inline(always)]
556fn parse_create_pool_event_optimized(
557 data: &[u8],
558 signature: Signature,
559 slot: u64,
560 tx_index: u64,
561 block_time_us: Option<i64>,
562 grpc_recv_us: i64,
563) -> Option<DexEvent> {
564 const CREATE_POOL_EVENT_LEN: usize = 326;
566 const REQUIRED_LEN: usize = CREATE_POOL_EVENT_LEN;
567 if data.len() < REQUIRED_LEN {
568 return None;
569 }
570
571 unsafe {
572 let timestamp = read_i64_unchecked(data, 0);
573 let index = read_u16_unchecked(data, 8);
574
575 let creator = read_pubkey_unchecked(data, 10);
576 let base_mint = read_pubkey_unchecked(data, 42);
577 let quote_mint = read_pubkey_unchecked(data, 74);
578
579 let base_mint_decimals = read_u8_unchecked(data, 106);
580 let quote_mint_decimals = read_u8_unchecked(data, 107);
581
582 let base_amount_in = read_u64_unchecked(data, 108);
583 let quote_amount_in = read_u64_unchecked(data, 116);
584 let pool_base_amount = read_u64_unchecked(data, 124);
585 let pool_quote_amount = read_u64_unchecked(data, 132);
586 let minimum_liquidity = read_u64_unchecked(data, 140);
587 let initial_liquidity = read_u64_unchecked(data, 148);
588 let lp_token_amount_out = read_u64_unchecked(data, 156);
589
590 let pool_bump = read_u8_unchecked(data, 164);
591
592 let pool = read_pubkey_unchecked(data, 165);
593 let lp_mint = read_pubkey_unchecked(data, 197);
594 let user_base_token_account = read_pubkey_unchecked(data, 229);
595 let user_quote_token_account = read_pubkey_unchecked(data, 261);
596 let coin_creator = read_pubkey_unchecked(data, 293);
597 let is_mayhem_mode = read_bool_unchecked(data, 325);
598
599 let metadata = EventMetadata {
600 signature,
601 slot,
602 tx_index,
603 block_time_us: block_time_us.unwrap_or(0),
604 grpc_recv_us,
605 recent_blockhash: None,
606 };
607
608 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
609 metadata,
610 timestamp,
611 index,
612 creator,
613 base_mint,
614 quote_mint,
615 base_mint_decimals,
616 quote_mint_decimals,
617 base_amount_in,
618 quote_amount_in,
619 pool_base_amount,
620 pool_quote_amount,
621 minimum_liquidity,
622 initial_liquidity,
623 lp_token_amount_out,
624 pool_bump,
625 pool,
626 lp_mint,
627 user_base_token_account,
628 user_quote_token_account,
629 coin_creator,
630 is_mayhem_mode,
631 is_cashback_coin: false,
632 }))
633 }
634}
635
636#[inline(always)]
638fn parse_add_liquidity_event_optimized(
639 data: &[u8],
640 signature: Signature,
641 slot: u64,
642 tx_index: u64,
643 block_time_us: Option<i64>,
644 grpc_recv_us: i64,
645) -> Option<DexEvent> {
646 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
647 if data.len() < REQUIRED_LEN {
648 return None;
649 }
650
651 unsafe {
652 let timestamp = read_i64_unchecked(data, 0);
653 let lp_token_amount_out = read_u64_unchecked(data, 8);
654 let max_base_amount_in = read_u64_unchecked(data, 16);
655 let max_quote_amount_in = read_u64_unchecked(data, 24);
656 let user_base_token_reserves = read_u64_unchecked(data, 32);
657 let user_quote_token_reserves = read_u64_unchecked(data, 40);
658 let pool_base_token_reserves = read_u64_unchecked(data, 48);
659 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
660 let base_amount_in = read_u64_unchecked(data, 64);
661 let quote_amount_in = read_u64_unchecked(data, 72);
662 let lp_mint_supply = read_u64_unchecked(data, 80);
663
664 let pool = read_pubkey_unchecked(data, 88);
665 let user = read_pubkey_unchecked(data, 120);
666 let user_base_token_account = read_pubkey_unchecked(data, 152);
667 let user_quote_token_account = read_pubkey_unchecked(data, 184);
668 let user_pool_token_account = read_pubkey_unchecked(data, 216);
669
670 let metadata = EventMetadata {
671 signature,
672 slot,
673 tx_index,
674 block_time_us: block_time_us.unwrap_or(0),
675 grpc_recv_us,
676 recent_blockhash: None,
677 };
678
679 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
680 metadata,
681 timestamp,
682 lp_token_amount_out,
683 max_base_amount_in,
684 max_quote_amount_in,
685 user_base_token_reserves,
686 user_quote_token_reserves,
687 pool_base_token_reserves,
688 pool_quote_token_reserves,
689 base_amount_in,
690 quote_amount_in,
691 lp_mint_supply,
692 pool,
693 user,
694 user_base_token_account,
695 user_quote_token_account,
696 user_pool_token_account,
697 }))
698 }
699}
700
701#[inline(always)]
703fn parse_remove_liquidity_event_optimized(
704 data: &[u8],
705 signature: Signature,
706 slot: u64,
707 tx_index: u64,
708 block_time_us: Option<i64>,
709 grpc_recv_us: i64,
710) -> Option<DexEvent> {
711 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
712 if data.len() < REQUIRED_LEN {
713 return None;
714 }
715
716 unsafe {
717 let timestamp = read_i64_unchecked(data, 0);
718 let lp_token_amount_in = read_u64_unchecked(data, 8);
719 let min_base_amount_out = read_u64_unchecked(data, 16);
720 let min_quote_amount_out = read_u64_unchecked(data, 24);
721 let user_base_token_reserves = read_u64_unchecked(data, 32);
722 let user_quote_token_reserves = read_u64_unchecked(data, 40);
723 let pool_base_token_reserves = read_u64_unchecked(data, 48);
724 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
725 let base_amount_out = read_u64_unchecked(data, 64);
726 let quote_amount_out = read_u64_unchecked(data, 72);
727 let lp_mint_supply = read_u64_unchecked(data, 80);
728
729 let pool = read_pubkey_unchecked(data, 88);
730 let user = read_pubkey_unchecked(data, 120);
731 let user_base_token_account = read_pubkey_unchecked(data, 152);
732 let user_quote_token_account = read_pubkey_unchecked(data, 184);
733 let user_pool_token_account = read_pubkey_unchecked(data, 216);
734
735 let metadata = EventMetadata {
736 signature,
737 slot,
738 tx_index,
739 block_time_us: block_time_us.unwrap_or(0),
740 grpc_recv_us,
741 recent_blockhash: None,
742 };
743
744 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
745 metadata,
746 timestamp,
747 lp_token_amount_in,
748 min_base_amount_out,
749 min_quote_amount_out,
750 user_base_token_reserves,
751 user_quote_token_reserves,
752 pool_base_token_reserves,
753 pool_quote_token_reserves,
754 base_amount_out,
755 quote_amount_out,
756 lp_mint_supply,
757 pool,
758 user,
759 user_base_token_account,
760 user_quote_token_account,
761 user_pool_token_account,
762 }))
763 }
764}
765
766#[inline(always)]
774pub fn get_event_type_fast(log: &str) -> Option<u64> {
775 extract_discriminator_simd(log)
776}
777
778#[inline(always)]
780pub fn is_event_type(log: &str, discriminator: u64) -> bool {
781 extract_discriminator_simd(log) == Some(discriminator)
782}
783
784#[inline(always)]
791pub fn parse_buy_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
792 validate_buy_payload_layout(data)?;
793
794 unsafe {
795 let timestamp = read_i64_unchecked(data, 0);
796 let base_amount_out = read_u64_unchecked(data, 8);
797 let max_quote_amount_in = read_u64_unchecked(data, 16);
798 let user_base_token_reserves = read_u64_unchecked(data, 24);
799 let user_quote_token_reserves = read_u64_unchecked(data, 32);
800 let pool_base_token_reserves = read_u64_unchecked(data, 40);
801 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
802 let quote_amount_in = read_u64_unchecked(data, 56);
803 let lp_fee_basis_points = read_u64_unchecked(data, 64);
804 let lp_fee = read_u64_unchecked(data, 72);
805 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
806 let protocol_fee = read_u64_unchecked(data, 88);
807 let quote_amount_in_with_lp_fee = read_u64_unchecked(data, 96);
808 let user_quote_amount_in = read_u64_unchecked(data, 104);
809
810 let pool = read_pubkey_unchecked(data, 112);
811 let user = read_pubkey_unchecked(data, 144);
812 let user_base_token_account = read_pubkey_unchecked(data, 176);
813 let user_quote_token_account = read_pubkey_unchecked(data, 208);
814 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
815 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
816 let coin_creator = read_pubkey_unchecked(data, 304);
817
818 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
819 let coin_creator_fee = read_u64_unchecked(data, 344);
820 let track_volume = read_bool_unchecked(data, 352);
821 let total_unclaimed_tokens = read_u64_unchecked(data, 353);
822 let total_claimed_tokens = read_u64_unchecked(data, 361);
823 let current_sol_volume = read_u64_unchecked(data, 369);
824 let last_update_timestamp = read_i64_unchecked(data, 377);
825
826 let mut offset = BUY_LEGACY_LEN;
828 let min_base_amount_out = if data.len() == BUY_LEGACY_LEN {
829 0
830 } else {
831 let value = read_u64_unchecked(data, offset);
832 offset += 8;
833 value
834 };
835
836 let ix_name = if offset + 4 <= data.len() {
838 let len = read_u32_unchecked(data, offset) as usize;
839 offset += 4;
840 if offset + len <= data.len() {
841 let string_bytes = &data[offset..offset + len];
842 let s = std::str::from_utf8(string_bytes).ok()?;
843 offset += len;
844 s.to_string()
845 } else {
846 String::new()
847 }
848 } else {
849 String::new()
850 };
851
852 let cashback_fee_basis_points =
853 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
854 offset += 8;
855 let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
856 offset += 8;
857 let buyback_fee_basis_points =
858 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
859 offset += 8;
860 let buyback_fee =
861 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
862 offset += 8;
863 let virtual_quote_reserves =
864 if offset + 16 <= data.len() { read_i128_unchecked(data, offset) } else { 0 };
865 offset += 16;
866 let can_boost = if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
867 offset += 1;
868 let base_supply =
869 if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
870
871 Some(DexEvent::PumpSwapBuy(PumpSwapBuyEvent {
872 metadata,
873 timestamp,
874 base_amount_out,
875 max_quote_amount_in,
876 user_base_token_reserves,
877 user_quote_token_reserves,
878 pool_base_token_reserves,
879 pool_quote_token_reserves,
880 quote_amount_in,
881 lp_fee_basis_points,
882 lp_fee,
883 protocol_fee_basis_points,
884 protocol_fee,
885 quote_amount_in_with_lp_fee,
886 user_quote_amount_in,
887 pool,
888 user,
889 user_base_token_account,
890 user_quote_token_account,
891 protocol_fee_recipient,
892 protocol_fee_recipient_token_account,
893 coin_creator,
894 coin_creator_fee_basis_points,
895 coin_creator_fee,
896 track_volume,
897 total_unclaimed_tokens,
898 total_claimed_tokens,
899 current_sol_volume,
900 last_update_timestamp,
901 min_base_amount_out,
902 ix_name,
903 cashback_fee_basis_points,
904 cashback,
905 buyback_fee_basis_points,
906 buyback_fee,
907 virtual_quote_reserves,
908 can_boost,
909 base_supply,
910 ..Default::default()
911 }))
912 }
913}
914
915#[inline(always)]
917pub fn parse_sell_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
918 validate_sell_payload_layout(data)?;
919 const CASHBACK_FEE_BASIS_POINTS_OFFSET: usize = 352;
920 const CASHBACK_OFFSET: usize = 360;
921 const CASHBACK_FIELDS_LEN: usize = 16;
922 unsafe {
923 let timestamp = read_i64_unchecked(data, 0);
924 let base_amount_in = read_u64_unchecked(data, 8);
925 let min_quote_amount_out = read_u64_unchecked(data, 16);
926 let user_base_token_reserves = read_u64_unchecked(data, 24);
927 let user_quote_token_reserves = read_u64_unchecked(data, 32);
928 let pool_base_token_reserves = read_u64_unchecked(data, 40);
929 let pool_quote_token_reserves = read_u64_unchecked(data, 48);
930 let quote_amount_out = read_u64_unchecked(data, 56);
931 let lp_fee_basis_points = read_u64_unchecked(data, 64);
932 let lp_fee = read_u64_unchecked(data, 72);
933 let protocol_fee_basis_points = read_u64_unchecked(data, 80);
934 let protocol_fee = read_u64_unchecked(data, 88);
935 let quote_amount_out_without_lp_fee = read_u64_unchecked(data, 96);
936 let user_quote_amount_out = read_u64_unchecked(data, 104);
937
938 let pool = read_pubkey_unchecked(data, 112);
939 let user = read_pubkey_unchecked(data, 144);
940 let user_base_token_account = read_pubkey_unchecked(data, 176);
941 let user_quote_token_account = read_pubkey_unchecked(data, 208);
942 let protocol_fee_recipient = read_pubkey_unchecked(data, 240);
943 let protocol_fee_recipient_token_account = read_pubkey_unchecked(data, 272);
944 let coin_creator = read_pubkey_unchecked(data, 304);
945
946 let coin_creator_fee_basis_points = read_u64_unchecked(data, 336);
947 let coin_creator_fee = read_u64_unchecked(data, 344);
948 let (cashback_fee_basis_points, cashback) =
949 if data.len() >= CASHBACK_FEE_BASIS_POINTS_OFFSET + CASHBACK_FIELDS_LEN {
950 (
951 read_u64_unchecked(data, CASHBACK_FEE_BASIS_POINTS_OFFSET),
952 read_u64_unchecked(data, CASHBACK_OFFSET),
953 )
954 } else {
955 (0, 0)
956 };
957 let buyback_fee_basis_points =
958 if data.len() >= 376 { read_u64_unchecked(data, 368) } else { 0 };
959 let buyback_fee = if data.len() >= 384 { read_u64_unchecked(data, 376) } else { 0 };
960 let virtual_quote_reserves =
961 if data.len() >= 400 { read_i128_unchecked(data, 384) } else { 0 };
962 let can_boost = if data.len() >= 401 { read_bool_unchecked(data, 400) } else { false };
963 let base_supply = if data.len() >= 409 { read_u64_unchecked(data, 401) } else { 0 };
964
965 Some(DexEvent::PumpSwapSell(PumpSwapSellEvent {
966 metadata,
967 timestamp,
968 base_amount_in,
969 min_quote_amount_out,
970 user_base_token_reserves,
971 user_quote_token_reserves,
972 pool_base_token_reserves,
973 pool_quote_token_reserves,
974 quote_amount_out,
975 lp_fee_basis_points,
976 lp_fee,
977 protocol_fee_basis_points,
978 protocol_fee,
979 quote_amount_out_without_lp_fee,
980 user_quote_amount_out,
981 pool,
982 user,
983 user_base_token_account,
984 user_quote_token_account,
985 protocol_fee_recipient,
986 protocol_fee_recipient_token_account,
987 coin_creator,
988 coin_creator_fee_basis_points,
989 coin_creator_fee,
990 cashback_fee_basis_points,
991 cashback,
992 buyback_fee_basis_points,
993 buyback_fee,
994 virtual_quote_reserves,
995 can_boost,
996 base_supply,
997 ..Default::default()
998 }))
999 }
1000}
1001
1002#[inline(always)]
1004pub fn parse_create_pool_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
1005 const CREATE_POOL_EVENT_LEN: usize = 326;
1006 const REQUIRED_LEN: usize = CREATE_POOL_EVENT_LEN;
1007 if data.len() < REQUIRED_LEN {
1008 return None;
1009 }
1010
1011 unsafe {
1012 let timestamp = read_i64_unchecked(data, 0);
1013 let index = read_u16_unchecked(data, 8);
1014
1015 let creator = read_pubkey_unchecked(data, 10);
1016 let base_mint = read_pubkey_unchecked(data, 42);
1017 let quote_mint = read_pubkey_unchecked(data, 74);
1018
1019 let base_mint_decimals = read_u8_unchecked(data, 106);
1020 let quote_mint_decimals = read_u8_unchecked(data, 107);
1021
1022 let base_amount_in = read_u64_unchecked(data, 108);
1023 let quote_amount_in = read_u64_unchecked(data, 116);
1024 let pool_base_amount = read_u64_unchecked(data, 124);
1025 let pool_quote_amount = read_u64_unchecked(data, 132);
1026 let minimum_liquidity = read_u64_unchecked(data, 140);
1027 let initial_liquidity = read_u64_unchecked(data, 148);
1028 let lp_token_amount_out = read_u64_unchecked(data, 156);
1029
1030 let pool_bump = read_u8_unchecked(data, 164);
1031
1032 let pool = read_pubkey_unchecked(data, 165);
1033 let lp_mint = read_pubkey_unchecked(data, 197);
1034 let user_base_token_account = read_pubkey_unchecked(data, 229);
1035 let user_quote_token_account = read_pubkey_unchecked(data, 261);
1036 let coin_creator = read_pubkey_unchecked(data, 293);
1037 let is_mayhem_mode = data.len() > 325 && read_bool_unchecked(data, 325);
1038
1039 Some(DexEvent::PumpSwapCreatePool(PumpSwapCreatePoolEvent {
1040 metadata,
1041 timestamp,
1042 index,
1043 creator,
1044 base_mint,
1045 quote_mint,
1046 base_mint_decimals,
1047 quote_mint_decimals,
1048 base_amount_in,
1049 quote_amount_in,
1050 pool_base_amount,
1051 pool_quote_amount,
1052 minimum_liquidity,
1053 initial_liquidity,
1054 lp_token_amount_out,
1055 pool_bump,
1056 pool,
1057 lp_mint,
1058 user_base_token_account,
1059 user_quote_token_account,
1060 coin_creator,
1061 is_mayhem_mode,
1062 is_cashback_coin: false,
1063 }))
1064 }
1065}
1066
1067#[inline(always)]
1069pub fn parse_add_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
1070 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
1071 if data.len() < REQUIRED_LEN {
1072 return None;
1073 }
1074
1075 unsafe {
1076 let timestamp = read_i64_unchecked(data, 0);
1077 let lp_token_amount_out = read_u64_unchecked(data, 8);
1078 let max_base_amount_in = read_u64_unchecked(data, 16);
1079 let max_quote_amount_in = read_u64_unchecked(data, 24);
1080 let user_base_token_reserves = read_u64_unchecked(data, 32);
1081 let user_quote_token_reserves = read_u64_unchecked(data, 40);
1082 let pool_base_token_reserves = read_u64_unchecked(data, 48);
1083 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
1084 let base_amount_in = read_u64_unchecked(data, 64);
1085 let quote_amount_in = read_u64_unchecked(data, 72);
1086 let lp_mint_supply = read_u64_unchecked(data, 80);
1087
1088 let pool = read_pubkey_unchecked(data, 88);
1089 let user = read_pubkey_unchecked(data, 120);
1090 let user_base_token_account = read_pubkey_unchecked(data, 152);
1091 let user_quote_token_account = read_pubkey_unchecked(data, 184);
1092 let user_pool_token_account = read_pubkey_unchecked(data, 216);
1093
1094 Some(DexEvent::PumpSwapLiquidityAdded(PumpSwapLiquidityAdded {
1095 metadata,
1096 timestamp,
1097 lp_token_amount_out,
1098 max_base_amount_in,
1099 max_quote_amount_in,
1100 user_base_token_reserves,
1101 user_quote_token_reserves,
1102 pool_base_token_reserves,
1103 pool_quote_token_reserves,
1104 base_amount_in,
1105 quote_amount_in,
1106 lp_mint_supply,
1107 pool,
1108 user,
1109 user_base_token_account,
1110 user_quote_token_account,
1111 user_pool_token_account,
1112 }))
1113 }
1114}
1115
1116#[inline(always)]
1118pub fn parse_remove_liquidity_from_data(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
1119 const REQUIRED_LEN: usize = 10 * 8 + 5 * 32;
1120 if data.len() < REQUIRED_LEN {
1121 return None;
1122 }
1123
1124 unsafe {
1125 let timestamp = read_i64_unchecked(data, 0);
1126 let lp_token_amount_in = read_u64_unchecked(data, 8);
1127 let min_base_amount_out = read_u64_unchecked(data, 16);
1128 let min_quote_amount_out = read_u64_unchecked(data, 24);
1129 let user_base_token_reserves = read_u64_unchecked(data, 32);
1130 let user_quote_token_reserves = read_u64_unchecked(data, 40);
1131 let pool_base_token_reserves = read_u64_unchecked(data, 48);
1132 let pool_quote_token_reserves = read_u64_unchecked(data, 56);
1133 let base_amount_out = read_u64_unchecked(data, 64);
1134 let quote_amount_out = read_u64_unchecked(data, 72);
1135 let lp_mint_supply = read_u64_unchecked(data, 80);
1136
1137 let pool = read_pubkey_unchecked(data, 88);
1138 let user = read_pubkey_unchecked(data, 120);
1139 let user_base_token_account = read_pubkey_unchecked(data, 152);
1140 let user_quote_token_account = read_pubkey_unchecked(data, 184);
1141 let user_pool_token_account = read_pubkey_unchecked(data, 216);
1142
1143 Some(DexEvent::PumpSwapLiquidityRemoved(PumpSwapLiquidityRemoved {
1144 metadata,
1145 timestamp,
1146 lp_token_amount_in,
1147 min_base_amount_out,
1148 min_quote_amount_out,
1149 user_base_token_reserves,
1150 user_quote_token_reserves,
1151 pool_base_token_reserves,
1152 pool_quote_token_reserves,
1153 base_amount_out,
1154 quote_amount_out,
1155 lp_mint_supply,
1156 pool,
1157 user,
1158 user_base_token_account,
1159 user_quote_token_account,
1160 user_pool_token_account,
1161 }))
1162 }
1163}
1164
1165#[cfg(feature = "perf-stats")]
1170pub fn get_perf_stats() -> (usize, usize) {
1171 let count = PARSE_COUNT.load(Ordering::Relaxed);
1172 let total_ns = PARSE_TIME_NS.load(Ordering::Relaxed);
1173 (count, total_ns)
1174}
1175
1176#[cfg(feature = "perf-stats")]
1177pub fn reset_perf_stats() {
1178 PARSE_COUNT.store(0, Ordering::Relaxed);
1179 PARSE_TIME_NS.store(0, Ordering::Relaxed);
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184 use super::*;
1185 use solana_sdk::{pubkey::Pubkey, signature::Signature};
1186
1187 fn metadata() -> EventMetadata {
1188 EventMetadata {
1189 signature: Signature::default(),
1190 slot: 0,
1191 tx_index: 0,
1192 block_time_us: 0,
1193 grpc_recv_us: 0,
1194 recent_blockhash: None,
1195 }
1196 }
1197
1198 fn write_u64(buf: &mut [u8], offset: usize, value: u64) {
1199 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1200 }
1201
1202 fn write_i64(buf: &mut [u8], offset: usize, value: i64) {
1203 buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1204 }
1205
1206 fn write_i128(buf: &mut [u8], offset: usize, value: i128) {
1207 buf[offset..offset + 16].copy_from_slice(&value.to_le_bytes());
1208 }
1209
1210 fn write_pubkey(buf: &mut [u8], offset: usize, value: Pubkey) {
1211 buf[offset..offset + 32].copy_from_slice(value.as_ref());
1212 }
1213
1214 fn build_sell_payload(include_cashback: bool, include_boost: bool) -> Vec<u8> {
1215 let len = if include_boost {
1216 409
1217 } else if include_cashback {
1218 368
1219 } else {
1220 352
1221 };
1222 let mut data = vec![0u8; len];
1223
1224 write_i64(&mut data, 0, 1_713_498_953);
1225 write_u64(&mut data, 8, 11);
1226 write_u64(&mut data, 16, 22);
1227 write_u64(&mut data, 24, 33);
1228 write_u64(&mut data, 32, 44);
1229 write_u64(&mut data, 40, 55);
1230 write_u64(&mut data, 48, 66);
1231 write_u64(&mut data, 56, 77);
1232 write_u64(&mut data, 64, 88);
1233 write_u64(&mut data, 72, 99);
1234 write_u64(&mut data, 80, 111);
1235 write_u64(&mut data, 88, 122);
1236 write_u64(&mut data, 96, 133);
1237 write_u64(&mut data, 104, 144);
1238
1239 write_pubkey(&mut data, 112, Pubkey::new_from_array([1; 32]));
1240 write_pubkey(&mut data, 144, Pubkey::new_from_array([2; 32]));
1241 write_pubkey(&mut data, 176, Pubkey::new_from_array([3; 32]));
1242 write_pubkey(&mut data, 208, Pubkey::new_from_array([4; 32]));
1243 write_pubkey(&mut data, 240, Pubkey::new_from_array([5; 32]));
1244 write_pubkey(&mut data, 272, Pubkey::new_from_array([6; 32]));
1245 write_pubkey(&mut data, 304, Pubkey::new_from_array([7; 32]));
1246
1247 write_u64(&mut data, 336, 155);
1248 write_u64(&mut data, 344, 166);
1249
1250 if include_cashback {
1251 write_u64(&mut data, 352, 177);
1252 write_u64(&mut data, 360, 188);
1253 }
1254 if include_boost {
1255 write_u64(&mut data, 368, 199);
1256 write_u64(&mut data, 376, 211);
1257 write_i128(&mut data, 384, -222);
1258 data[400] = 1;
1259 write_u64(&mut data, 401, 233);
1260 }
1261
1262 data
1263 }
1264
1265 fn build_buy_payload_with_boost() -> Vec<u8> {
1266 let mut data = vec![0u8; 457];
1267 write_u64(&mut data, 385, 11);
1268 data[393..397].copy_from_slice(&3u32.to_le_bytes());
1269 data[397..400].copy_from_slice(b"buy");
1270 write_u64(&mut data, 400, 22);
1271 write_u64(&mut data, 408, 33);
1272 write_u64(&mut data, 416, 44);
1273 write_u64(&mut data, 424, 55);
1274 write_i128(&mut data, 432, -66);
1275 data[448] = 1;
1276 write_u64(&mut data, 449, 77);
1277 data
1278 }
1279
1280 fn build_create_pool_payload(is_mayhem_mode: bool) -> Vec<u8> {
1281 let mut data = vec![0u8; 326];
1282
1283 write_i64(&mut data, 0, 1_713_498_953);
1284 data[8..10].copy_from_slice(&42u16.to_le_bytes());
1285 write_pubkey(&mut data, 10, Pubkey::new_from_array([1; 32]));
1286 write_pubkey(&mut data, 42, Pubkey::new_from_array([2; 32]));
1287 write_pubkey(&mut data, 74, Pubkey::new_from_array([3; 32]));
1288 data[106] = 6;
1289 data[107] = 9;
1290 write_u64(&mut data, 108, 11);
1291 write_u64(&mut data, 116, 22);
1292 write_u64(&mut data, 124, 33);
1293 write_u64(&mut data, 132, 44);
1294 write_u64(&mut data, 140, 55);
1295 write_u64(&mut data, 148, 66);
1296 write_u64(&mut data, 156, 77);
1297 data[164] = 8;
1298 write_pubkey(&mut data, 165, Pubkey::new_from_array([4; 32]));
1299 write_pubkey(&mut data, 197, Pubkey::new_from_array([5; 32]));
1300 write_pubkey(&mut data, 229, Pubkey::new_from_array([6; 32]));
1301 write_pubkey(&mut data, 261, Pubkey::new_from_array([7; 32]));
1302 write_pubkey(&mut data, 293, Pubkey::new_from_array([8; 32]));
1303 data[325] = u8::from(is_mayhem_mode);
1304
1305 data
1306 }
1307
1308 #[test]
1309 fn test_discriminator_simd() {
1310 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1312 let disc = extract_discriminator_simd(log);
1313 assert!(disc.is_some());
1314 }
1315
1316 #[test]
1317 fn test_parse_performance() {
1318 let log = "Program data: Z/RS H8v1d3cAAAAAAAAAAA=";
1320 let sig = Signature::default();
1321
1322 let start = std::time::Instant::now();
1323 for _ in 0..1000 {
1324 let _ = parse_log(log, sig, 0, 0, Some(0), 0);
1325 }
1326 let elapsed = start.elapsed();
1327
1328 println!("Average parse time: {} ns", elapsed.as_nanos() / 1000);
1329 }
1330
1331 #[test]
1332 fn parse_sell_from_data_preserves_cashback_fields() {
1333 let event = parse_sell_from_data(&build_sell_payload(true, false), metadata())
1334 .expect("expected pumpswap sell event");
1335
1336 let DexEvent::PumpSwapSell(event) = event else {
1337 panic!("expected PumpSwapSell event");
1338 };
1339
1340 assert_eq!(event.cashback_fee_basis_points, 177);
1341 assert_eq!(event.cashback, 188);
1342 assert_eq!(event.coin_creator_fee_basis_points, 155);
1343 assert_eq!(event.coin_creator_fee, 166);
1344 }
1345
1346 #[test]
1347 fn parse_sell_from_data_keeps_legacy_payload_compatible() {
1348 let event = parse_sell_from_data(&build_sell_payload(false, false), metadata())
1349 .expect("expected legacy pumpswap sell event");
1350
1351 let DexEvent::PumpSwapSell(event) = event else {
1352 panic!("expected PumpSwapSell event");
1353 };
1354
1355 assert_eq!(event.cashback_fee_basis_points, 0);
1356 assert_eq!(event.cashback, 0);
1357 assert_eq!(event.coin_creator_fee_basis_points, 155);
1358 assert_eq!(event.coin_creator_fee, 166);
1359 }
1360
1361 #[test]
1362 fn parse_sell_from_data_preserves_boost_fields() {
1363 let event = parse_sell_from_data(&build_sell_payload(true, true), metadata())
1364 .expect("expected upgraded pumpswap sell event");
1365 let DexEvent::PumpSwapSell(event) = event else {
1366 panic!("expected PumpSwapSell event");
1367 };
1368
1369 assert_eq!(event.buyback_fee_basis_points, 199);
1370 assert_eq!(event.buyback_fee, 211);
1371 assert_eq!(event.virtual_quote_reserves, -222);
1372 assert!(event.can_boost);
1373 assert_eq!(event.base_supply, 233);
1374 }
1375
1376 #[test]
1377 fn parse_buy_from_data_preserves_boost_fields() {
1378 let event = parse_buy_from_data(&build_buy_payload_with_boost(), metadata())
1379 .expect("expected upgraded pumpswap buy event");
1380 let DexEvent::PumpSwapBuy(event) = event else {
1381 panic!("expected PumpSwapBuy event");
1382 };
1383
1384 assert_eq!(event.ix_name, "buy");
1385 assert_eq!(event.cashback_fee_basis_points, 22);
1386 assert_eq!(event.cashback, 33);
1387 assert_eq!(event.buyback_fee_basis_points, 44);
1388 assert_eq!(event.buyback_fee, 55);
1389 assert_eq!(event.virtual_quote_reserves, -66);
1390 assert!(event.can_boost);
1391 assert_eq!(event.base_supply, 77);
1392 }
1393
1394 #[test]
1395 fn parse_buy_from_data_rejects_truncated_min_base_payload() {
1396 assert!(parse_buy_from_data(&vec![0u8; 385], metadata()).is_some());
1397 for len in 386..397 {
1398 assert!(parse_buy_from_data(&vec![0u8; len], metadata()).is_none());
1399 }
1400 assert!(parse_buy_from_data(&vec![0u8; 397], metadata()).is_some());
1401 }
1402
1403 #[test]
1404 fn parse_buy_from_data_rejects_partial_or_invalid_variable_tail() {
1405 let current = build_buy_payload_with_boost();
1406 for len in 401..416 {
1407 assert!(parse_buy_from_data(¤t[..len], metadata()).is_none());
1408 }
1409 assert!(parse_buy_from_data(¤t[..416], metadata()).is_some());
1410 for len in 417..457 {
1411 assert!(parse_buy_from_data(¤t[..len], metadata()).is_none());
1412 }
1413 assert!(parse_buy_from_data(¤t, metadata()).is_some());
1414
1415 let mut invalid_utf8 = vec![0u8; 398];
1416 invalid_utf8[393..397].copy_from_slice(&1u32.to_le_bytes());
1417 invalid_utf8[397] = 0xff;
1418 assert!(parse_buy_from_data(&invalid_utf8, metadata()).is_none());
1419
1420 let mut truncated_name = vec![0u8; 398];
1421 truncated_name[393..397].copy_from_slice(&2u32.to_le_bytes());
1422 assert!(parse_buy_from_data(&truncated_name, metadata()).is_none());
1423 }
1424
1425 #[test]
1426 fn parse_sell_from_data_accepts_only_complete_schema_versions() {
1427 let current = build_sell_payload(true, true);
1428 assert!(parse_sell_from_data(¤t[..352], metadata()).is_some());
1429 assert!(parse_sell_from_data(¤t[..368], metadata()).is_some());
1430 assert!(parse_sell_from_data(¤t, metadata()).is_some());
1431
1432 for len in (353..368).chain(369..409) {
1433 assert!(parse_sell_from_data(¤t[..len], metadata()).is_none());
1434 }
1435 }
1436
1437 #[test]
1438 fn parse_create_pool_from_data_reads_mayhem_mode() {
1439 let event = parse_create_pool_from_data(&build_create_pool_payload(true), metadata())
1440 .expect("expected pumpswap create pool event");
1441
1442 let DexEvent::PumpSwapCreatePool(event) = event else {
1443 panic!("expected PumpSwapCreatePool event");
1444 };
1445
1446 assert_eq!(event.index, 42);
1447 assert!(event.is_mayhem_mode);
1448 assert!(!event.is_cashback_coin);
1449 }
1450}