use crate::{
jsonrpc::{
self,
batch::{self, Batch},
JsonError,
},
method::Method,
types::Empty,
};
pub use curl;
use curl::easy::{Easy, List};
use serde::{de::DeserializeOwned, Serialize};
use std::{
cell::RefCell,
env,
io::{Read, Write},
};
use thiserror::Error;
pub struct Client {
handle: RefCell<Easy>,
}
impl Client {
pub fn new(url: impl AsRef<str>) -> Result<Self, Error> {
let mut handle = Easy::new();
handle.url(url.as_ref())?;
Ok(Self::with_handle(handle))
}
pub fn with_handle(handle: Easy) -> Self {
Self {
handle: RefCell::new(handle),
}
}
pub fn from_env() -> Self {
Self::new(env::var("ETHRPC").expect("missing ETHRPC environment variable")).unwrap()
}
fn roundtrip<T, R>(&self, request: T) -> Result<R, Error>
where
T: Serialize,
R: DeserializeOwned,
{
let request = serde_json::to_vec(&request)?;
let mut handle = self
.handle
.try_borrow_mut()
.expect("unexpected sharing of curl handle");
handle.post(true)?;
handle.http_headers({
let mut list = List::new();
list.append("Content-Type: application/json")?;
list.append(&format!("Content-Length: {}", request.len()))?;
list
})?;
let mut request = request.as_slice();
let mut response = Vec::new();
{
let mut transfer = handle.transfer();
transfer.read_function(|chunk| Ok(request.read(chunk).unwrap()))?;
transfer.write_function(|chunk| Ok(response.write(chunk).unwrap()))?;
transfer.perform()?;
}
let status = handle.response_code()?;
if !(200..300).contains(&status) {
return Err(Error::Status(
status,
String::from_utf8_lossy(&response).into_owned(),
));
}
let response = serde_json::from_slice(&response)?;
Ok(response)
}
pub fn call<M>(&self, method: M, params: M::Params) -> Result<M::Result, Error>
where
M: Method + Serialize,
{
jsonrpc::call(method, params, |request| self.roundtrip(request))
}
pub fn exec<M>(&self, method: M) -> Result<M::Result, Error>
where
M: Method<Params = Empty> + Serialize,
{
jsonrpc::call(method, Empty, |request| self.roundtrip(request))
}
pub fn batch<B>(&self, batch: B) -> Result<B::Values, Error>
where
B: Batch,
{
batch::call(batch, |requests| self.roundtrip(requests))
}
pub fn try_batch<B>(&self, batch: B) -> Result<B::Results, Error>
where
B: Batch,
{
batch::try_call(batch, |requests| self.roundtrip(requests))
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("JSON error: {0}")]
Json(#[from] JsonError),
#[error("HTTP error: {0}")]
Http(#[from] curl::Error),
#[error("HTTP {0} error: {1}")]
Status(u32, String),
#[error(transparent)]
Rpc(#[from] jsonrpc::Error),
#[error(transparent)]
Batch(#[from] batch::Error),
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::Json(err.into())
}
}