use std::cmp::min;
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::future::Future;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::Context;
pub use anyhow::Result;
use bitcoin::{BlockHash, Network, Script, Transaction, Txid};
use fedimint_core::bitcoinrpc::BitcoinRpcConfig;
use fedimint_core::fmt_utils::OptStacktrace;
use fedimint_core::task::TaskHandle;
use fedimint_core::txoproof::TxOutProof;
use fedimint_core::util::SafeUrl;
use fedimint_core::{apply, async_trait_maybe_send, dyn_newtype_define, Feerate};
use fedimint_logging::LOG_BLOCKCHAIN;
use lazy_static::lazy_static;
use tracing::info;
#[cfg(feature = "bitcoincore-rpc")]
pub mod bitcoincore;
#[cfg(feature = "electrum-client")]
mod electrum;
#[cfg(feature = "esplora-client")]
mod esplora;
const MAINNET_GENESIS_BLOCK_HASH: &str =
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
const TESTNET_GENESIS_BLOCK_HASH: &str =
"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
const SIGNET_GENESIS_BLOCK_HASH: &str =
"00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
lazy_static! {
static ref BITCOIN_RPC_REGISTRY: Mutex<BTreeMap<String, DynBitcoindRpcFactory>> =
Mutex::new(BTreeMap::from([
#[cfg(feature = "esplora-client")]
("esplora".to_string(), esplora::EsploraFactory.into()),
#[cfg(feature = "electrum-client")]
("electrum".to_string(), electrum::ElectrumFactory.into()),
#[cfg(feature = "bitcoincore-rpc")]
("bitcoind".to_string(), bitcoincore::BitcoindFactory.into()),
]));
}
pub fn create_bitcoind(config: &BitcoinRpcConfig, handle: TaskHandle) -> Result<DynBitcoindRpc> {
let registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
let maybe_factory = registry.get(&config.kind);
let factory = maybe_factory.with_context(|| {
anyhow::anyhow!(
"{} rpc not registered, available options: {:?}",
config.kind,
registry.keys()
)
})?;
factory.create_connection(&config.url, handle)
}
pub fn register_bitcoind(kind: String, factory: DynBitcoindRpcFactory) {
let mut registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
registry.insert(kind, factory);
}
pub trait IBitcoindRpcFactory: Debug + Send + Sync {
fn create_connection(&self, url: &SafeUrl, handle: TaskHandle) -> Result<DynBitcoindRpc>;
}
dyn_newtype_define! {
#[derive(Clone)]
pub DynBitcoindRpcFactory(Arc<IBitcoindRpcFactory>)
}
#[apply(async_trait_maybe_send!)]
pub trait IBitcoindRpc: Debug {
async fn get_network(&self) -> Result<bitcoin::Network>;
async fn get_block_count(&self) -> Result<u64>;
async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>>;
async fn submit_transaction(&self, transaction: Transaction);
async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
async fn is_tx_in_block(
&self,
txid: &Txid,
block_hash: &BlockHash,
block_height: u64,
) -> Result<bool>;
async fn watch_script_history(&self, script: &Script) -> Result<()>;
async fn get_script_history(&self, script: &Script) -> Result<Vec<Transaction>>;
async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
}
dyn_newtype_define! {
#[derive(Clone)]
pub DynBitcoindRpc(Arc<IBitcoindRpc>)
}
const RETRY_SLEEP_MIN_MS: Duration = Duration::from_millis(10);
const RETRY_SLEEP_MAX_MS: Duration = Duration::from_millis(5000);
#[derive(Debug)]
pub struct RetryClient<C> {
inner: C,
task_handle: TaskHandle,
}
impl<C> RetryClient<C> {
pub fn new(inner: C, task_handle: TaskHandle) -> Self {
Self { inner, task_handle }
}
async fn retry_call<T, F, R>(&self, call_fn: F) -> Result<T>
where
F: Fn() -> R,
R: Future<Output = Result<T>>,
{
let mut retry_time = RETRY_SLEEP_MIN_MS;
let ret = loop {
match call_fn().await {
Ok(ret) => {
break ret;
}
Err(e) => {
if self.task_handle.is_shutting_down() {
return Err(e);
}
info!(target: LOG_BLOCKCHAIN, "Bitcoind error {}, retrying", OptStacktrace(e));
std::thread::sleep(retry_time);
retry_time = min(RETRY_SLEEP_MAX_MS, retry_time * 2);
}
}
};
Ok(ret)
}
}
#[apply(async_trait_maybe_send!)]
impl<C> IBitcoindRpc for RetryClient<C>
where
C: IBitcoindRpc + Sync + Send,
{
async fn get_network(&self) -> Result<Network> {
self.retry_call(|| async { self.inner.get_network().await })
.await
}
async fn get_block_count(&self) -> Result<u64> {
self.retry_call(|| async { self.inner.get_block_count().await })
.await
}
async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
self.retry_call(|| async { self.inner.get_block_hash(height).await })
.await
}
async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>> {
self.retry_call(|| async { self.inner.get_fee_rate(confirmation_target).await })
.await
}
async fn submit_transaction(&self, transaction: Transaction) {
self.inner.submit_transaction(transaction.clone()).await;
}
async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>> {
self.retry_call(|| async { self.inner.get_tx_block_height(txid).await })
.await
}
async fn is_tx_in_block(
&self,
txid: &Txid,
block_hash: &BlockHash,
block_height: u64,
) -> Result<bool> {
self.retry_call(|| async {
self.inner
.is_tx_in_block(txid, block_hash, block_height)
.await
})
.await
}
async fn watch_script_history(&self, script: &Script) -> Result<()> {
self.retry_call(|| async { self.inner.watch_script_history(script).await })
.await
}
async fn get_script_history(&self, script: &Script) -> Result<Vec<Transaction>> {
self.retry_call(|| async { self.inner.get_script_history(script).await })
.await
}
async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof> {
self.retry_call(|| async { self.inner.get_txout_proof(txid).await })
.await
}
}