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