postchain_client/transport/client.rs
1//! Client module for interacting with Postchain blockchain nodes via REST API.
2//!
3//! This module provides functionality for:
4//! - Querying blockchain nodes
5//! - Managing transactions
6//! - Handling REST API communication
7//! - Error handling
8
9extern crate serde_json;
10extern crate url;
11
12use reqwest::{header::CONTENT_TYPE, Client};
13use url::Url;
14
15use serde_json::Value;
16use std::{error::Error, time::Duration};
17
18use crate::utils::transaction::{Transaction, TransactionConfirmationProofData, TransactionStatus};
19
20/// A REST client for interacting with Postchain blockchain nodes.
21///
22/// This client handles communication with blockchain nodes, including:
23/// - Transaction submission and status checking
24/// - Node discovery and management
25/// - Query execution
26/// - Error handling
27#[derive(Debug)]
28pub struct RestClient<'a> {
29 /// List of node URLs to connect to
30 pub node_url: Vec<&'a str>,
31 /// Request timeout in seconds
32 pub request_time_out: u64,
33 /// Number of attempts to poll for transaction status
34 pub poll_attemps: u64,
35 /// Interval between poll attempts in seconds
36 pub poll_attemp_interval_time: u64
37}
38
39/// Response types that can be returned from REST API calls.
40#[derive(Debug)]
41pub enum RestResponse {
42 /// Plain text response
43 String(String),
44 /// JSON response
45 Json(Value),
46 /// Binary response
47 Bytes(Vec<u8>),
48}
49
50/// HTTP methods supported by the REST client.
51#[derive(PartialEq, Eq, Clone, Copy)]
52pub enum RestRequestMethod {
53 /// HTTP GET method
54 GET,
55 /// HTTP POST method
56 POST,
57}
58
59impl<'a> Default for RestClient<'a> {
60 fn default() -> Self {
61 RestClient {
62 node_url: vec!["http://localhost:7740"],
63 request_time_out: 30,
64 poll_attemps: 5,
65 poll_attemp_interval_time: 5
66 }
67 }
68}
69
70/// Types of errors that can occur during REST operations
71#[derive(Debug)]
72pub enum TypeError {
73 /// Error from the reqwest client
74 FromReqClient,
75 /// Error from the REST API
76 FromRestApi,
77}
78
79/// Error type for REST operations
80#[derive(Debug)]
81pub struct RestError {
82 /// HTTP status code if available
83 pub status_code: Option<String>,
84 /// Error message if available
85 pub error_str: Option<String>,
86 /// JSON error response if available
87 pub error_json: Option<Value>,
88 /// Type of error that occurred
89 pub type_error: TypeError,
90}
91
92impl Error for RestError {}
93
94impl Default for RestError {
95 fn default() -> Self {
96 RestError {
97 status_code: None,
98 error_str: None,
99 error_json: None,
100 type_error: TypeError::FromRestApi,
101 }
102 }
103}
104
105impl std::fmt::Display for RestError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 let mut hsc = "N/A".to_string();
108 let mut err_str = "N/A".to_string();
109
110 if let Some(val) = &self.status_code {
111 hsc = val.clone();
112 }
113
114 if let Some(val) = &self.error_str {
115 err_str = val.clone();
116 }
117
118 write!(f, "{:?} {} {}", self.type_error, hsc, err_str)
119 }
120}
121
122impl<'a> RestClient<'a> {
123 /// Retrieves a list of node URLs from the blockchain directory.
124 ///
125 /// # Arguments
126 /// * `brid` - Blockchain RID (Resource Identifier)
127 ///
128 /// # Returns
129 /// * `Result<Vec<String>, RestError>` - List of node URLs on success, or error on failure
130 ///
131 /// # Example
132 /// ```no_run
133 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
134 /// let client = RestClient::default();
135 /// let nodes = client.get_nodes_from_directory("blockchain_rid").await?;
136 /// # Ok(())
137 /// # }
138 /// ```
139 pub async fn get_nodes_from_directory(&self, brid: &str) -> Result<Vec<String>, RestError> {
140 let directory_brid = self.get_blockchain_rid(0).await?;
141
142 let path_segments = &["query", &directory_brid];
143 let query_params = vec![
144 ("type", "cm_get_blockchain_api_urls"),
145 ("blockchain_rid", brid),
146 ];
147 let query_body_json = None;
148 let query_body_raw = None;
149
150 let resp = self
151 .postchain_rest_api(
152 RestRequestMethod::GET,
153 Some(path_segments),
154 Some(&query_params),
155 query_body_json,
156 query_body_raw
157 )
158 .await;
159
160 match resp {
161 Ok(RestResponse::Json(json_val)) => {
162 let list_of_nodes = json_val
163 .as_array()
164 .unwrap()
165 .iter()
166 .filter_map(|value| value.as_str().map(String::from))
167 .collect();
168 Ok(list_of_nodes)
169 }
170 Ok(RestResponse::String(str_val)) => Ok(vec![str_val]),
171 Ok(_) => Ok(vec!["nop".to_string()]),
172 Err(error) => {
173 tracing::error!("Can't get API urls from DC chain: {} because of error: {:?}", brid, error);
174 Err(error)
175 }
176 }
177 }
178
179 /// Retrieves the blockchain RID for a given blockchain IID.
180 ///
181 /// # Arguments
182 /// * `blockchain_iid` - Blockchain Instance Identifier
183 ///
184 /// # Returns
185 /// * `Result<String, RestError>` - Blockchain RID on success, or error on failure
186 pub async fn get_blockchain_rid(&self, blockchain_iid: u8) -> Result<String, RestError> {
187 let resp: Result<RestResponse, RestError> = self
188 .postchain_rest_api(
189 RestRequestMethod::GET,
190 Some(&[&format!("/brid/iid_{blockchain_iid}")]),
191 None,
192 None,
193 None
194 )
195 .await;
196
197 if let Err(error) = resp {
198 tracing::error!("Can't get blockchain RID with IID = {} because of error: {:?}", blockchain_iid, error);
199 return Err(error);
200 }
201
202 let resp_val: RestResponse = resp.unwrap();
203
204 match resp_val {
205 RestResponse::String(val) => Ok(val.to_string()),
206 _ => Ok("".to_string()),
207 }
208 }
209
210 /// Prints error information and determines if the error should be ignored.
211 ///
212 /// # Arguments
213 /// * `error` - The REST error to print
214 /// * `ignore_all_errors` - Whether to ignore all errors
215 ///
216 /// # Returns
217 /// * `bool` - Whether the error should stop execution
218 pub fn print_error(&self, error: &RestError, ignore_all_errors: bool) -> bool {
219 println!(">> Error(s)");
220
221 if let Some(error_str) = &error.error_str {
222 println!("{error_str}");
223 } else {
224 let val = &error.error_json.as_ref().unwrap();
225 let pprint = serde_json::to_string_pretty(val).unwrap();
226 println!("{pprint}");
227 }
228
229 if ignore_all_errors {
230 println!("Allow ignore this error");
231 return false
232 }
233
234 true
235 }
236
237 /// Detects the Merkle hash version used by a blockchain.
238 ///
239 /// This function queries the blockchain's configuration to determine which version
240 /// of the Merkle hash algorithm is being used. If the query fails or the version
241 /// information is not available, it defaults to version 1.
242 ///
243 /// # Arguments
244 /// * `brid` - The blockchain RID (Resource Identifier) as a hex-encoded string
245 ///
246 /// # Returns
247 /// * `u8` - The Merkle hash version number (defaults to 1 if not specified)
248 ///
249 /// # Example
250 /// ```no_run
251 /// # use postchain_client::transport::RestClient;
252 /// # async fn example() {
253 /// let client = RestClient::default();
254 /// let brid = "DCE5D72ED7E1675291AFE7F9D649D898C8D3E7411E52882D03D1B3D240BDD91B";
255 /// let hash_version = client.detect_merkle_hash_version(brid).await;
256 /// println!("Blockchain uses Merkle hash version {}", hash_version);
257 /// # }
258 /// ```
259 pub async fn detect_merkle_hash_version(&self, brid: &str) -> u8 {
260 tracing::info!("Detecting merkle hash version of blockchain: {}", brid);
261
262 let mut merkle_hash_version = 1;
263
264 if let Ok(RestResponse::Json(json_val)) = self.postchain_rest_api(
265 RestRequestMethod::GET,
266 Some(&["config", brid, "features"]),
267 None,
268 None,
269 None
270 ).await {
271 if let Some(version) = json_val["merkle_hash_version"].as_u64() {
272 merkle_hash_version = version as u8;
273 tracing::info!("Found merkle hash version = {}", merkle_hash_version);
274 return merkle_hash_version;
275 }
276 }
277
278 tracing::warn!("Failed to detect merkle hash version, using default version = {}", merkle_hash_version);
279 merkle_hash_version
280 }
281
282 /// Updates the list of node URLs used by the client.
283 ///
284 /// # Arguments
285 /// * `node_urls` - New list of node URLs to use
286 pub fn update_node_urls(&mut self, node_urls: &'a [String]) {
287 self.node_url = node_urls.iter().map(String::as_str).collect();
288 }
289
290 // Transaction status
291 // GET /tx/{blockchain_rid}/{transaction_rid}/status
292 /// Gets the status of a transaction without polling.
293 ///
294 /// # Arguments
295 /// * `blockchain_rid` - Blockchain RID
296 /// * `tx_rid` - Transaction RID
297 ///
298 /// # Returns
299 /// * `Result<TransactionStatus, RestError>` - Transaction status or error
300 pub async fn get_transaction_status(&self, blockchain_rid: &str, tx_rid: &str) -> Result<TransactionStatus, RestError> {
301 self.get_transaction_status_with_poll(blockchain_rid, tx_rid, 0).await
302 }
303
304/// Fetches and parses transaction-related data from the Postchain node.
305 ///
306 /// This is a generic helper function to retrieve data associated with a transaction
307 /// (like confirmation proofs or raw transaction data) from a Postchain node.
308 /// It handles the common logic of making the REST API call, extracting a specific
309 /// string field from the JSON response, and then parsing that string using a provided
310 /// parsing function.
311 ///
312 /// # Type Parameters
313 ///
314 /// * `R`: The expected return type after parsing the extracted string (e.g., `Transaction` or `TransactionConfirmationProofData`).
315 /// * `F`: A closure type that takes a string slice (`&str`) and returns a `Result<R, String>`.
316 /// This closure encapsulates the specific parsing logic (e.g., `Transaction::from_raw_data` or `Transaction::confirmation_proof`).
317 ///
318 /// # Arguments
319 ///
320 /// * `blockchain_rid` - A string slice representing the Blockchain RID.
321 /// * `tx_rid` - A string slice representing the Transaction RID.
322 /// * `endpoint_suffix` - An optional string slice that will be appended to the base
323 /// transaction path (`/tx/{blockchain_rid}/{tx_rid}`). For example, use "confirmationProof"
324 /// to get the confirmation proof, or `None` to get the raw transaction data.
325 /// * `field_name` - The name of the JSON field to extract the data from (e.g., "proof" or "tx").
326 /// * `parser_fn` - A closure or function pointer that takes the extracted string slice
327 /// and attempts to parse it into the desired return type `R`.
328 ///
329 /// # Returns
330 ///
331 /// A `Result<R, RestError>`:
332 /// - `Ok(R)`: On successful retrieval and parsing of the data.
333 /// - `Err(RestError)`: If the request fails, the response is not JSON, the specified
334 /// `field_name` is missing or invalid, or the `parser_fn` returns an error.
335 ///
336 /// # Errors
337 ///
338 /// This function can return a `RestError` in the following cases:
339 /// - If the underlying `postchain_rest_api` call fails (e.g., network issues).
340 /// - If the response from the node is not a JSON object.
341 /// - If the JSON response does not contain the `field_name`, or if its value is not a string.
342 /// - If the `parser_fn` fails to parse the extracted string.
343 ///
344 /// # Example (Conceptual Usage within other methods)
345 ///
346 /// ```rust
347 /// # use postchain_client::transport::{RestClient, RestError};
348 /// # use postchain_client::utils::transaction::{Transaction, TransactionConfirmationProofData};
349 /// # async fn _example_usage(client: &RestClient<'_>, blockchain_rid: &str, tx_rid: &str) -> Result<(), RestError> {
350 /// // How `get_confirmation_proof` would now use this generic function:
351 /// let proof_data: TransactionConfirmationProofData = client.get_transaction_data(
352 /// blockchain_rid,
353 /// tx_rid,
354 /// Some("confirmationProof"),
355 /// "proof",
356 /// |s| Transaction::confirmation_proof(s),
357 /// ).await?;
358 ///
359 /// // How `get_raw_transaction_data` would now use this generic function:
360 /// let raw_tx_data: Transaction = client.get_transaction_data(
361 /// blockchain_rid,
362 /// tx_rid,
363 /// None, // No suffix for raw transaction data
364 /// "tx",
365 /// |s| Transaction::from_raw_data(s),
366 /// ).await?;
367 /// # Ok(())
368 /// # }
369 /// ```
370 async fn get_transaction_data<R, F>(&self, blockchain_rid: &str, tx_rid: &str, endpoint_suffix: Option<&str>, field_name: &str, parser_fn: F ) -> Result<R, RestError>
371 where
372 F: FnOnce(&str) -> Result<R, String>,
373 {
374 let mut path_segments = vec!["tx", blockchain_rid, tx_rid];
375 if let Some(suffix) = endpoint_suffix {
376 path_segments.push(suffix);
377 }
378
379 let resp = self
380 .postchain_rest_api(
381 RestRequestMethod::GET,
382 Some(path_segments.as_slice()),
383 None,
384 None,
385 None,
386 )
387 .await?;
388
389 match resp {
390 RestResponse::Json(json_val) => {
391 match json_val.get(field_name).and_then(|v| v.as_str()) {
392 Some(data_str) => {
393 parser_fn(data_str).map_err(|e| RestError {
394 error_str: Some(format!(
395 "Failed to parse '{field_name}' field: {e}"
396 )),
397 ..RestError::default()
398 })
399 }
400 None => Err(RestError {
401 error_str: Some(format!(
402 "Missing or invalid '{field_name}' field in response"
403 )),
404 ..RestError::default()
405 }),
406 }
407 }
408 _ => Err(RestError {
409 error_str: Some(format!("Expected JSON response with '{field_name}' field")),
410 ..RestError::default()
411 }),
412 }
413 }
414
415 /// Retrieves the confirmation proof for a given transaction.
416 ///
417 /// This function makes a GET request to the `/tx/{blockchain_rid}/{tx_rid}/confirmationProof`
418 /// endpoint of the Postchain node to fetch the cryptographic proof that a transaction
419 /// has been confirmed on the blockchain.
420 ///
421 /// # Arguments
422 /// * `blockchain_rid` - A string slice representing the Blockchain RID (Resource Identifier)
423 /// * `tx_rid` - A string slice representing the Transaction RID (Resource Identifier)
424 ///
425 /// # Returns
426 /// * `Result<TransactionConfirmationProofData, RestError>` - Returns `Ok(TransactionConfirmationProofData)`
427 /// on successful retrieval and parsing of the proof, or `Err(RestError)` if the request fails,
428 /// the response is not JSON, or the 'proof' field is missing/invalid.
429 ///
430 /// # Errors
431 /// This function can return a `RestError` in the following cases:
432 /// - If the underlying `postchain_rest_api` call fails (e.g., network issues, node unreachable).
433 /// - If the response from the node is not a JSON object.
434 /// - If the JSON response does not contain a "proof" field, or if the "proof" field is not a string.
435 /// - If the string value of the "proof" field cannot be successfully parsed into a `TransactionConfirmationProofData` struct.
436 ///
437 /// # Example
438 /// ```no_run
439 /// # use postchain_client::transport::RestClient;
440 /// # use postchain_client::utils::transaction::TransactionConfirmationProofData;
441 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
442 /// let client = RestClient::default();
443 /// let blockchain_rid = "your_blockchain_rid_hex_string"; // Replace with actual blockchain RID
444 /// let tx_rid = "your_transaction_rid_hex_string"; // Replace with actual transaction RID
445 ///
446 /// match client.get_confirmation_proof(blockchain_rid, tx_rid).await {
447 /// Ok(proof_data) => {
448 /// println!("Successfully retrieved confirmation proof:");
449 /// println!("Block height: {}", proof_data.block_height);
450 /// // Further processing of proof_data...
451 /// },
452 /// Err(e) => {
453 /// eprintln!("Failed to get confirmation proof: {}", e);
454 /// }
455 /// }
456 /// # Ok(())
457 /// # }
458 /// ```
459 pub async fn get_confirmation_proof(&self, blockchain_rid: &str, tx_rid: &str) -> Result<TransactionConfirmationProofData, RestError> {
460 self.get_transaction_data(
461 blockchain_rid,
462 tx_rid,
463 Some("confirmationProof"),
464 "proof",
465 Transaction::confirmation_proof,
466 ).await
467 }
468
469 /// Retrieves the raw transaction data for a given transaction.
470 ///
471 /// This function makes a GET request to the `/tx/{blockchain_rid}/{tx_rid}` endpoint
472 /// of the Postchain node to fetch the raw hexadecimal representation of a transaction.
473 ///
474 /// # Arguments
475 /// * `blockchain_rid` - A string slice representing the Blockchain RID.
476 /// * `tx_rid` - A string slice representing the Transaction RID.
477 ///
478 /// # Returns
479 /// * `Result<Transaction, RestError>` - Returns `Ok(Transaction)` on successful retrieval
480 /// and parsing of the raw transaction data, or `Err(RestError)` if the request fails,
481 /// the response is not JSON, or the 'tx' field is missing/invalid.
482 ///
483 /// # Errors
484 /// This function can return a `RestError` in the following cases:
485 /// - If the underlying `postchain_rest_api` call fails (e.g., network issues, node unreachable).
486 /// - If the response from the node is not a JSON object.
487 /// - If the JSON response does not contain a "tx" field, or if the "tx" field is not a string.
488 /// - If the string value of the "tx" field cannot be successfully parsed into a `Transaction` struct
489 /// by `Transaction::from_raw_data`.
490 ///
491 /// # Example
492 /// ```no_run
493 /// # use postchain_client::transport::RestClient;
494 /// # use postchain_client::utils::transaction::Transaction;
495 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
496 /// let client = RestClient::default();
497 /// let blockchain_rid = "your_blockchain_rid_hex_string"; // Replace with actual blockchain RID
498 /// let tx_rid = "your_transaction_rid_hex_string"; // Replace with actual transaction RID
499 ///
500 /// match client.get_raw_transaction_data(blockchain_rid, tx_rid).await {
501 /// Ok(transaction) => {
502 /// println!("Successfully retrieved raw transaction data: {:?}", transaction);
503 /// // Further processing of transaction object...
504 /// },
505 /// Err(e) => {
506 /// eprintln!("Failed to get raw transaction data: {}", e);
507 /// }
508 /// }
509 /// # Ok(())
510 /// # }
511 /// ```
512 pub async fn get_raw_transaction_data(&self, blockchain_rid: &str, tx_rid: &str) -> Result<Transaction, RestError>{
513 self.get_transaction_data(blockchain_rid, tx_rid, None, "tx", |s| {
514 Transaction::from_raw_data(s)
515 }).await
516 }
517
518 /// Gets the status of a transaction with polling for confirmation.
519 ///
520 /// # Arguments
521 /// * `blockchain_rid` - Blockchain RID
522 /// * `tx_rid` - Transaction RID
523 /// * `attempts` - Number of polling attempts made so far
524 ///
525 /// # Returns
526 /// * `Result<TransactionStatus, RestError>` - Transaction status or error
527 pub async fn get_transaction_status_with_poll(&self, blockchain_rid: &str, tx_rid: &str, attempts: u64) -> Result<TransactionStatus, RestError> {
528 tracing::info!("Waiting for transaction status of blockchain RID: {} with tx: {} | attempt: {}", blockchain_rid, tx_rid, attempts);
529
530 if attempts >= self.poll_attemps {
531 tracing::warn!("Transaction status still in waiting status after {} attempts", attempts);
532 return Ok(TransactionStatus::WAITING);
533 }
534
535 let resp = self.postchain_rest_api(RestRequestMethod::GET,
536 Some(&["tx", blockchain_rid, tx_rid, "status"]),
537 None,
538 None,
539 None).await?;
540 match resp {
541 RestResponse::Json(val) => {
542 let status: serde_json::Map<String, Value> = serde_json::from_value(val).unwrap();
543 if let Some(status_value) = status.get("status") {
544 let status_value = status_value.as_str();
545 match status_value {
546 Some("waiting") => {
547 // Waiting for transaction rejected or confirmed!!!
548 // Interval time = 5 secs on each attempt
549 // Break after 5 attempts
550 tokio::time::sleep(Duration::from_secs(self.poll_attemp_interval_time)).await;
551 return Box::pin(self.get_transaction_status_with_poll(blockchain_rid, tx_rid, attempts + 1)).await;
552 },
553 Some("confirmed") => {
554 tracing::info!("Transaction confirmed!");
555 return Ok(TransactionStatus::CONFIRMED)
556 },
557 Some("rejected") => {
558 tracing::warn!("Transaction rejected!");
559 return Ok(TransactionStatus::REJECTED)
560 },
561 _ => return Ok(TransactionStatus::UNKNOWN)
562 };
563 }
564 Ok(TransactionStatus::UNKNOWN)
565 }
566 _ => {
567 Ok(TransactionStatus::UNKNOWN)
568 }
569 }
570 }
571
572 // Submit transaction
573 // POST /tx/{blockchainRid}
574 /// Sends a transaction to the blockchain.
575 ///
576 /// # Arguments
577 /// * `tx` - Transaction to send
578 ///
579 /// # Returns
580 /// * `Result<RestResponse, RestError>` - Response from the blockchain or error
581 pub async fn send_transaction(&self, tx: &Transaction) -> Result<RestResponse, RestError> {
582 let txe = tx.gvt_hex_encoded();
583
584 let resq_body: serde_json::Map<String, Value> =
585 vec![("tx".to_string(), serde_json::json!(txe))]
586 .into_iter()
587 .collect();
588
589 let blockchain_rid = hex::encode(tx.blockchain_rid.clone()).as_str().to_owned();
590
591 tracing::info!("Sending transaction to {}", blockchain_rid);
592
593 self
594 .postchain_rest_api(
595 RestRequestMethod::POST,
596 Some(&["tx", &blockchain_rid]),
597 None,
598 Some(serde_json::json!(resq_body)),
599 None
600 )
601 .await
602 }
603
604 // Make a query with GTV encoded response
605 // POST /query_gtv/{blockchainRid}
606 /// Executes a query on the blockchain.
607 ///
608 /// # Arguments
609 /// * `brid` - Blockchain RID
610 /// * `query_prefix` - Optional prefix for the query endpoint
611 /// * `query_type` - Type of query to execute
612 /// * `query_params` - Optional query parameters
613 /// * `query_args` - Optional query arguments
614 ///
615 /// # Returns
616 /// * `Result<RestResponse, RestError>` - Query response or error
617 pub async fn query<T: AsRef<str>>(
618 &self,
619 brid: &str,
620 query_prefix: Option<&str>,
621 query_type: &'a str,
622 query_params: Option<&'a mut Vec<(&'a str, &'a str)>>,
623 query_args: Option<&'a mut Vec<(T, crate::utils::operation::Params)>>,
624 ) -> Result<RestResponse, RestError> {
625 let query_prefix_str = query_prefix.unwrap_or("query_gtv");
626
627 let mut query_args_converted: Option<Vec<(&str, crate::utils::operation::Params)>> = query_args.map(|args| {
628 args.iter()
629 .map(|(key, params)| (key.as_ref(), params.clone()))
630 .collect()
631 });
632
633 let encode_str = crate::encoding::gtv::encode(query_type, query_args_converted.as_mut());
634
635 tracing::info!("Querying {} to {}", query_type, brid);
636
637 self.postchain_rest_api(
638 RestRequestMethod::POST,
639 Some(&[query_prefix_str, brid]),
640 query_params.as_deref(),
641 None,
642 Some(encode_str)
643 ).await
644 }
645
646 /// Makes a REST API request to a Postchain node.
647 ///
648 /// # Arguments
649 /// * `method` - HTTP method to use
650 /// * `path_segments` - URL path segments
651 /// * `query_params` - Query parameters
652 /// * `query_body_json` - JSON request body
653 /// * `query_body_raw` - Raw request body
654 ///
655 /// # Returns
656 /// * `Result<RestResponse, RestError>` - API response or error
657 async fn postchain_rest_api(
658 &self,
659 method: RestRequestMethod,
660 path_segments: Option<&[&str]>,
661 query_params: Option<&'a Vec<(&'a str, &'a str)>>,
662 query_body_json: Option<Value>,
663 query_body_raw: Option<Vec<u8>>
664 ) -> Result<RestResponse, RestError> {
665 let mut node_index: usize = 0;
666 loop {
667 let result = self.postchain_rest_api_with_poll(method,
668 path_segments, query_params,
669 query_body_json.clone(), query_body_raw.clone(), node_index).await;
670
671 if let Err(ref error) = result {
672 node_index += 1;
673
674 if node_index >= self.node_url.len() || error.status_code.is_some() {
675 return result;
676 }
677 tracing::info!("The API endpoint can't be reached; will try another one!");
678 continue;
679 }
680 return result;
681 }
682 }
683
684 /// Makes a REST API request with retry logic for failed nodes.
685 ///
686 /// # Arguments
687 /// * `method` - HTTP method to use
688 /// * `path_segments` - URL path segments
689 /// * `query_params` - Query parameters
690 /// * `query_body_json` - JSON request body
691 /// * `query_body_raw` - Raw request body
692 /// * `node_index` - Index of the node to try
693 ///
694 /// # Returns
695 /// * `Result<RestResponse, RestError>` - API response or error
696 async fn postchain_rest_api_with_poll(
697 &self,
698 method: RestRequestMethod,
699 path_segments: Option<&[&str]>,
700 query_params: Option<&'a Vec<(&'a str, &'a str)>>,
701 query_body_json: Option<Value>,
702 query_body_raw: Option<Vec<u8>>,
703 node_index: usize,
704 ) -> Result<RestResponse, RestError> {
705
706 let mut url = Url::parse(self.node_url[node_index]).unwrap();
707
708 tracing::info!("Requesting on API endpoint: {}", url);
709
710 if let Some(ps) = path_segments {
711 if !ps.is_empty() {
712 let psj = ps.join("/");
713 url.set_path(&psj);
714 }
715 }
716
717 if let Some(qp) = query_params {
718 if !qp.is_empty() {
719 for (name, value) in qp {
720 url.query_pairs_mut().append_pair(name, value);
721 }
722 }
723 }
724
725 if method == RestRequestMethod::POST
726 && query_body_json.is_none()
727 && query_body_raw.is_none()
728 {
729 let error_str = "Error: POST request need a body [json or binary].".to_string();
730
731 tracing::error!(error_str);
732
733 return Err(RestError {
734 type_error: TypeError::FromRestApi,
735 error_str: Some(error_str),
736 status_code: None,
737 ..Default::default()
738 });
739 }
740
741 let rest_client = Client::new();
742
743 let req_result = match method {
744 RestRequestMethod::GET => {
745 rest_client
746 .get(url.clone())
747 .timeout(Duration::from_secs(self.request_time_out))
748 .send()
749 .await
750 }
751
752 RestRequestMethod::POST => {
753 if let Some(qb) = query_body_json {
754 rest_client
755 .post(url.clone())
756 .timeout(Duration::from_secs(self.request_time_out))
757 .json(&qb)
758 .send()
759 .await
760 } else {
761 let r_body = reqwest::Body::from(query_body_raw.unwrap());
762 rest_client
763 .post(url.clone())
764 .timeout(Duration::from_secs(self.request_time_out))
765 .body(r_body)
766 .send()
767 .await
768 }
769 }
770 };
771
772 let req_result_match = match req_result {
773 Ok(resp) => {
774 let http_status_code = resp.status().to_string();
775 let http_resp_header = resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap();
776 let json_resp = http_resp_header.contains("application/json");
777 let octet_stream_resp = http_resp_header.contains("application/octet-stream");
778
779 if http_status_code.starts_with('4') || http_status_code.starts_with('5') {
780 let mut err = RestError {
781 status_code: Some(http_status_code),
782 type_error: TypeError::FromRestApi,
783 ..Default::default()
784 };
785
786 if json_resp {
787 let error_json = resp.json().await.unwrap();
788 err.error_json = Some(error_json);
789 } else {
790 let error_str = resp.text().await.unwrap();
791 err.error_str = Some(error_str);
792 }
793
794 tracing::error!("{:?}", err);
795
796 return Err(err);
797 }
798
799 let rest_resp: RestResponse;
800
801 if json_resp {
802 let val = resp.json().await.unwrap();
803 rest_resp = RestResponse::Json(val);
804 } else if octet_stream_resp {
805 let bytes = resp.bytes().await.unwrap();
806 rest_resp = RestResponse::Bytes(bytes.to_vec());
807 } else {
808 let val = resp.text().await.unwrap();
809 rest_resp = RestResponse::String(val);
810 }
811
812 Ok(rest_resp)
813 }
814 Err(error) => {
815 let rest_error = RestError {
816 error_str: Some(error.to_string()),
817 type_error: TypeError::FromReqClient,
818 ..Default::default()};
819
820 tracing::error!("{:?}", rest_error);
821
822 Err(rest_error)
823 },
824 };
825
826 req_result_match
827 }
828}
829
830#[tokio::test]
831async fn client_detect_merkle_hash_version() {
832 let rc = RestClient{
833 node_url: vec!["https://node11.devnet1.chromia.dev:7740"],
834 ..Default::default()
835 };
836
837 let blockchain_rid = "DCE5D72ED7E1675291AFE7F9D649D898C8D3E7411E52882D03D1B3D240BDD91B";
838
839 let merkle_hash_version = rc.detect_merkle_hash_version(blockchain_rid).await;
840
841 assert_eq!(merkle_hash_version, 2);
842}