diff --git a/Cargo.toml b/Cargo.toml index 1d90703..5598760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ hex = { package = "hex-conservative", version = "0.2" } log = "^0.4" minreq = { version = "2.11.0", features = ["json-using-serde"], optional = true } reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] } +serde_json = { version = "1.0.127" } [dev-dependencies] serde_json = "1.0" @@ -31,7 +32,7 @@ electrsd = { version = "0.28.0", features = ["legacy", "esplora_a33e97e1", "bitc lazy_static = "1.4.0" [features] -default = ["blocking", "async", "async-https"] +default = ["blocking", "blocking-https", "async", "async-https"] blocking = ["minreq", "minreq/proxy"] blocking-https = ["blocking", "minreq/https"] blocking-https-rustls = ["blocking", "minreq/https-rustls"] diff --git a/src/api.rs b/src/api.rs index d4dfa1e..9a5448d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,15 +2,470 @@ //! //! see: +use core::str; +use std::{collections::HashMap, str::FromStr}; + +use bitcoin::consensus::Decodable; pub use bitcoin::consensus::{deserialize, serialize}; +use bitcoin::hashes::sha256::Hash; pub use bitcoin::hex::FromHex; use bitcoin::Weight; pub use bitcoin::{ transaction, Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness, }; +use hex::DisplayHex; use serde::Deserialize; +// Transactions +// Addresses +// Blocks +// Mempool +// Fee Estimates + +// TODO: (@leonardo) Should we use an specific http crate, or implement our own parsing, etc... ? +/// An HTTP request method. +pub enum Method { + /// The GET method + Get, + /// The POST method + Post, +} + +// TODO: (@leonardo) Should we use an specific url crate, or implement our own parsing, etc... ? +/// A URL type for requests. +type Url = String; + +/// An minimal HTTP request. +pub struct Request { + pub method: Method, + pub url: Url, + pub body: Option>, + pub proxy: Option, + pub timeout: Option, + pub headers: HashMap, +} + +impl Request { + fn new(method: Method, url: Url, body: Option>) -> Self { + Self { + method, + url, + body, + proxy: None, + timeout: None, + headers: HashMap::new(), + } + } + + pub fn with_proxy(mut self, proxy: &str) -> Self { + self.proxy = Some(proxy.to_string()); + self + } + + pub fn with_timeout(mut self, timeout: u64) -> Self { + self.timeout = Some(timeout); + self + } + + pub fn with_header(mut self, key: &str, value: &str) -> Self { + self.headers.insert(key.to_string(), value.to_string()); + self + } +} + +#[derive(Debug)] +#[allow(unused)] +pub struct Response { + pub status_code: i32, + // reason_phrase: String, + // headers: HashMap, + // url: Url, + body: Vec, +} + +impl Response { + pub fn new(status_code: i32, body: Vec) -> Self { + Self { status_code, body } + } + + pub fn is_status_ok(&self) -> bool { + self.status_code == 200 + } + + pub fn is_status_not_found(&self) -> bool { + self.status_code == 404 + } + + pub fn as_str(&self) -> Result<&str, crate::Error> { + match str::from_utf8(&self.body) { + Ok(s) => Ok(s), + Err(e) => Err(crate::Error::InvalidUtf8InBody(e)), + } + } +} + +pub enum TransactionApi { + Tx(Txid), + TxInfo(Txid), + TxStatus(Txid), + TxMerkeBlockProof(Txid), + TxMerkleProof(Txid), + TxOutputStatus(Txid, u64), + Broadcast(Transaction), +} + +// pub enum EsploraApi { +// Tx(TransactionApi), +// Block(BlocksApi), +// Fee(FeeEstimatesApi), +// // Mempool, +// } + +impl Client for TransactionApi { + fn request(&self, base_url: &str) -> Request { + match self { + TransactionApi::Tx(txid) => { + Request::new(Method::Get, format!("{}/tx/{}/raw", base_url, txid), None) + } + TransactionApi::TxStatus(txid) => Request::new( + Method::Get, + format!("{}/tx/{}/status", base_url, txid), + None, + ), + TransactionApi::TxInfo(txid) => { + Request::new(Method::Get, format!("{}/tx/{}", base_url, txid), None) + } + TransactionApi::TxMerkeBlockProof(txid) => Request::new( + Method::Get, + format!("{}/tx/{}/merkleblock-proof", base_url, txid), + None, + ), + TransactionApi::TxMerkleProof(txid) => Request::new( + Method::Get, + format!("{}/tx/{}/merkle-proof", base_url, txid), + None, + ), + TransactionApi::TxOutputStatus(txid, index) => Request::new( + Method::Get, + format!("{}/tx/{}/outspend/{}", base_url, txid, index), + None, + ), + TransactionApi::Broadcast(tx) => Request::new( + Method::Post, + format!("{}/tx", base_url), + Some( + bitcoin::consensus::encode::serialize(tx) + .to_lower_hex_string() + .as_bytes() + .to_vec(), + ), + ), + } + } + + fn deserialize_decodable(&self, response: &Response) -> Result { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + match self { + TransactionApi::TxMerkeBlockProof(_) => { + let hex_str = response.as_str()?; + let hex_vec = Vec::from_hex(hex_str).unwrap(); // TODO: (@leonardo) remove this unwrap + deserialize::(&hex_vec).map_err(crate::Error::BitcoinEncoding) + } + _ => deserialize::(&response.body).map_err(crate::Error::BitcoinEncoding), + } + } + + fn deserialize_json( + &self, + response: &Response, + ) -> Result { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + serde_json::from_slice(&response.body).map_err(crate::Error::SerdeJsonError) + } + + fn deserialize_str(&self, _response: &Response) -> Result { + unimplemented!("It's currently not needed by `TransactionApi`") + } +} + +pub enum AddressApi { + ScriptHashTxHistory(Hash), + ScriptHashConfirmedTxHistory(Hash, Txid), +} + +impl Client for AddressApi { + fn request(&self, base_url: &str) -> Request { + match self { + AddressApi::ScriptHashTxHistory(script_hash) => Request::new( + Method::Get, + format!("{base_url}/scripthash/{:x}/txs", script_hash), + None, + ), + AddressApi::ScriptHashConfirmedTxHistory(script_hash, last_seen) => Request::new( + Method::Get, + format!( + "{base_url}/scripthash/{:x}/txs/chain/{}", + script_hash, last_seen + ), + None, + ), + } + } + + fn deserialize_decodable(&self, _response: &Response) -> Result { + unimplemented!("It's currently not needed by `AddressApi`") + } + + fn deserialize_json( + &self, + response: &Response, + ) -> Result { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + serde_json::from_slice(&response.body).map_err(crate::Error::SerdeJsonError) + } + + fn deserialize_str(&self, _response: &Response) -> Result { + unimplemented!("It's currently not needed by `AddressApi`") + } +} + +pub enum BlocksApi { + BlockTxIdAtIndex(BlockHash, usize), + BlockHeader(BlockHash), + BlockStatus(BlockHash), + BlockRaw(BlockHash), + BlockTipHeight, + BlockTipHash, + BlockHash(u32), + BlockSummaries(Option), +} + +impl Client for BlocksApi { + fn request(&self, base_url: &str) -> Request { + match self { + BlocksApi::BlockTxIdAtIndex(block_hash, index) => Request::new( + Method::Get, + format!("{base_url}/block/{block_hash}/txid/{index}"), + None, + ), + BlocksApi::BlockHeader(block_hash) => Request::new( + Method::Get, + format!("{base_url}/block/{block_hash}/header"), + None, + ), + BlocksApi::BlockStatus(block_hash) => Request::new( + Method::Get, + format!("{base_url}/block/{block_hash}/status"), + None, + ), + BlocksApi::BlockRaw(block_hash) => Request::new( + Method::Get, + format!("{base_url}/block/{block_hash}/raw"), + None, + ), + BlocksApi::BlockTipHeight => { + Request::new(Method::Get, format!("{base_url}/blocks/tip/height"), None) + } + BlocksApi::BlockTipHash => { + Request::new(Method::Get, format!("{base_url}/blocks/tip/hash"), None) + } + BlocksApi::BlockHash(block_height) => Request::new( + Method::Get, + format!("{base_url}/block-height/{block_height}"), + None, + ), + BlocksApi::BlockSummaries(block_height) => match block_height { + Some(height) => { + Request::new(Method::Get, format!("{base_url}/blocks/{height}"), None) + } + None => Request::new(Method::Get, format!("{base_url}/blocks"), None), + }, + } + } + + fn deserialize_decodable(&self, response: &Response) -> Result { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + match self { + BlocksApi::BlockHeader(_) => { + let hex_str = response.as_str()?; + let hex_vec = Vec::from_hex(hex_str).unwrap(); // TODO: (@leonardo) remove this unwrap + deserialize::(&hex_vec).map_err(crate::Error::BitcoinEncoding) + }, + BlocksApi::BlockRaw(_) => { + deserialize::(&response.body).map_err(crate::Error::BitcoinEncoding) + }, + _ => unimplemented!("It cannot be deserialized by `deserialize_decodable`, use either `deserialize_str` or `deserialize_json` instead.") + } + } + + fn deserialize_json( + &self, + response: &Response, + ) -> Result { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + match self { + BlocksApi::BlockTxIdAtIndex(_, _) => { + unimplemented!("It cannot be deserialized by `deserialize_json`, use `deserialize_str` instead.") + } + BlocksApi::BlockHeader(_) => unimplemented!("It cannot be deserialized by `deserialize_json`, use `deserialize_decodable` instead."), + BlocksApi::BlockStatus(_) => serde_json::from_slice(&response.body).map_err(crate::Error::SerdeJsonError) +, + BlocksApi::BlockRaw(_) => { + unimplemented!("It cannot be deserialized by `deserialize_str`, use `deserialize_decodable` instead.") + } + BlocksApi::BlockTipHeight => { + unimplemented!("It cannot be deserialized by `deserialize_json`, use `deserialize_str` instead.") + } + BlocksApi::BlockTipHash | BlocksApi::BlockHash(_) => { + unimplemented!("It cannot be deserialized by `deserialize_json`, use `deserialize_str` instead.") + } + BlocksApi::BlockSummaries(_) => serde_json::from_slice(&response.body).map_err(crate::Error::SerdeJsonError), + } + } + + // TODO: (@leonardo) how can we return proper error here instead of unwrap ? + fn deserialize_str(&self, response: &Response) -> Result + where + ::Err: std::fmt::Debug, + { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + // serde_json::from_slice(&response.body).map_err(crate::Error::SerdeJsonError) + + match self { + BlocksApi::BlockTxIdAtIndex(_, _) => { + Ok(T::from_str(response.as_str()?).unwrap()) + } + BlocksApi::BlockHeader(_) => unimplemented!("It cannot be deserialized by `deserialize_str`, use `deserialize_decodable` instead."), + BlocksApi::BlockStatus(_) => unimplemented!("It cannot be deserialized by `deserialize_str`, use `deserialize_json` instead.") +, + BlocksApi::BlockRaw(_) => { + unimplemented!("It cannot be deserialized by `deserialize_str`, use `deserialize_decodable` instead.") + } + BlocksApi::BlockTipHeight => { + let height = T::from_str(response.as_str()?).unwrap(); + Ok(height) + } + BlocksApi::BlockTipHash | BlocksApi::BlockHash(_) => { + Ok(T::from_str(response.as_str()?).unwrap()) + } + BlocksApi::BlockSummaries(_) => unimplemented!("It cannot be deserialized by `deserialize_str`, use `deserialize_json` instead."), + } + } +} + +pub enum FeeEstimatesApi { + FeeRate, +} + +impl Client for FeeEstimatesApi { + fn request(&self, base_url: &str) -> Request { + match self { + FeeEstimatesApi::FeeRate => { + Request::new(Method::Get, format!("{base_url}/fee-estimates"), None) + } + } + } + + fn deserialize_decodable(&self, _response: &Response) -> Result { + unimplemented!("It's currently not needed by `FeeEstimatesApi`") + } + + fn deserialize_json( + &self, + response: &Response, + ) -> Result { + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(crate::Error::StatusCode)?; + let message = response.as_str()?.to_string(); + return Err(crate::Error::HttpResponse { status, message }); + } + + serde_json::from_slice(&response.body).map_err(crate::Error::SerdeJsonError) + } + + fn deserialize_str(&self, _response: &Response) -> Result + where + ::Err: std::fmt::Debug, + { + unimplemented!("It's currently not needed by `FeeEstimatesApi`") + } +} + +#[derive(Debug)] +pub enum Error { + Client(E), + // Decoding(E), + // // InvalidUtf8InBody(str::Utf8Error), + // Error(crate::Error), +} + +pub trait Client { + fn request(&self, base_url: &str) -> Request; + + fn send(&self, base_url: &str, handler: &mut F) -> Result> + where + F: FnMut(Request) -> Result, + { + let request = self.request(base_url); + let response = handler(request).map_err(Error::Client)?; + + Ok(response) + } + + fn deserialize_decodable(&self, response: &Response) -> Result; + + // TODO: (@leonardo) Is there any way this can be unified with deserialize_decodable ? + // fn deserialize_hex(&self, response: &Response) -> Result; + + fn deserialize_json( + &self, + response: &Response, + ) -> Result; + + fn deserialize_str(&self, response: &Response) -> Result + where + ::Err: std::fmt::Debug; + + // fn execute(&self, executor: &mut F) -> Response; + + // fn json(&self) {} + + // fn hex(&self) {} + + // fn send(&self) {} +} + #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct PrevOut { pub value: u64, diff --git a/src/blocking.rs b/src/blocking.rs index 22c95fd..5f0d5d1 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -13,21 +13,53 @@ use std::collections::HashMap; use std::convert::TryFrom; -use std::str::FromStr; #[allow(unused_imports)] use log::{debug, error, info, trace}; use minreq::{Proxy, Request}; -use bitcoin::consensus::{deserialize, serialize, Decodable}; use bitcoin::hashes::{sha256, Hash}; -use bitcoin::hex::{DisplayHex, FromHex}; use bitcoin::{ block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid, }; -use crate::{BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus}; +use crate::{ + AddressApi, BlockStatus, BlockSummary, BlocksApi, Builder, Client, Error, FeeEstimatesApi, + MerkleProof, OutputStatus, TransactionApi, Tx, TxStatus, +}; + +pub(crate) fn handler(request: crate::Request) -> Result { + let mut minreq_request = match request.method { + crate::Method::Get => minreq::Request::new(minreq::Method::Get, request.url), + crate::Method::Post => minreq::Request::new(minreq::Method::Post, request.url) + .with_body(request.body.expect("It should've a non-empty body!")), + }; + + if let Some(proxy) = request.proxy { + let proxy = Proxy::new(proxy.as_str())?; + minreq_request = minreq_request.with_proxy(proxy); + } + + if let Some(timeout) = request.timeout { + minreq_request = minreq_request.with_timeout(timeout); + } + + if !request.headers.is_empty() { + for (key, value) in &request.headers { + minreq_request = minreq_request.with_header(key, value); + } + } + + let minreq_response = minreq_request.send()?; + + let response = crate::Response::new( + minreq_response.status_code, + minreq_response.as_bytes().to_vec(), + ); + + Ok(response) +} #[derive(Debug, Clone)] pub struct BlockingClient { @@ -78,116 +110,122 @@ impl BlockingClient { Ok(request) } - fn get_opt_response(&self, path: &str) -> Result, Error> { - match self.get_request(path)?.send() { - Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => Ok(Some(deserialize::(resp.as_bytes())?)), - Err(e) => Err(Error::Minreq(e)), - } - } - - fn get_opt_response_txid(&self, path: &str) -> Result, Error> { - match self.get_request(path)?.send() { - Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => Ok(Some( - Txid::from_str(resp.as_str().map_err(Error::Minreq)?).map_err(Error::HexToArray)?, - )), - Err(e) => Err(Error::Minreq(e)), - } - } - - fn get_opt_response_hex(&self, path: &str) -> Result, Error> { - match self.get_request(path)?.send() { - Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => { - let hex_str = resp.as_str().map_err(Error::Minreq)?; - let hex_vec = Vec::from_hex(hex_str).unwrap(); - deserialize::(&hex_vec) - .map_err(Error::BitcoinEncoding) - .map(|r| Some(r)) - } - Err(e) => Err(Error::Minreq(e)), - } - } - - fn get_response_hex(&self, path: &str) -> Result { - match self.get_request(path)?.send() { - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => { - let hex_str = resp.as_str().map_err(Error::Minreq)?; - let hex_vec = Vec::from_hex(hex_str).unwrap(); - deserialize::(&hex_vec).map_err(Error::BitcoinEncoding) - } - Err(e) => Err(Error::Minreq(e)), - } - } - - fn get_response_json<'a, T: serde::de::DeserializeOwned>( - &'a self, - path: &'a str, - ) -> Result { - let response = self.get_request(path)?.send(); - match response { - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => Ok(resp.json::().map_err(Error::Minreq)?), - Err(e) => Err(Error::Minreq(e)), - } - } - - fn get_opt_response_json( - &self, - path: &str, - ) -> Result, Error> { - match self.get_request(path)?.send() { - Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => Ok(Some(resp.json::()?)), - Err(e) => Err(Error::Minreq(e)), - } - } - - fn get_response_str(&self, path: &str) -> Result { - match self.get_request(path)?.send() { - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(resp) => Ok(resp.as_str()?.to_string()), - Err(e) => Err(Error::Minreq(e)), - } - } + // fn get_opt_response(&self, path: &str) -> Result, Error> { + // match self.get_request(path)?.send() { + // Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => Ok(Some(deserialize::(resp.as_bytes())?)), + // Err(e) => Err(Error::Minreq(e)), + // } + // } + + // fn get_opt_response_txid(&self, path: &str) -> Result, Error> { + // match self.get_request(path)?.send() { + // Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => Ok(Some( + // Txid::from_str(resp.as_str().map_err(Error::Minreq)?).map_err(Error::HexToArray)?, + // )), + // Err(e) => Err(Error::Minreq(e)), + // } + // } + + // fn get_opt_response_hex(&self, path: &str) -> Result, Error> { + // match self.get_request(path)?.send() { + // Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => { + // let hex_str = resp.as_str().map_err(Error::Minreq)?; + // let hex_vec = Vec::from_hex(hex_str).unwrap(); + // deserialize::(&hex_vec) + // .map_err(Error::BitcoinEncoding) + // .map(|r| Some(r)) + // } + // Err(e) => Err(Error::Minreq(e)), + // } + // } + + // fn get_response_hex(&self, path: &str) -> Result { + // match self.get_request(path)?.send() { + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => { + // let hex_str = resp.as_str().map_err(Error::Minreq)?; + // let hex_vec = Vec::from_hex(hex_str).unwrap(); + // deserialize::(&hex_vec).map_err(Error::BitcoinEncoding) + // } + // Err(e) => Err(Error::Minreq(e)), + // } + // } + + // fn get_response_json<'a, T: serde::de::DeserializeOwned>( + // &'a self, + // path: &'a str, + // ) -> Result { + // let response = self.get_request(path)?.send(); + // match response { + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => Ok(resp.json::().map_err(Error::Minreq)?), + // Err(e) => Err(Error::Minreq(e)), + // } + // } + + // fn get_opt_response_json( + // &self, + // path: &str, + // ) -> Result, Error> { + // match self.get_request(path)?.send() { + // Ok(resp) if is_status_not_found(resp.status_code) => Ok(None), + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => Ok(Some(resp.json::()?)), + // Err(e) => Err(Error::Minreq(e)), + // } + // } + + // fn get_response_str(&self, path: &str) -> Result { + // match self.get_request(path)?.send() { + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(resp) => Ok(resp.as_str()?.to_string()), + // Err(e) => Err(Error::Minreq(e)), + // } + // } /// Get a [`Transaction`] option given its [`Txid`] pub fn get_tx(&self, txid: &Txid) -> Result, Error> { - self.get_opt_response(&format!("/tx/{}/raw", txid)) + let tx_api = TransactionApi::Tx(*txid); + let response = tx_api.send(&self.url, &mut handler).unwrap(); + match tx_api.deserialize_decodable::(&response) { + Ok(transaction) => Ok(Some(transaction)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } } /// Get a [`Transaction`] given its [`Txid`]. @@ -199,6 +237,27 @@ impl BlockingClient { } } + /// Get the status of a [`Transaction`] given its [`Txid`]. + pub fn get_tx_status(&self, txid: &Txid) -> Result { + let tx_api = TransactionApi::TxStatus(*txid); + let response = tx_api.send(&self.url, &mut handler).unwrap(); + match tx_api.deserialize_json::(&response) { + Ok(tx_status) => Ok(tx_status), + Err(e) => Err(e), + } + } + + /// Get transaction info given it's [`Txid`]. + pub fn get_tx_info(&self, txid: &Txid) -> Result, Error> { + let tx_api = TransactionApi::TxInfo(*txid); + let response = tx_api.send(&self.url, &mut handler).unwrap(); + match tx_api.deserialize_json::(&response) { + Ok(tx) => Ok(Some(tx)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } + } + /// Get a [`Txid`] of a transaction given its index in a block with a given /// hash. pub fn get_txid_at_block_index( @@ -206,44 +265,62 @@ impl BlockingClient { block_hash: &BlockHash, index: usize, ) -> Result, Error> { - self.get_opt_response_txid(&format!("/block/{}/txid/{}", block_hash, index)) - } - - /// Get the status of a [`Transaction`] given its [`Txid`]. - pub fn get_tx_status(&self, txid: &Txid) -> Result { - self.get_response_json(&format!("/tx/{}/status", txid)) - } - - /// Get transaction info given it's [`Txid`]. - pub fn get_tx_info(&self, txid: &Txid) -> Result, Error> { - self.get_opt_response_json(&format!("/tx/{}", txid)) + let api = BlocksApi::BlockTxIdAtIndex(*block_hash, index); + let response = api.send(&self.url, &mut handler).unwrap(); + match api.deserialize_str::(&response) { + Ok(txid) => Ok(Some(txid)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } } /// Get a [`BlockHeader`] given a particular block hash. pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result { - self.get_response_hex(&format!("/block/{}/header", block_hash)) + let api = BlocksApi::BlockHeader(*block_hash); + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_decodable::(&response) } /// Get the [`BlockStatus`] given a particular [`BlockHash`]. pub fn get_block_status(&self, block_hash: &BlockHash) -> Result { - self.get_response_json(&format!("/block/{}/status", block_hash)) + let api = BlocksApi::BlockStatus(*block_hash); + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_json::(&response) } /// Get a [`Block`] given a particular [`BlockHash`]. pub fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result, Error> { - self.get_opt_response(&format!("/block/{}/raw", block_hash)) + let api = BlocksApi::BlockRaw(*block_hash); + let response = api.send(&self.url, &mut handler).unwrap(); + match api.deserialize_decodable::(&response) { + Ok(block) => Ok(Some(block)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } } /// Get a merkle inclusion proof for a [`Transaction`] with the given /// [`Txid`]. pub fn get_merkle_proof(&self, txid: &Txid) -> Result, Error> { - self.get_opt_response_json(&format!("/tx/{}/merkle-proof", txid)) + let tx_api = TransactionApi::TxMerkleProof(*txid); + let response = tx_api.send(&self.url, &mut handler).unwrap(); + match tx_api.deserialize_json::(&response) { + Ok(merkle_proof) => Ok(Some(merkle_proof)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } } /// Get a [`MerkleBlock`] inclusion proof for a [`Transaction`] with the /// given [`Txid`]. pub fn get_merkle_block(&self, txid: &Txid) -> Result, Error> { - self.get_opt_response_hex(&format!("/tx/{}/merkleblock-proof", txid)) + let tx_api = TransactionApi::TxMerkeBlockProof(*txid); + let response = tx_api.send(&self.url, &mut handler).unwrap(); + match tx_api.deserialize_decodable::(&response) { + Ok(merkle_block) => Ok(Some(merkle_block)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } } /// Get the spending status of an output given a [`Txid`] and the output @@ -253,60 +330,82 @@ impl BlockingClient { txid: &Txid, index: u64, ) -> Result, Error> { - self.get_opt_response_json(&format!("/tx/{}/outspend/{}", txid, index)) + let tx_api = TransactionApi::TxOutputStatus(*txid, index); + let response = tx_api.send(&self.url, &mut handler).unwrap(); + match tx_api.deserialize_json::(&response) { + Ok(output_status) => Ok(Some(output_status)), + Err(Error::HttpResponse { status: 404, .. }) => Ok(None), + Err(e) => Err(e), + } } /// Broadcast a [`Transaction`] to Esplora pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> { - let mut request = minreq::post(format!("{}/tx", self.url)).with_body( - serialize(transaction) - .to_lower_hex_string() - .as_bytes() - .to_vec(), - ); - - if let Some(proxy) = &self.proxy { - let proxy = Proxy::new(proxy.as_str())?; - request = request.with_proxy(proxy); - } + let tx_api = TransactionApi::Broadcast(transaction.clone()); + let response = tx_api.send(&self.url, &mut handler).unwrap(); - if let Some(timeout) = &self.timeout { - request = request.with_timeout(*timeout); + if !response.is_status_ok() { + let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?; + let message = response.as_str().unwrap_or_default().to_string(); + return Err(Error::HttpResponse { status, message }); } - match request.send() { - Ok(resp) if !is_status_ok(resp.status_code) => { - let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; - let message = resp.as_str().unwrap_or_default().to_string(); - Err(Error::HttpResponse { status, message }) - } - Ok(_resp) => Ok(()), - Err(e) => Err(Error::Minreq(e)), - } + Ok(()) + + // let mut request = minreq::post(format!("{}/tx", self.url)).with_body( + // serialize(transaction) + // .to_lower_hex_string() + // .as_bytes() + // .to_vec(), + // ); + + // if let Some(proxy) = &self.proxy { + // let proxy = Proxy::new(proxy.as_str())?; + // request = request.with_proxy(proxy); + // } + + // if let Some(timeout) = &self.timeout { + // request = request.with_timeout(*timeout); + // } + + // match request.send() { + // Ok(resp) if !is_status_ok(resp.status_code) => { + // let status = u16::try_from(resp.status_code).map_err(Error::StatusCode)?; + // let message = resp.as_str().unwrap_or_default().to_string(); + // Err(Error::HttpResponse { status, message }) + // } + // Ok(_resp) => Ok(()), + // Err(e) => Err(Error::Minreq(e)), + // } } /// Get the height of the current blockchain tip. pub fn get_height(&self) -> Result { - self.get_response_str("/blocks/tip/height") - .map(|s| u32::from_str(s.as_str()).map_err(Error::Parsing))? + let api = BlocksApi::BlockTipHeight; + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_str::(&response) } /// Get the [`BlockHash`] of the current blockchain tip. pub fn get_tip_hash(&self) -> Result { - self.get_response_str("/blocks/tip/hash") - .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))? + let api = BlocksApi::BlockTipHash; + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_str::(&response) } /// Get the [`BlockHash`] of a specific block height pub fn get_block_hash(&self, block_height: u32) -> Result { - self.get_response_str(&format!("/block-height/{}", block_height)) - .map(|s| BlockHash::from_str(s.as_str()).map_err(Error::HexToArray))? + let api = BlocksApi::BlockHash(block_height); + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_str::(&response) } /// Get an map where the key is the confirmation target (in number of /// blocks) and the value is the estimated feerate (in sat/vB). pub fn get_fee_estimates(&self) -> Result, Error> { - self.get_response_json("/fee-estimates") + let api = FeeEstimatesApi::FeeRate; + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_json::>(&response) } /// Get confirmed transaction history for the specified address/scripthash, @@ -319,11 +418,15 @@ impl BlockingClient { last_seen: Option, ) -> Result, Error> { let script_hash = sha256::Hash::hash(script.as_bytes()); - let path = match last_seen { - Some(last_seen) => format!("/scripthash/{:x}/txs/chain/{}", script_hash, last_seen), - None => format!("/scripthash/{:x}/txs", script_hash), + let address_api = match last_seen { + Some(last_seen) => AddressApi::ScriptHashConfirmedTxHistory(script_hash, last_seen), + None => AddressApi::ScriptHashTxHistory(script_hash), }; - self.get_response_json(&path) + let response = address_api.send(&self.url, &mut handler).unwrap(); + match address_api.deserialize_json::>(&response) { + Ok(txs) => Ok(txs), + Err(e) => Err(e), + } } /// Gets some recent block summaries starting at the tip or at `height` if @@ -332,18 +435,16 @@ impl BlockingClient { /// The maximum number of summaries returned depends on the backend itself: /// esplora returns `10` while [mempool.space](https://mempool.space/docs/api) returns `15`. pub fn get_blocks(&self, height: Option) -> Result, Error> { - let path = match height { - Some(height) => format!("/blocks/{}", height), - None => "/blocks".to_string(), - }; - self.get_response_json(&path) + let api = BlocksApi::BlockSummaries(height); + let response = api.send(&self.url, &mut handler).unwrap(); + api.deserialize_json::>(&response) } } -fn is_status_ok(status: i32) -> bool { - status == 200 -} +// fn is_status_ok(status: i32) -> bool { +// status == 200 +// } -fn is_status_not_found(status: i32) -> bool { - status == 404 -} +// fn is_status_not_found(status: i32) -> bool { +// status == 404 +// } diff --git a/src/lib.rs b/src/lib.rs index 7b7efc3..70becd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,7 +172,10 @@ pub enum Error { #[cfg(feature = "async")] Reqwest(::reqwest::Error), /// HTTP response error - HttpResponse { status: u16, message: String }, + HttpResponse { + status: u16, + message: String, + }, /// Invalid number returned Parsing(std::num::ParseIntError), /// Invalid status code, unable to convert to `u16` @@ -193,6 +196,10 @@ pub enum Error { InvalidHttpHeaderName(String), /// Invalid HTTP Header value specified InvalidHttpHeaderValue(String), + // TODO: (@leonardo) + InvalidUtf8InBody(core::str::Utf8Error), + /// Ran into a Serde error. + SerdeJsonError(serde_json::Error), } impl fmt::Display for Error {