Skip to main content

sol_parser_sdk/
wrapped_pumpfun_rpc_analyzer.rs

1//! Offline RPC analysis only. This module is never called by streaming parsers.
2//! Matching bytes are evidence for a candidate, not proof of a program's ABI.
3use crate::core::events::{EventMetadata, RawInstructionEvent};
4use crate::wrapped_pumpfun_parser::{pump_trade_name, WrappedPumpfunRule};
5use serde::{Deserialize, Serialize};
6use solana_sdk::pubkey::Pubkey;
7use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct WrappedPumpfunAnalysis {
11    pub program_id: Pubkey,
12    pub instruction_index: usize,
13    pub pump_instruction: Option<String>,
14    pub reason: String,
15    pub candidate_rule: Option<WrappedPumpfunRule>,
16}
17
18/// Analyze an RPC transaction converted by convert_rpc_to_grpc. Runtime-derived
19/// fields and ambiguous byte/account matches fail closed. No network access here;
20/// the accompanying example owns the RPC request.
21pub fn analyze_wrapped_pumpfun(
22    transaction: &Transaction,
23    meta: &TransactionStatusMeta,
24) -> Vec<WrappedPumpfunAnalysis> {
25    let Some(message) = &transaction.message else { return vec![] };
26    if meta.err.is_some() {
27        return vec![];
28    }
29    let keys: Vec<Option<Pubkey>> = message
30        .account_keys
31        .iter()
32        .chain(&meta.loaded_writable_addresses)
33        .chain(&meta.loaded_readonly_addresses)
34        .map(|k| Pubkey::try_from(k.as_slice()).ok())
35        .collect();
36    let get = |i: usize| keys.get(i).copied().flatten();
37    let pump = crate::instr::pump::PROGRAM_ID_PUBKEY;
38    let mut reports = vec![];
39    for (index, outer) in message.instructions.iter().enumerate() {
40        let Some(program_id) = get(outer.program_id_index as usize) else { continue };
41        if !crate::core::events::raw_instruction_dexes(program_id, &[]).is_empty()
42            || !outer.accounts.iter().any(|i| get(*i as usize) == Some(pump))
43        {
44            continue;
45        }
46        let trades: Vec<_> = meta
47            .inner_instructions
48            .iter()
49            .filter(|g| g.index as usize == index)
50            .flat_map(|g| &g.instructions)
51            .filter(|ix| {
52                get(ix.program_id_index as usize) == Some(pump)
53                    && ix.data.get(..8).and_then(pump_trade_name).is_some()
54            })
55            .collect();
56        let mut report = WrappedPumpfunAnalysis {
57            program_id,
58            instruction_index: index,
59            pump_instruction: trades
60                .first()
61                .and_then(|ix| pump_trade_name(&ix.data[..8]))
62                .map(str::to_owned),
63            reason: String::new(),
64            candidate_rule: None,
65        };
66        let other_dex = meta
67            .inner_instructions
68            .iter()
69            .filter(|g| g.index as usize == index)
70            .flat_map(|g| &g.instructions)
71            .filter_map(|ix| get(ix.program_id_index as usize))
72            .any(|pid| {
73                pid != pump && !crate::core::events::raw_instruction_dexes(pid, &[]).is_empty()
74            });
75        if other_dex {
76            report.reason = "multi_dex_route_not_supported".into();
77            reports.push(report);
78            continue;
79        }
80        if trades.len() != 1 {
81            report.reason = if trades.is_empty() {
82                "no_supported_pump_trade_cpi"
83            } else {
84                "multiple_pump_trade_cpis_not_supported"
85            }
86            .into();
87            reports.push(report);
88            continue;
89        }
90        let inner = trades[0];
91        let raw = RawInstructionEvent {
92            metadata: EventMetadata::default(),
93            dexes: vec![],
94            program_id,
95            instruction_index: index,
96            accounts: outer.accounts.iter().map(|i| get(*i as usize)).collect(),
97            account_indices: outer.accounts.clone(),
98            instruction_data: outer.data.clone(),
99        };
100        let mapped_accounts: Option<Vec<_>> =
101            inner.accounts.iter().map(|i| get(*i as usize)).collect();
102        match mapped_accounts.and_then(|accounts| infer_rule(&raw, &inner.data, &accounts)) {
103            Some(rule) => {
104                report.reason =
105                    "candidate_only_validate_independent_transactions_before_use".into();
106                report.candidate_rule = Some(rule);
107            }
108            None => report.reason = "cannot_match_unambiguous_input_parameters_or_accounts".into(),
109        }
110        reports.push(report);
111    }
112    reports
113}
114
115fn infer_rule(
116    raw: &RawInstructionEvent,
117    data: &[u8],
118    inner_accounts: &[Pubkey],
119) -> Option<WrappedPumpfunRule> {
120    if data.len() != 24 || raw.instruction_data.len() < 17 {
121        return None;
122    }
123    let accounts = raw.accounts.iter().copied().collect::<Option<Vec<_>>>()?;
124    let mut offsets = [0; 2];
125    let mut pattern: Vec<_> = raw.instruction_data.iter().copied().map(Some).collect();
126    for i in 0..2 {
127        let bytes = &data[8 + i * 8..16 + i * 8];
128        // At least one opcode byte precedes arguments. Never infer literal
129        // constants from the observed CPI or search at sub-field byte granularity.
130        let candidates: Vec<_> = raw
131            .instruction_data
132            .windows(8)
133            .enumerate()
134            .filter(|(offset, window)| *offset > 0 && *window == bytes)
135            .map(|(offset, _)| offset)
136            .collect();
137        if candidates.len() != 1 {
138            return None;
139        }
140        offsets[i] = candidates[0];
141        for b in &mut pattern[offsets[i]..offsets[i] + 8] {
142            *b = None;
143        }
144    }
145    if offsets[0].abs_diff(offsets[1]) < 8 {
146        return None;
147    }
148    let map = inner_accounts
149        .iter()
150        .map(|key| accounts.iter().position(|a| a == key))
151        .collect::<Option<Vec<_>>>()?;
152    let mut aliases = vec![];
153    for i in 0..accounts.len() {
154        if let Some(j) = accounts[..i].iter().position(|a| *a == accounts[i]) {
155            aliases.push((j, i));
156        }
157    }
158    // Bind canonical program/sysvar/global identities; variable mints and wallets
159    // remain variable. Aliases guard ambiguous mappings in the training sample.
160    let fixed = accounts
161        .iter()
162        .enumerate()
163        .filter(|(_, k)| {
164            **k == crate::instr::pump::PROGRAM_ID_PUBKEY
165                || **k == solana_sdk::pubkey!("11111111111111111111111111111111")
166                || **k == solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
167                || **k == solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
168                || **k == solana_sdk::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
169                || **k == solana_sdk::pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf")
170                || **k == solana_sdk::pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1")
171                || **k == solana_sdk::pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
172        })
173        .map(|(i, k)| (i, *k))
174        .collect();
175    Some(WrappedPumpfunRule {
176        rule_id: format!("{}:{}:candidate", raw.program_id, raw.instruction_index),
177        program_id: raw.program_id,
178        outer_account_count: accounts.len(),
179        data_pattern: pattern,
180        pump_discriminator: data[..8].try_into().ok()?,
181        argument_offsets: offsets,
182        account_map: map,
183        fixed_accounts: fixed,
184        equal_accounts: aliases,
185    })
186}