1use crate::core::events::*;
31
32
33pub mod discriminators {
39 pub const TRADE_EVENT: [u8; 16] = [
42 189, 219, 127, 211, 78, 230, 97, 238, 155, 167, 108, 32, 122, 76, 173, 64, ];
45
46 pub const CREATE_TOKEN_EVENT: [u8; 16] = [
48 27, 114, 169, 77, 222, 235, 99, 118,
49 155, 167, 108, 32, 122, 76, 173, 64,
50 ];
51
52 pub const COMPLETE_PUMP_AMM_MIGRATION_EVENT: [u8; 16] = [
54 189, 233, 93, 185, 92, 148, 234, 148,
55 155, 167, 108, 32, 122, 76, 173, 64,
56 ];
57}
58
59#[cfg(feature = "parse-zero-copy")]
64#[inline(always)]
65unsafe fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
66 let ptr = data.as_ptr().add(offset) as *const u64;
67 u64::from_le(ptr.read_unaligned())
68}
69
70#[cfg(feature = "parse-zero-copy")]
71#[inline(always)]
72unsafe fn read_i64_unchecked(data: &[u8], offset: usize) -> i64 {
73 let ptr = data.as_ptr().add(offset) as *const i64;
74 i64::from_le(ptr.read_unaligned())
75}
76
77#[cfg(feature = "parse-zero-copy")]
78#[inline(always)]
79unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
80 *data.get_unchecked(offset) == 1
81}
82
83#[cfg(feature = "parse-zero-copy")]
84#[inline(always)]
85unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> solana_sdk::pubkey::Pubkey {
86 use solana_sdk::pubkey::Pubkey;
87 let ptr = data.as_ptr().add(offset);
88 let mut bytes = [0u8; 32];
89 std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
90 Pubkey::new_from_array(bytes)
91}
92
93#[cfg(feature = "parse-zero-copy")]
94#[inline(always)]
95unsafe fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
96 if data.len() < offset + 4 {
97 return None;
98 }
99
100 let len = read_u32_unchecked(data, offset) as usize;
101 if data.len() < offset + 4 + len {
102 return None;
103 }
104
105 let string_bytes = &data[offset + 4..offset + 4 + len];
106 let s = std::str::from_utf8_unchecked(string_bytes);
107 Some((s, 4 + len))
108}
109
110#[cfg(feature = "parse-zero-copy")]
111#[inline(always)]
112unsafe fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
113 let ptr = data.as_ptr().add(offset) as *const u32;
114 u32::from_le(ptr.read_unaligned())
115}
116
117#[inline]
134pub fn parse_pumpfun_inner_instruction(
135 discriminator: &[u8; 16],
136 data: &[u8],
137 metadata: EventMetadata,
138 is_created_buy: bool,
139) -> Option<DexEvent> {
140 match discriminator {
141 &discriminators::TRADE_EVENT => parse_trade_event_inner(data, metadata, is_created_buy),
142 &discriminators::CREATE_TOKEN_EVENT => parse_create_event_inner(data, metadata),
143 &discriminators::COMPLETE_PUMP_AMM_MIGRATION_EVENT => parse_migrate_event_inner(data, metadata),
144 _ => None,
145 }
146}
147
148#[inline(always)]
156fn parse_trade_event_inner(data: &[u8], metadata: EventMetadata, is_created_buy: bool) -> Option<DexEvent> {
157 #[cfg(feature = "parse-borsh")]
158 {
159 parse_trade_event_inner_borsh(data, metadata, is_created_buy)
160 }
161
162 #[cfg(feature = "parse-zero-copy")]
163 {
164 parse_trade_event_inner_zero_copy(data, metadata, is_created_buy)
165 }
166}
167
168#[cfg(feature = "parse-borsh")]
172#[inline(always)]
173fn parse_trade_event_inner_borsh(data: &[u8], metadata: EventMetadata, is_created_buy: bool) -> Option<DexEvent> {
174 let mut event = borsh::from_slice::<PumpFunTradeEvent>(data).ok()?;
177 event.metadata = metadata;
178 event.is_created_buy = is_created_buy;
179 event.is_cashback_coin = event.cashback_fee_basis_points > 0;
180
181 match event.ix_name.as_str() {
183 "buy" => Some(DexEvent::PumpFunBuy(event)),
184 "sell" => Some(DexEvent::PumpFunSell(event)),
185 "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(event)),
186 _ => Some(DexEvent::PumpFunTrade(event)),
187 }
188}
189
190#[cfg(feature = "parse-zero-copy")]
194#[inline(always)]
195fn parse_trade_event_inner_zero_copy(data: &[u8], metadata: EventMetadata, is_created_buy: bool) -> Option<DexEvent> {
196 unsafe {
197 if data.len() < 32 + 8 + 8 + 1 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 8 + 8 + 32 + 8 + 8 {
199 return None;
200 }
201
202 let mut offset = 0;
203
204 let mint = read_pubkey_unchecked(data, offset);
205 offset += 32;
206
207 let sol_amount = read_u64_unchecked(data, offset);
208 offset += 8;
209
210 let token_amount = read_u64_unchecked(data, offset);
211 offset += 8;
212
213 let is_buy = read_bool_unchecked(data, offset);
214 offset += 1;
215
216 let user = read_pubkey_unchecked(data, offset);
217 offset += 32;
218
219 let timestamp = read_i64_unchecked(data, offset);
220 offset += 8;
221
222 let virtual_sol_reserves = read_u64_unchecked(data, offset);
223 offset += 8;
224
225 let virtual_token_reserves = read_u64_unchecked(data, offset);
226 offset += 8;
227
228 let real_sol_reserves = read_u64_unchecked(data, offset);
229 offset += 8;
230
231 let real_token_reserves = read_u64_unchecked(data, offset);
232 offset += 8;
233
234 let fee_recipient = read_pubkey_unchecked(data, offset);
235 offset += 32;
236
237 let fee_basis_points = read_u64_unchecked(data, offset);
238 offset += 8;
239
240 let fee = read_u64_unchecked(data, offset);
241 offset += 8;
242
243 let creator = read_pubkey_unchecked(data, offset);
244 offset += 32;
245
246 let creator_fee_basis_points = read_u64_unchecked(data, offset);
247 offset += 8;
248
249 let creator_fee = read_u64_unchecked(data, offset);
250 offset += 8;
251
252 let track_volume = if offset < data.len() {
254 read_bool_unchecked(data, offset)
255 } else {
256 false
257 };
258 offset += 1;
259
260 let total_unclaimed_tokens = if offset + 8 <= data.len() {
261 read_u64_unchecked(data, offset)
262 } else {
263 0
264 };
265 offset += 8;
266
267 let total_claimed_tokens = if offset + 8 <= data.len() {
268 read_u64_unchecked(data, offset)
269 } else {
270 0
271 };
272 offset += 8;
273
274 let current_sol_volume = if offset + 8 <= data.len() {
275 read_u64_unchecked(data, offset)
276 } else {
277 0
278 };
279 offset += 8;
280
281 let last_update_timestamp = if offset + 8 <= data.len() {
282 read_i64_unchecked(data, offset)
283 } else {
284 0
285 };
286 offset += 8;
287
288 let (ix_name, ix_name_len) = if offset + 4 <= data.len() {
289 if let Some((s, consumed)) = read_str_unchecked(data, offset) {
290 (s.to_string(), consumed)
291 } else {
292 (String::new(), 0)
293 }
294 } else {
295 (String::new(), 0)
296 };
297 offset += ix_name_len;
298
299 let mayhem_mode = if offset + 1 <= data.len() {
301 read_bool_unchecked(data, offset)
302 } else {
303 false
304 };
305 offset += 1;
306 let cashback_fee_basis_points = if offset + 8 <= data.len() {
307 read_u64_unchecked(data, offset)
308 } else {
309 0
310 };
311 offset += 8;
312 let cashback = if offset + 8 <= data.len() {
313 read_u64_unchecked(data, offset)
314 } else {
315 0
316 };
317
318 let trade_event = PumpFunTradeEvent {
320 metadata,
321 mint,
322 sol_amount,
323 token_amount,
324 is_buy,
325 is_created_buy,
326 user,
327 timestamp,
328 virtual_sol_reserves,
329 virtual_token_reserves,
330 real_sol_reserves,
331 real_token_reserves,
332 fee_recipient,
333 fee_basis_points,
334 fee,
335 creator,
336 creator_fee_basis_points,
337 creator_fee,
338 track_volume,
339 total_unclaimed_tokens,
340 total_claimed_tokens,
341 current_sol_volume,
342 last_update_timestamp,
343 ix_name: ix_name.clone(),
344 mayhem_mode,
345 cashback_fee_basis_points,
346 cashback,
347 is_cashback_coin: cashback_fee_basis_points > 0,
348 ..Default::default() };
350
351 match ix_name.as_str() {
353 "buy" => Some(DexEvent::PumpFunBuy(trade_event)),
354 "sell" => Some(DexEvent::PumpFunSell(trade_event)),
355 "buy_exact_sol_in" => Some(DexEvent::PumpFunBuyExactSolIn(trade_event)),
356 _ => Some(DexEvent::PumpFunTrade(trade_event)),
357 }
358 }
359}
360
361#[inline(always)]
369fn parse_create_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
370 #[cfg(feature = "parse-borsh")]
371 {
372 parse_create_event_inner_borsh(data, metadata)
373 }
374
375 #[cfg(feature = "parse-zero-copy")]
376 {
377 parse_create_event_inner_zero_copy(data, metadata)
378 }
379}
380
381#[cfg(feature = "parse-borsh")]
385#[inline(always)]
386fn parse_create_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
387 let mut event = borsh::from_slice::<PumpFunCreateTokenEvent>(data).ok()?;
389 event.metadata = metadata;
390 Some(DexEvent::PumpFunCreate(event))
391}
392
393#[cfg(feature = "parse-zero-copy")]
397#[inline(always)]
398fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
399 unsafe {
400 let mut offset = 0;
401
402 let (name, name_len) = read_str_unchecked(data, offset)?;
403 offset += name_len;
404
405 let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
406 offset += symbol_len;
407
408 let (uri, uri_len) = read_str_unchecked(data, offset)?;
409 offset += uri_len;
410
411 if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
412 return None;
413 }
414
415 let mint = read_pubkey_unchecked(data, offset);
416 offset += 32;
417
418 let bonding_curve = read_pubkey_unchecked(data, offset);
419 offset += 32;
420
421 let user = read_pubkey_unchecked(data, offset);
422 offset += 32;
423
424 let creator = read_pubkey_unchecked(data, offset);
425 offset += 32;
426
427 let timestamp = read_i64_unchecked(data, offset);
428 offset += 8;
429
430 let virtual_token_reserves = read_u64_unchecked(data, offset);
431 offset += 8;
432
433 let virtual_sol_reserves = read_u64_unchecked(data, offset);
434 offset += 8;
435
436 let real_token_reserves = read_u64_unchecked(data, offset);
437 offset += 8;
438
439 let token_total_supply = read_u64_unchecked(data, offset);
440 offset += 8;
441
442 let token_program = if offset + 32 <= data.len() {
443 read_pubkey_unchecked(data, offset)
444 } else {
445 solana_sdk::pubkey::Pubkey::default()
446 };
447 offset += 32;
448
449 let is_mayhem_mode = if offset < data.len() {
450 read_bool_unchecked(data, offset)
451 } else {
452 false
453 };
454 offset += 1;
455
456 let is_cashback_enabled = if offset < data.len() {
458 read_bool_unchecked(data, offset)
459 } else {
460 false
461 };
462
463 Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
464 metadata,
465 name: name.to_string(),
466 symbol: symbol.to_string(),
467 uri: uri.to_string(),
468 mint,
469 bonding_curve,
470 user,
471 creator,
472 timestamp,
473 virtual_token_reserves,
474 virtual_sol_reserves,
475 real_token_reserves,
476 token_total_supply,
477 token_program,
478 is_mayhem_mode,
479 is_cashback_enabled,
480 }))
481 }
482}
483
484#[inline(always)]
492fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
493 #[cfg(feature = "parse-borsh")]
494 {
495 parse_migrate_event_inner_borsh(data, metadata)
496 }
497
498 #[cfg(feature = "parse-zero-copy")]
499 {
500 parse_migrate_event_inner_zero_copy(data, metadata)
501 }
502}
503
504#[cfg(feature = "parse-borsh")]
508#[inline(always)]
509fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
510 const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32; if data.len() < MIGRATE_EVENT_SIZE {
514 return None;
515 }
516
517 let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
518 event.metadata = metadata;
519 Some(DexEvent::PumpFunMigrate(event))
520}
521
522#[cfg(feature = "parse-zero-copy")]
526#[inline(always)]
527fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
528 unsafe {
529 if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
530 return None;
531 }
532
533 let mut offset = 0;
534
535 let user = read_pubkey_unchecked(data, offset);
536 offset += 32;
537
538 let mint = read_pubkey_unchecked(data, offset);
539 offset += 32;
540
541 let mint_amount = read_u64_unchecked(data, offset);
542 offset += 8;
543
544 let sol_amount = read_u64_unchecked(data, offset);
545 offset += 8;
546
547 let pool_migration_fee = read_u64_unchecked(data, offset);
548 offset += 8;
549
550 let bonding_curve = read_pubkey_unchecked(data, offset);
551 offset += 32;
552
553 let timestamp = read_i64_unchecked(data, offset);
554 offset += 8;
555
556 let pool = read_pubkey_unchecked(data, offset);
557
558 Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
559 metadata,
560 user,
561 mint,
562 mint_amount,
563 sol_amount,
564 pool_migration_fee,
565 bonding_curve,
566 timestamp,
567 pool,
568 }))
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use solana_sdk::signature::Signature;
576
577 #[test]
578 fn test_discriminator_match() {
579 let disc = discriminators::TRADE_EVENT;
581 assert_eq!(disc.len(), 16);
582 }
583
584 #[test]
585 fn test_parse_trade_event_boundary() {
586 let metadata = EventMetadata {
588 signature: Signature::default(),
589 slot: 0,
590 tx_index: 0,
591 block_time_us: 0,
592 grpc_recv_us: 0,
593 };
594
595 let short_data = vec![0u8; 10];
596 let result = parse_trade_event_inner(&short_data, metadata);
597 assert!(result.is_none());
598 }
599}