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::{FmtCompact, SafeUrl};
10use fedimint_core::{apply, async_trait_maybe_send};
11use fedimint_logging::LOG_BITCOIND_CORE;
12use tracing::{debug, warn};
13
14use crate::{BitcoinRpcError, BlockchainInfo, IBitcoindRpc};
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 ) -> Result<Self, BitcoinRpcError> {
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 .map_err(|source| BitcoinRpcError::InvalidUrl {
48 url: default_url_str.clone(),
49 source: Box::new(source),
50 })?;
51 Self::create_watch_only_wallet(&default_client, wallet_name)?;
52
53 let wallet_url_str = format!("{url_str}/wallet/{wallet_name}");
54 let client = ::bitcoincore_rpc::Client::new(&wallet_url_str, auth).map_err(|source| {
55 BitcoinRpcError::InvalidUrl {
56 url: wallet_url_str.clone(),
57 source: Box::new(source),
58 }
59 })?;
60 Ok(Self { client, network })
61 }
62
63 fn create_watch_only_wallet(
64 client: &::bitcoincore_rpc::Client,
65 wallet_name: &str,
66 ) -> Result<(), BitcoinRpcError> {
67 let create_wallet = block_in_place(|| {
68 client.create_wallet(wallet_name, Some(true), Some(true), None, None)
69 });
70
71 match create_wallet {
72 Ok(_) => Ok(()),
73 Err(RpcError::JsonRpc(JsonRpcError::Rpc(rpc_err))) if rpc_err.code == -4 => {
74 Ok(())
76 }
77 Err(e) => Err(BitcoinRpcError::Backend(Box::new(e))),
78 }
79 }
80}
81
82#[apply(async_trait_maybe_send!)]
83impl IBitcoindRpc for BitcoindClient {
84 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>, BitcoinRpcError> {
85 let info = block_in_place(|| self.client.get_transaction(txid, Some(true)))
86 .map_err(|err| warn!(target: LOG_BITCOIND_CORE, err = %err.fmt_compact(), "Unable to get transaction"));
87 let height = match info.ok().and_then(|info| info.info.blockhash) {
88 None => None,
89 Some(hash) => Some(
90 block_in_place(|| self.client.get_block_header_info(&hash))
91 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?
92 .height,
93 ),
94 };
95 Ok(height.map(|h| h as u64))
96 }
97
98 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<(), BitcoinRpcError> {
99 let address = Address::from_script(script, self.network)
100 .map_err(|err| BitcoinRpcError::NonStandardScript(Box::new(err)))?
101 .to_string();
102 debug!(target: LOG_BITCOIND_CORE, %address, "Watching script history");
103
104 let descriptor = format!("addr({address})");
106 let descriptor_info = block_in_place(|| self.client.get_descriptor_info(&descriptor))
107 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
108 let checksum =
109 descriptor_info
110 .checksum
111 .ok_or_else(|| BitcoinRpcError::InvalidResponse {
112 message: "Descriptor info carries no checksum".to_string(),
113 })?;
114
115 let import_results = block_in_place(|| {
117 self.client.import_descriptors(ImportDescriptors {
118 descriptor: format!("{descriptor}#{checksum}"),
119 timestamp: bitcoincore_rpc::json::Timestamp::Now,
120 active: Some(false),
121 range: None,
122 next_index: None,
123 internal: None,
124 label: Some(address.clone()),
125 })
126 })
127 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
128
129 if import_results.iter().all(|r| r.success) {
131 Ok(())
132 } else {
133 Err(BitcoinRpcError::InvalidResponse {
134 message: format!(
135 "Importing descriptor failed: {:?}",
136 import_results
137 .into_iter()
138 .filter(|r| !r.success)
139 .collect::<Vec<_>>()
140 ),
141 })
142 }
143 }
144
145 async fn get_script_history(
146 &self,
147 script: &ScriptBuf,
148 ) -> Result<Vec<bitcoin::Transaction>, BitcoinRpcError> {
149 let address = Address::from_script(script, self.network)
150 .map_err(|err| BitcoinRpcError::NonStandardScript(Box::new(err)))?
151 .to_string();
152 let mut results = vec![];
153 let list = block_in_place(|| {
154 self.client
155 .list_transactions(Some(&address), None, None, Some(true))
156 })
157 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
158 for tx in list {
159 let tx = block_in_place(|| self.client.get_transaction(&tx.info.txid, Some(true)))
160 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
161 let raw_tx = tx
162 .transaction()
163 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
164 results.push(raw_tx);
165 }
166 Ok(results)
167 }
168
169 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof, BitcoinRpcError> {
170 TxOutProof::consensus_decode_whole(
171 &block_in_place(|| self.client.get_tx_out_proof(&[txid], None))
172 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?,
173 &ModuleDecoderRegistry::default(),
174 )
175 .map_err(BitcoinRpcError::Decode)
176 }
177
178 async fn get_info(&self) -> Result<BlockchainInfo, BitcoinRpcError> {
179 let info = block_in_place(|| self.client.get_blockchain_info())
180 .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
181 Ok(BlockchainInfo {
182 block_height: info.blocks,
183 synced: !info.initial_block_download,
184 })
185 }
186}