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