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(val) => match val {
162                RestResponse::Json(json_val) => {
163                    let list_of_nodes = json_val
164                        .as_array()
165                        .unwrap()
166                        .iter()
167                        .filter_map(|value| value.as_str().map(String::from))
168                        .collect();
169                    Ok(list_of_nodes)
170                }
171                RestResponse::String(str_val) => Ok(vec![str_val]),
172                _ => Ok(vec!["nop".to_string()]),
173            },
174            Err(error) => {
175                tracing::error!("Can't get API urls from DC chain: {} because of error: {:?}", brid, error);
176                Err(error)
177            }
178        }
179    }
180
181    /// Retrieves the blockchain RID for a given blockchain IID.
182    ///
183    /// # Arguments
184    /// * `blockchain_iid` - Blockchain Instance Identifier
185    ///
186    /// # Returns
187    /// * `Result<String, RestError>` - Blockchain RID on success, or error on failure
188    pub async fn get_blockchain_rid(&self, blockchain_iid: u8) -> Result<String, RestError> {
189        let resp: Result<RestResponse, RestError> = self
190            .postchain_rest_api(
191                RestRequestMethod::GET,
192                Some(&[&format!("/brid/iid_{}", blockchain_iid)]),
193                None,
194                None,
195                None
196            )
197            .await;
198
199        if let Err(error) = resp {
200            tracing::error!("Can't get blockchain RID with IID = {} because of error: {:?}", blockchain_iid, error);
201            return Err(error);
202        }
203
204        let resp_val: RestResponse = resp.unwrap();
205
206        match resp_val {
207            RestResponse::String(val) => Ok(val.to_string()),
208            _ => Ok("".to_string()),
209        }
210    }
211
212    /// Prints error information and determines if the error should be ignored.
213    ///
214    /// # Arguments
215    /// * `error` - The REST error to print
216    /// * `ignore_all_errors` - Whether to ignore all errors
217    ///
218    /// # Returns
219    /// * `bool` - Whether the error should stop execution
220    pub fn print_error(&self, error: &RestError, ignore_all_errors: bool) -> bool {
221        println!(">> Error(s)");
222
223        if let Some(error_str) = &error.error_str {
224            println!("{}", error_str);
225        } else {
226            let val = &error.error_json.as_ref().unwrap();
227            let pprint = serde_json::to_string_pretty(val).unwrap();
228            println!("{}", pprint);
229        }
230
231        if ignore_all_errors {
232            println!("Allow ignore this error");
233            return false
234        }
235
236        true
237    }
238
239    /// Updates the list of node URLs used by the client.
240    ///
241    /// # Arguments
242    /// * `node_urls` - New list of node URLs to use
243    pub fn update_node_urls(&mut self, node_urls: &'a Vec<String>) {
244        self.node_url = node_urls.iter().map(String::as_str).collect();
245    }
246
247    // Transaction status
248    // GET /tx/{blockchain_rid}/{transaction_rid}/status
249    /// Gets the status of a transaction without polling.
250    ///
251    /// # Arguments
252    /// * `blockchain_rid` - Blockchain RID
253    /// * `tx_rid` - Transaction RID
254    ///
255    /// # Returns
256    /// * `Result<TransactionStatus, RestError>` - Transaction status or error
257    pub async fn get_transaction_status(&self, blockchain_rid: &str, tx_rid: &str) -> Result<TransactionStatus, RestError> {
258        self.get_transaction_status_with_poll(blockchain_rid, tx_rid, 0).await
259    }
260
261    /// Gets the status of a transaction with polling for confirmation.
262    ///
263    /// # Arguments
264    /// * `blockchain_rid` - Blockchain RID
265    /// * `tx_rid` - Transaction RID
266    /// * `attempts` - Number of polling attempts made so far
267    ///
268    /// # Returns
269    /// * `Result<TransactionStatus, RestError>` - Transaction status or error
270    pub async fn get_transaction_status_with_poll(&self, blockchain_rid: &str, tx_rid: &str, attempts: u64) -> Result<TransactionStatus, RestError> {
271        tracing::info!("Waiting for transaction status of blockchain RID: {} with tx: {} | attempt: {}", blockchain_rid, tx_rid, attempts);
272
273        if attempts >= self.poll_attemps {
274            tracing::warn!("Transaction status still in waiting status after {} attempts", attempts);
275            return Ok(TransactionStatus::WAITING);
276        }
277
278        let resp = self.postchain_rest_api(RestRequestMethod::GET,
279            Some(&["tx", blockchain_rid, tx_rid, "status"]),
280            None,
281            None,
282            None).await?;
283        match resp {
284            RestResponse::Json(val) => {
285                let status: serde_json::Map<String, Value> = serde_json::from_value(val).unwrap();
286                if let Some(status_value) = status.get("status") {
287                    let status_value = status_value.as_str();
288                    let status_code = match status_value {
289                        Some("waiting") => {
290                            // Waiting for transaction rejected or confirmed!!!
291                            // Interval time = 5 secs on each attempt
292                            // Break after 5 attempts
293                            tokio::time::sleep(Duration::from_secs(self.poll_attemp_interval_time)).await;
294                            return Box::pin(self.get_transaction_status_with_poll(blockchain_rid, tx_rid, attempts + 1)).await;
295                        },
296                        Some("confirmed") => {
297                            tracing::info!("Transaction confirmed!");
298                            Ok(TransactionStatus::CONFIRMED)
299                        },
300                        Some("rejected") => {
301                            tracing::warn!("Transaction rejected!");
302                            Ok(TransactionStatus::REJECTED)
303                        },
304                        _ => Ok(TransactionStatus::UNKNOWN)
305                    };
306                    return status_code
307                }
308                Ok(TransactionStatus::UNKNOWN)
309            }
310            _ => {
311                Ok(TransactionStatus::UNKNOWN)
312            }
313        }
314    }
315
316    // Submit transaction
317    // POST /tx/{blockchainRid}
318    /// Sends a transaction to the blockchain.
319    ///
320    /// # Arguments
321    /// * `tx` - Transaction to send
322    ///
323    /// # Returns
324    /// * `Result<RestResponse, RestError>` - Response from the blockchain or error
325    pub async fn send_transaction(&self, tx: &Transaction<'a>) -> Result<RestResponse, RestError> {
326        let txe = tx.gvt_hex_encoded();
327
328        let resq_body: serde_json::Map<String, Value> =
329            vec![("tx".to_string(), serde_json::json!(txe))]
330                .into_iter()
331                .collect();
332
333        let blockchain_rid = hex::encode(tx.blockchain_rid.clone()).as_str().to_owned();
334
335        tracing::info!("Sending transaction to {}", blockchain_rid); 
336
337        self
338            .postchain_rest_api(
339                RestRequestMethod::POST,
340                Some(&["tx", &blockchain_rid]),
341                None,
342                Some(serde_json::json!(resq_body)),
343                None
344            )
345            .await
346    }
347
348    // Make a query with GTV encoded response
349    // POST /query_gtv/{blockchainRid}
350    /// Executes a query on the blockchain.
351    ///
352    /// # Arguments
353    /// * `brid` - Blockchain RID
354    /// * `query_prefix` - Optional prefix for the query endpoint
355    /// * `query_type` - Type of query to execute
356    /// * `query_params` - Optional query parameters
357    /// * `query_args` - Optional query arguments
358    ///
359    /// # Returns
360    /// * `Result<RestResponse, RestError>` - Query response or error
361    pub async fn query<T: AsRef<str>>(
362        &self,
363        brid: &str,
364        query_prefix: Option<&str>,
365        query_type: &'a str,
366        query_params: Option<&'a mut Vec<(&'a str, &'a str)>>,
367        query_args: Option<&'a mut Vec<(T, crate::utils::operation::Params)>>,
368    ) -> Result<RestResponse, RestError> {
369        let mut query_prefix_str = "query_gtv";
370
371        if let Some(val) = query_prefix {
372            query_prefix_str = val;
373        }
374
375        let mut query_args_converted: Option<Vec<(&str, crate::utils::operation::Params)>> = query_args.map(|args| {
376            args.iter()
377                .map(|(key, params)| (key.as_ref(), params.clone()))
378                .collect()
379        });
380
381        let encode_str = crate::encoding::gtv::encode(query_type, query_args_converted.as_mut().map(|v| v.as_mut()));      
382        
383        tracing::info!("Querying {} to {}", query_type, brid); 
384
385        self.postchain_rest_api(
386            RestRequestMethod::POST,
387            Some(&[query_prefix_str, brid]),
388            query_params.as_deref(),
389            None,
390            Some(encode_str)
391        ).await
392    }
393
394    /// Makes a REST API request to a Postchain node.
395    ///
396    /// # Arguments
397    /// * `method` - HTTP method to use
398    /// * `path_segments` - URL path segments
399    /// * `query_params` - Query parameters
400    /// * `query_body_json` - JSON request body
401    /// * `query_body_raw` - Raw request body
402    ///
403    /// # Returns
404    /// * `Result<RestResponse, RestError>` - API response or error
405    async fn postchain_rest_api(
406        &self,
407        method: RestRequestMethod,
408        path_segments: Option<&[&str]>,
409        query_params: Option<&'a Vec<(&'a str, &'a str)>>,
410        query_body_json: Option<Value>,
411        query_body_raw: Option<Vec<u8>>
412    ) -> Result<RestResponse, RestError> {
413        let mut node_index: usize = 0;
414        loop {
415            let result = self.postchain_rest_api_with_poll(method,
416                path_segments, query_params,
417                query_body_json.clone(), query_body_raw.clone(), node_index).await;
418
419            if let Err(ref error) = result {
420                node_index += 1;
421
422                if node_index < self.node_url.len() && error.status_code.is_none() {
423                    tracing::info!("The API endpoint can't be reached; will try another one!");
424                    continue;
425                }
426            }
427            return result;
428        }
429    }
430
431    /// Makes a REST API request with retry logic for failed nodes.
432    ///
433    /// # Arguments
434    /// * `method` - HTTP method to use
435    /// * `path_segments` - URL path segments
436    /// * `query_params` - Query parameters
437    /// * `query_body_json` - JSON request body
438    /// * `query_body_raw` - Raw request body
439    /// * `node_index` - Index of the node to try
440    ///
441    /// # Returns
442    /// * `Result<RestResponse, RestError>` - API response or error
443    async fn postchain_rest_api_with_poll(
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        node_index: usize,
451    ) -> Result<RestResponse, RestError> {
452
453        let mut url = Url::parse(&self.node_url[node_index]).unwrap();
454
455        tracing::info!("Requesting on API endpoint: {}", url);
456
457        if let Some(ps) = path_segments {
458            if !ps.is_empty() {
459                let psj = ps.join("/");
460                url.set_path(&psj);
461            }
462        }
463
464        if let Some(qp) = query_params {
465            if !qp.is_empty() {
466                for (name, value) in qp {
467                    url.query_pairs_mut().append_pair(name, value);
468                }
469            }
470        }
471
472        if method == RestRequestMethod::POST
473            && query_body_json.is_none()
474            && query_body_raw.is_none()
475        {
476            let error_str = "Error: POST request need a body [json or binary].".to_string();
477
478            tracing::error!(error_str);
479
480            return Err(RestError {
481                type_error: TypeError::FromRestApi,
482                error_str: Some(error_str),
483                status_code: None,
484                ..Default::default()
485            });
486        }
487
488        let rest_client = Client::new();
489
490        let req_result = match method {
491            RestRequestMethod::GET => {
492                rest_client
493                    .get(url.clone())
494                    .timeout(Duration::from_secs(self.request_time_out))
495                    .send()
496                    .await
497            }
498
499            RestRequestMethod::POST => {
500                if let Some(qb) = query_body_json {
501                    rest_client
502                        .post(url.clone())
503                        .timeout(Duration::from_secs(self.request_time_out))
504                        .json(&qb)
505                        .send()
506                        .await
507                } else {
508                    let r_body = reqwest::Body::from(query_body_raw.unwrap());
509                    rest_client
510                        .post(url.clone())
511                        .timeout(Duration::from_secs(self.request_time_out))
512                        .body(r_body)
513                        .send()
514                        .await
515                }
516            }
517        };
518
519        let req_result_match = match req_result {
520            Ok(resp) => {
521                let http_status_code = resp.status().to_string();
522                let http_resp_header = resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap();
523                let json_resp = http_resp_header.contains("application/json");
524                let octet_stream_resp = http_resp_header.contains("application/octet-stream");
525
526                if http_status_code.starts_with('4') || http_status_code.starts_with('5') {
527                    let mut err = RestError {
528                        status_code: Some(http_status_code),
529                        type_error: TypeError::FromRestApi,
530                        ..Default::default()
531                    };
532
533                    if json_resp {
534                        let error_json = resp.json().await.unwrap();
535                        err.error_json = Some(error_json);
536                    } else {
537                        let error_str = resp.text().await.unwrap();
538                        err.error_str = Some(error_str);
539                    }
540
541                    tracing::error!("{:?}", err);
542
543                    return Err(err);
544                }
545
546                let rest_resp: RestResponse;
547
548                if json_resp {
549                    let val = resp.json().await.unwrap();
550                    rest_resp = RestResponse::Json(val);
551                } else if octet_stream_resp {
552                    let bytes = resp.bytes().await.unwrap();
553                    rest_resp = RestResponse::Bytes(bytes.to_vec());
554                } else {
555                    let val = resp.text().await.unwrap();
556                    rest_resp = RestResponse::String(val);
557                }
558
559                Ok(rest_resp)
560            }
561            Err(error) => {
562                let rest_error = RestError {
563                    error_str: Some(error.to_string()),
564                    type_error: TypeError::FromReqClient,
565                    ..Default::default()};
566
567                tracing::error!("{:?}", rest_error);
568
569                Err(rest_error)
570            },
571        };
572
573        req_result_match
574    }
575}