sol_parser_sdk/
wrapped_pumpfun_rpc_analyzer.rs1use 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
18pub 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 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 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}