From 54d7e9acb189d8b10e941452d6c84cc00ee72d31 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Sun, 2 Feb 2025 17:00:16 +0200 Subject: [PATCH 1/7] remove electrum tip query on start --- lib/core/src/chain/bitcoin.rs | 20 ++------ lib/core/src/chain/liquid.rs | 91 +++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 37 deletions(-) diff --git a/lib/core/src/chain/bitcoin.rs b/lib/core/src/chain/bitcoin.rs index c429483..8f99cf3 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::time::Duration; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -76,7 +76,6 @@ pub trait BitcoinChainService: Send + Sync { pub(crate) struct HybridBitcoinChainService { client: Client, - tip: Mutex, config: Config, } impl HybridBitcoinChainService { @@ -88,14 +87,7 @@ impl HybridBitcoinChainService { 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()?; - - Ok(Self { - client, - tip: Mutex::new(tip), - config, - }) + Ok(Self { client, config }) } } @@ -122,12 +114,8 @@ impl BitcoinChainService for HybridBitcoinChainService { } }; - let mut tip = self.tip.lock().unwrap(); - if let Some(new_tip) = new_tip { - *tip = new_tip; - } - - Ok(tip.clone()) + let tip = new_tip.ok_or_else(|| anyhow!("Failed to get tip"))?; + Ok(tip) } fn broadcast(&self, tx: &Transaction) -> Result { diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 0f0cb03..d8bf5c6 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -1,17 +1,17 @@ -use std::sync::Mutex; use std::time::Duration; use anyhow::{anyhow, Result}; use async_trait::async_trait; use boltz_client::ToHex; +use electrum_client::{Client, ElectrumApi, HeaderNotification}; +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 +60,52 @@ pub trait LiquidChainService: Send + Sync { } pub(crate) struct HybridLiquidChainService { - electrum_client: ElectrumClient, - tip_client: Mutex, + client: Client, } 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), - }) + let client = electrum_url.build_client(&ElectrumOptions { timeout: Some(3) })?; + Ok(Self { client }) } } #[async_trait] impl LiquidChainService for HybridLiquidChainService { async fn tip(&self) -> Result { - Ok(self.tip_client.lock().unwrap().tip()?.height) + let mut maybe_popped_header = None; + while let Some(header) = self.client.block_headers_pop_raw()? { + maybe_popped_header = Some(header) + } + + let new_tip: Option = match maybe_popped_header { + Some(popped_header) => Some(popped_header.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) = self.client.block_headers_subscribe_raw() { + Some(header.try_into()?) + } else { + None + } + } + }; + + let tip: u32 = new_tip + .ok_or_else(|| anyhow!("Failed to get tip"))? + .height + .try_into()?; + Ok(tip) } async fn broadcast(&self, tx: &Transaction) -> Result { - Ok(self.electrum_client.broadcast(tx)?) + let txid = self + .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 +113,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.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 + .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![])) } 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 + .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( From f0e25807af0e9867397b2c7543222845922e920c Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Sun, 2 Feb 2025 17:19:34 +0200 Subject: [PATCH 2/7] fix clippy --- lib/core/src/chain/liquid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index d8bf5c6..8d00568 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -140,7 +140,7 @@ impl LiquidChainService for HybridLiquidChainService { .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>> { From 262254f998c77b793f693197172c6a5adfb566db Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Sun, 2 Feb 2025 18:29:35 +0200 Subject: [PATCH 3/7] simlify tip --- lib/core/src/chain/liquid.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 8d00568..a340682 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::{anyhow, Result}; use async_trait::async_trait; use boltz_client::ToHex; -use electrum_client::{Client, ElectrumApi, HeaderNotification}; +use electrum_client::{Client, ElectrumApi}; use elements::encode::serialize as elements_serialize; use log::info; use lwk_wollet::elements::hex::FromHex; @@ -75,29 +75,31 @@ impl HybridLiquidChainService { impl LiquidChainService for HybridLiquidChainService { async fn tip(&self) -> Result { let mut maybe_popped_header = None; + println!("Fetching block headers"); while let Some(header) = self.client.block_headers_pop_raw()? { maybe_popped_header = Some(header) } - let new_tip: Option = match maybe_popped_header { - Some(popped_header) => Some(popped_header.try_into()?), + 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 + println!("Fetching block headers none"); + // https://github.com/bitcoindevkit/rusprintln!("Fetching block headers");t-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) = self.client.block_headers_subscribe_raw() { - Some(header.try_into()?) + println!("Fetching block headers block_headers_subscribe_raw returned result"); + println!("header: {:?}", header.height); + Some(header.height.try_into()?) } else { + println!("Fetching block headers block_headers_subscribe_raw returned None"); None } } }; - let tip: u32 = new_tip - .ok_or_else(|| anyhow!("Failed to get tip"))? - .height - .try_into()?; + let tip: u32 = new_tip.ok_or_else(|| anyhow!("Failed to get tip"))?; Ok(tip) } From a5cc34d591b0c6f19da6a887b4e5632906116edf Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Sun, 2 Feb 2025 22:43:42 +0200 Subject: [PATCH 4/7] lazy initialization of electrum --- lib/core/src/chain/bitcoin.rs | 42 ++++++++++++++++++++++------------- lib/core/src/chain/liquid.rs | 33 +++++++++++++++++++-------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/lib/core/src/chain/bitcoin.rs b/lib/core/src/chain/bitcoin.rs index 8f99cf3..53de995 100644 --- a/lib/core/src/chain/bitcoin.rs +++ b/lib/core/src/chain/bitcoin.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{sync::OnceLock, time::Duration}; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -75,27 +75,35 @@ pub trait BitcoinChainService: Send + Sync { } pub(crate) struct HybridBitcoinChainService { - client: Client, + 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)?; - Ok(Self { client, config }) + 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) })?; + + 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) } @@ -106,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 @@ -119,13 +127,15 @@ impl BitcoinChainService for HybridBitcoinChainService { } 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); } @@ -134,7 +144,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) @@ -143,7 +153,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()) @@ -222,11 +232,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 a340682..5438496 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -1,3 +1,4 @@ +use std::sync::OnceLock; use std::time::Duration; use anyhow::{anyhow, Result}; @@ -60,23 +61,37 @@ pub trait LiquidChainService: Send + Sync { } pub(crate) struct HybridLiquidChainService { - client: Client, + client: OnceLock, + config: Config, } impl HybridLiquidChainService { pub(crate) fn new(config: Config) -> Result { - let electrum_url = ElectrumUrl::new(&config.liquid_electrum_url, true, true)?; + Ok(Self { + 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) })?; - Ok(Self { client }) + + let client = self.client.get_or_init(|| client); + Ok(client) } } #[async_trait] impl LiquidChainService for HybridLiquidChainService { async fn tip(&self) -> Result { + let client = self.get_client()?; let mut maybe_popped_header = None; println!("Fetching block headers"); - while let Some(header) = self.client.block_headers_pop_raw()? { + while let Some(header) = client.block_headers_pop_raw()? { maybe_popped_header = Some(header) } @@ -88,7 +103,7 @@ impl LiquidChainService for HybridLiquidChainService { // 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() { println!("Fetching block headers block_headers_subscribe_raw returned result"); println!("header: {:?}", header.height); Some(header.height.try_into()?) @@ -105,7 +120,7 @@ impl LiquidChainService for HybridLiquidChainService { async fn broadcast(&self, tx: &Transaction) -> Result { let txid = self - .client + .get_client()? .transaction_broadcast_raw(&elements_serialize(tx))?; Ok(Txid::from_raw_hash(txid.to_raw_hash())) } @@ -121,7 +136,7 @@ impl LiquidChainService for HybridLiquidChainService { .collect(); 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 = elements::encode::deserialize(&tx)?; result.push(tx); } @@ -136,7 +151,7 @@ impl LiquidChainService for HybridLiquidChainService { .collect(); let mut history_vec: Vec> = self - .client + .get_client()? .batch_script_get_history(&scripts)? .into_iter() .map(|e| e.into_iter().map(Into::into).collect()) @@ -152,7 +167,7 @@ impl LiquidChainService for HybridLiquidChainService { .collect(); Ok(self - .client + .get_client()? .batch_script_get_history(&scripts)? .into_iter() .map(|e| e.into_iter().map(Into::into).collect()) From e5d0e94e5837e27e354a62615485dcce6a79001d Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Mon, 3 Feb 2025 11:43:25 +0200 Subject: [PATCH 5/7] remove println statements --- lib/core/src/chain/liquid.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 5438496..4bb4064 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -90,7 +90,6 @@ impl LiquidChainService for HybridLiquidChainService { async fn tip(&self) -> Result { let client = self.get_client()?; let mut maybe_popped_header = None; - println!("Fetching block headers"); while let Some(header) = client.block_headers_pop_raw()? { maybe_popped_header = Some(header) } @@ -98,17 +97,13 @@ impl LiquidChainService for HybridLiquidChainService { let new_tip: Option = match maybe_popped_header { Some(popped_header) => Some(popped_header.height.try_into()?), None => { - println!("Fetching block headers none"); // https://github.com/bitcoindevkit/rusprintln!("Fetching block headers");t-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() { - println!("Fetching block headers block_headers_subscribe_raw returned result"); - println!("header: {:?}", header.height); Some(header.height.try_into()?) } else { - println!("Fetching block headers block_headers_subscribe_raw returned None"); None } } From 41aca3c8ec6b5fa2751f2140dbb40a676f22bc13 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Mon, 3 Feb 2025 12:53:55 +0200 Subject: [PATCH 6/7] fix comment --- lib/core/src/chain/liquid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 4bb4064..3d6aa52 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -97,7 +97,7 @@ impl LiquidChainService for HybridLiquidChainService { let new_tip: Option = match maybe_popped_header { Some(popped_header) => Some(popped_header.height.try_into()?), None => { - // https://github.com/bitcoindevkit/rusprintln!("Fetching block headers");t-electrum-client/issues/124 + // 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. From 24dac2e25850661c3cfb9e144d4abf7f601f6629 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Mon, 3 Feb 2025 14:52:31 +0200 Subject: [PATCH 7/7] Cleanup code --- lib/core/src/chain/bitcoin.rs | 3 +-- lib/core/src/chain/liquid.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/core/src/chain/bitcoin.rs b/lib/core/src/chain/bitcoin.rs index 53de995..96e5385 100644 --- a/lib/core/src/chain/bitcoin.rs +++ b/lib/core/src/chain/bitcoin.rs @@ -122,8 +122,7 @@ impl BitcoinChainService for HybridBitcoinChainService { } }; - let tip = new_tip.ok_or_else(|| anyhow!("Failed to get tip"))?; - Ok(tip) + new_tip.ok_or_else(|| anyhow!("Failed to get tip")) } fn broadcast(&self, tx: &Transaction) -> Result { diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 3d6aa52..0a25a0a 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -109,8 +109,7 @@ impl LiquidChainService for HybridLiquidChainService { } }; - let tip: u32 = new_tip.ok_or_else(|| anyhow!("Failed to get tip"))?; - Ok(tip) + new_tip.ok_or_else(|| anyhow!("Failed to get tip")) } async fn broadcast(&self, tx: &Transaction) -> Result {