1pub 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 format!(
32 "{}/{}/{}?friendly={}",
33 BASE_URL, V3_TRANSFER_STUB, tx_hash, friendly
34 )
35 }
36 }
37 }
38}
39
40impl Client {
47 pub fn new() -> Self {
48 Default::default()
49 }
50 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}