algorand_rs/
algod.rs

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
13/// Algod is the entry point to the creation of a cliend of the Algorand protocol daemon.
14/// ```
15/// use algorand_rs::algod::Algod;
16///
17/// fn main() -> Result<(), Box<dyn std::error::Error>> {
18///     let algod = Algod::new()
19///         .bind("http://localhost:4001")?
20///         .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")?
21///         .client()?;
22///
23///     Ok(())
24/// }
25/// ```
26pub struct Algod {
27    url: Option<Url>,
28    token: Option<ApiToken>,
29    headers: HeaderMap,
30}
31
32impl Algod {
33    /// Start the creation of a client.
34    pub fn new() -> Self {
35        Algod {
36            url: None,
37            token: None,
38            headers: HeaderMap::new(),
39        }
40    }
41
42    /// Bind to a URL.
43    pub fn bind(mut self, url: &str) -> Result<Self> {
44        self.url = Some(Url::parse(url)?);
45        Ok(self)
46    }
47
48    /// Use a token to authenticate.
49    pub fn auth(mut self, token: &str) -> Result<Self> {
50        self.token = Some(ApiToken::parse(token)?);
51        Ok(self)
52    }
53
54    /// Build a client for Algorand protocol daemon.
55    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
69/// Client for interacting with the Algorand protocol daemon.
70pub struct Client {
71    url: String,
72    token: String,
73    headers: HeaderMap,
74}
75
76impl Client {
77    /// Returns Ok if healthy
78    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    /// Retrieves the current version
88    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    /// Gets the current node status
100    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    /// Waits for a block to appear after the specified round and returns the node status at the time
112    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    /// Get the block for the given round
127    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    /// Gets the current supply reported by the ledger
139    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    /// Gets a list of unconfirmed transactions currently in the transaction pool
162    ///
163    /// Sorted by priority in decreasing order and truncated at the specified limit, or returns all if specified limit is 0
164    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    /// Get a specified pending transaction
177    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    /// Get a list of confirmed transactions, limited to filters if specified
192    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    /// Broadcasts a transaction to the network
229    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    /// Broadcasts a raw transaction to the network
238    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    /// Gets the information of a single transaction
251    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    /// Gets a specific confirmed transaction
263    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    /// Gets suggested fee in units of micro-Algos per byte
282    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    /// Gets parameters for constructing a new transaction
294    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}