fedimint_bitcoind/
bitcoincore.rs1use std::env;
2use std::io::Cursor;
3use std::path::PathBuf;
4
5use anyhow::{anyhow as format_err, bail};
6use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid};
7use bitcoincore_rpc::bitcoincore_rpc_json::EstimateMode;
8use bitcoincore_rpc::{Auth, RpcApi};
9use fedimint_core::encoding::Decodable;
10use fedimint_core::envs::{BitcoinRpcConfig, FM_BITCOIND_COOKIE_FILE_ENV};
11use fedimint_core::module::registry::ModuleDecoderRegistry;
12use fedimint_core::runtime::block_in_place;
13use fedimint_core::task::TaskHandle;
14use fedimint_core::txoproof::TxOutProof;
15use fedimint_core::util::SafeUrl;
16use fedimint_core::{apply, async_trait_maybe_send, Feerate};
17use fedimint_logging::LOG_CORE;
18use tracing::{info, warn};
19
20use crate::{DynBitcoindRpc, IBitcoindRpc, IBitcoindRpcFactory, RetryClient};
21
22#[derive(Debug)]
23pub struct BitcoindFactory;
24
25impl IBitcoindRpcFactory for BitcoindFactory {
26 fn create_connection(
27 &self,
28 url: &SafeUrl,
29 handle: TaskHandle,
30 ) -> anyhow::Result<DynBitcoindRpc> {
31 Ok(RetryClient::new(BitcoinClient::new(url)?, handle).into())
32 }
33}
34
35#[derive(Debug)]
36struct BitcoinClient {
37 client: ::bitcoincore_rpc::Client,
38 url: SafeUrl,
39}
40
41impl BitcoinClient {
42 fn new(url: &SafeUrl) -> anyhow::Result<Self> {
43 let safe_url = url.clone();
44 let (url, auth) = from_url_to_url_auth(url)?;
45 Ok(Self {
46 client: ::bitcoincore_rpc::Client::new(&url, auth)?,
47 url: safe_url,
48 })
49 }
50}
51
52#[apply(async_trait_maybe_send!)]
53impl IBitcoindRpc for BitcoinClient {
54 async fn get_network(&self) -> anyhow::Result<Network> {
55 let network = block_in_place(|| self.client.get_blockchain_info())?;
56 Ok(network.chain)
57 }
58
59 async fn get_block_count(&self) -> anyhow::Result<u64> {
60 block_in_place(|| self.client.get_block_count())
62 .map(|height| height + 1)
63 .map_err(anyhow::Error::from)
64 }
65
66 async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
67 block_in_place(|| self.client.get_block_hash(height)).map_err(anyhow::Error::from)
68 }
69
70 async fn get_fee_rate(&self, confirmation_target: u16) -> anyhow::Result<Option<Feerate>> {
71 let fee = block_in_place(|| {
72 self.client
73 .estimate_smart_fee(confirmation_target, Some(EstimateMode::Conservative))
74 });
75 Ok(fee?.fee_rate.map(|per_kb| Feerate {
76 sats_per_kvb: per_kb.to_sat(),
77 }))
78 }
79
80 async fn submit_transaction(&self, transaction: Transaction) {
81 use bitcoincore_rpc::jsonrpc::Error::Rpc;
82 use bitcoincore_rpc::Error::JsonRpc;
83 match block_in_place(|| self.client.send_raw_transaction(&transaction)) {
84 Err(JsonRpc(Rpc(e))) if e.code == -27 => (),
89 Err(e) => info!(?e, "Error broadcasting transaction"),
90 Ok(_) => (),
91 }
92 }
93
94 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
95 let info = block_in_place(|| self.client.get_raw_transaction_info(txid, None))
96 .map_err(|error| info!(?error, "Unable to get raw transaction"));
97 let height = match info.ok().and_then(|info| info.blockhash) {
98 None => None,
99 Some(hash) => Some(block_in_place(|| self.client.get_block_header_info(&hash))?.height),
100 };
101 Ok(height.map(|h| h as u64))
102 }
103
104 async fn is_tx_in_block(
105 &self,
106 txid: &Txid,
107 block_hash: &BlockHash,
108 block_height: u64,
109 ) -> anyhow::Result<bool> {
110 let block_info = block_in_place(|| self.client.get_block_info(block_hash))?;
111 anyhow::ensure!(
112 block_info.height as u64 == block_height,
113 "Block height for block hash does not match expected height"
114 );
115 Ok(block_info.tx.contains(txid))
116 }
117
118 async fn watch_script_history(&self, script: &ScriptBuf) -> anyhow::Result<()> {
119 warn!(target: LOG_CORE, "Wallet operations are broken on bitcoind. Use different backend.");
120 block_in_place(|| {
123 self.client
124 .import_address_script(script, Some(&script.to_string()), Some(false), None)
125 })?;
126
127 Ok(())
128 }
129
130 async fn get_script_history(&self, script: &ScriptBuf) -> anyhow::Result<Vec<Transaction>> {
131 let mut results = vec![];
132 let list = block_in_place(|| {
133 self.client
134 .list_transactions(Some(&script.to_string()), None, None, Some(true))
135 })?;
136 for tx in list {
137 let raw_tx = block_in_place(|| self.client.get_raw_transaction(&tx.info.txid, None))?;
138 results.push(raw_tx);
139 }
140 Ok(results)
141 }
142
143 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
144 TxOutProof::consensus_decode(
145 &mut Cursor::new(block_in_place(|| {
146 self.client.get_tx_out_proof(&[txid], None)
147 })?),
148 &ModuleDecoderRegistry::default(),
149 )
150 .map_err(|error| format_err!("Could not decode tx: {}", error))
151 }
152
153 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
154 BitcoinRpcConfig {
155 kind: "bitcoind".to_string(),
156 url: self.url.clone(),
157 }
158 }
159}
160
161pub fn from_url_to_url_auth(url: &SafeUrl) -> anyhow::Result<(String, Auth)> {
163 Ok((
164 (if let Some(port) = url.port() {
165 format!(
166 "{}://{}:{port}",
167 url.scheme(),
168 url.host_str().unwrap_or("127.0.0.1")
169 )
170 } else {
171 format!(
172 "{}://{}",
173 url.scheme(),
174 url.host_str().unwrap_or("127.0.0.1")
175 )
176 }),
177 match (
178 !url.username().is_empty(),
179 env::var(FM_BITCOIND_COOKIE_FILE_ENV),
180 ) {
181 (true, Ok(_)) => {
182 bail!("When {FM_BITCOIND_COOKIE_FILE_ENV} is set, the url auth part must be empty.")
183 }
184 (true, Err(_)) => Auth::UserPass(
185 url.username().to_owned(),
186 url.password()
187 .ok_or_else(|| format_err!("Password missing for {}", url.username()))?
188 .to_owned(),
189 ),
190 (false, Ok(path)) => Auth::CookieFile(PathBuf::from(path)),
191 (false, Err(_)) => Auth::None,
192 },
193 ))
194}