Skip to main content

sol_parser_sdk/
wrapped_pumpfun_parser.rs

1//! Explicitly opted-in, offline-validated wrapper rules. No RPC, clocks, or I/O.
2//! A rule describes an observed input layout, not proof of future CPI execution.
3use crate::core::events::{DexEvent, RawInstructionEvent};
4use serde::{Deserialize, Serialize};
5use solana_sdk::pubkey::Pubkey;
6
7/// Compatibility name for the raw outer instruction payload. Rule identifiers
8/// belong to the caller's rule registry, not the transaction's provenance.
9pub type ThirdProgram = crate::core::events::RawInstructionPayload;
10
11/// Candidate rules must be reviewed and tested on independent transactions
12/// before the caller opts in. Never install analyzer output automatically.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct WrappedPumpfunRule {
15    pub rule_id: String,
16    pub program_id: Pubkey,
17    pub outer_account_count: usize,
18    /// Exact byte guards; None denotes a copied argument byte.
19    pub data_pattern: Vec<Option<u8>>,
20    pub pump_discriminator: [u8; 8],
21    /// One outer offset for each u64 Pump argument, in IDL order.
22    pub argument_offsets: [usize; 2],
23    /// Pump account index -> outer account index, retaining duplicates.
24    pub account_map: Vec<usize>,
25    pub fixed_accounts: Vec<(usize, Pubkey)>,
26    /// Aliases observed during inference must stay equal, otherwise the inferred
27    /// mapping is ambiguous (e.g. base/quote token programs happened to match).
28    pub equal_accounts: Vec<(usize, usize)>,
29}
30
31/// Narrow initial scope: fixed two-u64 v2 trades. Multi-hop and runtime-derived
32/// amounts are deliberately unsupported. Expand only with IDL-backed tests.
33pub fn pump_trade_name(disc: &[u8]) -> Option<&'static str> {
34    use crate::instr::pump::discriminators::*;
35    match disc {
36        d if d == BUY_V2 => Some("buy_v2"),
37        d if d == BUY_EXACT_QUOTE_IN_V2 => Some("buy_exact_quote_in_v2"),
38        d if d == SELL_V2 => Some("sell_v2"),
39        _ => None,
40    }
41}
42
43/// Pure reconstruction. Missing ALT accounts, changed bytes or changed account
44/// constraints return None; the caller retains the original RawInstruction.
45pub fn restore_instruction(
46    raw: &RawInstructionEvent,
47    rule: &WrappedPumpfunRule,
48) -> Option<([u8; 24], Vec<Pubkey>)> {
49    if raw.program_id != rule.program_id
50        || raw.accounts.len() != rule.outer_account_count
51        || raw.instruction_data.len() != rule.data_pattern.len()
52        || rule.data_pattern.is_empty()
53        || rule.data_pattern.iter().all(Option::is_none)
54        || pump_trade_name(&rule.pump_discriminator).is_none()
55        || raw
56            .instruction_data
57            .iter()
58            .zip(&rule.data_pattern)
59            .any(|(actual, expected)| expected.is_some_and(|v| v != *actual))
60    {
61        return None;
62    }
63    if raw.accounts.iter().any(Option::is_none) {
64        return None;
65    }
66    let accounts = &raw.accounts;
67    for &(index, key) in &rule.fixed_accounts {
68        if accounts.get(index) != Some(&Some(key)) {
69            return None;
70        }
71    }
72    for &(a, b) in &rule.equal_accounts {
73        if accounts.get(a).is_none() || accounts.get(a) != accounts.get(b) {
74            return None;
75        }
76    }
77    let mut data = [0u8; 24];
78    data[..8].copy_from_slice(&rule.pump_discriminator);
79    for (i, offset) in rule.argument_offsets.iter().copied().enumerate() {
80        data[8 + i * 8..16 + i * 8]
81            .copy_from_slice(raw.instruction_data.get(offset..offset.checked_add(8)?)?);
82    }
83    let mapped = rule
84        .account_map
85        .iter()
86        .map(|i| accounts.get(*i).copied().flatten())
87        .collect::<Option<Vec<_>>>()?;
88    Some((data, mapped))
89}
90
91/// Use from a Shred RawInstruction callback with a caller-owned rule registry.
92/// Returns PumpFunTrade with input parameters, never fabricated executed amounts.
93/// Existing gRPC parsing does not call this function.
94pub fn parse_wrapped_pumpfun_instruction(
95    raw: &RawInstructionEvent,
96    rule: &WrappedPumpfunRule,
97    filter: Option<&crate::grpc::EventTypeFilter>,
98) -> Option<DexEvent> {
99    if filter.is_some_and(|f| !f.includes_pumpfun()) {
100        return None;
101    }
102    let (data, accounts) = restore_instruction(raw, rule)?;
103    let m = &raw.metadata;
104    let event = crate::instr::pump::parse_instruction(
105        &data,
106        &accounts,
107        m.signature,
108        m.slot,
109        m.tx_index,
110        Some(m.block_time_us),
111        m.grpc_recv_us,
112    )?;
113    let mut trade = match event {
114        DexEvent::PumpFunTrade(t)
115        | DexEvent::PumpFunBuy(t)
116        | DexEvent::PumpFunSell(t)
117        | DexEvent::PumpFunBuyExactSolIn(t) => t,
118        _ => return None,
119    };
120    trade.metadata = m.clone();
121    trade.sol_amount = 0;
122    trade.quote_amount = 0;
123    trade.token_amount = 0;
124    trade.third_program = Some(raw.into());
125    trade.parse_status = crate::core::events::PumpFunParseStatus::InstructionDecoded;
126    let event = DexEvent::PumpFunTrade(trade);
127    filter.is_none_or(|f| f.should_include_dex_event(&event)).then_some(event)
128}