diff --git a/lib/core/src/chain/bitcoin.rs b/lib/core/src/chain/bitcoin.rs index c429483..96e5385 100644 --- a/lib/core/src/chain/bitcoin.rs +++ b/lib/core/src/chain/bitcoin.rs @@ -1,4 +1,4 @@ -use std::{sync::Mutex, time::Duration}; +use std::{sync::OnceLock, time::Duration}; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -75,35 +75,35 @@ pub trait BitcoinChainService: Send + Sync { } pub(crate) struct HybridBitcoinChainService { - client: Client, - tip: Mutex, + client: OnceLock, config: Config, } impl HybridBitcoinChainService { pub fn new(config: Config) -> Result { - Self::with_options(config, ElectrumOptions { timeout: Some(3) }) + Ok(Self { + config, + client: OnceLock::new(), + }) } - /// Creates an Electrum client specifying non default options like timeout - pub fn with_options(config: Config, options: ElectrumOptions) -> Result { - let electrum_url = ElectrumUrl::new(&config.bitcoin_electrum_url, true, true)?; - let client = electrum_url.build_client(&options)?; - let header = client.block_headers_subscribe_raw()?; - let tip: HeaderNotification = header.try_into()?; + fn get_client(&self) -> Result<&Client> { + if let Some(c) = self.client.get() { + return Ok(c); + } + let electrum_url = ElectrumUrl::new(&self.config.bitcoin_electrum_url, true, true)?; + let client = electrum_url.build_client(&ElectrumOptions { timeout: Some(3) })?; - Ok(Self { - client, - tip: Mutex::new(tip), - config, - }) + let client = self.client.get_or_init(|| client); + Ok(client) } } #[async_trait] impl BitcoinChainService for HybridBitcoinChainService { fn tip(&self) -> Result { + let client = self.get_client()?; let mut maybe_popped_header = None; - while let Some(header) = self.client.block_headers_pop_raw()? { + while let Some(header) = client.block_headers_pop_raw()? { maybe_popped_header = Some(header) } @@ -114,7 +114,7 @@ impl BitcoinChainService for HybridBitcoinChainService { // It might be that the client has reconnected and subscriptions don't persist // across connections. Calling `client.ping()` won't help here because the // successful retry will prevent us knowing about the reconnect. - if let Ok(header) = self.client.block_headers_subscribe_raw() { + if let Ok(header) = client.block_headers_subscribe_raw() { Some(header.try_into()?) } else { None @@ -122,22 +122,19 @@ impl BitcoinChainService for HybridBitcoinChainService { } }; - let mut tip = self.tip.lock().unwrap(); - if let Some(new_tip) = new_tip { - *tip = new_tip; - } - - Ok(tip.clone()) + new_tip.ok_or_else(|| anyhow!("Failed to get tip")) } fn broadcast(&self, tx: &Transaction) -> Result { - let txid = self.client.transaction_broadcast_raw(&serialize(&tx))?; + let txid = self + .get_client()? + .transaction_broadcast_raw(&serialize(&tx))?; Ok(Txid::from_raw_hash(txid.to_raw_hash())) } fn get_transactions(&self, txids: &[Txid]) -> Result> { let mut result = vec![]; - for tx in self.client.batch_transaction_get_raw(txids)? { + for tx in self.get_client()?.batch_transaction_get_raw(txids)? { let tx: Transaction = deserialize(&tx)?; result.push(tx); } @@ -146,7 +143,7 @@ impl BitcoinChainService for HybridBitcoinChainService { fn get_script_history(&self, script: &Script) -> Result> { Ok(self - .client + .get_client()? .script_get_history(script)? .into_iter() .map(Into::into) @@ -155,7 +152,7 @@ impl BitcoinChainService for HybridBitcoinChainService { fn get_scripts_history(&self, scripts: &[&Script]) -> Result>> { Ok(self - .client + .get_client()? .batch_script_get_history(scripts)? .into_iter() .map(|v| v.into_iter().map(Into::into).collect()) @@ -234,11 +231,11 @@ impl BitcoinChainService for HybridBitcoinChainService { } fn script_get_balance(&self, script: &Script) -> Result { - Ok(self.client.script_get_balance(script)?) + Ok(self.get_client()?.script_get_balance(script)?) } fn scripts_get_balance(&self, scripts: &[&Script]) -> Result> { - Ok(self.client.batch_script_get_balance(scripts)?) + Ok(self.get_client()?.batch_script_get_balance(scripts)?) } async fn script_get_balance_with_retry( diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 0f0cb03..0a25a0a 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -1,17 +1,18 @@ -use std::sync::Mutex; +use std::sync::OnceLock; use std::time::Duration; use anyhow::{anyhow, Result}; use async_trait::async_trait; use boltz_client::ToHex; +use electrum_client::{Client, ElectrumApi}; +use elements::encode::serialize as elements_serialize; use log::info; -use lwk_wollet::clients::blocking::BlockchainBackend; use lwk_wollet::elements::hex::FromHex; -use lwk_wollet::ElectrumOptions; +use lwk_wollet::{bitcoin, elements, ElectrumOptions}; use lwk_wollet::{ elements::{Address, OutPoint, Script, Transaction, Txid}, hashes::{sha256, Hash}, - ElectrumClient, ElectrumUrl, History, + ElectrumUrl, History, }; use crate::prelude::Utxo; @@ -60,32 +61,62 @@ pub trait LiquidChainService: Send + Sync { } pub(crate) struct HybridLiquidChainService { - electrum_client: ElectrumClient, - tip_client: Mutex, + client: OnceLock, + config: Config, } impl HybridLiquidChainService { pub(crate) fn new(config: Config) -> Result { - let electrum_url = ElectrumUrl::new(&config.liquid_electrum_url, true, true)?; - let electrum_client = - ElectrumClient::with_options(&electrum_url, ElectrumOptions { timeout: Some(3) })?; - let tip_client = - ElectrumClient::with_options(&electrum_url, ElectrumOptions { timeout: Some(3) })?; Ok(Self { - electrum_client, - tip_client: Mutex::new(tip_client), + config, + client: OnceLock::new(), }) } + + fn get_client(&self) -> Result<&Client> { + if let Some(c) = self.client.get() { + return Ok(c); + } + let electrum_url = ElectrumUrl::new(&self.config.liquid_electrum_url, true, true)?; + let client = electrum_url.build_client(&ElectrumOptions { timeout: Some(3) })?; + + let client = self.client.get_or_init(|| client); + Ok(client) + } } #[async_trait] impl LiquidChainService for HybridLiquidChainService { async fn tip(&self) -> Result { - Ok(self.tip_client.lock().unwrap().tip()?.height) + let client = self.get_client()?; + let mut maybe_popped_header = None; + while let Some(header) = client.block_headers_pop_raw()? { + maybe_popped_header = Some(header) + } + + let new_tip: Option = match maybe_popped_header { + Some(popped_header) => Some(popped_header.height.try_into()?), + None => { + // https://github.com/bitcoindevkit/rust-electrum-client/issues/124 + // It might be that the client has reconnected and subscriptions don't persist + // across connections. Calling `client.ping()` won't help here because the + // successful retry will prevent us knowing about the reconnect. + if let Ok(header) = client.block_headers_subscribe_raw() { + Some(header.height.try_into()?) + } else { + None + } + } + }; + + new_tip.ok_or_else(|| anyhow!("Failed to get tip")) } async fn broadcast(&self, tx: &Transaction) -> Result { - Ok(self.electrum_client.broadcast(tx)?) + let txid = self + .get_client()? + .transaction_broadcast_raw(&elements_serialize(tx))?; + Ok(Txid::from_raw_hash(txid.to_raw_hash())) } async fn get_transaction_hex(&self, txid: &Txid) -> Result> { @@ -93,19 +124,48 @@ impl LiquidChainService for HybridLiquidChainService { } async fn get_transactions(&self, txids: &[Txid]) -> Result> { - Ok(self.electrum_client.get_transactions(txids)?) + let txids: Vec = txids + .iter() + .map(|t| bitcoin::Txid::from_raw_hash(t.to_raw_hash())) + .collect(); + + let mut result = vec![]; + for tx in self.get_client()?.batch_transaction_get_raw(&txids)? { + let tx: Transaction = elements::encode::deserialize(&tx)?; + result.push(tx); + } + Ok(result) } async fn get_script_history(&self, script: &Script) -> Result> { - let mut history_vec = self.electrum_client.get_scripts_history(&[script])?; + let scripts = &[script]; + let scripts: Vec<&bitcoin::Script> = scripts + .iter() + .map(|t| bitcoin::Script::from_bytes(t.as_bytes())) + .collect(); + + let mut history_vec: Vec> = self + .get_client()? + .batch_script_get_history(&scripts)? + .into_iter() + .map(|e| e.into_iter().map(Into::into).collect()) + .collect(); let h = history_vec.pop(); - Ok(h.unwrap_or(vec![])) + Ok(h.unwrap_or_default()) } async fn get_scripts_history(&self, scripts: &[&Script]) -> Result>> { - self.electrum_client - .get_scripts_history(scripts) - .map_err(Into::into) + let scripts: Vec<&bitcoin::Script> = scripts + .iter() + .map(|t| bitcoin::Script::from_bytes(t.as_bytes())) + .collect(); + + Ok(self + .get_client()? + .batch_script_get_history(&scripts)? + .into_iter() + .map(|e| e.into_iter().map(Into::into).collect()) + .collect()) } async fn get_script_history_with_retry(