helium_jsonrpc/
lib.rs

1use async_trait::async_trait;
2use futures::TryFutureExt;
3use serde::{de::DeserializeOwned, Deserialize, Serialize};
4use std::time::Duration;
5use std::time::SystemTime;
6
7pub mod error;
8
9pub use error::{Error, Result};
10pub mod blocks;
11pub mod transactions;
12
13/// The default timeout for API requests
14pub const DEFAULT_TIMEOUT: u64 = 120;
15/// The default base URL if none is specified.
16pub const DEFAULT_BASE_URL: &str = "http://127.0.0.1:4467";
17/// A utility constant to pass an empty query slice to the various client fetch
18/// functions
19pub const NO_QUERY: &[&str; 0] = &[""; 0];
20
21pub const JSON_RPC: &str = "2.0";
22
23pub const BLOCK_HEIGHT: &str = "block_height";
24pub const BLOCK_GET: &str = "block_get";
25pub const TXN_GET: &str = "transaction_get";
26
27#[derive(Clone, Serialize, Deserialize, Debug)]
28#[serde(untagged)]
29pub(crate) enum Response<T> {
30    Data { result: T, id: String },
31    Error { id: String, error: ErrorElement },
32}
33
34#[derive(Clone, Serialize, Deserialize, Debug)]
35pub(crate) struct ErrorElement {
36    message: String,
37    code: i32,
38}
39
40#[derive(Clone, Debug)]
41pub struct Client {
42    base_url: String,
43    client: reqwest::Client,
44}
45
46impl Default for Client {
47    fn default() -> Self {
48        Self::new_with_base_url(DEFAULT_BASE_URL.to_string())
49    }
50}
51
52impl Client {
53    /// Create a new client using a given base URL and a default
54    /// timeout. The library will use absoluate paths based on this
55    /// base_url.
56    pub fn new_with_base_url(base_url: String) -> Self {
57        Self::new_with_timeout(base_url, DEFAULT_TIMEOUT)
58    }
59
60    /// Create a new client using a given base URL, and request
61    /// timeout value.  The library will use absoluate paths based on
62    /// the given base_url.
63    pub fn new_with_timeout(base_url: String, timeout: u64) -> Self {
64        let client = reqwest::Client::builder()
65            .gzip(true)
66            .timeout(Duration::from_secs(timeout))
67            .build()
68            .unwrap();
69        Self { base_url, client }
70    }
71
72    pub(crate) async fn post<T, R>(&self, path: &str, json: &T) -> Result<Result<R>>
73    where
74        T: Serialize + ?Sized,
75        R: 'static + DeserializeOwned + std::marker::Send,
76    {
77        let request_url = format!("{}{}", self.base_url, path);
78        let response = self
79            .client
80            .post(&request_url)
81            .json(json)
82            .send()
83            .map_err(error::Error::from)
84            .await?;
85
86        let response = response.error_for_status().map_err(error::Error::from)?;
87        let v: Response<R> = response.json().await.map_err(error::Error::from)?;
88        Ok(match v {
89            Response::Data { result, .. } => Ok(result),
90            Response::Error { error, .. } => Err(Error::NodeError(error.message, error.code)),
91        })
92    }
93}
94
95#[allow(non_camel_case_types)]
96#[derive(Clone, Debug, Serialize)]
97#[serde(rename_all = "snake_case")]
98pub(crate) enum Params {
99    Hash(String),
100    Height(u64),
101    None(String),
102}
103
104#[derive(Clone, Debug, Serialize)]
105pub(crate) struct NodeCall {
106    jsonrpc: String,
107    id: String,
108    method: String,
109    params: Option<Params>,
110}
111
112impl NodeCall {
113    pub(crate) fn height() -> Self {
114        NodeCall {
115            jsonrpc: JSON_RPC.to_string(),
116            id: now_millis(),
117            method: BLOCK_HEIGHT.to_string(),
118            params: Some(Params::None("null".to_string())),
119        }
120    }
121    pub(crate) fn block(height: u64) -> Self {
122        NodeCall {
123            jsonrpc: JSON_RPC.to_string(),
124            id: now_millis(),
125            method: BLOCK_GET.to_string(),
126            params: Some(Params::Height(height)),
127        }
128    }
129    pub(crate) fn transaction(hash: String) -> Self {
130        NodeCall {
131            jsonrpc: JSON_RPC.to_string(),
132            id: now_millis(),
133            method: TXN_GET.to_string(),
134            params: Some(Params::Hash(hash)),
135        }
136    }
137}
138
139fn now_millis() -> String {
140    let ms = SystemTime::now()
141        .duration_since(SystemTime::UNIX_EPOCH)
142        .unwrap();
143    ms.as_millis().to_string()
144}
145
146#[async_trait]
147pub trait IntoVec {
148    type Item;
149
150    async fn into_vec(self) -> Result<Vec<Self::Item>>;
151}
152
153#[cfg(test)]
154mod test {
155    use super::*;
156    use tokio::test;
157
158    #[test]
159    async fn txn_err() {
160        let client = Client::default();
161        let txn = transactions::get(&client, "1gidN7e6OKn405Fru_0sGhsqca3lTsrfGKrM4dwM").await;
162        let er = match txn {
163            Err(e) => format!("{}", e),
164            _ => panic!("??"),
165        };
166        assert_eq!(er, "error code -100 from node: No transaction: <<\"1gidN7e6OKn405Fru_0sGhsqca3lTsrfGKrM4dwM\">>");
167    }
168}