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 }