Skip to main content

solana_tx_parser/
dex_parser.rs

1//! Main DEX parser: trades, liquidity, transfers, meme events.
2
3use crate::constants::{dex_programs, get_program_name};
4use crate::instruction_classifier::InstructionClassifier;
5use crate::parsers::{
6    jupiter::JupiterParser,
7    meteora::MeteoraParser,
8    orca::OrcaParser,
9    pumpfun::PumpfunParser,
10    pumpswap::PumpswapParser,
11    raydium::RaydiumParser,
12};
13use crate::transaction_adapter::TransactionAdapter;
14use crate::transaction_utils::TransactionUtils;
15use crate::types::{ParseConfig, ParseResult, TokenAmount, TradeInfo, TransactionStatus};
16use crate::utils::get_final_swap;
17use std::collections::HashMap;
18
19pub struct DexParser;
20
21impl DexParser {
22    pub fn new() -> Self {
23        Self
24    }
25
26    pub fn parse_trades(
27        &self,
28        tx: &crate::types::SolanaTransactionInput,
29        config: Option<ParseConfig>,
30    ) -> Vec<TradeInfo> {
31        self.parse_with_classifier(tx, config.unwrap_or_default(), ParseType::Trades)
32            .trades
33    }
34
35    pub fn parse_all(
36        &self,
37        tx: &crate::types::SolanaTransactionInput,
38        config: Option<ParseConfig>,
39    ) -> ParseResult {
40        self.parse_with_classifier(tx, config.unwrap_or_default(), ParseType::All)
41    }
42
43    fn parse_with_classifier(
44        &self,
45        tx: &crate::types::SolanaTransactionInput,
46        config: ParseConfig,
47        parse_type: ParseType,
48    ) -> ParseResult {
49        let mut result = ParseResult {
50            state: true,
51            fee: TokenAmount {
52                amount: "0".to_string(),
53                ui_amount: Some(0.0),
54                decimals: 9,
55            },
56            aggregate_trade: None,
57            trades: Vec::new(),
58            liquidities: Vec::new(),
59            transfers: Vec::new(),
60            meme_events: Vec::new(),
61            slot: tx.slot,
62            timestamp: tx.block_time.unwrap_or(0),
63            signature: String::new(),
64            signer: Vec::new(),
65            compute_units: 0,
66            tx_status: TransactionStatus::Unknown,
67            msg: None,
68            sol_balance_change: None,
69            token_balance_change: None,
70        };
71
72        let adapter = TransactionAdapter::new(tx, Some(config.clone()));
73        result.signature = adapter.signature();
74        result.signer = adapter.signers();
75        result.timestamp = adapter.block_time();
76        result.compute_units = adapter.compute_units();
77        result.tx_status = adapter.tx_status();
78        result.fee = adapter.fee();
79
80        let classifier = InstructionClassifier::new(&adapter);
81        let utils = TransactionUtils::new(&adapter);
82        let dex_info = utils.get_dex_info(&classifier);
83        let all_program_ids = classifier.get_all_program_ids();
84
85        if let Some(ref filter_ids) = config.program_ids {
86            if !all_program_ids.iter().any(|id| filter_ids.contains(id)) {
87                result.state = false;
88                return result;
89            }
90        }
91
92        let transfer_actions = utils.get_transfer_actions(&[
93            "mintTo",
94            "burn",
95            "mintToChecked",
96            "burnChecked",
97        ]);
98
99        if parse_type == ParseType::Trades || parse_type == ParseType::All {
100            // Jupiter first
101            let jupiter_ids = [
102                dex_programs::JUPITER.id,
103                dex_programs::JUPITER_DCA.id,
104                dex_programs::JUPITER_VA.id,
105                dex_programs::JUPITER_LIMIT_ORDER_V2.id,
106            ];
107            if dex_info.program_id.as_ref().map(|p| jupiter_ids.contains(&p.as_str())) == Some(true) {
108                let program_id = dex_info.program_id.as_deref().unwrap_or("");
109                let instructions = classifier.get_instructions(program_id);
110                if program_id == dex_programs::JUPITER.id {
111                    let parser = JupiterParser::new(
112                        &adapter,
113                        crate::types::DexInfo {
114                            program_id: Some(program_id.to_string()),
115                            amm: Some(get_program_name(program_id).to_string()),
116                            route: dex_info.route.clone(),
117                        },
118                        transfer_actions.clone(),
119                        instructions,
120                    );
121                    let trades = parser.process_trades();
122                    if !trades.is_empty() {
123                        if config.aggregate_trades {
124                            result.aggregate_trade =
125                                get_final_swap(&trades, dex_info.amm.as_deref(), dex_info.route.as_deref());
126                        } else {
127                            result.trades = trades;
128                        }
129                        if !result.trades.is_empty() || result.aggregate_trade.is_some() {
130                            return result;
131                        }
132                    }
133                }
134            }
135
136            for program_id in &all_program_ids {
137                if config.program_ids.as_ref().map(|p| !p.contains(program_id)).unwrap_or(false) {
138                    continue;
139                }
140                if config.ignore_program_ids.as_ref().map(|p| p.contains(program_id)).unwrap_or(false) {
141                    continue;
142                }
143                let instructions = classifier.get_instructions(program_id);
144                let dex_info_here = crate::types::DexInfo {
145                    program_id: Some(program_id.clone()),
146                    amm: Some(get_program_name(program_id).to_string()),
147                    route: dex_info.route.clone(),
148                };
149                if program_id == dex_programs::JUPITER.id {
150                    let parser = JupiterParser::new(&adapter, dex_info_here.clone(), transfer_actions.clone(), instructions);
151                    result.trades.extend(parser.process_trades());
152                } else if program_id == dex_programs::RAYDIUM_V4.id
153                    || program_id == dex_programs::RAYDIUM_AMM.id
154                    || program_id == dex_programs::RAYDIUM_CPMM.id
155                    || program_id == dex_programs::RAYDIUM_CL.id
156                    || program_id == dex_programs::RAYDIUM_ROUTE.id
157                {
158                    let parser = RaydiumParser::new(&adapter, dex_info_here.clone(), transfer_actions.clone(), instructions);
159                    result.trades.extend(parser.process_trades());
160                } else if program_id == dex_programs::ORCA.id {
161                    let parser = OrcaParser::new(&adapter, dex_info_here.clone(), transfer_actions.clone(), instructions);
162                    result.trades.extend(parser.process_trades());
163                } else if program_id == dex_programs::METEORA.id
164                    || program_id == dex_programs::METEORA_DAMM.id
165                    || program_id == dex_programs::METEORA_DAMM_V2.id
166                {
167                    let parser = MeteoraParser::new(&adapter, dex_info_here.clone(), transfer_actions.clone(), instructions);
168                    result.trades.extend(parser.process_trades());
169                } else if program_id == dex_programs::PUMP_FUN.id {
170                    let parser = PumpfunParser::new(&adapter, dex_info_here.clone(), transfer_actions.clone(), instructions);
171                    result.trades.extend(parser.process_trades());
172                } else if program_id == dex_programs::PUMP_SWAP.id {
173                    let parser = PumpswapParser::new(&adapter, dex_info_here.clone(), transfer_actions.clone(), instructions);
174                    result.trades.extend(parser.process_trades());
175                }
176            }
177
178            if result.trades.len() > 1 {
179                let mut seen = HashMap::new();
180                result.trades.retain(|t| {
181                    let key = format!("{}-{}", t.idx, t.signature);
182                    if seen.insert(key.clone(), ()).is_some() {
183                        false
184                    } else {
185                        true
186                    }
187                });
188                if config.aggregate_trades {
189                    result.aggregate_trade =
190                        get_final_swap(&result.trades, dex_info.amm.as_deref(), dex_info.route.as_deref());
191                }
192            }
193        }
194
195        result.sol_balance_change = adapter
196            .get_account_sol_balance_changes(false)
197            .remove(&adapter.signer());
198        let token_changes = adapter.get_account_token_balance_changes(true);
199        result.token_balance_change = token_changes.get(&adapter.signer()).cloned();
200
201        result
202    }
203}
204
205impl Default for DexParser {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211#[derive(PartialEq, Eq)]
212enum ParseType {
213    Trades,
214    All,
215}