#![allow(missing_docs)]
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::time::Duration;
use thiserror::Error;
use url::Url;
#[derive(Error, Debug)]
pub enum JsonRpcError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("JSON serialization/deserialization failed: {0}")]
Json(#[from] serde_json::Error),
#[error("JSON-RPC error: code={code}, message={message}")]
Rpc { code: i32, message: String },
#[error("Invalid URL: {0}")]
InvalidUrl(#[from] url::ParseError),
#[error("General error: {0}")]
General(#[from] anyhow::Error),
}
#[derive(Debug, Clone)]
pub struct JsonRpcClient {
pub base_url: Url,
pub http: Client,
}
impl JsonRpcClient {
pub fn new(base_url: Url) -> Result<Self, JsonRpcError> {
let http = Client::builder().timeout(Duration::from_secs(30)).build()?;
Ok(Self { base_url, http })
}
pub fn with_client(base_url: Url, http: Client) -> Result<Self, JsonRpcError> {
Ok(Self { base_url, http })
}
pub async fn call<T: DeserializeOwned>(
&self,
method: &str,
params: Value,
) -> Result<T, JsonRpcError> {
let request_body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params
});
let response = self
.http
.post(self.base_url.clone())
.json(&request_body)
.send()
.await?;
let response_text = response.text().await?;
let response_json: Value = serde_json::from_str(&response_text)?;
if let Some(error) = response_json.get("error") {
let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) as i32;
let message = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error")
.to_string();
return Err(JsonRpcError::Rpc { code, message });
}
let result = response_json.get("result").cloned().unwrap_or(Value::Null);
Ok(serde_json::from_value(result)?)
}
pub async fn call_v2<T: DeserializeOwned>(
&self,
method_path: &str,
payload: Option<Value>,
) -> Result<T, JsonRpcError> {
let url = self.base_url.join(method_path)?;
let request = match payload {
Some(body) => self.http.post(url).json(&body),
None => self.http.get(url),
};
let response = request.send().await?;
let response_text = response.text().await?;
Ok(serde_json::from_str(&response_text)?)
}
pub async fn call_v3<T: DeserializeOwned>(
&self,
method: &str,
params: Value,
) -> Result<T, JsonRpcError> {
self.call(method, params).await
}
}
pub fn canonical_json(value: &Value) -> String {
crate::canonjson::canonicalize(value)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_canonical_json() {
let value = json!({
"b": 2,
"a": 1,
"c": {
"z": 3,
"y": 4
}
});
let canonical = canonical_json(&value);
assert!(canonical.contains(r#""a":1"#));
assert!(canonical.contains(r#""b":2"#));
let a_pos = canonical.find(r#""a":"#).unwrap();
let b_pos = canonical.find(r#""b":"#).unwrap();
assert!(a_pos < b_pos);
}
}