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
12pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[derive(Debug, Serialize, Deserialize)]
275 pub struct NodeStatus {
276 #[serde(rename = "lastRound")]
278 pub last_round: Round,
279
280 #[serde(rename = "lastConsensusVersion")]
282 pub last_version: String,
283
284 #[serde(rename = "nextConsensusVersion")]
286 pub next_version: String,
287
288 #[serde(rename = "nextConsensusVersionRound")]
290 pub next_version_round: Round,
291
292 #[serde(rename = "nextConsensusVersionSupported")]
294 pub next_version_supported: bool,
295
296 #[serde(rename = "timeSinceLastRound")]
298 pub time_since_last_round: i64,
299
300 #[serde(rename = "catchupTime")]
302 pub catchup_time: i64,
303 }
304
305 #[derive(Debug, Serialize, Deserialize)]
307 pub struct TransactionID {
308 #[serde(rename = "txId")]
310 pub tx_id: String,
311 }
312
313 #[derive(Debug, Serialize, Deserialize)]
315 pub struct Account {
316 pub round: Round,
318
319 pub address: String,
321
322 pub amount: MicroAlgos,
324
325 #[serde(rename = "pendingrewards")]
327 pub pending_rewards: MicroAlgos,
328
329 #[serde(rename = "amountwithoutpendingrewards")]
331 pub amount_without_pending_rewards: u64,
332
333 pub rewards: MicroAlgos,
335
336 pub status: String,
341 }
342
343 #[derive(Debug, Serialize, Deserialize)]
345 pub struct Transaction {
346 #[serde(rename = "type")]
348 pub txn_type: String,
349
350 #[serde(rename = "tx")]
352 pub transaction_id: String,
353
354 pub from: String,
356
357 pub fee: MicroAlgos,
359
360 #[serde(rename = "first-round")]
362 pub first_round: Round,
363
364 #[serde(rename = "last-round")]
366 pub last_round: Round,
367
368 #[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 #[serde(skip_serializing_if = "Option::is_none")]
379 pub round: Option<u64>,
380
381 #[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 #[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 #[derive(Debug, Serialize, Deserialize)]
409 pub struct PaymentTransactionType {
410 pub to: String,
412
413 #[serde(rename = "close", skip_serializing_if = "Option::is_none")]
415 pub close_remainder_to: Option<String>,
416
417 #[serde(rename = "closeamount", skip_serializing_if = "Option::is_none")]
419 pub close_amount: Option<MicroAlgos>,
420
421 pub amount: MicroAlgos,
423
424 #[serde(rename = "torewards", skip_serializing_if = "Option::is_none")]
427 pub to_rewards: Option<u64>,
428
429 #[serde(rename = "closerewards", skip_serializing_if = "Option::is_none")]
432 pub close_rewards: Option<u64>,
433 }
434
435 #[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 #[derive(Debug, Serialize, Deserialize)]
444 pub struct TransactionFee {
445 pub fee: MicroAlgos,
449 }
450
451 #[derive(Debug, Serialize, Deserialize)]
453 pub struct TransactionParams {
454 pub fee: MicroAlgos,
458
459 #[serde(rename = "genesisID")]
461 pub genesis_id: String,
462
463 #[serde(rename = "genesishashb64", deserialize_with = "deserialize_hash")]
465 pub genesis_hash: HashDigest,
466
467 #[serde(rename = "lastRound")]
469 pub last_round: Round,
470
471 #[serde(rename = "consensusVersion")]
473 pub consensus_version: String,
474 }
475
476 #[derive(Debug, Serialize, Deserialize)]
478 pub struct Block {
479 pub hash: String,
481
482 #[serde(rename = "previousBlockHash")]
484 pub previous_block_hash: String,
485
486 pub seed: String,
488
489 pub proposer: String,
491
492 pub round: Round,
494
495 pub period: u64,
497
498 #[serde(rename = "txnRoot")]
504 pub transactions_root: String,
505
506 #[serde(rename = "reward", default, skip_serializing_if = "Option::is_none")]
510 pub rewards_level: Option<MicroAlgos>,
511
512 #[serde(rename = "rate", default, skip_serializing_if = "Option::is_none")]
514 pub rewards_rate: Option<MicroAlgos>,
515
516 #[serde(rename = "frac", default, skip_serializing_if = "Option::is_none")]
519 pub rewards_residue: Option<MicroAlgos>,
520
521 #[serde(rename = "txns", default, skip_serializing_if = "Option::is_none")]
523 pub transactions: Option<TransactionList>,
524
525 pub timestamp: i64,
527 #[serde(flatten)]
528 pub upgrade_state: UpgradeState,
529 #[serde(flatten)]
530 pub upgrade_vote: UpgradeVote,
531 }
532
533 #[derive(Debug, Serialize, Deserialize)]
535 pub struct UpgradeState {
536 #[serde(rename = "currentProtocol")]
538 current_protocol: String,
539
540 #[serde(rename = "nextProtocol")]
542 next_protocol: String,
543
544 #[serde(rename = "nextProtocolApprovals")]
546 next_protocol_approvals: u64,
547
548 #[serde(rename = "nextProtocolVoteBefore")]
550 next_protocol_vote_before: Round,
551
552 #[serde(rename = "nextProtocolSwitchOn")]
554 next_protocol_switch_on: Round,
555 }
556
557 #[derive(Debug, Serialize, Deserialize)]
559 pub struct UpgradeVote {
560 #[serde(rename = "upgradePropose")]
562 upgrade_propose: String,
563
564 #[serde(rename = "upgradeApprove")]
566 upgrade_approve: bool,
567 }
568
569 #[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 #[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 #[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}