use crate::core::events::*;
pub mod discriminators {
pub const TRADE_EVENT: [u8; 16] = [
189, 219, 127, 211, 78, 230, 97, 238, 155, 167, 108, 32, 122, 76, 173, 64, ];
pub const CREATE_TOKEN_EVENT: [u8; 16] =
[27, 114, 169, 77, 222, 235, 99, 118, 155, 167, 108, 32, 122, 76, 173, 64];
pub const COMPLETE_PUMP_AMM_MIGRATION_EVENT: [u8; 16] =
[189, 233, 93, 185, 92, 148, 234, 148, 155, 167, 108, 32, 122, 76, 173, 64];
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
unsafe fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
let ptr = data.as_ptr().add(offset) as *const u64;
u64::from_le(ptr.read_unaligned())
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
unsafe fn read_i64_unchecked(data: &[u8], offset: usize) -> i64 {
let ptr = data.as_ptr().add(offset) as *const i64;
i64::from_le(ptr.read_unaligned())
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
unsafe fn read_bool_unchecked(data: &[u8], offset: usize) -> bool {
*data.get_unchecked(offset) == 1
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
unsafe fn read_pubkey_unchecked(data: &[u8], offset: usize) -> solana_sdk::pubkey::Pubkey {
use solana_sdk::pubkey::Pubkey;
let ptr = data.as_ptr().add(offset);
let mut bytes = [0u8; 32];
std::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 32);
Pubkey::new_from_array(bytes)
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
unsafe fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
if data.len() < offset + 4 {
return None;
}
let len = read_u32_unchecked(data, offset) as usize;
if data.len() < offset + 4 + len {
return None;
}
let string_bytes = &data[offset + 4..offset + 4 + len];
let s = std::str::from_utf8_unchecked(string_bytes);
Some((s, 4 + len))
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
unsafe fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
let ptr = data.as_ptr().add(offset) as *const u32;
u32::from_le(ptr.read_unaligned())
}
#[inline]
pub fn parse_pumpfun_inner_instruction(
discriminator: &[u8; 16],
data: &[u8],
metadata: EventMetadata,
is_created_buy: bool,
) -> Option<DexEvent> {
match discriminator {
&discriminators::TRADE_EVENT => parse_trade_event_inner(data, metadata, is_created_buy),
&discriminators::CREATE_TOKEN_EVENT => parse_create_event_inner(data, metadata),
&discriminators::COMPLETE_PUMP_AMM_MIGRATION_EVENT => {
parse_migrate_event_inner(data, metadata)
}
_ => None,
}
}
#[inline(always)]
fn parse_trade_event_inner(
data: &[u8],
metadata: EventMetadata,
is_created_buy: bool,
) -> Option<DexEvent> {
#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
{
parse_trade_event_inner_borsh(data, metadata, is_created_buy)
}
#[cfg(feature = "parse-zero-copy")]
{
parse_trade_event_inner_zero_copy(data, metadata, is_created_buy)
}
}
#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
#[inline(always)]
fn parse_trade_event_inner_borsh(
data: &[u8],
metadata: EventMetadata,
is_created_buy: bool,
) -> Option<DexEvent> {
crate::logs::pump::parse_trade_from_data(data, metadata, is_created_buy)
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
fn parse_trade_event_inner_zero_copy(
data: &[u8],
metadata: EventMetadata,
is_created_buy: bool,
) -> Option<DexEvent> {
unsafe {
if data.len() < 32 + 8 + 8 + 1 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 8 + 8 + 32 + 8 + 8 {
return None;
}
let mut offset = 0;
let mint = read_pubkey_unchecked(data, offset);
offset += 32;
let sol_amount = read_u64_unchecked(data, offset);
offset += 8;
let token_amount = read_u64_unchecked(data, offset);
offset += 8;
let is_buy = read_bool_unchecked(data, offset);
offset += 1;
let user = read_pubkey_unchecked(data, offset);
offset += 32;
let timestamp = read_i64_unchecked(data, offset);
offset += 8;
let virtual_sol_reserves = read_u64_unchecked(data, offset);
offset += 8;
let virtual_token_reserves = read_u64_unchecked(data, offset);
offset += 8;
let real_sol_reserves = read_u64_unchecked(data, offset);
offset += 8;
let real_token_reserves = read_u64_unchecked(data, offset);
offset += 8;
let fee_recipient = read_pubkey_unchecked(data, offset);
offset += 32;
let fee_basis_points = read_u64_unchecked(data, offset);
offset += 8;
let fee = read_u64_unchecked(data, offset);
offset += 8;
let creator = read_pubkey_unchecked(data, offset);
offset += 32;
let creator_fee_basis_points = read_u64_unchecked(data, offset);
offset += 8;
let creator_fee = read_u64_unchecked(data, offset);
offset += 8;
let track_volume =
if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
offset += 1;
let total_unclaimed_tokens =
if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
offset += 8;
let total_claimed_tokens =
if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
offset += 8;
let current_sol_volume =
if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
offset += 8;
let last_update_timestamp =
if offset + 8 <= data.len() { read_i64_unchecked(data, offset) } else { 0 };
offset += 8;
let (ix_name, ix_name_len) = if offset + 4 <= data.len() {
if let Some((s, consumed)) = read_str_unchecked(data, offset) {
(s.to_string(), consumed)
} else {
(String::new(), 0)
}
} else {
(String::new(), 0)
};
offset += ix_name_len;
let mayhem_mode =
if offset + 1 <= data.len() { read_bool_unchecked(data, offset) } else { false };
offset += 1;
let cashback_fee_basis_points =
if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
offset += 8;
let cashback = if offset + 8 <= data.len() { read_u64_unchecked(data, offset) } else { 0 };
offset += 8;
let (
buyback_fee_basis_points,
buyback_fee,
shareholders,
quote_mint,
quote_amount,
virtual_quote_reserves,
real_quote_reserves,
) = crate::logs::pump::read_trade_event_extensions(data, &mut offset)?;
let trade_event = PumpFunTradeEvent {
metadata,
mint,
sol_amount,
token_amount,
is_buy,
is_created_buy,
user,
timestamp,
virtual_sol_reserves,
virtual_token_reserves,
real_sol_reserves,
real_token_reserves,
fee_recipient,
fee_basis_points,
fee,
creator,
creator_fee_basis_points,
creator_fee,
track_volume,
total_unclaimed_tokens,
total_claimed_tokens,
current_sol_volume,
last_update_timestamp,
ix_name: ix_name.clone(),
mayhem_mode,
cashback_fee_basis_points,
cashback,
buyback_fee_basis_points,
buyback_fee,
shareholders,
quote_mint,
quote_amount,
virtual_quote_reserves,
real_quote_reserves,
is_cashback_coin: cashback_fee_basis_points > 0,
..Default::default() };
match ix_name.as_str() {
"buy" | "buy_v2" => Some(DexEvent::PumpFunBuy(trade_event)),
"sell" | "sell_v2" => Some(DexEvent::PumpFunSell(trade_event)),
"buy_exact_sol_in" | "buy_exact_quote_in_v2" => {
Some(DexEvent::PumpFunBuyExactSolIn(trade_event))
}
_ => Some(DexEvent::PumpFunTrade(trade_event)),
}
}
}
#[inline(always)]
fn parse_create_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
{
parse_create_event_inner_borsh(data, metadata)
}
#[cfg(feature = "parse-zero-copy")]
{
parse_create_event_inner_zero_copy(data, metadata)
}
}
#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
#[inline(always)]
fn parse_create_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
let mut event = borsh::from_slice::<PumpFunCreateTokenEvent>(data).ok()?;
event.metadata = metadata;
Some(DexEvent::PumpFunCreate(event))
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
fn parse_create_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
unsafe {
let mut offset = 0;
let (name, name_len) = read_str_unchecked(data, offset)?;
offset += name_len;
let (symbol, symbol_len) = read_str_unchecked(data, offset)?;
offset += symbol_len;
let (uri, uri_len) = read_str_unchecked(data, offset)?;
offset += uri_len;
if data.len() < offset + 32 + 32 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 32 + 1 {
return None;
}
let mint = read_pubkey_unchecked(data, offset);
offset += 32;
let bonding_curve = read_pubkey_unchecked(data, offset);
offset += 32;
let user = read_pubkey_unchecked(data, offset);
offset += 32;
let creator = read_pubkey_unchecked(data, offset);
offset += 32;
let timestamp = read_i64_unchecked(data, offset);
offset += 8;
let virtual_token_reserves = read_u64_unchecked(data, offset);
offset += 8;
let virtual_sol_reserves = read_u64_unchecked(data, offset);
offset += 8;
let real_token_reserves = read_u64_unchecked(data, offset);
offset += 8;
let token_total_supply = read_u64_unchecked(data, offset);
offset += 8;
let token_program = if offset + 32 <= data.len() {
read_pubkey_unchecked(data, offset)
} else {
solana_sdk::pubkey::Pubkey::default()
};
offset += 32;
let is_mayhem_mode =
if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
offset += 1;
let is_cashback_enabled =
if offset < data.len() { read_bool_unchecked(data, offset) } else { false };
Some(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
metadata,
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
mint,
bonding_curve,
user,
creator,
timestamp,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
token_total_supply,
token_program,
is_mayhem_mode,
is_cashback_enabled,
}))
}
}
#[inline(always)]
fn parse_migrate_event_inner(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
{
parse_migrate_event_inner_borsh(data, metadata)
}
#[cfg(feature = "parse-zero-copy")]
{
parse_migrate_event_inner_zero_copy(data, metadata)
}
}
#[cfg(all(feature = "parse-borsh", not(feature = "parse-zero-copy")))]
#[inline(always)]
fn parse_migrate_event_inner_borsh(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
const MIGRATE_EVENT_SIZE: usize = 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32;
if data.len() < MIGRATE_EVENT_SIZE {
return None;
}
let mut event = borsh::from_slice::<PumpFunMigrateEvent>(&data[..MIGRATE_EVENT_SIZE]).ok()?;
event.metadata = metadata;
Some(DexEvent::PumpFunMigrate(event))
}
#[cfg(feature = "parse-zero-copy")]
#[inline(always)]
fn parse_migrate_event_inner_zero_copy(data: &[u8], metadata: EventMetadata) -> Option<DexEvent> {
unsafe {
if data.len() < 32 + 32 + 8 + 8 + 8 + 32 + 8 + 32 {
return None;
}
let mut offset = 0;
let user = read_pubkey_unchecked(data, offset);
offset += 32;
let mint = read_pubkey_unchecked(data, offset);
offset += 32;
let mint_amount = read_u64_unchecked(data, offset);
offset += 8;
let sol_amount = read_u64_unchecked(data, offset);
offset += 8;
let pool_migration_fee = read_u64_unchecked(data, offset);
offset += 8;
let bonding_curve = read_pubkey_unchecked(data, offset);
offset += 32;
let timestamp = read_i64_unchecked(data, offset);
offset += 8;
let pool = read_pubkey_unchecked(data, offset);
Some(DexEvent::PumpFunMigrate(PumpFunMigrateEvent {
metadata,
user,
mint,
mint_amount,
sol_amount,
pool_migration_fee,
bonding_curve,
timestamp,
pool,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::{pubkey::Pubkey, signature::Signature};
fn push_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn push_i64(out: &mut Vec<u8>, value: i64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn push_pubkey(out: &mut Vec<u8>, value: Pubkey) {
out.extend_from_slice(value.as_ref());
}
fn trade_event_data_without_buyback_tail(ix_name: &str) -> Vec<u8> {
let mut data = Vec::new();
push_pubkey(&mut data, Pubkey::new_unique()); push_u64(&mut data, 1_000); push_u64(&mut data, 2_000); data.push(1); push_pubkey(&mut data, Pubkey::new_unique()); push_i64(&mut data, 123); push_u64(&mut data, 10); push_u64(&mut data, 20); push_u64(&mut data, 30); push_u64(&mut data, 40); push_pubkey(&mut data, Pubkey::new_unique()); push_u64(&mut data, 50); push_u64(&mut data, 60); push_pubkey(&mut data, Pubkey::new_unique()); push_u64(&mut data, 70); push_u64(&mut data, 80); data.push(1); push_u64(&mut data, 90); push_u64(&mut data, 100); push_u64(&mut data, 110); push_i64(&mut data, 120); data.extend_from_slice(&(ix_name.len() as u32).to_le_bytes());
data.extend_from_slice(ix_name.as_bytes());
data.push(1); push_u64(&mut data, 130); push_u64(&mut data, 140); data
}
#[test]
fn test_discriminator_match() {
let disc = discriminators::TRADE_EVENT;
assert_eq!(disc.len(), 16);
}
#[test]
fn test_parse_trade_event_boundary() {
let metadata = EventMetadata {
signature: Signature::default(),
slot: 0,
tx_index: 0,
block_time_us: 0,
grpc_recv_us: 0,
recent_blockhash: None,
};
let short_data = vec![0u8; 10];
let result = parse_trade_event_inner(&short_data, metadata, false);
assert!(result.is_none());
}
#[test]
fn trade_event_parser_accepts_payload_without_latest_tail() {
let metadata = EventMetadata {
signature: Signature::default(),
slot: 10,
tx_index: 0,
block_time_us: 0,
grpc_recv_us: 0,
recent_blockhash: None,
};
let data = trade_event_data_without_buyback_tail("buy_exact_sol_in");
let event =
parse_pumpfun_inner_instruction(&discriminators::TRADE_EVENT, &data, metadata, true)
.expect("legacy tail-compatible trade event");
match event {
DexEvent::PumpFunBuyExactSolIn(t) => {
assert_eq!(t.sol_amount, 1_000);
assert_eq!(t.token_amount, 2_000);
assert_eq!(t.ix_name, "buy_exact_sol_in");
assert!(t.track_volume);
assert!(t.mayhem_mode);
assert_eq!(t.cashback_fee_basis_points, 130);
assert_eq!(t.cashback, 140);
assert!(t.is_created_buy);
assert_eq!(t.buyback_fee_basis_points, 0);
assert!(t.shareholders.is_empty());
assert_eq!(t.quote_mint, Pubkey::default());
}
other => panic!("expected exact buy trade, got {other:?}"),
}
}
}