use std::collections::HashMap;
use std::io;
use std::io::Read;
use std::str::FromStr;
use std::time::Duration;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use ureq::{Agent, Proxy, Response};
use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::{Block, BlockHash, BlockHeader, MerkleBlock, Script, Transaction, Txid};
use crate::{BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus};
#[derive(Debug, Clone)]
pub struct BlockingClient {
url: String,
agent: Agent,
}
impl BlockingClient {
pub fn from_builder(builder: Builder) -> Result<Self, Error> {
let mut agent_builder = ureq::AgentBuilder::new();
if let Some(timeout) = builder.timeout {
agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
}
if let Some(proxy) = &builder.proxy {
agent_builder = agent_builder.proxy(Proxy::new(proxy)?);
}
Ok(Self::from_agent(builder.base_url, agent_builder.build()))
}
pub fn from_agent(url: String, agent: Agent) -> Self {
BlockingClient { url, agent }
}
pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
let resp = self
.agent
.get(&format!("{}/tx/{}/raw", self.url, txid))
.call();
match resp {
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(Error::HttpResponse(code))
}
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
match self.get_tx(txid) {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(Error::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
pub fn get_txid_at_block_index(
&self,
block_hash: &BlockHash,
index: usize,
) -> Result<Option<Txid>, Error> {
let resp = self
.agent
.get(&format!("{}/block/{}/txid/{}", self.url, block_hash, index))
.call();
match resp {
Ok(resp) => Ok(Some(Txid::from_str(&resp.into_string()?)?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(Error::HttpResponse(code))
}
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
let resp = self
.agent
.get(&format!("{}/tx/{}/status", self.url, txid))
.call();
match resp {
Ok(resp) => Ok(resp.into_json()?),
Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}
}
#[deprecated(
since = "0.2.0",
note = "Deprecated to improve alignment with Esplora API. Users should use `get_block_hash` and `get_header_by_hash` methods directly."
)]
pub fn get_header(&self, block_height: u32) -> Result<BlockHeader, Error> {
let block_hash = self.get_block_hash(block_height)?;
self.get_header_by_hash(&block_hash)
}
pub fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
let resp = self
.agent
.get(&format!("{}/block/{}/header", self.url, block_hash))
.call();
match resp {
Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
let resp = self
.agent
.get(&format!("{}/block/{}/status", self.url, block_hash))
.call();
match resp {
Ok(resp) => Ok(resp.into_json()?),
Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
let resp = self
.agent
.get(&format!("{}/block/{}/raw", self.url, block_hash))
.call();
match resp {
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(Error::HttpResponse(code))
}
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_merkle_proof(&self, txid: &Txid) -> Result<Option<MerkleProof>, Error> {
let resp = self
.agent
.get(&format!("{}/tx/{}/merkle-proof", self.url, txid))
.call();
match resp {
Ok(resp) => Ok(Some(resp.into_json()?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(Error::HttpResponse(code))
}
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_merkle_block(&self, txid: &Txid) -> Result<Option<MerkleBlock>, Error> {
let resp = self
.agent
.get(&format!("{}/tx/{}/merkleblock-proof", self.url, txid))
.call();
match resp {
Ok(resp) => Ok(Some(deserialize(&Vec::from_hex(&resp.into_string()?)?)?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(Error::HttpResponse(code))
}
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_output_status(
&self,
txid: &Txid,
index: u64,
) -> Result<Option<OutputStatus>, Error> {
let resp = self
.agent
.get(&format!("{}/tx/{}/outspend/{}", self.url, txid, index))
.call();
match resp {
Ok(resp) => Ok(Some(resp.into_json()?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(Error::HttpResponse(code))
}
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn broadcast(&self, transaction: &Transaction) -> Result<(), Error> {
let resp = self
.agent
.post(&format!("{}/tx", self.url))
.send_string(&serialize(transaction).to_hex());
match resp {
Ok(_) => Ok(()), Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_height(&self) -> Result<u32, Error> {
let resp = self
.agent
.get(&format!("{}/blocks/tip/height", self.url))
.call();
match resp {
Ok(resp) => Ok(resp.into_string()?.parse()?),
Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_tip_hash(&self) -> Result<BlockHash, Error> {
let resp = self
.agent
.get(&format!("{}/blocks/tip/hash", self.url))
.call();
Self::process_block_result(resp)
}
pub fn get_block_hash(&self, block_height: u32) -> Result<BlockHash, Error> {
let resp = self
.agent
.get(&format!("{}/block-height/{}", self.url, block_height))
.call();
if let Err(ureq::Error::Status(code, _)) = resp {
if is_status_not_found(code) {
return Err(Error::HeaderHeightNotFound(block_height));
}
}
Self::process_block_result(resp)
}
fn process_block_result(response: Result<Response, ureq::Error>) -> Result<BlockHash, Error> {
match response {
Ok(resp) => Ok(BlockHash::from_str(&resp.into_string()?)?),
Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}
}
pub fn get_fee_estimates(&self) -> Result<HashMap<String, f64>, Error> {
let resp = self
.agent
.get(&format!("{}/fee-estimates", self.url,))
.call();
let map = match resp {
Ok(resp) => {
let map: HashMap<String, f64> = resp.into_json()?;
Ok(map)
}
Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
Err(e) => Err(Error::Ureq(e)),
}?;
Ok(map)
}
pub fn scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<Tx>, Error> {
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
let url = match last_seen {
Some(last_seen) => format!(
"{}/scripthash/{}/txs/chain/{}",
self.url, script_hash, last_seen
),
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
};
Ok(self.agent.get(&url).call()?.into_json()?)
}
pub fn get_blocks(&self, height: Option<u32>) -> Result<Vec<BlockSummary>, Error> {
let url = match height {
Some(height) => format!("{}/blocks/{}", self.url, height),
None => format!("{}/blocks", self.url),
};
Ok(self.agent.get(&url).call()?.into_json()?)
}
pub fn url(&self) -> &str {
&self.url
}
pub fn agent(&self) -> &Agent {
&self.agent
}
}
fn is_status_not_found(status: u16) -> bool {
status == 404
}
fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
let mut buf: Vec<u8> = vec![];
resp.into_reader()
.take((BYTES_LIMIT + 1) as u64)
.read_to_end(&mut buf)?;
if buf.len() > BYTES_LIMIT {
return Err(io::Error::new(
io::ErrorKind::Other,
"response too big for into_bytes",
));
}
Ok(buf)
}
impl From<ureq::Error> for Error {
fn from(e: ureq::Error) -> Self {
match e {
ureq::Error::Status(code, _) => Error::HttpResponse(code),
e => Error::Ureq(e),
}
}
}