fedimint_bitcoind/
bitcoincore.rs1use bitcoin::{Address, ScriptBuf, Txid};
2use bitcoincore_rpc::json::ImportDescriptors;
3use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError;
4use bitcoincore_rpc::{Auth, Error as RpcError, RpcApi};
5use fedimint_core::encoding::Decodable;
6use fedimint_core::module::registry::ModuleDecoderRegistry;
7use fedimint_core::task::block_in_place;
8use fedimint_core::txoproof::TxOutProof;
9use fedimint_core::util::SafeUrl;
10use fedimint_core::{apply, async_trait_maybe_send};
11use fedimint_logging::LOG_BITCOIND_CORE;
12use tracing::{debug, warn};
13
14use crate::{IBitcoindRpc, format_err};
15
16#[derive(Debug)]
17pub struct BitcoindClient {
18 client: ::bitcoincore_rpc::Client,
19 network: bitcoin::Network,
20}
21
22impl BitcoindClient {
23 pub fn new(
24 url: &SafeUrl,
25 username: String,
26 password: String,
27 wallet_name: &str,
28 network: bitcoin::Network,
29 ) -> anyhow::Result<Self> {
30 let auth = Auth::UserPass(username, password);
31 let url_str = if let Some(port) = url.port() {
32 format!(
33 "{}://{}:{port}",
34 url.scheme(),
35 url.host_str().unwrap_or("127.0.0.1")
36 )
37 } else {
38 format!(
39 "{}://{}",
40 url.scheme(),
41 url.host_str().unwrap_or("127.0.0.1")
42 )
43 };
44
45 let default_url_str = format!("{url_str}/wallet/");
46 let default_client = ::bitcoincore_rpc::Client::new(&default_url_str, auth.clone())?;
47 Self::create_watch_only_wallet(&default_client, wallet_name)?;
48
49 let wallet_url_str = format!("{url_str}/wallet/{wallet_name}");
50 let client = ::bitcoincore_rpc::Client::new(&wallet_url_str, auth)?;
51 Ok(Self { client, network })
52 }
53
54 fn create_watch_only_wallet(
55 client: &::bitcoincore_rpc::Client,
56 wallet_name: &str,
57 ) -> anyhow::Result<()> {
58 let create_wallet = block_in_place(|| {
59 client.create_wallet(wallet_name, Some(true), Some(true), None, None)
60 });
61
62 match create_wallet {
63 Ok(_) => Ok(()),
64 Err(RpcError::JsonRpc(JsonRpcError::Rpc(rpc_err))) if rpc_err.code == -4 => {
65 Ok(())
67 }
68 Err(e) => Err(e.into()),
69 }
70 }
71}
72
73#[apply(async_trait_maybe_send!)]
74impl IBitcoindRpc for BitcoindClient {
75 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
76 let info = block_in_place(|| self.client.get_transaction(txid, Some(true)))
77 .map_err(|error| warn!(target: LOG_BITCOIND_CORE, ?error, "Unable to get transaction"));
78 let height = match info.ok().and_then(|info| info.info.blockhash) {
79 None => None,
80 Some(hash) => Some(block_in_place(|| self.client.get_block_header_info(&hash))?.height),
81 };
82 Ok(height.map(|h| h as u64))
83 }
84
85 async fn watch_script_history(&self, script: &ScriptBuf) -> anyhow::Result<()> {
86 let address = Address::from_script(script, self.network)?.to_string();
87 debug!(target: LOG_BITCOIND_CORE, %address, "Watching script history");
88
89 let descriptor = format!("addr({address})");
91 let descriptor_info = block_in_place(|| self.client.get_descriptor_info(&descriptor))?;
92 let checksum = descriptor_info
93 .checksum
94 .ok_or(anyhow::anyhow!("No checksum"))?;
95
96 let import_results = block_in_place(|| {
98 self.client.import_descriptors(ImportDescriptors {
99 descriptor: format!("{descriptor}#{checksum}"),
100 timestamp: bitcoincore_rpc::json::Timestamp::Now,
101 active: Some(false),
102 range: None,
103 next_index: None,
104 internal: None,
105 label: Some(address.clone()),
106 })
107 })?;
108
109 if import_results.iter().all(|r| r.success) {
111 Ok(())
112 } else {
113 Err(anyhow::anyhow!(
114 "Importing descriptor failed: {:?}",
115 import_results
116 .into_iter()
117 .filter(|r| !r.success)
118 .collect::<Vec<_>>()
119 ))
120 }
121 }
122
123 async fn get_script_history(
124 &self,
125 script: &ScriptBuf,
126 ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
127 let address = Address::from_script(script, self.network)?.to_string();
128 let mut results = vec![];
129 let list = block_in_place(|| {
130 self.client
131 .list_transactions(Some(&address), None, None, Some(true))
132 })?;
133 for tx in list {
134 let tx = block_in_place(|| self.client.get_transaction(&tx.info.txid, Some(true)))?;
135 let raw_tx = tx.transaction()?;
136 results.push(raw_tx);
137 }
138 Ok(results)
139 }
140
141 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
142 TxOutProof::consensus_decode_whole(
143 &block_in_place(|| self.client.get_tx_out_proof(&[txid], None))?,
144 &ModuleDecoderRegistry::default(),
145 )
146 .map_err(|error| format_err!("Could not decode tx: {}", error))
147 }
148}