Skip to main content

so_defi_solanafm/
lib.rs

1//! basic api bindings for solscan
2pub mod types;
3
4use std::str::FromStr;
5
6use solana_program::pubkey::Pubkey;
7pub use types::*;
8
9use anyhow::{anyhow, Result};
10
11#[derive(Clone)]
12pub struct Client {
13    c: reqwest::blocking::Client,
14    version: API,
15}
16
17#[derive(Clone, Copy)]
18pub enum API {
19    V3,
20}
21
22impl API {
23    pub fn get_transaction_url(&self, tx_hash: &str, friendly: bool) -> String {
24        match self {
25            API::V3 => {
26                // https://hyper.solana.fm
27                // /
28                // v3/transfers
29                // /4Nc7GGX1....
30                // ?friendly=true
31                format!(
32                    "{}/{}/{}?friendly={}",
33                    BASE_URL, V3_TRANSFER_STUB, tx_hash, friendly
34                )
35            }
36        }
37    }
38}
39
40/*
41curl -X 'GET' \
42  'https://public-api.solscan.io/transaction/4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9' \
43  -H 'accept: application/json'
44*/
45
46impl Client {
47    pub fn new() -> Self {
48        Default::default()
49    }
50    /// returns all transfers within the given transaction
51    pub fn get_transfers(
52        &self,
53        tx_hash: &str,
54        friendly: bool,
55        filter_tx_fees: bool,
56    ) -> Result<Vec<TransferEntry>> {
57        let client = self
58            .c
59            .get(self.version.get_transaction_url(tx_hash, friendly))
60            .header("accept", "application/json");
61        let response = client.send()?;
62
63        let mut response: serde_json::Value = serde_json::from_str(&response.text()?)?;
64        match response.get_mut("Transfers") {
65            Some(i) => match i.get_mut(tx_hash) {
66                Some(i) => {
67                    if let Some(i_array) = i.as_array() {
68                        let transfer_entries = i_array
69                            .into_iter()
70                            .filter_map(|i| {
71                                match serde_json::from_value::<_TransferEntry>(i.clone()) {
72                                    Ok(entry) => Some(entry),
73                                    Err(err) => {
74                                        println!("failed to deserialize {:#?}", err);
75                                        None
76                                    }
77                                }
78                            })
79                            .filter_map(|transfer| {
80                                let action: Action =
81                                    if let Ok(action) = TryFrom::try_from(&transfer) {
82                                        action
83                                    } else {
84                                        return None;
85                                    };
86
87                                let source = if let Ok(key) = Pubkey::from_str(&transfer.source) {
88                                    key
89                                } else {
90                                    return None;
91                                };
92                                if action.eq(&Action::PayTxFees) && filter_tx_fees {
93                                    return None;
94                                } else if action.eq(&Action::PayTxFees) && !filter_tx_fees {
95                                    return Some(TransferEntry {
96                                        action,
97                                        source,
98                                        ..Default::default()
99                                    });
100                                } else {
101                                    Some(TransferEntry {
102                                        action,
103                                        source,
104                                        source_association: if let Ok(key) =
105                                            Pubkey::from_str(&transfer.source_association)
106                                        {
107                                            Some(key)
108                                        } else {
109                                            return None;
110                                        },
111                                        destination: if let Ok(key) =
112                                            Pubkey::from_str(&transfer.source)
113                                        {
114                                            Some(key)
115                                        } else {
116                                            return None;
117                                        },
118                                        destination_association: if let Ok(key) =
119                                            Pubkey::from_str(&transfer.source)
120                                        {
121                                            Some(key)
122                                        } else {
123                                            return None;
124                                        },
125                                    })
126                                }
127                            })
128                            .collect::<Vec<_>>();
129                        if transfer_entries.is_empty() {
130                            return Err(anyhow!("found no transfer entries"));
131                        }
132
133                        return Ok(transfer_entries);
134                    } else {
135                        return Err(anyhow!("failed to get transfers array"));
136                    }
137                }
138                None => return Err(anyhow!("failed to get transfers by tx hash")),
139            },
140            None => return Err(anyhow!("failed to get Transfer key")),
141        }
142    }
143}
144
145impl Default for Client {
146    fn default() -> Self {
147        Self {
148            c: reqwest::blocking::Client::new(),
149            version: API::V3,
150        }
151    }
152}
153
154#[cfg(test)]
155mod test {
156    use super::*;
157    #[test]
158    fn test_client() {
159        let client = Client::default();
160        let transfers = client.get_transfers("4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9", true, false).unwrap();
161        assert_eq!(transfers.len(), 8);
162        let transfers = client.get_transfers("4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9", true, true).unwrap();
163        assert_eq!(transfers.len(), 7);
164
165        let token_transfers = TokenTransferEntries::from(transfers);
166
167        let fee_tokens = token_transfers.filter_by_destination(
168            Pubkey::from_str("HgX4fugaghRMRNtyBdBygsWSFoREphwiSSTaDQSYns7Z").unwrap(),
169        );
170        assert_eq!(fee_tokens.len(), 3);
171    }
172    #[test]
173    fn test_api() {
174        let api = API::V3;
175        let url = api.get_transaction_url("4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9", true);
176        assert_eq!(url, "https://hyper.solana.fm/v3/transfers/4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9?friendly=true");
177        let url = api.get_transaction_url("4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9", false);
178        assert_eq!(url, "https://hyper.solana.fm/v3/transfers/4Nc7GGX139JFvt4TkccxzMtBRNDU4y4xAzExPcVncPBq1fMmgNW9xwHq5avLYDkT1r1zLbW2o1GFWmmn2UTwce9?friendly=false");
179    }
180}