algosdk/
algod.rs

1use reqwest::header::HeaderMap;
2
3use crate::algod::models::{
4    Account, Block, NodeStatus, PendingTransactions, Supply, Transaction, TransactionFee,
5    TransactionID, TransactionList, TransactionParams, Version,
6};
7use crate::transaction::SignedTransaction;
8use crate::{Error, Round};
9
10const AUTH_HEADER: &str = "X-Algo-API-Token";
11
12/// Client for interacting with the Algorand protocol daemon
13pub struct AlgodClient {
14    url: String,
15    token: String,
16    headers: HeaderMap,
17}
18
19impl AlgodClient {
20    pub fn new(address: &str, token: &str) -> AlgodClient {
21        AlgodClient::new_with_headers(address, token, HeaderMap::new())
22    }
23
24    pub fn new_with_headers(address: &str, token: &str, headers: HeaderMap) -> AlgodClient {
25        AlgodClient {
26            url: address.to_string(),
27            token: token.to_string(),
28            headers,
29        }
30    }
31
32    /// Returns Ok if healthy
33    pub fn health(&self) -> Result<(), Error> {
34        let _ = reqwest::Client::new()
35            .get(&format!("{}/health", self.url))
36            .headers(self.headers.clone())
37            .send()?
38            .error_for_status()?;
39        Ok(())
40    }
41
42    /// Retrieves the current version
43    pub fn versions(&self) -> Result<Version, Error> {
44        let response = reqwest::Client::new()
45            .get(&format!("{}/versions", self.url))
46            .headers(self.headers.clone())
47            .header(AUTH_HEADER, &self.token)
48            .send()?
49            .error_for_status()?
50            .json()?;
51        Ok(response)
52    }
53
54    /// Gets the current node status
55    pub fn status(&self) -> Result<NodeStatus, Error> {
56        let response = reqwest::Client::new()
57            .get(&format!("{}/v1/status", self.url))
58            .header(AUTH_HEADER, &self.token)
59            .headers(self.headers.clone())
60            .send()?
61            .error_for_status()?
62            .json()?;
63        Ok(response)
64    }
65
66    /// Waits for a block to appear after the specified round and returns the node status at the time
67    pub fn status_after_block(&self, round: Round) -> Result<NodeStatus, Error> {
68        let response = reqwest::Client::new()
69            .get(&format!(
70                "{}/v1/status/wait-for-block-after/{}",
71                self.url, round.0
72            ))
73            .header(AUTH_HEADER, &self.token)
74            .headers(self.headers.clone())
75            .send()?
76            .error_for_status()?
77            .json()?;
78        Ok(response)
79    }
80
81    /// Get the block for the given round
82    pub fn block(&self, round: Round) -> Result<Block, Error> {
83        let response = reqwest::Client::new()
84            .get(&format!("{}/v1/block/{}", self.url, round.0))
85            .header(AUTH_HEADER, &self.token)
86            .headers(self.headers.clone())
87            .send()?
88            .error_for_status()?
89            .json()?;
90        Ok(response)
91    }
92
93    /// Gets the current supply reported by the ledger
94    pub fn ledger_supply(&self) -> Result<Supply, Error> {
95        let response = reqwest::Client::new()
96            .get(&format!("{}/v1/ledger/supply", self.url))
97            .header(AUTH_HEADER, &self.token)
98            .headers(self.headers.clone())
99            .send()?
100            .error_for_status()?
101            .json()?;
102        Ok(response)
103    }
104
105    pub fn account_information(&self, address: &str) -> Result<Account, Error> {
106        let response = reqwest::Client::new()
107            .get(&format!("{}/v1/account/{}", self.url, address))
108            .header(AUTH_HEADER, &self.token)
109            .headers(self.headers.clone())
110            .send()?
111            .error_for_status()?
112            .json()?;
113        Ok(response)
114    }
115
116    /// Gets a list of unconfirmed transactions currently in the transaction pool
117    ///
118    /// Sorted by priority in decreasing order and truncated at the specified limit, or returns all if specified limit is 0
119    pub fn pending_transactions(&self, limit: u64) -> Result<PendingTransactions, Error> {
120        let response = reqwest::Client::new()
121            .get(&format!("{}/v1/transactions/pending", self.url))
122            .header(AUTH_HEADER, &self.token)
123            .headers(self.headers.clone())
124            .query(&[("max", limit.to_string())])
125            .send()?
126            .error_for_status()?
127            .json()?;
128        Ok(response)
129    }
130
131    /// Get a specified pending transaction
132    pub fn pending_transaction_information(
133        &self,
134        transaction_id: &str,
135    ) -> Result<Transaction, Error> {
136        let response = reqwest::Client::new()
137            .get(&format!(
138                "{}/v1/transactions/pending/{}",
139                self.url, transaction_id
140            ))
141            .header(AUTH_HEADER, &self.token)
142            .headers(self.headers.clone())
143            .send()?
144            .error_for_status()?
145            .json()?;
146        Ok(response)
147    }
148
149    /// Get a list of confirmed transactions, limited to filters if specified
150    pub fn transactions(
151        &self,
152        address: &str,
153        first_round: Option<Round>,
154        last_round: Option<Round>,
155        from_date: Option<String>,
156        to_date: Option<String>,
157        limit: Option<u64>,
158    ) -> Result<TransactionList, Error> {
159        let mut query = Vec::new();
160        if let Some(first_round) = first_round {
161            query.push(("firstRound", first_round.0.to_string()))
162        }
163        if let Some(last_round) = last_round {
164            query.push(("lastRound", last_round.0.to_string()))
165        }
166        if let Some(from_date) = from_date {
167            query.push(("fromDate", from_date))
168        }
169        if let Some(to_date) = to_date {
170            query.push(("toDate", to_date))
171        }
172        if let Some(limit) = limit {
173            query.push(("max", limit.to_string()))
174        }
175        let response = reqwest::Client::new()
176            .get(&format!("{}/v1/account/{}/transactions", self.url, address))
177            .header(AUTH_HEADER, &self.token)
178            .headers(self.headers.clone())
179            .query(&query)
180            .send()?
181            .error_for_status()?
182            .json()?;
183        Ok(response)
184    }
185
186    /// Broadcasts a transaction to the network
187    pub fn send_transaction(
188        &self,
189        signed_transaction: &SignedTransaction,
190    ) -> Result<TransactionID, Error> {
191        let bytes = rmp_serde::to_vec_named(signed_transaction)?;
192        self.raw_transaction(&bytes)
193    }
194
195    /// Broadcasts a raw transaction to the network
196    pub fn raw_transaction(&self, raw: &[u8]) -> Result<TransactionID, Error> {
197        let response = reqwest::Client::new()
198            .post(&format!("{}/v1/transactions", self.url))
199            .header(AUTH_HEADER, &self.token)
200            .headers(self.headers.clone())
201            .body(raw.to_vec())
202            .send()?
203            .error_for_status()?
204            .json()?;
205        Ok(response)
206    }
207
208    /// Gets the information of a single transaction
209    pub fn transaction(&self, transaction_id: &str) -> Result<Transaction, Error> {
210        let response = reqwest::Client::new()
211            .get(&format!("{}/v1/transaction/{}", self.url, transaction_id))
212            .header(AUTH_HEADER, &self.token)
213            .headers(self.headers.clone())
214            .send()?
215            .error_for_status()?
216            .json()?;
217        Ok(response)
218    }
219
220    /// Gets a specific confirmed transaction
221    pub fn transaction_information(
222        &self,
223        address: &str,
224        transaction_id: &str,
225    ) -> Result<Transaction, Error> {
226        let response = reqwest::Client::new()
227            .get(&format!(
228                "{}/v1/account/{}/transaction/{}",
229                self.url, address, transaction_id
230            ))
231            .header(AUTH_HEADER, &self.token)
232            .headers(self.headers.clone())
233            .send()?
234            .error_for_status()?
235            .json()?;
236        Ok(response)
237    }
238
239    /// Gets suggested fee in units of micro-Algos per byte
240    pub fn suggested_fee(&self) -> Result<TransactionFee, Error> {
241        let response = reqwest::Client::new()
242            .get(&format!("{}/v1/transactions/fee", self.url))
243            .header(AUTH_HEADER, &self.token)
244            .headers(self.headers.clone())
245            .send()?
246            .error_for_status()?
247            .json()?;
248        Ok(response)
249    }
250
251    /// Gets parameters for constructing a new transaction
252    pub fn transaction_params(&self) -> Result<TransactionParams, Error> {
253        let response = reqwest::Client::new()
254            .get(&format!("{}/v1/transactions/params", self.url))
255            .header(AUTH_HEADER, &self.token)
256            .headers(self.headers.clone())
257            .send()?
258            .error_for_status()?
259            .json()?;
260        Ok(response)
261    }
262}
263
264pub mod models {
265    use serde::{Deserialize, Serialize};
266
267    use crate::util::deserialize_bytes;
268    use crate::util::deserialize_hash;
269    use crate::HashDigest;
270    use crate::MicroAlgos;
271    use crate::Round;
272
273    /// The information about a node status
274    #[derive(Debug, Serialize, Deserialize)]
275    pub struct NodeStatus {
276        /// the last round seen
277        #[serde(rename = "lastRound")]
278        pub last_round: Round,
279
280        /// The last consensus version supported
281        #[serde(rename = "lastConsensusVersion")]
282        pub last_version: String,
283
284        /// Next version of consensus protocol to use
285        #[serde(rename = "nextConsensusVersion")]
286        pub next_version: String,
287
288        /// The round at which the next consensus version will apply
289        #[serde(rename = "nextConsensusVersionRound")]
290        pub next_version_round: Round,
291
292        /// Whether the next consensus version is supported by this node
293        #[serde(rename = "nextConsensusVersionSupported")]
294        pub next_version_supported: bool,
295
296        /// Time since last round in nanoseconds
297        #[serde(rename = "timeSinceLastRound")]
298        pub time_since_last_round: i64,
299
300        // Catchup time in nanoseconds
301        #[serde(rename = "catchupTime")]
302        pub catchup_time: i64,
303    }
304
305    /// TransactionID Description
306    #[derive(Debug, Serialize, Deserialize)]
307    pub struct TransactionID {
308        /// The string encoding of the transaction hash
309        #[serde(rename = "txId")]
310        pub tx_id: String,
311    }
312
313    /// Account Description
314    #[derive(Debug, Serialize, Deserialize)]
315    pub struct Account {
316        /// The round for which this information is relevant
317        pub round: Round,
318
319        /// The account public key
320        pub address: String,
321
322        /// The total number of MicroAlgos in the account
323        pub amount: MicroAlgos,
324
325        /// the amount of MicroAlgos of pending rewards in this account.
326        #[serde(rename = "pendingrewards")]
327        pub pending_rewards: MicroAlgos,
328
329        /// the amount of MicroAlgos in the account, without the pending rewards.
330        #[serde(rename = "amountwithoutpendingrewards")]
331        pub amount_without_pending_rewards: u64,
332
333        /// Rewards indicates the total rewards of MicroAlgos the account has recieved
334        pub rewards: MicroAlgos,
335
336        /// Status indicates the delegation status of the account's MicroAlgos
337        /// Offline - indicates that the associated account is delegated.
338        /// Online  - indicates that the associated account used as part of the delegation pool.
339        /// NotParticipating - indicates that the associated account is neither a delegator nor a delegate.
340        pub status: String,
341    }
342
343    /// Transaction contains all fields common to all transactions and serves as an envelope to all transactions type
344    #[derive(Debug, Serialize, Deserialize)]
345    pub struct Transaction {
346        /// The transaction type
347        #[serde(rename = "type")]
348        pub txn_type: String,
349
350        /// The transaction ID
351        #[serde(rename = "tx")]
352        pub transaction_id: String,
353
354        /// The sender's address
355        pub from: String,
356
357        /// Fee is the transaction fee
358        pub fee: MicroAlgos,
359
360        /// The first valid round for this transaction
361        #[serde(rename = "first-round")]
362        pub first_round: Round,
363
364        /// The last valid round for this transaction
365        #[serde(rename = "last-round")]
366        pub last_round: Round,
367
368        /// Note is a free form data
369        #[serde(
370            rename = "noteb64",
371            default,
372            skip_serializing_if = "Vec::is_empty",
373            deserialize_with = "deserialize_bytes"
374        )]
375        pub note: Vec<u8>,
376
377        /// The block number this transaction appeared in
378        #[serde(skip_serializing_if = "Option::is_none")]
379        pub round: Option<u64>,
380
381        /// Indicates the transaction was evicted from this node's transaction
382        /// pool (if non-empty).  A non-empty pool_error does not guarantee that the
383        /// transaction will never be committed; other nodes may not have evicted the
384        /// transaction and may attempt to commit it in the future.
385        #[serde(
386            rename = "poolerror",
387            default,
388            skip_serializing_if = "String::is_empty"
389        )]
390        pub pool_error: String,
391
392        #[serde(default, skip_serializing_if = "Option::is_none")]
393        pub payment: Option<PaymentTransactionType>,
394
395        /// the amount of pending rewards applied to the from
396        /// account as part of this transaction.
397        #[serde(rename = "fromrewards", skip_serializing_if = "Option::is_none")]
398        pub from_rewards: Option<u64>,
399
400        #[serde(rename = "genesisID")]
401        pub genesis_id: String,
402
403        #[serde(rename = "genesishashb64", deserialize_with = "deserialize_hash")]
404        pub genesis_hash: HashDigest,
405    }
406
407    /// PaymentTransactionType contains the additional fields for a payment Transaction
408    #[derive(Debug, Serialize, Deserialize)]
409    pub struct PaymentTransactionType {
410        /// To is the receiver's address
411        pub to: String,
412
413        /// The address the sender closed to
414        #[serde(rename = "close", skip_serializing_if = "Option::is_none")]
415        pub close_remainder_to: Option<String>,
416
417        /// The amount sent to close_remainder_to, for committed transaction
418        #[serde(rename = "closeamount", skip_serializing_if = "Option::is_none")]
419        pub close_amount: Option<MicroAlgos>,
420
421        /// The amount of MicroAlgos intended to be transferred
422        pub amount: MicroAlgos,
423
424        /// The amount of pending rewards applied to the To account
425        /// as part of this transaction.
426        #[serde(rename = "torewards", skip_serializing_if = "Option::is_none")]
427        pub to_rewards: Option<u64>,
428
429        /// The amount of pending rewards applied to the CloseRemainderTo
430        /// account as part of this transaction.
431        #[serde(rename = "closerewards", skip_serializing_if = "Option::is_none")]
432        pub close_rewards: Option<u64>,
433    }
434
435    /// TransactionList contains a list of transactions
436    #[derive(Debug, Serialize, Deserialize)]
437    pub struct TransactionList {
438        #[serde(default, skip_serializing_if = "Vec::is_empty")]
439        pub transactions: Vec<Transaction>,
440    }
441
442    /// TransactionFee contains the suggested fee
443    #[derive(Debug, Serialize, Deserialize)]
444    pub struct TransactionFee {
445        /// Transaction fee in units of micro-Algos per byte.
446        /// Fee may fall to zero but transactions must still have a fee of
447        /// at least MinTxnFee for the current network protocol.
448        pub fee: MicroAlgos,
449    }
450
451    /// TransactionParams contains the parameters that help a client construct a new transaction.
452    #[derive(Debug, Serialize, Deserialize)]
453    pub struct TransactionParams {
454        /// Transaction fee in units of micro-Algos per byte.
455        /// Fee may fall to zero but transactions must still have a fee of
456        /// at least MinTxnFee for the current network protocol.
457        pub fee: MicroAlgos,
458
459        /// Genesis ID
460        #[serde(rename = "genesisID")]
461        pub genesis_id: String,
462
463        /// Genesis hash
464        #[serde(rename = "genesishashb64", deserialize_with = "deserialize_hash")]
465        pub genesis_hash: HashDigest,
466
467        // The last round seen
468        #[serde(rename = "lastRound")]
469        pub last_round: Round,
470
471        // The consensus protocol version as of last_round.
472        #[serde(rename = "consensusVersion")]
473        pub consensus_version: String,
474    }
475
476    /// Block contains a block information
477    #[derive(Debug, Serialize, Deserialize)]
478    pub struct Block {
479        /// The current block hash
480        pub hash: String,
481
482        /// The previous block hash
483        #[serde(rename = "previousBlockHash")]
484        pub previous_block_hash: String,
485
486        /// The sortition seed
487        pub seed: String,
488
489        /// The address of this block proposer
490        pub proposer: String,
491
492        /// The current round on which this block was appended to the chain
493        pub round: Round,
494
495        /// Period is the period on which the block was confirmed
496        pub period: u64,
497
498        /// TransactionsRoot authenticates the set of transactions appearing in the block.
499        /// More specifically, it's the root of a merkle tree whose leaves are the block's Txids, in lexicographic order.
500        /// For the empty block, it's 0.
501        /// Note that the TxnRoot does not authenticate the signatures on the transactions, only the transactions themselves.
502        /// Two blocks with the same transactions but in a different order and with different signatures will have the same TxnRoot.
503        #[serde(rename = "txnRoot")]
504        pub transactions_root: String,
505
506        /// Specifies how many rewards, in MicroAlgos,
507        /// have been distributed to each config.Protocol.RewardUnit
508        /// of MicroAlgos since genesis.
509        #[serde(rename = "reward", default, skip_serializing_if = "Option::is_none")]
510        pub rewards_level: Option<MicroAlgos>,
511
512        /// The number of new MicroAlgos added to the participation stake from rewards at the next round.
513        #[serde(rename = "rate", default, skip_serializing_if = "Option::is_none")]
514        pub rewards_rate: Option<MicroAlgos>,
515
516        /// The number of leftover MicroAlgos after the distribution of RewardsRate/rewardUnits
517        /// MicroAlgos for every reward unit in the next round.
518        #[serde(rename = "frac", default, skip_serializing_if = "Option::is_none")]
519        pub rewards_residue: Option<MicroAlgos>,
520
521        /// The list of transactions in this block
522        #[serde(rename = "txns", default, skip_serializing_if = "Option::is_none")]
523        pub transactions: Option<TransactionList>,
524
525        /// TimeStamp in seconds since epoch
526        pub timestamp: i64,
527        #[serde(flatten)]
528        pub upgrade_state: UpgradeState,
529        #[serde(flatten)]
530        pub upgrade_vote: UpgradeVote,
531    }
532
533    /// UpgradeState contains the information about a current state of an upgrade
534    #[derive(Debug, Serialize, Deserialize)]
535    pub struct UpgradeState {
536        /// A string that represents the current protocol
537        #[serde(rename = "currentProtocol")]
538        current_protocol: String,
539
540        /// A string that represents the next proposed protocol
541        #[serde(rename = "nextProtocol")]
542        next_protocol: String,
543
544        /// The number of blocks which approved the protocol upgrade
545        #[serde(rename = "nextProtocolApprovals")]
546        next_protocol_approvals: u64,
547
548        /// The deadline round for this protocol upgrade (No votes will be consider after this round)
549        #[serde(rename = "nextProtocolVoteBefore")]
550        next_protocol_vote_before: Round,
551
552        /// The round on which the protocol upgrade will take effect
553        #[serde(rename = "nextProtocolSwitchOn")]
554        next_protocol_switch_on: Round,
555    }
556
557    /// UpgradeVote represents the vote of the block proposer with respect to protocol upgrades.
558    #[derive(Debug, Serialize, Deserialize)]
559    pub struct UpgradeVote {
560        /// Indicates a proposed upgrade
561        #[serde(rename = "upgradePropose")]
562        upgrade_propose: String,
563
564        /// Indicates a yes vote for the current proposal
565        #[serde(rename = "upgradeApprove")]
566        upgrade_approve: bool,
567    }
568
569    /// Supply represents the current supply of MicroAlgos in the system
570    #[derive(Debug, Serialize, Deserialize)]
571    pub struct Supply {
572        round: Round,
573        #[serde(rename = "totalMoney")]
574        total_money: MicroAlgos,
575        #[serde(rename = "onlineMoney")]
576        online_money: MicroAlgos,
577    }
578
579    /// PendingTransactions represents a potentially truncated list of transactions currently in the
580    /// node's transaction pool.
581    #[derive(Debug, Serialize, Deserialize)]
582    pub struct PendingTransactions {
583        #[serde(rename = "truncatedTxns")]
584        truncated_txns: TransactionList,
585        #[serde(rename = "totalTxns")]
586        total_txns: u64,
587    }
588
589    /// Version contains the current algod version.
590    #[derive(Debug, Serialize, Deserialize)]
591    pub struct Version {
592        #[serde(default, skip_serializing_if = "Vec::is_empty")]
593        pub versions: Vec<String>,
594        pub genesis_id: String,
595        #[serde(rename = "genesis_hash_b64", deserialize_with = "deserialize_hash")]
596        pub genesis_hash: HashDigest,
597    }
598}