use std::{
env, mem,
path::{Path, PathBuf},
sync::Arc,
thread::sleep,
time::Duration,
};
use bitcoin::consensus::encode;
use brk_error::{Error, Result};
use brk_types::{
BlockHash, Height, MempoolEntryInfo, Sats, Transaction, TxIn, TxOut, TxStatus, TxWithHex, Txid,
Vout,
};
pub mod backend;
pub use backend::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, TxOutInfo};
use backend::ClientInner;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct Client(Arc<ClientInner>);
impl Client {
pub fn new(url: &str, auth: Auth) -> Result<Self> {
Self::new_with(url, auth, 1_000_000, Duration::from_secs(1))
}
pub fn new_with(
url: &str,
auth: Auth,
max_retries: usize,
retry_delay: Duration,
) -> Result<Self> {
Ok(Self(Arc::new(ClientInner::new(
url,
auth,
max_retries,
retry_delay,
)?)))
}
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
self.0.get_blockchain_info()
}
pub fn get_block<'a, H>(&self, hash: &'a H) -> Result<bitcoin::Block>
where
&'a H: Into<&'a bitcoin::BlockHash>,
{
self.0.get_block(hash.into())
}
pub fn get_block_count(&self) -> Result<u64> {
self.0.get_block_count()
}
pub fn get_last_height(&self) -> Result<Height> {
self.0.get_block_count().map(Height::from)
}
pub fn get_block_hash<H>(&self, height: H) -> Result<BlockHash>
where
H: Into<u64> + Copy,
{
self.0.get_block_hash(height.into()).map(BlockHash::from)
}
pub fn get_block_hashes_range<H1, H2>(&self, start: H1, end: H2) -> Result<Vec<BlockHash>>
where
H1: Into<u64>,
H2: Into<u64>,
{
self.0
.get_block_hashes_range(start.into(), end.into())
.map(|v| v.into_iter().map(BlockHash::from).collect())
}
pub fn get_block_header<'a, H>(&self, hash: &'a H) -> Result<bitcoin::block::Header>
where
&'a H: Into<&'a bitcoin::BlockHash>,
{
self.0.get_block_header(hash.into())
}
pub fn get_block_info<'a, H>(&self, hash: &'a H) -> Result<BlockInfo>
where
&'a H: Into<&'a bitcoin::BlockHash>,
{
self.0.get_block_info(hash.into())
}
pub fn get_block_header_info<'a, H>(&self, hash: &'a H) -> Result<BlockHeaderInfo>
where
&'a H: Into<&'a bitcoin::BlockHash>,
{
self.0.get_block_header_info(hash.into())
}
pub fn get_transaction<'a, T, H>(
&self,
txid: &'a T,
block_hash: Option<&'a H>,
) -> brk_error::Result<bitcoin::Transaction>
where
&'a T: Into<&'a bitcoin::Txid>,
&'a H: Into<&'a bitcoin::BlockHash>,
{
let tx = self.get_raw_transaction(txid, block_hash)?;
Ok(tx)
}
pub fn get_mempool_transaction(&self, txid: &Txid) -> Result<TxWithHex> {
let hex = self.get_raw_transaction_hex(txid, None as Option<&BlockHash>)?;
let mut tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
let input = mem::take(&mut tx.input)
.into_iter()
.map(|txin| -> Result<TxIn> {
let txout_result = self.get_tx_out(
(&txin.previous_output.txid).into(),
txin.previous_output.vout.into(),
Some(true),
)?;
let is_coinbase = txout_result.as_ref().is_none_or(|r| r.coinbase);
let txout = if let Some(txout_result) = txout_result {
Some(TxOut::from((
txout_result.script_pub_key,
txout_result.value,
)))
} else {
None
};
let witness = txin
.witness
.iter()
.map(bitcoin::hex::DisplayHex::to_lower_hex_string)
.collect();
Ok(TxIn {
is_coinbase,
prevout: txout,
txid: txin.previous_output.txid.into(),
vout: txin.previous_output.vout.into(),
script_sig: txin.script_sig,
script_sig_asm: (),
witness,
sequence: txin.sequence.into(),
inner_redeem_script_asm: (),
inner_witness_script_asm: (),
})
})
.collect::<Result<Vec<_>>>()?;
let mut tx = Transaction {
index: None,
txid: txid.clone(),
version: tx.version.into(),
total_sigop_cost: tx.total_sigop_cost(|_| None),
weight: tx.weight().into(),
lock_time: tx.lock_time.into(),
total_size: tx.total_size(),
fee: Sats::default(),
input,
output: tx.output.into_iter().map(TxOut::from).collect(),
status: TxStatus::UNCONFIRMED,
};
tx.compute_fee();
Ok(TxWithHex::new(tx, hex))
}
pub fn get_tx_out(
&self,
txid: &Txid,
vout: Vout,
include_mempool: Option<bool>,
) -> Result<Option<TxOutInfo>> {
self.0.get_tx_out(txid.into(), vout.into(), include_mempool)
}
pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
self.0
.get_raw_mempool()
.map(|v| unsafe { mem::transmute(v) })
}
pub fn get_raw_mempool_verbose(&self) -> Result<Vec<MempoolEntryInfo>> {
let result = self.0.get_raw_mempool_verbose()?;
Ok(result
.into_iter()
.map(
|(txid, entry): (bitcoin::Txid, backend::RawMempoolEntry)| MempoolEntryInfo {
txid: txid.into(),
vsize: entry.vsize,
weight: entry.weight,
fee: Sats::from(entry.base_fee_sats),
ancestor_count: entry.ancestor_count,
ancestor_size: entry.ancestor_size,
ancestor_fee: Sats::from(entry.ancestor_fee_sats),
depends: entry.depends.into_iter().map(Txid::from).collect(),
},
)
.collect())
}
pub fn get_raw_transaction<'a, T, H>(
&self,
txid: &'a T,
block_hash: Option<&'a H>,
) -> brk_error::Result<bitcoin::Transaction>
where
&'a T: Into<&'a bitcoin::Txid>,
&'a H: Into<&'a bitcoin::BlockHash>,
{
let hex = self.get_raw_transaction_hex(txid, block_hash)?;
let tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
Ok(tx)
}
pub fn get_raw_transaction_hex<'a, T, H>(
&self,
txid: &'a T,
block_hash: Option<&'a H>,
) -> Result<String>
where
&'a T: Into<&'a bitcoin::Txid>,
&'a H: Into<&'a bitcoin::BlockHash>,
{
self.0
.get_raw_transaction_hex(txid.into(), block_hash.map(|h| h.into()))
}
pub fn send_raw_transaction(&self, hex: &str) -> Result<Txid> {
self.0.send_raw_transaction(hex).map(Txid::from)
}
pub fn is_in_main_chain(&self, hash: &BlockHash) -> Result<bool> {
let block_info = self.get_block_info(hash)?;
Ok(block_info.confirmations > 0)
}
pub fn get_closest_valid_height(&self, hash: BlockHash) -> Result<(Height, BlockHash)> {
debug!("Get closest valid height...");
match self.get_block_header_info(&hash) {
Ok(block_info) => {
if self.is_in_main_chain(&hash)? {
return Ok((block_info.height.into(), hash));
}
let mut hash =
block_info
.previous_block_hash
.map(BlockHash::from)
.ok_or(Error::NotFound(
"Genesis block has no previous block".into(),
))?;
loop {
if self.is_in_main_chain(&hash)? {
let current_info = self.get_block_header_info(&hash)?;
return Ok((current_info.height.into(), hash));
}
let info = self.get_block_header_info(&hash)?;
hash = info
.previous_block_hash
.map(BlockHash::from)
.ok_or(Error::NotFound(
"Reached genesis without finding main chain".into(),
))?;
}
}
Err(_) => Err(Error::NotFound("Block hash not found in blockchain".into())),
}
}
pub fn wait_for_synced_node(&self) -> Result<()> {
let is_synced = || -> Result<bool> {
let info = self.get_blockchain_info()?;
Ok(info.headers == info.blocks)
};
if !is_synced()? {
info!("Waiting for node to sync...");
while !is_synced()? {
sleep(Duration::from_secs(1))
}
}
Ok(())
}
#[cfg(feature = "bitcoincore-rpc")]
pub fn call<F, T>(&self, f: F) -> Result<T, bitcoincore_rpc::Error>
where
F: Fn(&bitcoincore_rpc::Client) -> Result<T, bitcoincore_rpc::Error>,
{
self.0.call_with_retry(f)
}
#[cfg(feature = "bitcoincore-rpc")]
pub fn call_once<F, T>(&self, f: F) -> Result<T, bitcoincore_rpc::Error>
where
F: Fn(&bitcoincore_rpc::Client) -> Result<T, bitcoincore_rpc::Error>,
{
self.0.call_once(f)
}
pub fn default_url() -> &'static str {
"http://localhost:8332"
}
pub fn default_bitcoin_path() -> PathBuf {
if env::consts::OS == "macos" {
Self::default_mac_bitcoin_path()
} else {
Self::default_linux_bitcoin_path()
}
}
pub fn default_linux_bitcoin_path() -> PathBuf {
Path::new(&env::var("HOME").unwrap()).join(".bitcoin")
}
pub fn default_mac_bitcoin_path() -> PathBuf {
Path::new(&env::var("HOME").unwrap())
.join("Library")
.join("Application Support")
.join("Bitcoin")
}
}