use bitcoin::{Address, ScriptBuf, Txid};
use bitcoincore_rpc::json::ImportDescriptors;
use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError;
use bitcoincore_rpc::{Auth, Error as RpcError, RpcApi};
use fedimint_core::encoding::Decodable;
use fedimint_core::module::registry::ModuleDecoderRegistry;
use fedimint_core::task::block_in_place;
use fedimint_core::txoproof::TxOutProof;
use fedimint_core::util::{FmtCompact, SafeUrl};
use fedimint_core::{apply, async_trait_maybe_send};
use fedimint_logging::LOG_BITCOIND_CORE;
use tracing::{debug, warn};
use crate::{BlockchainInfo, IBitcoindRpc, format_err};
#[derive(Debug)]
pub struct BitcoindClient {
client: ::bitcoincore_rpc::Client,
network: bitcoin::Network,
}
impl BitcoindClient {
pub fn new(
url: &SafeUrl,
username: String,
password: String,
wallet_name: &str,
network: bitcoin::Network,
) -> anyhow::Result<Self> {
let auth = Auth::UserPass(username, password);
let url_str = if let Some(port) = url.port() {
format!(
"{}://{}:{port}",
url.scheme(),
url.host_str().unwrap_or("127.0.0.1")
)
} else {
format!(
"{}://{}",
url.scheme(),
url.host_str().unwrap_or("127.0.0.1")
)
};
let default_url_str = format!("{url_str}/wallet/");
let default_client = ::bitcoincore_rpc::Client::new(&default_url_str, auth.clone())?;
Self::create_watch_only_wallet(&default_client, wallet_name)?;
let wallet_url_str = format!("{url_str}/wallet/{wallet_name}");
let client = ::bitcoincore_rpc::Client::new(&wallet_url_str, auth)?;
Ok(Self { client, network })
}
fn create_watch_only_wallet(
client: &::bitcoincore_rpc::Client,
wallet_name: &str,
) -> anyhow::Result<()> {
let create_wallet = block_in_place(|| {
client.create_wallet(wallet_name, Some(true), Some(true), None, None)
});
match create_wallet {
Ok(_) => Ok(()),
Err(RpcError::JsonRpc(JsonRpcError::Rpc(rpc_err))) if rpc_err.code == -4 => {
Ok(())
}
Err(e) => Err(e.into()),
}
}
}
#[apply(async_trait_maybe_send!)]
impl IBitcoindRpc for BitcoindClient {
async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
let info = block_in_place(|| self.client.get_transaction(txid, Some(true)))
.map_err(|err| warn!(target: LOG_BITCOIND_CORE, err = %err.fmt_compact(), "Unable to get transaction"));
let height = match info.ok().and_then(|info| info.info.blockhash) {
None => None,
Some(hash) => Some(block_in_place(|| self.client.get_block_header_info(&hash))?.height),
};
Ok(height.map(|h| h as u64))
}
async fn watch_script_history(&self, script: &ScriptBuf) -> anyhow::Result<()> {
let address = Address::from_script(script, self.network)?.to_string();
debug!(target: LOG_BITCOIND_CORE, %address, "Watching script history");
let descriptor = format!("addr({address})");
let descriptor_info = block_in_place(|| self.client.get_descriptor_info(&descriptor))?;
let checksum = descriptor_info
.checksum
.ok_or(anyhow::anyhow!("No checksum"))?;
let import_results = block_in_place(|| {
self.client.import_descriptors(ImportDescriptors {
descriptor: format!("{descriptor}#{checksum}"),
timestamp: bitcoincore_rpc::json::Timestamp::Now,
active: Some(false),
range: None,
next_index: None,
internal: None,
label: Some(address.clone()),
})
})?;
if import_results.iter().all(|r| r.success) {
Ok(())
} else {
Err(anyhow::anyhow!(
"Importing descriptor failed: {:?}",
import_results
.into_iter()
.filter(|r| !r.success)
.collect::<Vec<_>>()
))
}
}
async fn get_script_history(
&self,
script: &ScriptBuf,
) -> anyhow::Result<Vec<bitcoin::Transaction>> {
let address = Address::from_script(script, self.network)?.to_string();
let mut results = vec![];
let list = block_in_place(|| {
self.client
.list_transactions(Some(&address), None, None, Some(true))
})?;
for tx in list {
let tx = block_in_place(|| self.client.get_transaction(&tx.info.txid, Some(true)))?;
let raw_tx = tx.transaction()?;
results.push(raw_tx);
}
Ok(results)
}
async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
TxOutProof::consensus_decode_whole(
&block_in_place(|| self.client.get_tx_out_proof(&[txid], None))?,
&ModuleDecoderRegistry::default(),
)
.map_err(|error| format_err!("Could not decode tx: {}", error))
}
async fn get_info(&self) -> anyhow::Result<BlockchainInfo> {
let info = block_in_place(|| self.client.get_blockchain_info())
.map_err(|err| anyhow::anyhow!("Unable to get blockchain info {err}"))?;
Ok(BlockchainInfo {
block_height: info.blocks,
synced: !info.initial_block_download,
})
}
}