1use crate::error::{AlgodBuildError, Result};
2use crate::models::{
3 Account, Block, NodeStatus, PendingTransactions, Round, Supply, Transaction, TransactionFee,
4 TransactionID, TransactionList, TransactionParams, Version,
5};
6use crate::transaction::SignedTransaction;
7use crate::util::ApiToken;
8use reqwest::header::HeaderMap;
9use url::Url;
10
11const AUTH_HEADER: &str = "X-Algo-API-Token";
12
13pub struct Algod {
27 url: Option<Url>,
28 token: Option<ApiToken>,
29 headers: HeaderMap,
30}
31
32impl Algod {
33 pub fn new() -> Self {
35 Algod {
36 url: None,
37 token: None,
38 headers: HeaderMap::new(),
39 }
40 }
41
42 pub fn bind(mut self, url: &str) -> Result<Self> {
44 self.url = Some(Url::parse(url)?);
45 Ok(self)
46 }
47
48 pub fn auth(mut self, token: &str) -> Result<Self> {
50 self.token = Some(ApiToken::parse(token)?);
51 Ok(self)
52 }
53
54 pub fn client(self) -> Result<Client> {
56 match (self.url, self.token) {
57 (Some(url), Some(token)) => Ok(Client {
58 url: url.into_string(),
59 token: token.to_string(),
60 headers: self.headers,
61 }),
62 (None, Some(_)) => Err(AlgodBuildError::UnitializedUrl.into()),
63 (Some(_), None) => Err(AlgodBuildError::UnitializedToken.into()),
64 (None, None) => Err(AlgodBuildError::UnitializedUrl.into()),
65 }
66 }
67}
68
69pub struct Client {
71 url: String,
72 token: String,
73 headers: HeaderMap,
74}
75
76impl Client {
77 pub fn health(&self) -> Result<()> {
79 let _ = reqwest::Client::new()
80 .get(&format!("{}health", self.url))
81 .headers(self.headers.clone())
82 .send()?
83 .error_for_status()?;
84 Ok(())
85 }
86
87 pub fn versions(&self) -> Result<Version> {
89 let response = reqwest::Client::new()
90 .get(&format!("{}versions", self.url))
91 .headers(self.headers.clone())
92 .header(AUTH_HEADER, &self.token)
93 .send()?
94 .error_for_status()?
95 .json()?;
96 Ok(response)
97 }
98
99 pub fn status(&self) -> Result<NodeStatus> {
101 let response = reqwest::Client::new()
102 .get(&format!("{}v1/status", self.url))
103 .header(AUTH_HEADER, &self.token)
104 .headers(self.headers.clone())
105 .send()?
106 .error_for_status()?
107 .json()?;
108 Ok(response)
109 }
110
111 pub fn status_after_block(&self, round: Round) -> Result<NodeStatus> {
113 let response = reqwest::Client::new()
114 .get(&format!(
115 "{}v1/status/wait-for-block-after/{}",
116 self.url, round.0
117 ))
118 .header(AUTH_HEADER, &self.token)
119 .headers(self.headers.clone())
120 .send()?
121 .error_for_status()?
122 .json()?;
123 Ok(response)
124 }
125
126 pub fn block(&self, round: Round) -> Result<Block> {
128 let response = reqwest::Client::new()
129 .get(&format!("{}v1/block/{}", self.url, round.0))
130 .header(AUTH_HEADER, &self.token)
131 .headers(self.headers.clone())
132 .send()?
133 .error_for_status()?
134 .json()?;
135 Ok(response)
136 }
137
138 pub fn ledger_supply(&self) -> Result<Supply> {
140 let response = reqwest::Client::new()
141 .get(&format!("{}v1/ledger/supply", self.url))
142 .header(AUTH_HEADER, &self.token)
143 .headers(self.headers.clone())
144 .send()?
145 .error_for_status()?
146 .json()?;
147 Ok(response)
148 }
149
150 pub fn account_information(&self, address: &str) -> Result<Account> {
151 let response = reqwest::Client::new()
152 .get(&format!("{}v1/account/{}", self.url, address))
153 .header(AUTH_HEADER, &self.token)
154 .headers(self.headers.clone())
155 .send()?
156 .error_for_status()?
157 .json()?;
158 Ok(response)
159 }
160
161 pub fn pending_transactions(&self, limit: u64) -> Result<PendingTransactions> {
165 let response = reqwest::Client::new()
166 .get(&format!("{}v1/transactions/pending", self.url))
167 .header(AUTH_HEADER, &self.token)
168 .headers(self.headers.clone())
169 .query(&[("max", limit.to_string())])
170 .send()?
171 .error_for_status()?
172 .json()?;
173 Ok(response)
174 }
175
176 pub fn pending_transaction_information(&self, transaction_id: &str) -> Result<Transaction> {
178 let response = reqwest::Client::new()
179 .get(&format!(
180 "{}v1/transactions/pending/{}",
181 self.url, transaction_id
182 ))
183 .header(AUTH_HEADER, &self.token)
184 .headers(self.headers.clone())
185 .send()?
186 .error_for_status()?
187 .json()?;
188 Ok(response)
189 }
190
191 pub fn transactions(
193 &self,
194 address: &str,
195 first_round: Option<Round>,
196 last_round: Option<Round>,
197 from_date: Option<String>,
198 to_date: Option<String>,
199 limit: Option<u64>,
200 ) -> Result<TransactionList> {
201 let mut query = Vec::new();
202 if let Some(first_round) = first_round {
203 query.push(("firstRound", first_round.0.to_string()))
204 }
205 if let Some(last_round) = last_round {
206 query.push(("lastRound", last_round.0.to_string()))
207 }
208 if let Some(from_date) = from_date {
209 query.push(("fromDate", from_date))
210 }
211 if let Some(to_date) = to_date {
212 query.push(("toDate", to_date))
213 }
214 if let Some(limit) = limit {
215 query.push(("max", limit.to_string()))
216 }
217 let response = reqwest::Client::new()
218 .get(&format!("{}v1/account/{}/transactions", self.url, address))
219 .header(AUTH_HEADER, &self.token)
220 .headers(self.headers.clone())
221 .query(&query)
222 .send()?
223 .error_for_status()?
224 .json()?;
225 Ok(response)
226 }
227
228 pub fn send_transaction(
230 &self,
231 signed_transaction: &SignedTransaction,
232 ) -> Result<TransactionID> {
233 let bytes = rmp_serde::to_vec_named(signed_transaction)?;
234 self.raw_transaction(&bytes)
235 }
236
237 pub fn raw_transaction(&self, raw: &[u8]) -> Result<TransactionID> {
239 let response = reqwest::Client::new()
240 .post(&format!("{}v1/transactions", self.url))
241 .header(AUTH_HEADER, &self.token)
242 .headers(self.headers.clone())
243 .body(raw.to_vec())
244 .send()?
245 .error_for_status()?
246 .json()?;
247 Ok(response)
248 }
249
250 pub fn transaction(&self, transaction_id: &str) -> Result<Transaction> {
252 let response = reqwest::Client::new()
253 .get(&format!("{}v1/transaction/{}", self.url, transaction_id))
254 .header(AUTH_HEADER, &self.token)
255 .headers(self.headers.clone())
256 .send()?
257 .error_for_status()?
258 .json()?;
259 Ok(response)
260 }
261
262 pub fn transaction_information(
264 &self,
265 address: &str,
266 transaction_id: &str,
267 ) -> Result<Transaction> {
268 let response = reqwest::Client::new()
269 .get(&format!(
270 "{}/v1/account/{}/transaction/{}",
271 self.url, address, transaction_id
272 ))
273 .header(AUTH_HEADER, &self.token)
274 .headers(self.headers.clone())
275 .send()?
276 .error_for_status()?
277 .json()?;
278 Ok(response)
279 }
280
281 pub fn suggested_fee(&self) -> Result<TransactionFee> {
283 let response = reqwest::Client::new()
284 .get(&format!("{}/v1/transactions/fee", self.url))
285 .header(AUTH_HEADER, &self.token)
286 .headers(self.headers.clone())
287 .send()?
288 .error_for_status()?
289 .json()?;
290 Ok(response)
291 }
292
293 pub fn transaction_params(&self) -> Result<TransactionParams> {
295 let response = reqwest::Client::new()
296 .get(&format!("{}/v1/transactions/params", self.url))
297 .header(AUTH_HEADER, &self.token)
298 .headers(self.headers.clone())
299 .send()?
300 .error_for_status()?
301 .json()?;
302 Ok(response)
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_valid_client_builder() -> Result<()> {
312 let algod = Algod::new()
313 .bind("http://localhost:4001")?
314 .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")?
315 .client();
316
317 assert!(algod.ok().is_some());
318
319 Ok(())
320 }
321
322 #[test]
323 #[should_panic(expected = "UnitializedToken")]
324 fn test_client_builder_with_no_token() {
325 let _ = Algod::new()
326 .bind("http://localhost:4001")
327 .unwrap()
328 .client()
329 .unwrap();
330 }
331
332 #[test]
333 #[should_panic(expected = "RelativeUrlWithoutBase")]
334 fn test_client_builder_with_a_bad_url() {
335 let _ = Algod::new().bind("bad-url").unwrap();
336 }
337
338 #[test]
339 #[should_panic(expected = "UnitializedUrl")]
340 fn test_client_builder_with_no_url() {
341 let _ = Algod::new()
342 .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
343 .unwrap()
344 .client()
345 .unwrap();
346 }
347
348 #[test]
349 fn test_client_builder_with_a_token_too_short() -> Result<()> {
350 let algod = Algod::new().auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
351
352 assert!(algod.err().is_some());
353
354 Ok(())
355 }
356}