#![deny(clippy::pedantic)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
pub mod metrics;
use std::env;
use std::fmt::Debug;
use std::sync::Arc;
use anyhow::{Result, format_err};
use bitcoin::{ScriptBuf, Transaction, Txid};
use esplora_client::{AsyncClient, Builder};
use fedimint_core::envs::FM_FORCE_BITCOIN_RPC_URL_ENV;
use fedimint_core::time::now;
use fedimint_core::txoproof::TxOutProof;
use fedimint_core::util::{FmtCompactResultAnyhow as _, SafeUrl};
use fedimint_core::{apply, async_trait_maybe_send};
use fedimint_logging::LOG_BITCOIND;
use fedimint_metrics::HistogramExt as _;
use tracing::trace;
use crate::metrics::{BITCOIND_RPC_DURATION_SECONDS, BITCOIND_RPC_REQUESTS_TOTAL};
#[cfg(feature = "bitcoincore")]
pub mod bitcoincore;
#[derive(Debug, Clone)]
pub struct BlockchainInfo {
pub block_height: u64,
pub synced: bool,
}
pub fn create_esplora_rpc(url: &SafeUrl) -> Result<DynBitcoindRpc> {
let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
.ok()
.map(|s| SafeUrl::parse(&s))
.transpose()?
.unwrap_or_else(|| url.clone());
Ok(EsploraClient::new(&url)?.into_dyn())
}
pub type DynBitcoindRpc = Arc<dyn IBitcoindRpc + Send + Sync>;
#[apply(async_trait_maybe_send!)]
pub trait IBitcoindRpc: Debug + Send + Sync + 'static {
async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
async fn get_info(&self) -> Result<BlockchainInfo>;
fn into_dyn(self) -> DynBitcoindRpc
where
Self: Sized,
{
Arc::new(self)
}
}
pub struct BitcoindTracked {
inner: DynBitcoindRpc,
name: &'static str,
}
impl std::fmt::Debug for BitcoindTracked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BitcoindTracked")
.field("name", &self.name)
.field("inner", &self.inner)
.finish()
}
}
impl BitcoindTracked {
pub fn new(inner: DynBitcoindRpc, name: &'static str) -> Self {
Self { inner, name }
}
fn record_call<T>(&self, method: &str, result: &Result<T>) {
let result_label = if result.is_ok() { "success" } else { "error" };
BITCOIND_RPC_REQUESTS_TOTAL
.with_label_values(&[method, self.name, result_label])
.inc();
}
}
macro_rules! tracked_call {
($self:ident, $method:expr, $call:expr) => {{
trace!(
target: LOG_BITCOIND,
method = $method,
name = $self.name,
"starting bitcoind rpc"
);
let start = now();
let timer = BITCOIND_RPC_DURATION_SECONDS
.with_label_values(&[$method, $self.name])
.start_timer_ext();
let result = $call;
timer.observe_duration();
$self.record_call($method, &result);
let duration_ms = now()
.duration_since(start)
.unwrap_or_default()
.as_secs_f64()
* 1000.0;
trace!(
target: LOG_BITCOIND,
method = $method,
name = $self.name,
duration_ms,
error = %result.fmt_compact_result_anyhow(),
"completed bitcoind rpc"
);
result
}};
}
#[apply(async_trait_maybe_send!)]
impl IBitcoindRpc for BitcoindTracked {
async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>> {
tracked_call!(
self,
"get_tx_block_height",
self.inner.get_tx_block_height(txid).await
)
}
async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()> {
tracked_call!(
self,
"watch_script_history",
self.inner.watch_script_history(script).await
)
}
async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>> {
tracked_call!(
self,
"get_script_history",
self.inner.get_script_history(script).await
)
}
async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof> {
tracked_call!(
self,
"get_txout_proof",
self.inner.get_txout_proof(txid).await
)
}
async fn get_info(&self) -> Result<BlockchainInfo> {
tracked_call!(self, "get_info", self.inner.get_info().await)
}
}
#[derive(Debug)]
pub struct EsploraClient {
client: AsyncClient,
}
impl EsploraClient {
pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
Ok(Self {
client: Builder::new(url.as_str().trim_end_matches('/')).build_async()?,
})
}
}
#[apply(async_trait_maybe_send!)]
impl IBitcoindRpc for EsploraClient {
async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
Ok(self
.client
.get_tx_status(txid)
.await?
.block_height
.map(u64::from))
}
async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> {
Ok(())
}
async fn get_script_history(
&self,
script: &ScriptBuf,
) -> anyhow::Result<Vec<bitcoin::Transaction>> {
const MAX_TX_HISTORY: usize = 1000;
let mut transactions = Vec::new();
let mut last_seen: Option<Txid> = None;
loop {
let page = self.client.scripthash_txs(script, last_seen).await?;
if page.is_empty() {
break;
}
for tx in &page {
transactions.push(tx.to_tx());
}
if transactions.len() >= MAX_TX_HISTORY {
return Err(format_err!(
"Script history exceeds maximum limit of {}",
MAX_TX_HISTORY
));
}
last_seen = Some(page.last().expect("page not empty").txid);
}
Ok(transactions)
}
async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
let proof = self
.client
.get_merkle_block(&txid)
.await?
.ok_or(format_err!("No merkle proof found"))?;
Ok(TxOutProof {
block_header: proof.header,
merkle_proof: proof.txn,
})
}
async fn get_info(&self) -> anyhow::Result<BlockchainInfo> {
let height = self.client.get_height().await?;
Ok(BlockchainInfo {
block_height: u64::from(height),
synced: true,
})
}
}