Skip to content

Commit

Permalink
Use reqwest client
Browse files Browse the repository at this point in the history
  • Loading branch information
jfldde committed Sep 16, 2024
1 parent fb809b0 commit 1aae7cc
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 135 deletions.
5 changes: 3 additions & 2 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ path = "src/lib.rs"
bitcoincore-rpc-json = { version = "0.18.0", path = "../json" }

log = "0.4.5"
jsonrpc-async = "2.0.2"
reqwest = { version = "0.12.5", default-features = false, features = ["json", "rustls-tls"] }
async-trait = "0.1.42"
url = "2.5.1"

# Used for deserialization of JSON.
serde = "1"
serde_json = "1"
serde_json = { version = "1", features = [ "raw_value" ] }

[dev-dependencies]
tempfile = "3.3.0"
Expand Down
126 changes: 64 additions & 62 deletions client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ use std::{fmt, result};
use crate::{bitcoin, deserialize_hex};
use async_trait::async_trait;
use bitcoin::hex::DisplayHex;
use jsonrpc_async;
use reqwest::Client as ReqwestClient;
use serde::de::DeserializeOwned;
use serde::{self, Serialize};
use serde_json::{self};
use serde_json::{self, json, Value};
use url::Url;

use crate::bitcoin::address::{NetworkChecked, NetworkUnchecked};
use crate::bitcoin::hashes::hex::FromHex;
use crate::bitcoin::secp256k1::ecdsa::Signature;
use crate::bitcoin::{
Address, Amount, Block, OutPoint, PrivateKey, PublicKey, Script, Transaction,
};
use log::Level::{Debug, Trace, Warn};
use log::Level;

use crate::error::*;
use crate::json;
Expand Down Expand Up @@ -1308,7 +1310,8 @@ pub trait RpcApi: Sized {

/// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
pub struct Client {
client: jsonrpc_async::client::Client,
client: ReqwestClient,
url: String,
}

impl fmt::Debug for Client {
Expand All @@ -1322,77 +1325,76 @@ impl Client {
///
/// Can only return [Err] when using cookie authentication.
pub async fn new(url: &str, auth: Auth) -> Result<Self> {
let (user, pass) = auth.get_user_pass()?;
jsonrpc_async::client::Client::simple_http(url, user, pass)
.await
.map(|client| Client {
client,
})
.map_err(|e| super::error::Error::JsonRpc(e.into()))
}

/// Create a new Client using the given [jsonrpc_async::Client].
pub fn from_jsonrpc(client: jsonrpc_async::client::Client) -> Client {
Client {
client,
let mut parsed_url = Url::parse(url)?;

if let (Some(user), pass) = auth.get_user_pass()? {
parsed_url
.set_username(&user)
.map_err(|_| Error::Auth("Failed to set username".to_string()))?;
parsed_url
.set_password(pass.as_deref())
.map_err(|_| Error::Auth("Failed to set password".to_string()))?;
}
}

/// Get the underlying JSONRPC client.
pub fn get_jsonrpc_client(&self) -> &jsonrpc_async::client::Client {
&self.client
Ok(Self {
client: ReqwestClient::new(),
url: parsed_url.to_string(),
})
}

// /// Get the underlying JSONRPC client.
// pub fn get_jsonrpc_client(&self) -> &ReqwestClient {
// &self.client
// }
}

#[async_trait]
impl RpcApi for Client {
/// Call an `cmd` rpc with given `args` list
async fn call<T: for<'a> serde::de::Deserialize<'a>>(
&self,
cmd: &str,
args: &[serde_json::Value],
) -> Result<T> {
let raw_args: Vec<_> = args
.iter()
.map(|a| {
let json_string = serde_json::to_string(a)?;
serde_json::value::RawValue::from_string(json_string) // we can't use to_raw_value here due to compat with Rust 1.29
})
.map(|a| a.map_err(|e| Error::Json(e)))
.collect::<Result<Vec<_>>>()?;
let req = self.client.build_request(&cmd, &raw_args);
if log_enabled!(Debug) {
debug!(target: "bitcoincore_rpc", "JSON-RPC request: {} {}", cmd, serde_json::Value::from(args));
async fn call<T: DeserializeOwned>(&self, cmd: &str, args: &[Value]) -> Result<T> {
let request = json!({
"jsonrpc": "2.0",
"id": "rust-client",
"method": cmd,
"params": args,
});

if log_enabled!(Level::Debug) {
debug!(target: "bitcoincore_rpc", "JSON-RPC request: {} {}", cmd, json!(args));
}

let resp = self.client.send_request(req).await.map_err(Error::from);
log_response(cmd, &resp);
Ok(resp?.result()?)
let req_builder = self.client.post(&self.url).json(&request);

let response = req_builder.send().await.unwrap();
let status = response.status();
let response_text = response.text().await.unwrap();

self.log_response(cmd, &status, &response_text);

if !status.is_success() {
return Err(Error::ReturnedError(format!("HTTP error {}: {}", status, response_text)));
}

let response: Value = serde_json::from_str(&response_text)?;
if let Some(error) = response.get("error") {
return Err(Error::ReturnedError(error.to_string()));
}

let result = response.get("result").ok_or_else(|| {
Error::ReturnedError("Missing 'result' field in response".to_string())
})?;

Ok(serde_json::from_value(result.clone())?)
}
}

fn log_response(cmd: &str, resp: &Result<jsonrpc_async::Response>) {
if log_enabled!(Warn) || log_enabled!(Debug) || log_enabled!(Trace) {
match resp {
Err(ref e) => {
if log_enabled!(Debug) {
debug!(target: "bitcoincore_rpc", "JSON-RPC failed parsing reply of {}: {:?}", cmd, e);
}
}
Ok(ref resp) => {
if let Some(ref e) = resp.error {
if log_enabled!(Debug) {
debug!(target: "bitcoincore_rpc", "JSON-RPC error for {}: {:?}", cmd, e);
}
} else if log_enabled!(Trace) {
// we can't use to_raw_value here due to compat with Rust 1.29
let def = serde_json::value::RawValue::from_string(
serde_json::Value::Null.to_string(),
)
.unwrap();
let result = resp.result.as_ref().unwrap_or(&def);
trace!(target: "bitcoincore_rpc", "JSON-RPC response for {}: {}", cmd, result);
}
impl Client {
fn log_response(&self, cmd: &str, status: &reqwest::StatusCode, response: &str) {
if log_enabled!(Level::Debug) {
if status.is_success() {
debug!(target: "bitcoincore_rpc", "JSON-RPC response for {}: {}", cmd, response);
} else {
debug!(target: "bitcoincore_rpc", "JSON-RPC error for {}: {} - {}", cmd, status, response);
}
}
}
Expand Down
19 changes: 14 additions & 5 deletions client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@

use std::{error, fmt, io};

use crate::bitcoin;
use crate::bitcoin::hashes::hex;
use crate::bitcoin::secp256k1;
use jsonrpc_async;
use crate::{bitcoin, jsonrpc_error};
use serde_json;

/// The error type for errors produced in this library.
#[derive(Debug)]
pub enum Error {
JsonRpc(jsonrpc_async::error::Error),
JsonRpc(jsonrpc_error::Error),
Hex(hex::HexToBytesError),
Json(serde_json::error::Error),
BitcoinSerialization(bitcoin::consensus::encode::Error),
Expand All @@ -31,14 +30,22 @@ pub enum Error {
UnexpectedStructure,
/// The daemon returned an error string.
ReturnedError(String),
Auth(String),
UrlParse(url::ParseError),
}

impl From<jsonrpc_async::error::Error> for Error {
fn from(e: jsonrpc_async::error::Error) -> Error {
impl From<jsonrpc_error::Error> for Error {
fn from(e: jsonrpc_error::Error) -> Error {
Error::JsonRpc(e)
}
}

impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Error {
Error::UrlParse(e)
}
}

impl From<hex::HexToBytesError> for Error {
fn from(e: hex::HexToBytesError) -> Error {
Error::Hex(e)
Expand Down Expand Up @@ -88,6 +95,8 @@ impl fmt::Display for Error {
Error::InvalidCookieFile => write!(f, "invalid cookie file"),
Error::UnexpectedStructure => write!(f, "the JSON result had an unexpected structure"),
Error::ReturnedError(ref s) => write!(f, "the daemon returned an error string: {}", s),
Error::Auth(ref s) => write!(f, "Auth error: {}", s),
Error::UrlParse(ref s) => write!(f, "Url error: {}", s),
}
}
}
Expand Down
97 changes: 97 additions & 0 deletions client/src/jsonrpc_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Rust JSON-RPC Library
// Written in 2015 by
// Andrew Poelstra <[email protected]>
//
// To the extent possible under law, the author(s) have dedicated all
// copyright and related and neighboring rights to this software to
// the public domain worldwide. This software is distributed without
// any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication
// along with this software.
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
//

//! # Error handling
//!
//! Some useful methods for creating Error objects
//!
use std::{error, fmt};

use serde::{Deserialize, Serialize};
use serde_json;

/// A library error
#[derive(Debug)]
pub enum Error {
/// A transport error
Transport(Box<dyn error::Error + Send + Sync>),
/// Json error
Json(serde_json::Error),
/// Error response
Rpc(RpcError),
/// Response to a request did not have the expected nonce
NonceMismatch,
/// Response to a request had a jsonrpc field other than "2.0"
VersionMismatch,
/// Batches can't be empty
EmptyBatch,
/// Too many responses returned in batch
WrongBatchResponseSize,
/// Batch response contained a duplicate ID
BatchDuplicateResponseId(serde_json::Value),
/// Batch response contained an ID that didn't correspond to any request ID
WrongBatchResponseId(serde_json::Value),
}

impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Error {
Error::Json(e)
}
}

impl From<RpcError> for Error {
fn from(e: RpcError) -> Error {
Error::Rpc(e)
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::Transport(ref e) => write!(f, "transport error: {}", e),
Error::Json(ref e) => write!(f, "JSON decode error: {}", e),
Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r),
Error::BatchDuplicateResponseId(ref v) => {
write!(f, "duplicate RPC batch response ID: {}", v)
}
Error::WrongBatchResponseId(ref v) => write!(f, "wrong RPC batch response ID: {}", v),
Error::NonceMismatch => write!(f, "Nonce of response did not match nonce of request"),
Error::VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""),
Error::EmptyBatch => write!(f, "batches can't be empty"),
Error::WrongBatchResponseSize => write!(f, "too many responses returned in batch"),
}
}
}

impl error::Error for Error {
fn cause(&self) -> Option<&dyn error::Error> {
match *self {
Error::Transport(ref e) => Some(&**e),
Error::Json(ref e) => Some(e),
_ => None,
}
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
/// A JSONRPC error object
pub struct RpcError {
/// The integer identifier of the error
pub code: i32,
/// A string describing the error
pub message: String,
/// Additional data specific to the error
pub data: Option<Box<serde_json::value::RawValue>>,
}
6 changes: 3 additions & 3 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ extern crate log;
#[macro_use] // `macro_use` is needed for v1.24.0 compilation.
extern crate serde;

pub extern crate jsonrpc_async;

pub extern crate bitcoincore_rpc_json;
pub use crate::json::bitcoin;
use bitcoincore_rpc_json::bitcoin::hex::FromHex;
pub use bitcoincore_rpc_json as json;
use bitcoincore_rpc_json::bitcoin::hex::FromHex;
use json::bitcoin::consensus::{Decodable, ReadExt};

mod client;
mod error;
mod jsonrpc_error;
mod queryable;

pub use crate::client::*;
pub use crate::error::Error;
pub use crate::jsonrpc_error::Error as RpcError;
pub use crate::queryable::*;

fn deserialize_hex<T: Decodable>(hex: &str) -> Result<T> {
Expand Down
Loading

0 comments on commit 1aae7cc

Please sign in to comment.