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
13pub const DEFAULT_TIMEOUT: u64 = 120;
15pub const DEFAULT_BASE_URL: &str = "http://127.0.0.1:4467";
17pub 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 pub fn new_with_base_url(base_url: String) -> Self {
57 Self::new_with_timeout(base_url, DEFAULT_TIMEOUT)
58 }
59
60 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}