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