Skip to main content

solana_tx_parser/
transaction_utils.rs

1//! Transaction utilities: DEX info, transfer actions, swap data.
2
3use crate::constants::{dex_programs, FEE_ACCOUNTS, SYSTEM_PROGRAMS};
4use crate::instruction_classifier::InstructionClassifier;
5use crate::transaction_adapter::TransactionAdapter;
6use crate::types::{DexInfo, TransferData, TransferInfoInner};
7use crate::utils::convert_to_ui_amount;
8use std::collections::HashMap;
9
10pub struct TransactionUtils<'a> {
11    adapter: &'a TransactionAdapter<'a>,
12}
13
14impl<'a> TransactionUtils<'a> {
15    pub fn new(adapter: &'a TransactionAdapter<'a>) -> Self {
16        Self { adapter }
17    }
18
19    pub fn get_dex_info(&self, classifier: &InstructionClassifier<'a>) -> DexInfo {
20        let program_ids = classifier.get_all_program_ids();
21        if program_ids.is_empty() {
22            return DexInfo::default();
23        }
24        for program_id in &program_ids {
25            let id = program_id.as_str();
26            if id == dex_programs::JUPITER.id {
27                return DexInfo {
28                    program_id: Some(program_id.clone()),
29                    route: Some(dex_programs::JUPITER.name.to_string()),
30                    amm: None,
31                };
32            }
33            if id == dex_programs::JUPITER_DCA.id {
34                return DexInfo {
35                    program_id: Some(program_id.clone()),
36                    route: Some(dex_programs::JUPITER_DCA.name.to_string()),
37                    amm: None,
38                };
39            }
40            if id == dex_programs::RAYDIUM_V4.id {
41                return DexInfo {
42                    program_id: Some(program_id.clone()),
43                    route: None,
44                    amm: Some(dex_programs::RAYDIUM_V4.name.to_string()),
45                };
46            }
47            if id == dex_programs::METEORA.id {
48                return DexInfo {
49                    program_id: Some(program_id.clone()),
50                    route: None,
51                    amm: Some(dex_programs::METEORA.name.to_string()),
52                };
53            }
54            if id == dex_programs::ORCA.id {
55                return DexInfo {
56                    program_id: Some(program_id.clone()),
57                    route: None,
58                    amm: Some(dex_programs::ORCA.name.to_string()),
59                };
60            }
61            if id == dex_programs::PUMP_FUN.id {
62                return DexInfo {
63                    program_id: Some(program_id.clone()),
64                    route: None,
65                    amm: Some(dex_programs::PUMP_FUN.name.to_string()),
66                };
67            }
68            if id == dex_programs::PUMP_SWAP.id {
69                return DexInfo {
70                    program_id: Some(program_id.clone()),
71                    route: None,
72                    amm: Some(dex_programs::PUMP_SWAP.name.to_string()),
73                };
74            }
75        }
76        DexInfo {
77            program_id: program_ids.first().cloned(),
78            ..Default::default()
79        }
80    }
81
82    /// Build transfer actions from inner and outer instructions (compiled SPL transfer/transferChecked only).
83    pub fn get_transfer_actions(
84        &self,
85        extra_types: &[&str],
86    ) -> HashMap<String, Vec<TransferData>> {
87        let mut actions: HashMap<String, Vec<TransferData>> = HashMap::new();
88        // Outer instructions
89        for (outer_index, raw) in self.adapter.raw_instructions().iter().enumerate() {
90            let program_id = self.adapter.get_instruction_program_id(raw);
91            if SYSTEM_PROGRAMS.contains(&program_id.as_str()) {
92                continue;
93            }
94            let group_key = format!("{}:{}", program_id, outer_index);
95            if let Some(transfer) = self.parse_compiled_action(raw, &outer_index.to_string(), extra_types) {
96                let is_fee = FEE_ACCOUNTS.contains(&transfer.info.destination.as_str())
97                    || transfer.info.destination_owner.as_ref().map(|o| FEE_ACCOUNTS.contains(&o.as_str())).unwrap_or(false);
98                let mut t = transfer;
99                if is_fee {
100                    t.is_fee = Some(true);
101                }
102                actions.entry(group_key).or_default().push(t);
103            }
104        }
105        let inner = match self.adapter.raw_inner_instructions() {
106            Some(i) => i,
107            None => return actions,
108        };
109        for set in inner {
110            let outer_index = set.index as usize;
111            let outer_program_id = self
112                .adapter
113                .raw_instructions()
114                .get(outer_index)
115                .map(|r| self.adapter.get_instruction_program_id(r))
116                .unwrap_or_default();
117            for (inner_index, raw) in set.instructions.iter().enumerate() {
118                let group_key = format!("{}:{}-{}", outer_program_id, outer_index, inner_index);
119                if let Some(transfer) = self.parse_compiled_action(
120                    raw,
121                    &format!("{}-{}", outer_index, inner_index),
122                    extra_types,
123                ) {
124                    let is_fee = FEE_ACCOUNTS.contains(&transfer.info.destination.as_str())
125                        || transfer
126                            .info
127                            .destination_owner
128                            .as_ref()
129                            .map(|o| FEE_ACCOUNTS.contains(&o.as_str()))
130                            .unwrap_or(false);
131                    let mut t = transfer;
132                    if is_fee {
133                        t.is_fee = Some(true);
134                    }
135                    actions.entry(group_key).or_default().push(t);
136                }
137            }
138        }
139        actions
140    }
141
142    fn parse_compiled_action(
143        &self,
144        raw: &crate::types::RawInstruction,
145        idx: &str,
146        _extra_types: &[&str],
147    ) -> Option<TransferData> {
148        let data = &raw.data;
149        if data.is_empty() {
150            return None;
151        }
152        let program_id = self.adapter.get_instruction_program_id(raw);
153        let accounts: Vec<String> = raw
154            .account_key_indexes
155            .iter()
156            .filter_map(|&i| self.adapter.get_account_key(i as usize))
157            .collect();
158        match (program_id.as_str(), data[0]) {
159            (crate::constants::TOKEN_PROGRAM_ID, crate::constants::spl_token_instruction::TRANSFER) => {
160                if accounts.len() < 2 {
161                    return None;
162                }
163                let amount = if data.len() >= 9 {
164                    u64::from_le_bytes(data[1..9].try_into().ok()?)
165                } else {
166                    return None;
167                };
168                let source = accounts[0].clone();
169                let destination = accounts[1].clone();
170                let token1 = self.adapter.spl_token_map.get(&destination).map(|t| t.mint.clone());
171                let token2 = self.adapter.spl_token_map.get(&source).map(|t| t.mint.clone());
172                let mint = crate::utils::get_transfer_token_mint(
173                    token1.as_deref(),
174                    token2.as_deref(),
175                )?;
176                let decimals = self.adapter.get_token_decimals(&mint);
177                let (sb, db, spb, dpb) = {
178                    let sb = self.adapter.get_token_account_balance(&[source.clone()]);
179                    let db = self.adapter.get_token_account_balance(&[destination.clone()]);
180                    let spb = self.adapter.get_token_account_pre_balance(&[source.clone()]);
181                    let dpb = self.adapter.get_token_account_pre_balance(&[destination.clone()]);
182                    (
183                        sb.into_iter().next().flatten(),
184                        db.into_iter().next().flatten(),
185                        spb.into_iter().next().flatten(),
186                        dpb.into_iter().next().flatten(),
187                    )
188                };
189                Some(TransferData {
190                    transfer_type: "transfer".to_string(),
191                    program_id: program_id.clone(),
192                    info: TransferInfoInner {
193                        authority: accounts.get(2).cloned(),
194                        destination,
195                        destination_owner: self.adapter.get_token_account_owner(&accounts[1]),
196                        mint: mint.clone(),
197                        source: source.clone(),
198                        token_amount: crate::types::TokenAmount {
199                            amount: amount.to_string(),
200                            ui_amount: Some(convert_to_ui_amount(amount, decimals)),
201                            decimals,
202                        },
203                        source_balance: sb,
204                        source_pre_balance: spb,
205                        destination_balance: db,
206                        destination_pre_balance: dpb,
207                    },
208                    idx: idx.to_string(),
209                    timestamp: self.adapter.block_time(),
210                    signature: self.adapter.signature(),
211                    is_fee: None,
212                })
213            }
214            (crate::constants::TOKEN_PROGRAM_ID, crate::constants::spl_token_instruction::TRANSFER_CHECKED)
215            | (crate::constants::TOKEN_2022_PROGRAM_ID, crate::constants::spl_token_instruction::TRANSFER_CHECKED) => {
216                if accounts.len() < 3 || data.len() < 10 {
217                    return None;
218                }
219                let amount = u64::from_le_bytes(data[1..9].try_into().ok()?);
220                let decimals = data[9];
221                let source = accounts[0].clone();
222                let mint = accounts[1].clone();
223                let destination = accounts[2].clone();
224                let (sb, db, spb, dpb) = {
225                    let sb = self.adapter.get_token_account_balance(&[source.clone()]);
226                    let db = self.adapter.get_token_account_balance(&[destination.clone()]);
227                    let spb = self.adapter.get_token_account_pre_balance(&[source.clone()]);
228                    let dpb = self.adapter.get_token_account_pre_balance(&[destination.clone()]);
229                    (
230                        sb.into_iter().next().flatten(),
231                        db.into_iter().next().flatten(),
232                        spb.into_iter().next().flatten(),
233                        dpb.into_iter().next().flatten(),
234                    )
235                };
236                Some(TransferData {
237                    transfer_type: "transferChecked".to_string(),
238                    program_id,
239                    info: TransferInfoInner {
240                        authority: accounts.get(3).cloned(),
241                        destination,
242                        destination_owner: self.adapter.get_token_account_owner(&accounts[2]),
243                        mint,
244                        source,
245                        token_amount: crate::types::TokenAmount {
246                            amount: amount.to_string(),
247                            ui_amount: Some(convert_to_ui_amount(amount, decimals)),
248                            decimals,
249                        },
250                        source_balance: sb,
251                        source_pre_balance: spb,
252                        destination_balance: db,
253                        destination_pre_balance: dpb,
254                    },
255                    idx: idx.to_string(),
256                    timestamp: self.adapter.block_time(),
257                    signature: self.adapter.signature(),
258                    is_fee: None,
259                })
260            }
261            _ => None,
262        }
263    }
264
265    /// Get transfers for a specific instruction (by program_id, outer_index, optional inner_index).
266    pub fn get_transfers_for_instruction(
267        transfer_actions: &HashMap<String, Vec<TransferData>>,
268        program_id: &str,
269        outer_index: usize,
270        inner_index: Option<usize>,
271    ) -> Vec<TransferData> {
272        let key = match inner_index {
273            Some(i) => format!("{}:{}-{}", program_id, outer_index, i),
274            None => format!("{}:{}", program_id, outer_index),
275        };
276        let transfers = transfer_actions.get(&key).cloned().unwrap_or_default();
277        transfers
278            .into_iter()
279            .filter(|t| matches!(t.transfer_type.as_str(), "transfer" | "transferChecked"))
280            .collect()
281    }
282
283    /// Build TradeInfo from transfer list (swap: 2+ tokens, determine in/out by signer).
284    pub fn process_swap_data(
285        &self,
286        transfers: &[TransferData],
287        dex_info: &DexInfo,
288        skip_native: bool,
289    ) -> Option<crate::types::TradeInfo> {
290        use crate::constants::tokens;
291        if transfers.len() < 2 {
292            return None;
293        }
294        let mut unique_mints: Vec<String> = Vec::new();
295        let mut seen = std::collections::HashSet::new();
296        for t in transfers {
297            if skip_native && t.info.mint == tokens::NATIVE {
298                continue;
299            }
300            if !seen.insert(t.info.mint.clone()) {
301                continue;
302            }
303            unique_mints.push(t.info.mint.clone());
304        }
305        if unique_mints.len() < 2 {
306            return None;
307        }
308        let signer = self.get_swap_signer();
309        let (input_mint, output_mint, input_raw, output_raw, fee) =
310            self.sum_token_amounts(transfers, &unique_mints[0], &unique_mints[unique_mints.len() - 1], &signer)?;
311        let in_dec = self.adapter.get_token_decimals(&input_mint);
312        let out_dec = self.adapter.get_token_decimals(&output_mint);
313        let trade_type = crate::utils::get_trade_type(&input_mint, &output_mint);
314        let mut trade = crate::types::TradeInfo {
315            user: signer.clone(),
316            trade_type,
317            pool: vec![],
318            input_token: crate::types::TokenInfo {
319                mint: input_mint.clone(),
320                amount: crate::utils::convert_to_ui_amount_u128(input_raw, in_dec),
321                amount_raw: input_raw.to_string(),
322                decimals: in_dec,
323                authority: None,
324                destination: None,
325                destination_owner: None,
326                source: None,
327            },
328            output_token: crate::types::TokenInfo {
329                mint: output_mint.clone(),
330                amount: crate::utils::convert_to_ui_amount_u128(output_raw, out_dec),
331                amount_raw: output_raw.to_string(),
332                decimals: out_dec,
333                authority: None,
334                destination: None,
335                destination_owner: None,
336                source: None,
337            },
338            slippage_bps: None,
339            fee: None,
340            fees: None,
341            program_id: dex_info.program_id.clone(),
342            amm: dex_info.amm.clone(),
343            amms: None,
344            route: dex_info.route.clone(),
345            slot: self.adapter.slot(),
346            timestamp: self.adapter.block_time(),
347            signature: self.adapter.signature(),
348            idx: transfers.first().map(|t| t.idx.clone()).unwrap_or_default(),
349            signer: Some(self.adapter.signers()),
350        };
351        if let Some(fee_transfer) = fee {
352            trade.fee = Some(crate::types::FeeInfo {
353                mint: fee_transfer.info.mint.clone(),
354                amount: fee_transfer.info.token_amount.ui_amount.unwrap_or(0.0),
355                amount_raw: fee_transfer.info.token_amount.amount.clone(),
356                decimals: fee_transfer.info.token_amount.decimals,
357                dex: None,
358                type_: None,
359                recipient: None,
360            });
361        }
362        Some(trade)
363    }
364
365    fn get_swap_signer(&self) -> String {
366        if self.adapter.account_keys.contains(&dex_programs::JUPITER_DCA.id.to_string()) {
367            self.adapter.get_account_key(2).unwrap_or_else(|| self.adapter.signer())
368        } else {
369            self.adapter.signer()
370        }
371    }
372
373    fn sum_token_amounts(
374        &self,
375        transfers: &[TransferData],
376        input_mint: &str,
377        output_mint: &str,
378        _signer: &str,
379    ) -> Option<(String, String, u128, u128, Option<TransferData>)> {
380        let mut input_raw: u128 = 0;
381        let mut output_raw: u128 = 0;
382        let mut fee_transfer: Option<TransferData> = None;
383        let mut seen = std::collections::HashSet::new();
384        for t in transfers {
385            let dest_owner = t.info.destination_owner.as_deref().unwrap_or("");
386            if FEE_ACCOUNTS.contains(&t.info.destination.as_str()) || FEE_ACCOUNTS.contains(&dest_owner) {
387                fee_transfer = Some(t.clone());
388                continue;
389            }
390            let key = format!("{}-{}", t.info.token_amount.amount, t.info.mint);
391            if !seen.insert(key) {
392                continue;
393            }
394            let amount: u128 = t.info.token_amount.amount.parse().unwrap_or(0);
395            if t.info.mint == input_mint {
396                input_raw += amount;
397            }
398            if t.info.mint == output_mint {
399                output_raw += amount;
400            }
401        }
402        Some((
403            input_mint.to_string(),
404            output_mint.to_string(),
405            input_raw,
406            output_raw,
407            fee_transfer,
408        ))
409    }
410}