1use crate::constants::{
4 spl_token_instruction, tokens, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
5};
6use crate::types::{
7 BalanceChange, ParseConfig, PoolEventType, TokenAmount, TokenInfo, TransactionStatus,
8 InnerInstructionSet, RawInstruction, SolanaTransactionInput, TokenBalanceInput,
9};
10use crate::utils::{convert_to_ui_amount, get_transfer_token_mint};
11use std::collections::HashMap;
12
13pub struct TransactionAdapter<'a> {
14 tx: &'a SolanaTransactionInput,
15 pub config: Option<ParseConfig>,
16 pub account_keys: Vec<String>,
17 pub spl_token_map: HashMap<String, TokenInfo>,
18 pub spl_decimals_map: HashMap<String, u8>,
19}
20
21impl<'a> TransactionAdapter<'a> {
22 pub fn new(tx: &'a SolanaTransactionInput, config: Option<ParseConfig>) -> Self {
23 let account_keys = Self::extract_account_keys(tx);
24 let mut adapter = Self {
25 tx,
26 config,
27 account_keys: account_keys.clone(),
28 spl_token_map: HashMap::new(),
29 spl_decimals_map: HashMap::new(),
30 };
31 adapter.extract_token_info();
32 adapter
33 }
34
35 fn extract_account_keys(tx: &SolanaTransactionInput) -> Vec<String> {
36 let mut keys = tx.account_keys.clone();
37 if let Some(ref meta) = tx.meta {
38 if let Some(ref loaded) = meta.loaded_addresses {
39 keys.extend(loaded.writable.clone());
40 keys.extend(loaded.readonly.clone());
41 }
42 }
43 keys
44 }
45
46 pub fn slot(&self) -> u64 {
47 self.tx.slot
48 }
49
50 pub fn block_time(&self) -> i64 {
51 self.tx.block_time.unwrap_or(0)
52 }
53
54 pub fn signature(&self) -> String {
55 if let Some(sig) = self.tx.signatures.first() {
56 bs58::encode(sig).into_string()
57 } else {
58 String::new()
59 }
60 }
61
62 pub fn signer(&self) -> String {
63 self.get_account_key(0).unwrap_or_else(|| String::new())
64 }
65
66 pub fn signers(&self) -> Vec<String> {
67 let n = 1.min(self.account_keys.len());
68 self.account_keys.iter().take(n).cloned().collect()
69 }
70
71 pub fn fee(&self) -> TokenAmount {
72 let fee = self.tx.meta.as_ref().and_then(|m| m.fee).unwrap_or(0);
73 TokenAmount {
74 amount: fee.to_string(),
75 ui_amount: Some(convert_to_ui_amount(fee, 9)),
76 decimals: 9,
77 }
78 }
79
80 pub fn compute_units(&self) -> u64 {
81 self.tx
82 .meta
83 .as_ref()
84 .and_then(|m| m.compute_units_consumed)
85 .unwrap_or(0)
86 }
87
88 pub fn tx_status(&self) -> TransactionStatus {
89 match &self.tx.meta {
90 None => TransactionStatus::Unknown,
91 Some(m) => {
92 if m.err.is_none() {
93 TransactionStatus::Success
94 } else {
95 TransactionStatus::Failed
96 }
97 }
98 }
99 }
100
101 pub fn instructions(&self) -> Vec<ParsedInstructionRef> {
102 self.tx
103 .instructions
104 .iter()
105 .enumerate()
106 .map(|(i, raw)| self.raw_to_parsed(raw, i))
107 .collect()
108 }
109
110 pub fn inner_instructions(&self) -> Option<&[InnerInstructionSet]> {
111 self.tx.inner_instructions.as_deref()
112 }
113
114 pub fn raw_instructions(&self) -> &[RawInstruction] {
115 &self.tx.instructions
116 }
117
118 pub fn raw_inner_instructions(&self) -> Option<&[InnerInstructionSet]> {
119 self.tx.inner_instructions.as_deref()
120 }
121
122 fn raw_to_parsed(&self, raw: &RawInstruction, outer_index: usize) -> ParsedInstructionRef {
123 let program_id = self
124 .get_account_key(raw.program_id_index as usize)
125 .unwrap_or_default();
126 let accounts: Vec<String> = raw
127 .account_key_indexes
128 .iter()
129 .filter_map(|&i| self.get_account_key(i as usize))
130 .collect();
131 ParsedInstructionRef {
132 program_id,
133 accounts,
134 data: raw.data.clone(),
135 outer_index,
136 }
137 }
138
139 pub fn get_instruction(
140 &self,
141 raw: &RawInstruction,
142 program_id_index: u8,
143 ) -> crate::types::ParsedInstruction {
144 let program_id = self
145 .get_account_key(program_id_index as usize)
146 .unwrap_or_default();
147 let accounts: Vec<String> = raw
148 .account_key_indexes
149 .iter()
150 .filter_map(|&i| self.get_account_key(i as usize))
151 .collect();
152 crate::types::ParsedInstruction {
153 program_id,
154 accounts,
155 data: raw.data.clone(),
156 parsed: None,
157 }
158 }
159
160 pub fn get_instruction_program_id(&self, raw: &RawInstruction) -> String {
161 self.get_account_key(raw.program_id_index as usize)
162 .unwrap_or_default()
163 }
164
165 pub fn get_account_key(&self, index: usize) -> Option<String> {
166 self.account_keys.get(index).cloned()
167 }
168
169 pub fn get_account_index(&self, address: &str) -> Option<usize> {
170 self.account_keys.iter().position(|k| k == address)
171 }
172
173 pub fn is_supported_token(&self, mint: &str) -> bool {
174 matches!(
175 mint,
176 tokens::SOL
177 | tokens::USDC
178 | tokens::USDT
179 | tokens::USD1
180 | tokens::USDG
181 | tokens::PYUSD
182 | tokens::EURC
183 | tokens::USDY
184 | tokens::FDUSD
185 )
186 }
187
188 pub fn get_token_decimals(&self, mint: &str) -> u8 {
189 *self.spl_decimals_map.get(mint).unwrap_or(&0)
190 }
191
192 pub fn get_pool_event_base(
193 &self,
194 pool_event_type: PoolEventType,
195 program_id: &str,
196 ) -> PoolEventBase {
197 PoolEventBase {
198 user: self.signer(),
199 pool_event_type,
200 program_id: program_id.to_string(),
201 amm: crate::constants::get_program_name(program_id).to_string(),
202 slot: self.slot(),
203 timestamp: self.block_time(),
204 signature: self.signature(),
205 }
206 }
207
208 pub fn pre_balances(&self) -> Option<&[u64]> {
209 self.tx.meta.as_ref()?.pre_balances.as_deref()
210 }
211
212 pub fn post_balances(&self) -> Option<&[u64]> {
213 self.tx.meta.as_ref()?.post_balances.as_deref()
214 }
215
216 pub fn pre_token_balances(&self) -> Option<&[TokenBalanceInput]> {
217 self.tx.meta.as_ref()?.pre_token_balances.as_deref()
218 }
219
220 pub fn post_token_balances(&self) -> Option<&[TokenBalanceInput]> {
221 self.tx.meta.as_ref()?.post_token_balances.as_deref()
222 }
223
224 pub fn get_token_account_owner(&self, account_key: &str) -> Option<String> {
225 let post = self.post_token_balances()?;
226 let index = self.get_account_index(account_key)?;
227 post.iter()
228 .find(|b| b.account_index == index as u32)
229 .and_then(|b| b.owner.clone())
230 }
231
232 pub fn get_token_account_balance(&self, account_keys: &[String]) -> Vec<Option<TokenAmount>> {
233 account_keys
234 .iter()
235 .map(|key| {
236 if key.is_empty() {
237 return None;
238 }
239 let post = self.post_token_balances()?;
240 let index = self.get_account_index(key)?;
241 let bal = post.iter().find(|b| b.account_index == index as u32)?;
242 Some(TokenAmount {
243 amount: bal.ui_token_amount.amount.clone(),
244 ui_amount: bal.ui_token_amount.ui_amount,
245 decimals: bal.ui_token_amount.decimals,
246 })
247 })
248 .collect()
249 }
250
251 pub fn get_token_account_pre_balance(
252 &self,
253 account_keys: &[String],
254 ) -> Vec<Option<TokenAmount>> {
255 account_keys
256 .iter()
257 .map(|key| {
258 if key.is_empty() {
259 return None;
260 }
261 let pre = self.pre_token_balances()?;
262 let index = self.get_account_index(key)?;
263 let bal = pre.iter().find(|b| b.account_index == index as u32)?;
264 Some(TokenAmount {
265 amount: bal.ui_token_amount.amount.clone(),
266 ui_amount: bal.ui_token_amount.ui_amount,
267 decimals: bal.ui_token_amount.decimals,
268 })
269 })
270 .collect()
271 }
272
273 pub fn get_account_balance(&self, account_keys: &[String]) -> Vec<Option<TokenAmount>> {
274 account_keys
275 .iter()
276 .map(|key| {
277 if key.is_empty() {
278 return None;
279 }
280 let index = self.get_account_index(key)?;
281 let post = self.post_balances()?;
282 let amount = *post.get(index)?;
283 Some(TokenAmount {
284 amount: amount.to_string(),
285 ui_amount: Some(convert_to_ui_amount(amount, 9)),
286 decimals: 9,
287 })
288 })
289 .collect()
290 }
291
292 pub fn get_account_pre_balance(&self, account_keys: &[String]) -> Vec<Option<TokenAmount>> {
293 account_keys
294 .iter()
295 .map(|key| {
296 if key.is_empty() {
297 return None;
298 }
299 let index = self.get_account_index(key)?;
300 let pre = self.pre_balances()?;
301 let amount = *pre.get(index)?;
302 Some(TokenAmount {
303 amount: amount.to_string(),
304 ui_amount: Some(convert_to_ui_amount(amount, 9)),
305 decimals: 9,
306 })
307 })
308 .collect()
309 }
310
311 fn extract_token_info(&mut self) {
312 self.extract_token_balances();
313 self.extract_token_from_instructions();
314 if !self.spl_token_map.contains_key(tokens::SOL) {
315 self.spl_token_map.insert(
316 tokens::SOL.to_string(),
317 TokenInfo {
318 mint: tokens::SOL.to_string(),
319 amount: 0.0,
320 amount_raw: "0".to_string(),
321 decimals: 9,
322 authority: None,
323 destination: None,
324 destination_owner: None,
325 source: None,
326 },
327 );
328 }
329 self.spl_decimals_map.insert(tokens::SOL.to_string(), 9);
330 }
331
332 fn extract_token_balances(&mut self) {
333 let post: Vec<_> = match self.post_token_balances() {
334 Some(p) => p.to_vec(),
335 None => return,
336 };
337 for balance in post {
338 let mint = match &balance.mint {
339 Some(m) => m.clone(),
340 None => continue,
341 };
342 let account_key = self
343 .get_account_key(balance.account_index as usize)
344 .unwrap_or_default();
345 if !self.spl_token_map.contains_key(&account_key) {
346 self.spl_token_map.insert(
347 account_key,
348 TokenInfo {
349 mint: mint.clone(),
350 amount: balance.ui_token_amount.ui_amount.unwrap_or(0.0),
351 amount_raw: balance.ui_token_amount.amount.clone(),
352 decimals: balance.ui_token_amount.decimals,
353 authority: None,
354 destination: None,
355 destination_owner: balance.owner.clone(),
356 source: None,
357 },
358 );
359 }
360 self.spl_decimals_map
361 .insert(mint, balance.ui_token_amount.decimals);
362 }
363 }
364
365 fn extract_token_from_instructions(&mut self) {
366 for raw in &self.tx.instructions {
367 self.extract_from_compiled_transfer(raw);
368 }
369 if let Some(inner) = &self.tx.inner_instructions {
370 for set in inner {
371 for raw in &set.instructions {
372 self.extract_from_compiled_transfer(raw);
373 }
374 }
375 }
376 }
377
378 fn extract_from_compiled_transfer(&mut self, ix: &RawInstruction) {
379 let data = &ix.data;
380 if data.is_empty() {
381 return;
382 }
383 let program_id = self
384 .get_account_key(ix.program_id_index as usize)
385 .unwrap_or_default();
386 if program_id != TOKEN_PROGRAM_ID && program_id != TOKEN_2022_PROGRAM_ID {
387 return;
388 }
389 let accounts: Vec<String> = ix
390 .account_key_indexes
391 .iter()
392 .filter_map(|&i| self.get_account_key(i as usize))
393 .collect();
394 let (source, destination, mint, decimals) = match data[0] {
395 spl_token_instruction::TRANSFER => {
396 if accounts.len() < 2 {
397 return;
398 }
399 let source = accounts[0].clone();
400 let dest = accounts[1].clone();
401 let token1 = self.spl_token_map.get(&dest).map(|t| t.mint.clone());
402 let token2 = self.spl_token_map.get(&source).map(|t| t.mint.clone());
403 let mint =
404 get_transfer_token_mint(token1.as_deref(), token2.as_deref());
405 (Some(source), Some(dest), mint, None)
406 }
407 spl_token_instruction::TRANSFER_CHECKED => {
408 if accounts.len() < 3 {
409 return;
410 }
411 let dec = if data.len() >= 10 { Some(data[9]) } else { None };
412 (
413 Some(accounts[0].clone()),
414 Some(accounts[2].clone()),
415 Some(accounts[1].clone()),
416 dec,
417 )
418 }
419 spl_token_instruction::MINT_TO | spl_token_instruction::MINT_TO_CHECKED => {
420 if accounts.len() < 2 {
421 return;
422 }
423 let dec = if data.len() >= 10 { Some(data[9]) } else { None };
424 (
425 None,
426 Some(accounts[1].clone()),
427 Some(accounts[0].clone()),
428 dec,
429 )
430 }
431 spl_token_instruction::BURN | spl_token_instruction::BURN_CHECKED => {
432 if accounts.len() < 2 {
433 return;
434 }
435 let dec = if data.len() >= 10 { Some(data[9]) } else { None };
436 (
437 Some(accounts[0].clone()),
438 None,
439 Some(accounts[1].clone()),
440 dec,
441 )
442 }
443 _ => return,
444 };
445 if let Some(m) = &mint {
446 if let Some(d) = decimals {
447 self.spl_decimals_map.insert(m.clone(), d);
448 }
449 }
450 for acc in [source, destination].into_iter().flatten() {
451 if !self.spl_token_map.contains_key(&acc) {
452 self.spl_token_map.insert(
453 acc,
454 TokenInfo {
455 mint: mint.clone().unwrap_or_else(|| tokens::SOL.to_string()),
456 amount: 0.0,
457 amount_raw: "0".to_string(),
458 decimals: decimals.unwrap_or(9),
459 authority: None,
460 destination: None,
461 destination_owner: None,
462 source: None,
463 },
464 );
465 }
466 }
467 }
468
469 pub fn get_account_sol_balance_changes(
470 &self,
471 _is_owner: bool,
472 ) -> HashMap<String, BalanceChange> {
473 let mut changes = HashMap::new();
474 let pre = self.pre_balances().unwrap_or(&[]);
475 let post = self.post_balances().unwrap_or(&[]);
476 for (index, key) in self.account_keys.iter().enumerate() {
477 let pre_bal = pre.get(index).copied().unwrap_or(0);
478 let post_bal = post.get(index).copied().unwrap_or(0);
479 let change = post_bal as i64 - pre_bal as i64;
480 if change != 0 {
481 changes.insert(
482 key.clone(),
483 BalanceChange {
484 pre: TokenAmount {
485 amount: pre_bal.to_string(),
486 ui_amount: Some(convert_to_ui_amount(pre_bal, 9)),
487 decimals: 9,
488 },
489 post: TokenAmount {
490 amount: post_bal.to_string(),
491 ui_amount: Some(convert_to_ui_amount(post_bal, 9)),
492 decimals: 9,
493 },
494 change: TokenAmount {
495 amount: change.abs().to_string(),
496 ui_amount: Some(convert_to_ui_amount(change.unsigned_abs(), 9)),
497 decimals: 9,
498 },
499 },
500 );
501 }
502 }
503 changes
504 }
505
506 pub fn get_account_token_balance_changes(
507 &self,
508 _is_owner: bool,
509 ) -> HashMap<String, HashMap<String, BalanceChange>> {
510 let mut changes: HashMap<String, HashMap<String, BalanceChange>> = HashMap::new();
511 let pre = self.pre_token_balances().unwrap_or(&[]);
512 let post = self.post_token_balances().unwrap_or(&[]);
513 for balance in pre {
514 let key = self
515 .get_account_key(balance.account_index as usize)
516 .unwrap_or_default();
517 let mint = balance.mint.clone().unwrap_or_default();
518 if mint.is_empty() {
519 continue;
520 }
521 changes.entry(key).or_default().insert(
522 mint.clone(),
523 BalanceChange {
524 pre: TokenAmount {
525 amount: balance.ui_token_amount.amount.clone(),
526 ui_amount: balance.ui_token_amount.ui_amount,
527 decimals: balance.ui_token_amount.decimals,
528 },
529 post: TokenAmount {
530 amount: "0".to_string(),
531 ui_amount: Some(0.0),
532 decimals: balance.ui_token_amount.decimals,
533 },
534 change: TokenAmount {
535 amount: "0".to_string(),
536 ui_amount: Some(0.0),
537 decimals: balance.ui_token_amount.decimals,
538 },
539 },
540 );
541 }
542 for balance in post {
543 let key = self
544 .get_account_key(balance.account_index as usize)
545 .unwrap_or_default();
546 let mint = balance.mint.clone().unwrap_or_default();
547 if mint.is_empty() {
548 continue;
549 }
550 let entry = changes.entry(key).or_default();
551 if let Some(existing) = entry.get_mut(&mint) {
552 existing.post = TokenAmount {
553 amount: balance.ui_token_amount.amount.clone(),
554 ui_amount: balance.ui_token_amount.ui_amount,
555 decimals: balance.ui_token_amount.decimals,
556 };
557 let pre_amount: u128 = existing.pre.amount.parse().unwrap_or(0);
558 let post_amount: u128 = balance.ui_token_amount.amount.parse().unwrap_or(0);
559 let change_amount = post_amount as i128 - pre_amount as i128;
560 existing.change = TokenAmount {
561 amount: change_amount.abs().to_string(),
562 ui_amount: Some(
563 (balance.ui_token_amount.ui_amount.unwrap_or(0.0))
564 - (existing.pre.ui_amount.unwrap_or(0.0)),
565 ),
566 decimals: balance.ui_token_amount.decimals,
567 };
568 if change_amount == 0 {
569 entry.remove(&mint);
570 }
571 } else {
572 entry.insert(
573 mint,
574 BalanceChange {
575 pre: TokenAmount {
576 amount: "0".to_string(),
577 ui_amount: Some(0.0),
578 decimals: balance.ui_token_amount.decimals,
579 },
580 post: TokenAmount {
581 amount: balance.ui_token_amount.amount.clone(),
582 ui_amount: balance.ui_token_amount.ui_amount,
583 decimals: balance.ui_token_amount.decimals,
584 },
585 change: TokenAmount {
586 amount: balance.ui_token_amount.amount.clone(),
587 ui_amount: balance.ui_token_amount.ui_amount,
588 decimals: balance.ui_token_amount.decimals,
589 },
590 },
591 );
592 }
593 }
594 changes
595 }
596}
597
598pub struct ParsedInstructionRef {
599 pub program_id: String,
600 pub accounts: Vec<String>,
601 pub data: Vec<u8>,
602 pub outer_index: usize,
603}
604
605pub struct PoolEventBase {
606 pub user: String,
607 pub pool_event_type: PoolEventType,
608 pub program_id: String,
609 pub amm: String,
610 pub slot: u64,
611 pub timestamp: i64,
612 pub signature: String,
613}