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}