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, 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        return 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        return 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 mut 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(&mut 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".to_string(), brid, &"features".to_string()]),
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 Vec<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    /// Gets the status of a transaction with polling for confirmation.
305    ///
306    /// # Arguments
307    /// * `blockchain_rid` - Blockchain RID
308    /// * `tx_rid` - Transaction RID
309    /// * `attempts` - Number of polling attempts made so far
310    ///
311    /// # Returns
312    /// * `Result<TransactionStatus, RestError>` - Transaction status or error
313    pub async fn get_transaction_status_with_poll(&self, blockchain_rid: &str, tx_rid: &str, attempts: u64) -> Result<TransactionStatus, RestError> {
314        tracing::info!("Waiting for transaction status of blockchain RID: {} with tx: {} | attempt: {}", blockchain_rid, tx_rid, attempts);
315
316        if attempts >= self.poll_attemps {
317            tracing::warn!("Transaction status still in waiting status after {} attempts", attempts);
318            return Ok(TransactionStatus::WAITING);
319        }
320
321        let resp = self.postchain_rest_api(RestRequestMethod::GET,
322            Some(&["tx", blockchain_rid, tx_rid, "status"]),
323            None,
324            None,
325            None).await?;
326        match resp {
327            RestResponse::Json(val) => {
328                let status: serde_json::Map<String, Value> = serde_json::from_value(val).unwrap();
329                if let Some(status_value) = status.get("status") {
330                    let status_value = status_value.as_str();
331                    match status_value {
332                        Some("waiting") => {
333                            // Waiting for transaction rejected or confirmed!!!
334                            // Interval time = 5 secs on each attempt
335                            // Break after 5 attempts
336                            tokio::time::sleep(Duration::from_secs(self.poll_attemp_interval_time)).await;
337                            return Box::pin(self.get_transaction_status_with_poll(blockchain_rid, tx_rid, attempts + 1)).await;
338                        },
339                        Some("confirmed") => {
340                            tracing::info!("Transaction confirmed!");
341                            return Ok(TransactionStatus::CONFIRMED)
342                        },
343                        Some("rejected") => {
344                            tracing::warn!("Transaction rejected!");
345                            return Ok(TransactionStatus::REJECTED)
346                        },
347                        _ => return Ok(TransactionStatus::UNKNOWN)
348                    };
349                }
350                Ok(TransactionStatus::UNKNOWN)
351            }
352            _ => {
353                Ok(TransactionStatus::UNKNOWN)
354            }
355        }
356    }
357
358    // Submit transaction
359    // POST /tx/{blockchainRid}
360    /// Sends a transaction to the blockchain.
361    ///
362    /// # Arguments
363    /// * `tx` - Transaction to send
364    ///
365    /// # Returns
366    /// * `Result<RestResponse, RestError>` - Response from the blockchain or error
367    pub async fn send_transaction(&self, tx: &Transaction<'a>) -> Result<RestResponse, RestError> {
368        let txe = tx.gvt_hex_encoded();
369
370        let resq_body: serde_json::Map<String, Value> =
371            vec![("tx".to_string(), serde_json::json!(txe))]
372                .into_iter()
373                .collect();
374
375        let blockchain_rid = hex::encode(tx.blockchain_rid.clone()).as_str().to_owned();
376
377        tracing::info!("Sending transaction to {}", blockchain_rid); 
378
379        self
380            .postchain_rest_api(
381                RestRequestMethod::POST,
382                Some(&["tx", &blockchain_rid]),
383                None,
384                Some(serde_json::json!(resq_body)),
385                None
386            )
387            .await
388    }
389
390    // Make a query with GTV encoded response
391    // POST /query_gtv/{blockchainRid}
392    /// Executes a query on the blockchain.
393    ///
394    /// # Arguments
395    /// * `brid` - Blockchain RID
396    /// * `query_prefix` - Optional prefix for the query endpoint
397    /// * `query_type` - Type of query to execute
398    /// * `query_params` - Optional query parameters
399    /// * `query_args` - Optional query arguments
400    ///
401    /// # Returns
402    /// * `Result<RestResponse, RestError>` - Query response or error
403    pub async fn query<T: AsRef<str>>(
404        &self,
405        brid: &str,
406        query_prefix: Option<&str>,
407        query_type: &'a str,
408        query_params: Option<&'a mut Vec<(&'a str, &'a str)>>,
409        query_args: Option<&'a mut Vec<(T, crate::utils::operation::Params)>>,
410    ) -> Result<RestResponse, RestError> {
411        let query_prefix_str = query_prefix.unwrap_or("query_gtv");
412
413        let mut query_args_converted: Option<Vec<(&str, crate::utils::operation::Params)>> = query_args.map(|args| {
414            args.iter()
415                .map(|(key, params)| (key.as_ref(), params.clone()))
416                .collect()
417        });
418
419        let encode_str = crate::encoding::gtv::encode(query_type, query_args_converted.as_mut().map(|v| v.as_mut()));      
420        
421        tracing::info!("Querying {} to {}", query_type, brid); 
422
423        self.postchain_rest_api(
424            RestRequestMethod::POST,
425            Some(&[query_prefix_str, brid]),
426            query_params.as_deref(),
427            None,
428            Some(encode_str)
429        ).await
430    }
431
432    /// Makes a REST API request to a Postchain node.
433    ///
434    /// # Arguments
435    /// * `method` - HTTP method to use
436    /// * `path_segments` - URL path segments
437    /// * `query_params` - Query parameters
438    /// * `query_body_json` - JSON request body
439    /// * `query_body_raw` - Raw request body
440    ///
441    /// # Returns
442    /// * `Result<RestResponse, RestError>` - API response or error
443    async fn postchain_rest_api(
444        &self,
445        method: RestRequestMethod,
446        path_segments: Option<&[&str]>,
447        query_params: Option<&'a Vec<(&'a str, &'a str)>>,
448        query_body_json: Option<Value>,
449        query_body_raw: Option<Vec<u8>>
450    ) -> Result<RestResponse, RestError> {
451        let mut node_index: usize = 0;
452        loop {
453            let result = self.postchain_rest_api_with_poll(method,
454                path_segments, query_params,
455                query_body_json.clone(), query_body_raw.clone(), node_index).await;
456
457            if let Err(ref error) = result {
458                node_index += 1;
459
460                if node_index >= self.node_url.len() || error.status_code.is_some() {
461                    return result;
462                }
463                tracing::info!("The API endpoint can't be reached; will try another one!");
464                continue;
465            }
466            return result;
467        }
468    }
469
470    /// Makes a REST API request with retry logic for failed nodes.
471    ///
472    /// # Arguments
473    /// * `method` - HTTP method to use
474    /// * `path_segments` - URL path segments
475    /// * `query_params` - Query parameters
476    /// * `query_body_json` - JSON request body
477    /// * `query_body_raw` - Raw request body
478    /// * `node_index` - Index of the node to try
479    ///
480    /// # Returns
481    /// * `Result<RestResponse, RestError>` - API response or error
482    async fn postchain_rest_api_with_poll(
483        &self,
484        method: RestRequestMethod,
485        path_segments: Option<&[&str]>,
486        query_params: Option<&'a Vec<(&'a str, &'a str)>>,
487        query_body_json: Option<Value>,
488        query_body_raw: Option<Vec<u8>>,
489        node_index: usize,
490    ) -> Result<RestResponse, RestError> {
491
492        let mut url = Url::parse(&self.node_url[node_index]).unwrap();
493
494        tracing::info!("Requesting on API endpoint: {}", url);
495
496        if let Some(ps) = path_segments {
497            if !ps.is_empty() {
498                let psj = ps.join("/");
499                url.set_path(&psj);
500            }
501        }
502
503        if let Some(qp) = query_params {
504            if !qp.is_empty() {
505                for (name, value) in qp {
506                    url.query_pairs_mut().append_pair(name, value);
507                }
508            }
509        }
510
511        if method == RestRequestMethod::POST
512            && query_body_json.is_none()
513            && query_body_raw.is_none()
514        {
515            let error_str = "Error: POST request need a body [json or binary].".to_string();
516
517            tracing::error!(error_str);
518
519            return Err(RestError {
520                type_error: TypeError::FromRestApi,
521                error_str: Some(error_str),
522                status_code: None,
523                ..Default::default()
524            });
525        }
526
527        let rest_client = Client::new();
528
529        let req_result = match method {
530            RestRequestMethod::GET => {
531                rest_client
532                    .get(url.clone())
533                    .timeout(Duration::from_secs(self.request_time_out))
534                    .send()
535                    .await
536            }
537
538            RestRequestMethod::POST => {
539                if let Some(qb) = query_body_json {
540                    rest_client
541                        .post(url.clone())
542                        .timeout(Duration::from_secs(self.request_time_out))
543                        .json(&qb)
544                        .send()
545                        .await
546                } else {
547                    let r_body = reqwest::Body::from(query_body_raw.unwrap());
548                    rest_client
549                        .post(url.clone())
550                        .timeout(Duration::from_secs(self.request_time_out))
551                        .body(r_body)
552                        .send()
553                        .await
554                }
555            }
556        };
557
558        let req_result_match = match req_result {
559            Ok(resp) => {
560                let http_status_code = resp.status().to_string();
561                let http_resp_header = resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap();
562                let json_resp = http_resp_header.contains("application/json");
563                let octet_stream_resp = http_resp_header.contains("application/octet-stream");
564
565                if http_status_code.starts_with('4') || http_status_code.starts_with('5') {
566                    let mut err = RestError {
567                        status_code: Some(http_status_code),
568                        type_error: TypeError::FromRestApi,
569                        ..Default::default()
570                    };
571
572                    if json_resp {
573                        let error_json = resp.json().await.unwrap();
574                        err.error_json = Some(error_json);
575                    } else {
576                        let error_str = resp.text().await.unwrap();
577                        err.error_str = Some(error_str);
578                    }
579
580                    tracing::error!("{:?}", err);
581
582                    return Err(err);
583                }
584
585                let rest_resp: RestResponse;
586
587                if json_resp {
588                    let val = resp.json().await.unwrap();
589                    rest_resp = RestResponse::Json(val);
590                } else if octet_stream_resp {
591                    let bytes = resp.bytes().await.unwrap();
592                    rest_resp = RestResponse::Bytes(bytes.to_vec());
593                } else {
594                    let val = resp.text().await.unwrap();
595                    rest_resp = RestResponse::String(val);
596                }
597
598                Ok(rest_resp)
599            }
600            Err(error) => {
601                let rest_error = RestError {
602                    error_str: Some(error.to_string()),
603                    type_error: TypeError::FromReqClient,
604                    ..Default::default()};
605
606                tracing::error!("{:?}", rest_error);
607
608                Err(rest_error)
609            },
610        };
611
612        req_result_match
613    }
614}
615
616#[tokio::test]
617async fn client_detect_merkle_hash_version() {
618    let rc = RestClient{
619        node_url: vec!["https://node11.devnet1.chromia.dev:7740"],
620        ..Default::default()
621    };
622
623    let blockchain_rid = "DCE5D72ED7E1675291AFE7F9D649D898C8D3E7411E52882D03D1B3D240BDD91B";
624
625    let merkle_hash_version = rc.detect_merkle_hash_version(blockchain_rid).await;
626
627    assert_eq!(merkle_hash_version, 2);
628}