1use 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 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 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 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 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}