/ fedimint-bitcoind / src / bitcoincore.rs
bitcoincore.rs
  1  use std::env;
  2  use std::io::Cursor;
  3  use std::path::PathBuf;
  4  
  5  use anyhow::{anyhow as format_err, bail};
  6  use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid};
  7  use bitcoincore_rpc::bitcoincore_rpc_json::EstimateMode;
  8  use bitcoincore_rpc::{Auth, RpcApi};
  9  use fedimint_core::encoding::Decodable;
 10  use fedimint_core::envs::FM_BITCOIND_COOKIE_FILE_ENV;
 11  use fedimint_core::module::registry::ModuleDecoderRegistry;
 12  use fedimint_core::runtime::block_in_place;
 13  use fedimint_core::task::TaskHandle;
 14  use fedimint_core::txoproof::TxOutProof;
 15  use fedimint_core::util::SafeUrl;
 16  use fedimint_core::{apply, async_trait_maybe_send, Feerate};
 17  use fedimint_logging::LOG_CORE;
 18  use tracing::{info, warn};
 19  
 20  use crate::{DynBitcoindRpc, IBitcoindRpc, IBitcoindRpcFactory, RetryClient};
 21  
 22  #[derive(Debug)]
 23  pub struct BitcoindFactory;
 24  
 25  impl 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)]
 36  struct BitcoinClient(::bitcoincore_rpc::Client);
 37  
 38  impl BitcoinClient {
 39      fn new(url: &SafeUrl) -> anyhow::Result<Self> {
 40          let (url, auth) = from_url_to_url_auth(url)?;
 41          Ok(Self(::bitcoincore_rpc::Client::new(&url, auth)?))
 42      }
 43  }
 44  
 45  #[apply(async_trait_maybe_send!)]
 46  impl IBitcoindRpc for BitcoinClient {
 47      async fn get_network(&self) -> anyhow::Result<Network> {
 48          let network = block_in_place(|| self.0.get_blockchain_info())?;
 49          Ok(match network.chain.as_str() {
 50              "main" => Network::Bitcoin,
 51              "test" => Network::Testnet,
 52              "regtest" => Network::Regtest,
 53              "signet" => Network::Signet,
 54              n => panic!("Unknown Network \"{n}\""),
 55          })
 56      }
 57  
 58      async fn get_block_count(&self) -> anyhow::Result<u64> {
 59          // The RPC function is confusingly named and actually returns the block height
 60          block_in_place(|| self.0.get_block_count())
 61              .map(|height| height + 1)
 62              .map_err(anyhow::Error::from)
 63      }
 64  
 65      async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
 66          block_in_place(|| self.0.get_block_hash(height)).map_err(anyhow::Error::from)
 67      }
 68  
 69      async fn get_fee_rate(&self, confirmation_target: u16) -> anyhow::Result<Option<Feerate>> {
 70          let fee = block_in_place(|| {
 71              self.0
 72                  .estimate_smart_fee(confirmation_target, Some(EstimateMode::Conservative))
 73          });
 74          Ok(fee?.fee_rate.map(|per_kb| Feerate {
 75              sats_per_kvb: per_kb.to_sat(),
 76          }))
 77      }
 78  
 79      async fn submit_transaction(&self, transaction: Transaction) {
 80          use bitcoincore_rpc::jsonrpc::Error::Rpc;
 81          use bitcoincore_rpc::Error::JsonRpc;
 82          match block_in_place(|| self.0.send_raw_transaction(&transaction)) {
 83              // Bitcoin core's RPC will return error code -27 if a transaction is already in a block.
 84              // This is considered a success case, so we don't surface the error log.
 85              //
 86              // https://github.com/bitcoin/bitcoin/blob/daa56f7f665183bcce3df146f143be37f33c123e/src/rpc/protocol.h#L48
 87              Err(JsonRpc(Rpc(e))) if e.code == -27 => (),
 88              Err(e) => info!(?e, "Error broadcasting transaction"),
 89              Ok(_) => (),
 90          }
 91      }
 92  
 93      async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
 94          let info = block_in_place(|| self.0.get_raw_transaction_info(txid, None))
 95              .map_err(|error| info!(?error, "Unable to get raw transaction"));
 96          let height = match info.ok().and_then(|info| info.blockhash) {
 97              None => None,
 98              Some(hash) => Some(block_in_place(|| self.0.get_block_header_info(&hash))?.height),
 99          };
100          Ok(height.map(|h| h as u64))
101      }
102  
103      async fn watch_script_history(&self, script: &ScriptBuf) -> anyhow::Result<()> {
104          warn!(target: LOG_CORE, "Wallet operations are broken on bitcoind. Use different backend.");
105          // start watching for this script in our wallet to avoid the need to rescan the
106          // blockchain, labeling it so we can reference it later
107          block_in_place(|| {
108              self.0
109                  .import_address_script(script, Some(&script.to_string()), Some(false), None)
110          })?;
111  
112          Ok(())
113      }
114  
115      async fn get_script_history(&self, script: &ScriptBuf) -> anyhow::Result<Vec<Transaction>> {
116          let mut results = vec![];
117          let list = block_in_place(|| {
118              self.0
119                  .list_transactions(Some(&script.to_string()), None, None, Some(true))
120          })?;
121          for tx in list {
122              let raw_tx = block_in_place(|| self.0.get_raw_transaction(&tx.info.txid, None))?;
123              results.push(raw_tx);
124          }
125          Ok(results)
126      }
127  
128      async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
129          TxOutProof::consensus_decode(
130              &mut Cursor::new(block_in_place(|| self.0.get_tx_out_proof(&[txid], None))?),
131              &ModuleDecoderRegistry::default(),
132          )
133          .map_err(|error| format_err!("Could not decode tx: {}", error))
134      }
135  }
136  
137  // TODO: Make private
138  pub fn from_url_to_url_auth(url: &SafeUrl) -> anyhow::Result<(String, Auth)> {
139      Ok((
140          (if let Some(port) = url.port() {
141              format!(
142                  "{}://{}:{port}",
143                  url.scheme(),
144                  url.host_str().unwrap_or("127.0.0.1")
145              )
146          } else {
147              format!(
148                  "{}://{}",
149                  url.scheme(),
150                  url.host_str().unwrap_or("127.0.0.1")
151              )
152          }),
153          match (
154              !url.username().is_empty(),
155              env::var(FM_BITCOIND_COOKIE_FILE_ENV),
156          ) {
157              (true, Ok(_)) => {
158                  bail!("When {FM_BITCOIND_COOKIE_FILE_ENV} is set, the url auth part must be empty.")
159              }
160              (true, Err(_)) => Auth::UserPass(
161                  url.username().to_owned(),
162                  url.password()
163                      .ok_or_else(|| format_err!("Password missing for {}", url.username()))?
164                      .to_owned(),
165              ),
166              (false, Ok(path)) => Auth::CookieFile(PathBuf::from(path)),
167              (false, Err(_)) => Auth::None,
168          },
169      ))
170  }