use crate::core::{
events::*, merger::merge_events, pumpfun_fee_enrich::enrich_pumpfun_same_tx_post_merge,
};
use crate::grpc::types::EventTypeFilter;
use crate::instr::read_pubkey_fast;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
use std::collections::HashMap;
use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
#[inline]
pub fn parse_instructions_enhanced(
meta: &TransactionStatusMeta,
transaction: &Option<Transaction>,
sig: Signature,
slot: u64,
tx_idx: u64,
block_us: Option<i64>,
grpc_us: i64,
filter: Option<&EventTypeFilter>,
) -> Vec<DexEvent> {
let Some(tx) = transaction else { return Vec::new() };
let Some(msg) = &tx.message else { return Vec::new() };
let recent_blockhash = if msg.recent_blockhash.is_empty() {
None
} else {
Some(bs58::encode(&msg.recent_blockhash).into_string())
};
if !should_parse_instructions(filter) {
return Vec::new();
}
let is_created_buy = crate::logs::optimized_matcher::detect_pumpfun_create(&meta.log_messages);
let keys_len = msg.account_keys.len();
let writable_len = meta.loaded_writable_addresses.len();
let get_key = |i: usize| -> Option<&Vec<u8>> {
if i < keys_len {
msg.account_keys.get(i)
} else if i < keys_len + writable_len {
meta.loaded_writable_addresses.get(i - keys_len)
} else {
meta.loaded_readonly_addresses.get(i - keys_len - writable_len)
}
};
let mut result = Vec::with_capacity(8);
let mut invokes: HashMap<Pubkey, Vec<(i32, i32)>> = HashMap::with_capacity(8);
for (i, ix) in msg.instructions.iter().enumerate() {
let pid = get_key(ix.program_id_index as usize)
.map_or(Pubkey::default(), |k| read_pubkey_fast(k));
invokes.entry(pid).or_default().push((i as i32, -1));
if let Some(event) = parse_outer_instruction(
&ix.data,
&pid,
sig,
slot,
tx_idx,
block_us,
grpc_us,
&ix.accounts,
&get_key,
filter,
is_created_buy,
) {
result.push((i, None, event)); }
}
for inner in &meta.inner_instructions {
let outer_idx = inner.index as usize;
for (j, inner_ix) in inner.instructions.iter().enumerate() {
let pid = get_key(inner_ix.program_id_index as usize)
.map_or(Pubkey::default(), |k| read_pubkey_fast(k));
invokes.entry(pid).or_default().push((outer_idx as i32, j as i32));
let event = parse_inner_compiled_instruction_if_supported(
&inner_ix.data,
&pid,
sig,
slot,
tx_idx,
block_us,
grpc_us,
&inner_ix.accounts,
&get_key,
filter,
)
.or_else(|| {
parse_inner_instruction(
&inner_ix.data,
&pid,
sig,
slot,
tx_idx,
block_us,
grpc_us,
filter,
is_created_buy,
)
});
if let Some(event) = event {
result.push((outer_idx, Some(j), event)); }
}
}
let mut merged = merge_instruction_events(result);
enrich_pumpfun_same_tx_post_merge(&mut merged);
for e in merged.iter_mut() {
if let Some(m) = e.metadata_mut() {
m.recent_blockhash = recent_blockhash.clone();
}
}
let mut final_result = Vec::with_capacity(merged.len());
for mut event in merged {
crate::core::account_dispatcher::fill_accounts_with_owned_keys(
&mut event,
meta,
transaction,
&invokes,
);
crate::core::common_filler::fill_data(&mut event, meta, transaction, &invokes);
final_result.push(event);
}
final_result
}
#[inline(always)]
fn parse_compiled_instruction<'a>(
data: &[u8],
program_id: &Pubkey,
sig: Signature,
slot: u64,
tx_idx: u64,
block_us: Option<i64>,
grpc_us: i64,
account_indices: &[u8],
get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
filter: Option<&EventTypeFilter>,
) -> Option<DexEvent> {
if data.len() < 8 {
return None;
}
const STACK_CAP: usize = 64;
if account_indices.len() <= STACK_CAP {
let mut stack = [Pubkey::default(); STACK_CAP];
let mut n = 0usize;
for &idx in account_indices {
let k = get_key(idx as usize)?;
stack[n] = read_pubkey_fast(k);
n += 1;
}
crate::instr::parse_instruction_unified(
data,
&stack[..n],
sig,
slot,
tx_idx,
block_us,
grpc_us,
filter,
program_id,
)
} else {
let accounts: Vec<Pubkey> = account_indices
.iter()
.map(|&idx| get_key(idx as usize).map(|k| read_pubkey_fast(k)))
.collect::<Option<_>>()?;
crate::instr::parse_instruction_unified(
data, &accounts, sig, slot, tx_idx, block_us, grpc_us, filter, program_id,
)
}
}
#[inline(always)]
fn is_supported_inner_compiled_instruction(data: &[u8], program_id: &Pubkey) -> bool {
crate::instr::normal_instruction_data_may_parse(program_id, data)
}
#[inline(always)]
fn parse_inner_compiled_instruction_if_supported<'a>(
data: &[u8],
program_id: &Pubkey,
sig: Signature,
slot: u64,
tx_idx: u64,
block_us: Option<i64>,
grpc_us: i64,
account_indices: &[u8],
get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
filter: Option<&EventTypeFilter>,
) -> Option<DexEvent> {
if !is_supported_inner_compiled_instruction(data, program_id) {
return None;
}
parse_compiled_instruction(
data,
program_id,
sig,
slot,
tx_idx,
block_us,
grpc_us,
account_indices,
get_key,
filter,
)
}
#[inline(always)]
fn parse_outer_instruction<'a>(
data: &[u8],
program_id: &Pubkey,
sig: Signature,
slot: u64,
tx_idx: u64,
block_us: Option<i64>,
grpc_us: i64,
account_indices: &[u8],
get_key: &dyn Fn(usize) -> Option<&'a Vec<u8>>,
filter: Option<&EventTypeFilter>,
_is_created_buy: bool,
) -> Option<DexEvent> {
parse_compiled_instruction(
data,
program_id,
sig,
slot,
tx_idx,
block_us,
grpc_us,
account_indices,
get_key,
filter,
)
}
#[inline(always)]
fn parse_inner_instruction(
data: &[u8],
program_id: &Pubkey,
sig: Signature,
slot: u64,
tx_idx: u64,
block_us: Option<i64>,
grpc_us: i64,
filter: Option<&EventTypeFilter>,
is_created_buy: bool,
) -> Option<DexEvent> {
if data.len() < 16 {
return None;
}
let metadata = EventMetadata {
signature: sig,
slot,
tx_index: tx_idx,
block_time_us: block_us.unwrap_or(0),
grpc_recv_us: grpc_us,
recent_blockhash: None, };
let mut discriminator = [0u8; 16];
discriminator.copy_from_slice(&data[..16]);
let inner_data = &data[16..];
use crate::instr::{all_inner, program_ids, pump_amm_inner, pump_inner, raydium_clmm_inner};
let event = if *program_id == program_ids::PUMPFUN_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_pumpfun() {
return None;
}
}
pump_inner::parse_pumpfun_inner_instruction(
&discriminator,
inner_data,
metadata,
is_created_buy,
)
} else if *program_id == program_ids::PUMPSWAP_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_pumpswap() {
return None;
}
}
pump_amm_inner::parse_pumpswap_inner_instruction(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::PUMP_FEES_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_pump_fees() {
return None;
}
}
all_inner::pump_fees::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::RAYDIUM_CLMM_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_raydium_clmm() {
return None;
}
}
raydium_clmm_inner::parse_raydium_clmm_inner_instruction(
&discriminator,
inner_data,
metadata,
)
} else if *program_id == program_ids::RAYDIUM_CPMM_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_raydium_cpmm() {
return None;
}
}
all_inner::raydium_cpmm::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::RAYDIUM_AMM_V4_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_raydium_amm_v4() {
return None;
}
}
all_inner::raydium_amm::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::ORCA_WHIRLPOOL_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_orca_whirlpool() {
return None;
}
}
all_inner::orca::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::METEORA_POOLS_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_meteora_pools() {
return None;
}
}
all_inner::meteora_amm::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::METEORA_DAMM_V2_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_meteora_damm_v2() {
return None;
}
}
all_inner::meteora_damm::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::METEORA_DLMM_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_meteora_dlmm() {
return None;
}
}
all_inner::meteora_dlmm::parse(&discriminator, inner_data, metadata)
} else if *program_id == program_ids::RAYDIUM_LAUNCHLAB_PROGRAM_ID {
if let Some(f) = filter {
if !f.includes_raydium_launchlab() {
return None;
}
}
all_inner::raydium_launchlab::parse(&discriminator, inner_data, metadata)
} else {
None
};
if filter.map(|f| event.as_ref().is_some_and(|e| f.should_include_dex_event(e))).unwrap_or(true)
{
event
} else {
None
}
}
#[inline(always)]
fn merge_instruction_events(events: Vec<(usize, Option<usize>, DexEvent)>) -> Vec<DexEvent> {
if events.is_empty() {
return Vec::new();
}
let mut events = events;
events.sort_by_key(|(outer, inner, _)| (*outer, inner.map_or(0, |i| i + 1)));
let mut result = Vec::with_capacity(events.len());
let mut pending_outer: Option<(usize, DexEvent)> = None;
for (outer_idx, inner_idx, event) in events {
match inner_idx {
None => {
if let Some((_, outer_event)) = pending_outer.take() {
result.push(outer_event);
}
pending_outer = Some((outer_idx, event));
}
Some(_) => {
if let Some((pending_outer_idx, mut outer_event)) = pending_outer.take() {
if pending_outer_idx == outer_idx {
merge_events(&mut outer_event, event);
pending_outer = Some((outer_idx, outer_event));
} else {
result.push(outer_event);
result.push(event);
}
} else {
result.push(event);
}
}
}
}
if let Some((_, outer_event)) = pending_outer {
result.push(outer_event);
}
result
}
#[inline(always)]
fn should_parse_instructions(filter: Option<&EventTypeFilter>) -> bool {
let Some(filter) = filter else { return true };
if filter.include_only.is_none() {
return true;
}
if filter.includes_pumpfun() {
return true;
}
if filter.includes_pump_fees() {
return true;
}
filter.includes_pumpswap()
|| filter.includes_raydium_launchlab()
|| filter.includes_raydium_cpmm()
|| filter.includes_raydium_clmm()
|| filter.includes_raydium_amm_v4()
|| filter.includes_orca_whirlpool()
|| filter.includes_meteora_pools()
|| filter.includes_meteora_damm_v2()
|| filter.includes_meteora_dlmm()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::events::{PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, PUMPFUN_WSOL_QUOTE_MINT};
use yellowstone_grpc_proto::prelude::{
CompiledInstruction, InnerInstruction, InnerInstructions, Message, MessageHeader,
};
fn pk(s: &str) -> Pubkey {
s.parse().unwrap()
}
fn usdc_mint() -> Pubkey {
pk("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
}
fn pubkey_bytes(key: Pubkey) -> Vec<u8> {
key.to_bytes().to_vec()
}
fn decode_b58(s: &str) -> Vec<u8> {
bs58::decode(s).into_vec().unwrap()
}
fn str_arg(s: &str, out: &mut Vec<u8>) {
out.extend_from_slice(&(s.len() as u32).to_le_bytes());
out.extend_from_slice(s.as_bytes());
}
fn create_v2_data() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&crate::instr::pump::discriminators::CREATE_V2);
str_arg("Alt Coin", &mut data);
str_arg("ALT", &mut data);
str_arg("https://example.invalid/alt.json", &mut data);
data.extend_from_slice(Pubkey::new_unique().as_ref());
data.push(1);
data.push(1);
data
}
fn grpc_pumpfun_create_v2_tx(
static_len: usize,
writable_len: usize,
program_idx: u8,
ix_accounts: Vec<u8>,
account_overrides: Vec<(usize, Pubkey)>,
) -> (TransactionStatusMeta, Option<Transaction>) {
let mut account_keys: Vec<Pubkey> = (0..static_len).map(|_| Pubkey::new_unique()).collect();
account_keys[program_idx as usize] = crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
let readonly_len = account_overrides
.iter()
.filter(|(global_idx, _)| *global_idx >= static_len + writable_len)
.map(|(global_idx, _)| global_idx - static_len - writable_len + 1)
.max()
.unwrap_or_default();
let mut loaded_writable = vec![Pubkey::new_unique(); writable_len];
let mut loaded_readonly = vec![Pubkey::new_unique(); readonly_len];
for (global_idx, key) in account_overrides {
if global_idx < static_len {
account_keys[global_idx] = key;
} else if global_idx < static_len + writable_len {
loaded_writable[global_idx - static_len] = key;
} else {
loaded_readonly[global_idx - static_len - writable_len] = key;
}
}
let meta = TransactionStatusMeta {
loaded_writable_addresses: loaded_writable.into_iter().map(pubkey_bytes).collect(),
loaded_readonly_addresses: loaded_readonly.into_iter().map(pubkey_bytes).collect(),
..Default::default()
};
let tx = Transaction {
signatures: vec![Signature::default().as_ref().to_vec()],
message: Some(Message {
header: Some(MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 0,
}),
account_keys: account_keys.into_iter().map(pubkey_bytes).collect(),
recent_blockhash: vec![0; 32],
instructions: vec![CompiledInstruction {
program_id_index: program_idx as u32,
accounts: ix_accounts,
data: create_v2_data(),
}],
versioned: true,
address_table_lookups: Vec::new(),
}),
};
(meta, Some(tx))
}
fn create_v2_accounts(
account_len: usize,
program_idx: u8,
mint_idx: u8,
user_idx: u8,
token_program_idx: u8,
quote_tail: Option<(u8, u8, u8)>,
) -> Vec<u8> {
let mut accounts: Vec<u8> = (0..account_len).map(|i| i as u8).collect();
accounts[0] = mint_idx;
accounts[5] = user_idx;
accounts[7] = token_program_idx;
if account_len > 15 {
accounts[15] = program_idx;
}
if let Some((quote_idx, quote_vault_idx, quote_token_program_idx)) = quote_tail {
accounts[16] = quote_idx;
accounts[17] = quote_vault_idx;
accounts[18] = quote_token_program_idx;
}
accounts
}
fn parse_create_v2_from_grpc(
meta: &TransactionStatusMeta,
tx: &Option<Transaction>,
) -> crate::core::events::PumpFunCreateTokenEvent {
let events = parse_instructions_enhanced(
meta,
tx,
Signature::default(),
123,
0,
Some(456),
789,
None,
);
assert_eq!(events.len(), 1);
match &events[0] {
DexEvent::PumpFunCreate(e) => {
assert_eq!(e.ix_name, "create_v2");
e.clone()
}
DexEvent::PumpFunCreateV2(e) => {
assert_eq!(e.ix_name, "create_v2");
crate::core::events::PumpFunCreateTokenEvent {
metadata: e.metadata.clone(),
name: e.name.clone(),
symbol: e.symbol.clone(),
uri: e.uri.clone(),
mint: e.mint,
bonding_curve: e.bonding_curve,
user: e.user,
creator: e.creator,
timestamp: e.timestamp,
virtual_token_reserves: e.virtual_token_reserves,
virtual_sol_reserves: e.virtual_sol_reserves,
real_token_reserves: e.real_token_reserves,
token_total_supply: e.token_total_supply,
mint_authority: e.mint_authority,
associated_bonding_curve: e.associated_bonding_curve,
global: e.global,
system_program: e.system_program,
token_program: e.token_program,
associated_token_program: e.associated_token_program,
mayhem_program_id: e.mayhem_program_id,
global_params: e.global_params,
sol_vault: e.sol_vault,
mayhem_state: e.mayhem_state,
mayhem_token_vault: e.mayhem_token_vault,
event_authority: e.event_authority,
program: e.program,
quote_mint: e.quote_mint,
quote_vault: e.quote_vault,
quote_token_program: e.quote_token_program,
virtual_quote_reserves: e.virtual_quote_reserves,
ix_name: e.ix_name.clone(),
is_mayhem_mode: e.is_mayhem_mode,
is_cashback_enabled: e.is_cashback_enabled,
observed_fee_recipient: e.observed_fee_recipient,
}
}
other => panic!("expected PumpFun create_v2 event, got {other:?}"),
}
}
#[test]
fn test_should_parse_instructions() {
assert!(should_parse_instructions(None));
let filter = EventTypeFilter { include_only: None, exclude_types: None };
assert!(should_parse_instructions(Some(&filter)));
use crate::grpc::types::EventType;
let filter = EventTypeFilter {
include_only: Some(vec![EventType::PumpFunMigrate]),
exclude_types: None,
};
assert!(should_parse_instructions(Some(&filter)));
let filter = EventTypeFilter {
include_only: Some(vec![EventType::PumpFunTrade]),
exclude_types: None,
};
assert!(should_parse_instructions(Some(&filter)));
for event_type in [
EventType::PumpSwapTrade,
EventType::PumpFeesUpdateFeeShares,
EventType::RaydiumLaunchlabTrade,
EventType::RaydiumCpmmSwap,
EventType::RaydiumClmmSwap,
EventType::RaydiumAmmV4Swap,
EventType::OrcaWhirlpoolSwap,
EventType::MeteoraPoolsSwap,
EventType::MeteoraDammV2Swap,
EventType::MeteoraDammV2InitializePool,
EventType::MeteoraDlmmSwap,
] {
let filter = EventTypeFilter::include_only(vec![event_type]);
assert!(
should_parse_instructions(Some(&filter)),
"instruction parsing should be enabled for {event_type:?}"
);
}
let filter = EventTypeFilter::include_only(vec![EventType::MeteoraDbcSwap]);
assert!(
!should_parse_instructions(Some(&filter)),
"DBC events are log-only until an instruction parser is implemented"
);
let filter = EventTypeFilter::include_only(vec![
EventType::AccountPumpFunGlobal,
EventType::AccountRaydiumClmmPoolState,
EventType::AccountRaydiumCpmmPoolState,
EventType::AccountOrcaWhirlpool,
]);
assert!(
!should_parse_instructions(Some(&filter)),
"account-only non-Pump filters should stay on the account update path"
);
}
#[test]
fn test_merge_instruction_events() {
use solana_sdk::signature::Signature;
let metadata = EventMetadata {
signature: Signature::default(),
slot: 100,
tx_index: 1,
block_time_us: 1000,
grpc_recv_us: 2000,
recent_blockhash: None,
};
let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
metadata: metadata.clone(),
bonding_curve: Pubkey::new_unique(),
..Default::default()
});
let inner_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
metadata: metadata.clone(),
sol_amount: 1000,
token_amount: 2000,
..Default::default()
});
let events = vec![
(0, None, outer_event), (0, Some(0), inner_event), ];
let result = merge_instruction_events(events);
assert_eq!(result.len(), 1);
if let DexEvent::PumpFunTrade(trade) = &result[0] {
assert_eq!(trade.sol_amount, 1000); assert_eq!(trade.token_amount, 2000); assert_ne!(trade.bonding_curve, Pubkey::default()); } else {
panic!("Expected PumpFunTrade event");
}
}
#[test]
fn test_merge_instruction_events_chains_multiple_inners_same_outer() {
use solana_sdk::signature::Signature;
let metadata = EventMetadata {
signature: Signature::default(),
slot: 100,
tx_index: 1,
block_time_us: 1000,
grpc_recv_us: 2000,
recent_blockhash: None,
};
let bc = Pubkey::new_unique();
let fee = Pubkey::new_unique();
let outer_event = DexEvent::PumpFunTrade(PumpFunTradeEvent {
metadata: metadata.clone(),
bonding_curve: bc,
..Default::default()
});
let inner_trade = DexEvent::PumpFunTrade(PumpFunTradeEvent {
metadata: metadata.clone(),
sol_amount: 1000,
token_amount: 2000,
is_buy: true,
..Default::default()
});
let inner_fee_only = DexEvent::PumpFunTrade(PumpFunTradeEvent {
metadata: metadata.clone(),
fee_recipient: fee,
..Default::default()
});
let events =
vec![(0, None, outer_event), (0, Some(0), inner_trade), (0, Some(1), inner_fee_only)];
let result = merge_instruction_events(events);
assert_eq!(result.len(), 1);
if let DexEvent::PumpFunTrade(trade) = &result[0] {
assert_eq!(trade.bonding_curve, bc);
assert_eq!(trade.sol_amount, 1000);
assert_eq!(trade.token_amount, 2000);
assert_eq!(trade.fee_recipient, fee);
} else {
panic!("Expected PumpFunTrade event");
}
}
#[test]
fn grpc_pumpswap_inner_create_pool_cpi_reads_cashback_flag() {
let signature = "v5rg9RMc6D4pMsAqD8TrmXGFwHQBePFDWXBbtsQmP5gttLBKvExSEiPcGMipaDWP61VdWaxEyJCr7oXPxFH4DQf";
let static_keys = [
"9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
"CRfzaig7jyogshSi4Lydsg3RXm3Ta9Gg4oMVTV7UcYej",
"6sFov2ot9waASAUCLf3hUDc9UXSxw36nE1ehbJqA37XS",
"F4brPQAt8DR6bN7DLhXzyLUJ77NFYUCokxmnS7cmgvki",
"HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC",
"4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv",
"2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo",
"HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu",
"GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4",
"H1e1uYxxkSeJpjKeqajizBTCMXc4wun1vqgNGiFgsXru",
"56pZVJ6T5Dy3MZ56YcAcHM9YqEbyustsjS9MNNNh16cC",
"GzZSwyjsKKmMHtEdMggC9fB1bowTm3Vzhs6hxMQfviVu",
"ComputeBudget111111111111111111111111111111",
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
"9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump",
"SysvarRent111111111111111111111111111111111",
];
let loaded_writable = ["39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"];
let loaded_readonly = [
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
"So11111111111111111111111111111111111111112",
"11111111111111111111111111111111",
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
];
let meta = TransactionStatusMeta {
loaded_writable_addresses: loaded_writable.iter().map(|s| pubkey_bytes(pk(s))).collect(),
loaded_readonly_addresses: loaded_readonly.iter().map(|s| pubkey_bytes(pk(s))).collect(),
inner_instructions: vec![InnerInstructions {
index: 2,
instructions: vec![InnerInstruction {
program_id_index: 20,
accounts: vec![4, 21, 5, 14, 18, 8, 6, 7, 9, 10, 11, 19, 22, 22, 23, 24, 25, 20],
data: decode_b58(
"iPiwDbPRj3YavFpj3AxMZtPvSSQKdH3Uw8kaPUDj2NXDsWjrQx5ndF39nxYypLG2dVKDtBiBz3jsJ6gvzU",
),
stack_height: Some(2),
}],
}],
..Default::default()
};
let tx = Some(Transaction {
signatures: vec![pk("11111111111111111111111111111111").as_ref().to_vec()],
message: Some(Message {
header: Some(MessageHeader::default()),
account_keys: static_keys.iter().map(|s| pubkey_bytes(pk(s))).collect(),
recent_blockhash: vec![0; 32],
instructions: vec![
CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
CompiledInstruction { program_id_index: 12, accounts: vec![], data: vec![0] },
CompiledInstruction { program_id_index: 13, accounts: vec![], data: vec![0] },
],
versioned: true,
address_table_lookups: Vec::new(),
}),
});
let events = parse_instructions_enhanced(
&meta,
&tx,
signature.parse().unwrap(),
427_039_576,
0,
Some(1_781_687_252_000_000),
789,
None,
);
assert_eq!(events.len(), 1, "{signature}");
match &events[0] {
DexEvent::PumpSwapCreatePool(e) => {
assert_eq!(e.index, 0, "{signature}");
assert_eq!(e.base_amount_in, 206_900_000_000_000, "{signature}");
assert_eq!(e.quote_amount_in, 84_990_359_912, "{signature}");
assert_eq!(
e.coin_creator,
pk("4DrtsW86GarGJJeYrBwYCjoyMgDPG95QWSGhFHvCkU2s"),
"{signature}"
);
assert!(!e.is_mayhem_mode, "{signature}");
assert!(e.is_cashback_coin, "{signature}");
assert_eq!(
e.pool,
pk("HJKRc3JtgmattaPBFp1XqAhymk9FtJQjZZWZ9LtCMDLC"),
"{signature}"
);
assert_eq!(
e.creator,
pk("4pVPfQmUZPDUgzTC5VAuad82wpaf4yzvSWVvFQBs73sv"),
"{signature}"
);
assert_eq!(
e.base_mint,
pk("9JmruaWd8Dscxs1GBVbnckGWWsoVdJwg9DDFGFW9pump"),
"{signature}"
);
assert_eq!(
e.quote_mint,
pk("So11111111111111111111111111111111111111112"),
"{signature}"
);
assert_eq!(
e.lp_mint,
pk("GywAHNZRk8qjAiekaXgk5mBqweMibW31KGnUHzMN5Ht4"),
"{signature}"
);
assert_eq!(
e.user_base_token_account,
pk("2m3hPFQ17Vn2gdeoxCr4M8Tx9jcLtTjiLtqnpyz7Tizo"),
"{signature}"
);
assert_eq!(
e.user_quote_token_account,
pk("HC5ix2JxmZQ9sNiPFbFsFuXfu7GHt2RT2UoQVFWskfhu"),
"{signature}"
);
}
other => panic!("expected PumpSwapCreatePool for {signature}, got {other:?}"),
}
}
#[test]
fn grpc_pumpfun_create_v2_resolves_alt_loaded_quote_mint_cases() {
struct Case {
signature: &'static str,
name: &'static str,
static_len: usize,
writable_len: usize,
program_idx: u8,
account_len: usize,
mint_idx: u8,
mint: &'static str,
user_idx: u8,
user: &'static str,
token_program_idx: u8,
quote_idx: u8,
quote_mint: Pubkey,
quote_vault_idx: u8,
quote_vault: &'static str,
quote_token_program_idx: u8,
}
let token_2022_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
let spl_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
let cases = [
Case {
signature: "4GCVgY2FnT1s4q5zemnPL4mzSbuhUTgQo9mc9jewhLZzsCXKe8ehz6xD4QDJE853CLrF6doJbf4JNwJVeEYLA4De",
name: "19-account WSOL quote in ALT",
static_len: 15,
writable_len: 7,
program_idx: 12,
account_len: 19,
mint_idx: 1,
mint: "CGY36MoFU627gPH4TLM5NP4Xnvhz6Nesc71TQecPpump",
user_idx: 0,
user: "Aqje5DsN4u2PHmQxGF9PKfpsDGwQRCBhWeLKHCFhSMXk",
token_program_idx: 24,
quote_idx: 27,
quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
quote_vault_idx: 7,
quote_vault: "CWR85PmUfzNNgmNN9Ref8L8BvMibZ1tzchiT5bTZpJhn",
quote_token_program_idx: 28,
},
Case {
signature: "5HwZKTwcGFjSBPugSX5hE9JSq5wKmUooK3tLXuEoyDDzrTvHu7op3XDbhBXuteiC5EePNPh8TC1j6Fns47YvnyeG",
name: "19-account WSOL quote in ALT with exact quote buy",
static_len: 20,
writable_len: 7,
program_idx: 15,
account_len: 19,
mint_idx: 1,
mint: "7NSSfLGsjNHzKxrgggQ56C2UdKxJVJvrECJR3dsbBuuG",
user_idx: 0,
user: "2bBRwhGoL4fRZk6g8NnhBZywsF8PdLJnBRfWDCEMogD2",
token_program_idx: 31,
quote_idx: 28,
quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
quote_vault_idx: 4,
quote_vault: "6jFz2oefpJUE6opjA7vxs3iXou7YYyb6e6E4LN2BFs1W",
quote_token_program_idx: 30,
},
Case {
signature: "3MVawF6EPtG7rEPXdsyQfQUBLv3epRVNpNS4tRE4uwTPMqLNPqhuABwxU3QZH4uD6CuVupcpGchpNRK5HTbHRLNK",
name: "19-account USDC quote in ALT",
static_len: 19,
writable_len: 6,
program_idx: 16,
account_len: 19,
mint_idx: 1,
mint: "FUsqvH5x8QUrxmJhspt6meQZtfBr17m2YsTFuVsYpump",
user_idx: 0,
user: "9Gg6Mf8tq9zLSpK8qccrQiue3iE7wmyeogKkGZpnz2w5",
token_program_idx: 27,
quote_idx: 30,
quote_mint: usdc_mint(),
quote_vault_idx: 6,
quote_vault: "7SLtvqMx4bPoWSbPcnWBWpBem3RXbKraWUsiApXjB1VL",
quote_token_program_idx: 31,
},
Case {
signature: "oY9YQbie16Bw11GsqbAPVnW6YjMHAj3kP9sufjcuQjdfcU86iUY8CiSaDrvu4QXJFnGY4jqQc2Kc1YVuAzujvyv",
name: "20-account WSOL quote in ALT",
static_len: 15,
writable_len: 7,
program_idx: 12,
account_len: 20,
mint_idx: 1,
mint: "Bv3zjsdJ5KuA9KsGirqssC8pVJwCeCeyLjo4Hqpfpump",
user_idx: 0,
user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
token_program_idx: 24,
quote_idx: 27,
quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
quote_vault_idx: 7,
quote_vault: "9QdMAuwtpnHSzjTQcTkjU1GFSs2gNtR66sdQofFv5P7B",
quote_token_program_idx: 28,
},
Case {
signature: "3jWGFYXT5V33Qc2roEBFDRAWHeybDowr53dSdnYSRkrPdYybU7oyEH9BfgSRxkgFHVKmUjv4e5T33AEnhJvBCuP2",
name: "19-account WSOL quote in ALT with later buy",
static_len: 18,
writable_len: 7,
program_idx: 13,
account_len: 19,
mint_idx: 1,
mint: "5i8AZEBc8o5dhfnTQdD3QTVejgbjitwQ1ADHg1jZpump",
user_idx: 0,
user: "2b2N2p7xCS9ibDqxwYgXpDSTniJwwye7n93WYuzmr74s",
token_program_idx: 27,
quote_idx: 30,
quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
quote_vault_idx: 7,
quote_vault: "9QB9SyXGDbHUsvvF8XMbYH5ioJMHKHhXTjQDoL56uHT7",
quote_token_program_idx: 31,
},
Case {
signature: "2dZAucKwr4n5Lqu3BtJ4P8JsjCDtUXJzthadddfURraEJRTgn6XWaTNUNBbgUfP5c2wcVdubqViQhr48eWsgRqPX",
name: "19-account USDC quote in ALT exact quote buy",
static_len: 19,
writable_len: 6,
program_idx: 15,
account_len: 19,
mint_idx: 1,
mint: "DsE8Ptubc1HWWethf9ant4eV9YnofEv5kfGyLdj7jk2Y",
user_idx: 0,
user: "easy7tXgADWkRMNjFRS2XsLXUAaKH5tEPodh9g7kcX8",
token_program_idx: 28,
quote_idx: 33,
quote_mint: usdc_mint(),
quote_vault_idx: 7,
quote_vault: "8QTKfEBf5yChuos4eTzQPbV3jXveCu5GkNKLFoS8oS7t",
quote_token_program_idx: 27,
},
Case {
signature: "4h9kYjzYpqqyYZuFnjf14zRwrGyChCuKAYVy6a4ZBig19bydEYsHwp6VbiKqTzT3pLf6NXnf6E25dn1NiU8LR4YB",
name: "20-account WSOL quote in ALT with jit account",
static_len: 15,
writable_len: 7,
program_idx: 12,
account_len: 20,
mint_idx: 1,
mint: "6EvDE4a7Yw8F65oy6UhhN3JBshGk9tV3b2yxNyhypump",
user_idx: 0,
user: "2SWqdMbn1FJVUMUEpuyP2St8BPRtqJYXJPWFfmZr486q",
token_program_idx: 24,
quote_idx: 27,
quote_mint: PUMPFUN_WSOL_QUOTE_MINT,
quote_vault_idx: 7,
quote_vault: "27jyvk4PUYjcDQkKn8VGT9zNdAxZWWjqALpRUpjMqc2y",
quote_token_program_idx: 28,
},
];
for case in cases {
let (meta, tx) = grpc_pumpfun_create_v2_tx(
case.static_len,
case.writable_len,
case.program_idx,
create_v2_accounts(
case.account_len,
case.program_idx,
case.mint_idx,
case.user_idx,
case.token_program_idx,
Some((case.quote_idx, case.quote_vault_idx, case.quote_token_program_idx)),
),
vec![
(case.mint_idx as usize, pk(case.mint)),
(case.user_idx as usize, pk(case.user)),
(case.token_program_idx as usize, token_2022_program),
(case.quote_idx as usize, case.quote_mint),
(case.quote_vault_idx as usize, pk(case.quote_vault)),
(case.quote_token_program_idx as usize, spl_token_program),
],
);
let loaded_key_location = |global_idx: u8| -> (&'static str, usize) {
let idx = global_idx as usize;
if idx < case.static_len {
("static", idx)
} else if idx < case.static_len + case.writable_len {
("writable", idx - case.static_len)
} else {
("readonly", idx - case.static_len - case.writable_len)
}
};
assert_eq!(
meta.loaded_writable_addresses.len(),
case.writable_len,
"{}: {}",
case.name,
case.signature
);
for (global_idx, expected_key) in [
(case.token_program_idx, token_2022_program),
(case.quote_idx, case.quote_mint),
(case.quote_token_program_idx, spl_token_program),
] {
match loaded_key_location(global_idx) {
("static", _) => {}
("writable", offset) => assert_eq!(
read_pubkey_fast(&meta.loaded_writable_addresses[offset]),
expected_key,
"{}: writable loaded key {global_idx}: {}",
case.name,
case.signature
),
("readonly", offset) => assert_eq!(
read_pubkey_fast(&meta.loaded_readonly_addresses[offset]),
expected_key,
"{}: readonly loaded key {global_idx}: {}",
case.name,
case.signature
),
_ => unreachable!(),
}
}
let create = parse_create_v2_from_grpc(&meta, &tx);
assert_eq!(create.mint, pk(case.mint), "{}: {}", case.name, case.signature);
assert_eq!(create.user, pk(case.user), "{}: {}", case.name, case.signature);
assert_eq!(
create.token_program, token_2022_program,
"{}: {}",
case.name, case.signature
);
assert_eq!(create.quote_mint, case.quote_mint, "{}: {}", case.name, case.signature);
assert_eq!(
create.quote_vault,
pk(case.quote_vault),
"{}: {}",
case.name,
case.signature
);
assert_eq!(
create.quote_token_program, spl_token_program,
"{}: {}",
case.name, case.signature
);
}
}
#[test]
fn grpc_pumpfun_create_v2_16_account_uses_sol_sentinel_without_quote_tail() {
let signature = "H6azwLqtRtrnVNC5iwcjYM9idU3e9SRyLZXTwjfJGJxA4X7dZL7vyhFAJNvQy7bb6bmQNmFHUt1KkkPPmhdge3G";
let mint = pk("HhL4NuFWAfHScNBUksxN6YNXbMNbcSkH4LJaWgZkpump");
let user = pk("25jZ7EwnKfZo2DZgHM27pbU5Tf54PYG8jc7qNL3gtkxG");
let token_program = crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID;
let (meta, tx) = grpc_pumpfun_create_v2_tx(
16,
5,
12,
create_v2_accounts(16, 12, 1, 0, 24, None),
vec![(1, mint), (0, user), (24, token_program)],
);
let create = parse_create_v2_from_grpc(&meta, &tx);
assert_eq!(create.mint, mint, "{signature}");
assert_eq!(create.user, user, "{signature}");
assert_eq!(create.token_program, token_program, "{signature}");
assert_eq!(create.quote_mint, PUMPFUN_SOLSCAN_SOL_QUOTE_MINT, "{signature}");
assert_eq!(create.quote_vault, Pubkey::default(), "{signature}");
assert_eq!(create.quote_token_program, Pubkey::default(), "{signature}");
}
#[test]
fn grpc_pumpfun_create_v2_rejects_program_id_as_quote_mint() {
let quote_vault = Pubkey::new_unique();
let quote_token_program = crate::accounts::program_ids::SPL_TOKEN_PROGRAM_ID;
let (meta, tx) = grpc_pumpfun_create_v2_tx(
19,
6,
16,
create_v2_accounts(19, 16, 1, 0, 27, Some((30, 6, 31))),
vec![
(27, crate::accounts::program_ids::SPL_TOKEN_2022_PROGRAM_ID),
(30, crate::instr::program_ids::PUMPFUN_PROGRAM_ID),
(6, quote_vault),
(31, quote_token_program),
],
);
let create = parse_create_v2_from_grpc(&meta, &tx);
assert_eq!(create.quote_mint, Pubkey::default());
assert_eq!(create.quote_vault, Pubkey::default());
assert_eq!(create.quote_token_program, Pubkey::default());
}
}