diff --git a/client/Cargo.toml b/client/Cargo.toml index 914e6a9f..7860b23e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -23,11 +23,13 @@ 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" } [dev-dependencies] tempfile = "3.3.0" diff --git a/client/src/client.rs b/client/src/client.rs index 8e456edf..c5ed266e 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -8,6 +8,7 @@ // If not, see . // +use log::Level::{Debug, Trace, Warn}; use std::collections::HashMap; use std::fs::File; use std::io::{BufRead, BufReader}; @@ -15,12 +16,13 @@ use std::iter::FromIterator; use std::path::PathBuf; use std::{fmt, result}; +use crate::transport::ReqwestTransport; use crate::{bitcoin, deserialize_hex}; use async_trait::async_trait; use bitcoin::hex::DisplayHex; -use jsonrpc_async; +use jsonrpc_async::Client as JsonRpcClient; use serde::{self, Serialize}; -use serde_json::{self}; +use url::Url; use crate::bitcoin::address::{NetworkChecked, NetworkUnchecked}; use crate::bitcoin::hashes::hex::FromHex; @@ -28,7 +30,6 @@ use crate::bitcoin::secp256k1::ecdsa::Signature; use crate::bitcoin::{ Address, Amount, Block, OutPoint, PrivateKey, PublicKey, Script, Transaction, }; -use log::Level::{Debug, Trace, Warn}; use crate::error::*; use crate::json; @@ -1308,7 +1309,7 @@ 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: JsonRpcClient, } impl fmt::Debug for Client { @@ -1319,28 +1320,24 @@ impl fmt::Debug for Client { impl Client { /// Creates a client to a bitcoind JSON-RPC server. - /// - /// Can only return [Err] when using cookie authentication. pub async fn new(url: &str, auth: Auth) -> Result { - 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 + let transport = ReqwestTransport::new(parsed_url); + let client = JsonRpcClient::with_transport(transport); + + Ok(Self { + client, + }) } } @@ -1385,11 +1382,8 @@ fn log_response(cmd: &str, resp: &Result) { 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 def = + serde_json::value::to_raw_value(&serde_json::value::Value::Null).unwrap(); let result = resp.result.as_ref().unwrap_or(&def); trace!(target: "bitcoincore_rpc", "JSON-RPC response for {}: {}", cmd, result); } diff --git a/client/src/error.rs b/client/src/error.rs index 3d8b7a58..c3b4e811 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -13,7 +13,6 @@ use std::{error, fmt, io}; use crate::bitcoin; use crate::bitcoin::hashes::hex; use crate::bitcoin::secp256k1; -use jsonrpc_async; use serde_json; /// The error type for errors produced in this library. @@ -31,6 +30,8 @@ pub enum Error { UnexpectedStructure, /// The daemon returned an error string. ReturnedError(String), + Auth(String), + UrlParse(url::ParseError), } impl From for Error { @@ -39,6 +40,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: url::ParseError) -> Error { + Error::UrlParse(e) + } +} + impl From for Error { fn from(e: hex::HexToBytesError) -> Error { Error::Hex(e) @@ -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), } } } diff --git a/client/src/lib.rs b/client/src/lib.rs index a33ddc0f..b830a1bf 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -22,21 +22,21 @@ 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 queryable; +mod transport; pub use crate::client::*; pub use crate::error::Error; pub use crate::queryable::*; +pub use jsonrpc_async::Error as RpcError; fn deserialize_hex(hex: &str) -> Result { let buf = Vec::::from_hex(hex)?; diff --git a/client/src/transport.rs b/client/src/transport.rs new file mode 100644 index 00000000..cc876133 --- /dev/null +++ b/client/src/transport.rs @@ -0,0 +1,50 @@ +use async_trait::async_trait; +use jsonrpc_async::Transport; +use url::Url; + +pub struct ReqwestTransport { + client: reqwest::Client, + url: Url, +} + +impl ReqwestTransport { + pub fn new(url: Url) -> Self { + Self { + client: reqwest::Client::new(), + url, + } + } + + async fn request(&self, req: impl serde::Serialize) -> Result + where + R: for<'a> serde::de::Deserialize<'a>, + { + let response = self.client.post(self.url.clone()).json(&req).send().await?; + response.json().await + } +} + +#[async_trait] +impl Transport for ReqwestTransport { + async fn send_request( + &self, + r: jsonrpc_async::Request<'_>, + ) -> Result { + Ok(self.request(r).await.map_err(|e| jsonrpc_async::Error::Transport(e.into()))?) + } + + async fn send_batch( + &self, + _rs: &[jsonrpc_async::Request<'_>], + ) -> Result, jsonrpc_async::Error> { + unimplemented!() + } + + fn fmt_target(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let (Some(host), Some(port)) = (self.url.host(), self.url.port()) { + write!(f, "http://{}:{}{}", host, port, self.url.path()) + } else { + write!(f, "http://{:?}", self.url) + } + } +} diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs index 9c083791..d29930e6 100644 --- a/integration_test/src/main.rs +++ b/integration_test/src/main.rs @@ -19,8 +19,7 @@ use std::str::FromStr; use bitcoin::absolute::LockTime; use bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bitcoincore_rpc::json; -use bitcoincore_rpc::jsonrpc_async::error::Error as JsonRpcError; -use bitcoincore_rpc::{Auth, Client, Error, RpcApi}; +use bitcoincore_rpc::{Auth, Client, Error, RpcApi, RpcError as JsonRpcError}; use crate::json::BlockStatsFields as BsFields; use bitcoin::consensus::encode::{deserialize, serialize_hex}; @@ -839,12 +838,12 @@ async fn test_test_mempool_accept(cl: &Client) { .await .unwrap(); let res = cl.test_mempool_accept(&[&tx]).await.unwrap(); - assert!(!res[0].allowed); + assert!(!res[0].allowed.unwrap()); assert!(res[0].reject_reason.is_some()); let signed = cl.sign_raw_transaction_with_wallet(&tx, None, None).await.unwrap().transaction().unwrap(); let res = cl.test_mempool_accept(&[&signed]).await.unwrap(); - assert!(res[0].allowed, "not allowed: {:?}", res[0].reject_reason); + assert!(res[0].allowed.unwrap(), "not allowed: {:?}", res[0].reject_reason); } async fn test_wallet_create_funded_psbt(cl: &Client) {