electrum.rs
1 use std::fmt; 2 3 use anyhow::anyhow as format_err; 4 use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid}; 5 use electrum_client::ElectrumApi; 6 use electrum_client::Error::Protocol; 7 use fedimint_core::runtime::block_in_place; 8 use fedimint_core::task::TaskHandle; 9 use fedimint_core::txoproof::TxOutProof; 10 use fedimint_core::util::SafeUrl; 11 use fedimint_core::{apply, async_trait_maybe_send, Feerate}; 12 use hex::ToHex; 13 use serde_json::{Map, Value}; 14 use tracing::{info, warn}; 15 16 use crate::{DynBitcoindRpc, IBitcoindRpc, IBitcoindRpcFactory, RetryClient}; 17 18 #[derive(Debug)] 19 pub struct ElectrumFactory; 20 21 impl IBitcoindRpcFactory for ElectrumFactory { 22 fn create_connection( 23 &self, 24 url: &SafeUrl, 25 handle: TaskHandle, 26 ) -> anyhow::Result<DynBitcoindRpc> { 27 Ok(RetryClient::new(ElectrumClient::new(url)?, handle).into()) 28 } 29 } 30 31 pub struct ElectrumClient(electrum_client::Client); 32 33 impl ElectrumClient { 34 fn new(url: &SafeUrl) -> anyhow::Result<Self> { 35 Ok(Self(electrum_client::Client::new(url.as_str())?)) 36 } 37 } 38 39 impl fmt::Debug for ElectrumClient { 40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 f.write_str("ElectrumClient") 42 } 43 } 44 45 #[apply(async_trait_maybe_send!)] 46 impl IBitcoindRpc for ElectrumClient { 47 async fn get_network(&self) -> anyhow::Result<Network> { 48 let resp = block_in_place(|| self.0.server_features())?; 49 Ok(match resp.genesis_hash.encode_hex::<String>().as_str() { 50 crate::MAINNET_GENESIS_BLOCK_HASH => Network::Bitcoin, 51 crate::TESTNET_GENESIS_BLOCK_HASH => Network::Testnet, 52 crate::SIGNET_GENESIS_BLOCK_HASH => Network::Signet, 53 hash => { 54 warn!("Unknown genesis hash {hash} - assuming regtest"); 55 Network::Regtest 56 } 57 }) 58 } 59 60 async fn get_block_count(&self) -> anyhow::Result<u64> { 61 Ok(block_in_place(|| self.0.block_headers_subscribe_raw())?.height as u64 + 1) 62 } 63 64 async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> { 65 let result = block_in_place(|| self.0.block_headers(height as usize, 1))?; 66 Ok(result 67 .headers 68 .first() 69 .ok_or_else(|| format_err!("empty block headers response"))? 70 .block_hash()) 71 } 72 73 async fn get_fee_rate(&self, confirmation_target: u16) -> anyhow::Result<Option<Feerate>> { 74 let estimate = block_in_place(|| self.0.estimate_fee(confirmation_target as usize))?; 75 let min_fee = block_in_place(|| self.0.relay_fee())?; 76 77 // convert fee rate estimate or min fee to sats 78 let sats_per_kvb = estimate.max(min_fee) * 100_000_000f64; 79 Ok(Some(Feerate { 80 sats_per_kvb: sats_per_kvb.ceil() as u64, 81 })) 82 } 83 84 async fn submit_transaction(&self, transaction: Transaction) { 85 let mut bytes = vec![]; 86 bitcoin::consensus::Encodable::consensus_encode(&transaction, &mut bytes) 87 .expect("can't fail"); 88 match block_in_place(|| self.0.transaction_broadcast_raw(&bytes)) { 89 Err(Protocol(Value::Object(e))) if is_already_submitted_error(&e) => (), 90 Err(e) => info!(?e, "Error broadcasting transaction"), 91 Ok(_) => (), 92 } 93 } 94 95 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> { 96 let tx = block_in_place(|| self.0.transaction_get(txid)) 97 .map_err(|error| info!(?error, "Unable to get raw transaction")); 98 match tx.ok() { 99 None => Ok(None), 100 Some(tx) => { 101 let output = tx 102 .output 103 .first() 104 .ok_or(format_err!("Transaction must contain at least one output"))?; 105 let history = block_in_place(|| self.0.script_get_history(&output.script_pubkey))?; 106 Ok(history.first().map(|history| history.height as u64)) 107 } 108 } 109 } 110 111 async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> { 112 // no watching needed on electrs, has all the history already 113 Ok(()) 114 } 115 116 async fn get_script_history( 117 &self, 118 script: &ScriptBuf, 119 ) -> anyhow::Result<Vec<bitcoin::Transaction>> { 120 let mut results = vec![]; 121 let transactions = block_in_place(|| self.0.script_get_history(script))?; 122 for history in transactions.into_iter() { 123 results.push(block_in_place(|| self.0.transaction_get(&history.tx_hash))?); 124 } 125 Ok(results) 126 } 127 128 async fn get_txout_proof(&self, _txid: Txid) -> anyhow::Result<TxOutProof> { 129 // FIXME: Not sure how to implement for electrum yet, but the client cannot use 130 // electrum regardless right now 131 unimplemented!() 132 } 133 } 134 135 /// Parses errors from electrum-client to determine if the transaction is 136 /// already submitted and can be ignored. 137 /// 138 /// Electrs [maps] daemon errors to a generic error code (2) instead of using 139 /// the error codes returned from bitcoin core's RPC (-27). There's an open [PR] 140 /// to use the correct error codes, but until that's available we match the 141 /// error based on the message text. 142 /// 143 /// [maps]: https://github.com/romanz/electrs/blob/v0.9.13/src/electrum.rs#L110 144 /// [PR]: https://github.com/romanz/electrs/pull/942 145 fn is_already_submitted_error(error: &Map<String, Value>) -> bool { 146 // TODO: Filter `electrs` errors using codes instead of string when available in 147 // `electrum-client` 148 // https://github.com/fedimint/fedimint/issues/3731 149 match error.get("message").and_then(|value| value.as_str()) { 150 Some(message) => message == "Transaction already in block chain", 151 None => false, 152 } 153 } 154 155 #[cfg(test)] 156 mod tests { 157 use serde_json::{json, Map, Value}; 158 159 use crate::electrum::is_already_submitted_error; 160 161 fn message_to_json(message: &str) -> Map<String, Value> { 162 let as_value = json!({"code": 2, "message": message}); 163 as_value 164 .as_object() 165 .expect("should parse as object") 166 .to_owned() 167 } 168 169 #[test] 170 fn should_parse_transaction_already_submitted_errors() { 171 let already_submitted_error = message_to_json("Transaction already in block chain"); 172 assert!(is_already_submitted_error(&already_submitted_error)); 173 174 let different_error_message = 175 message_to_json("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate"); 176 assert!(!is_already_submitted_error(&different_error_message)); 177 178 let unknown_error_object = message_to_json(""); 179 assert!(!is_already_submitted_error(&unknown_error_object)); 180 } 181 }