Skip to main content

solana_tx_parser/
transaction_adapter.rs

1//! Adapter for unified transaction data access.
2
3use 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}