diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index 20f997a..32d3fbd 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -44,10 +44,7 @@ impl log::Log for UniffiBindingLogger { /// If used, this must be called before `connect` pub fn set_logger(logger: Box) -> Result<(), SdkError> { - UniffiBindingLogger::init(logger).map_err(|_| SdkError::Generic { - err: "Logger already created".into(), - })?; - Ok(()) + UniffiBindingLogger::init(logger).map_err(|_| SdkError::generic("Logger already created")) } pub fn connect(req: ConnectRequest) -> Result, SdkError> { diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index 4817d86..27db65d 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -54,9 +54,7 @@ pub async fn connect(req: ConnectRequest) -> Result /// If used, this must be called before `connect`. It can only be called once. pub fn breez_log_stream(s: StreamSink) -> Result<()> { - DartBindingLogger::init(s).map_err(|_| SdkError::Generic { - err: "Log stream already created".into(), - })?; + DartBindingLogger::init(s).map_err(|_| SdkError::generic("Log stream already created"))?; Ok(()) } diff --git a/lib/core/src/chain/bitcoin.rs b/lib/core/src/chain/bitcoin.rs index d3ef4f9..cf08420 100644 --- a/lib/core/src/chain/bitcoin.rs +++ b/lib/core/src/chain/bitcoin.rs @@ -53,6 +53,9 @@ pub trait BitcoinChainService: Send + Sync { /// Return the confirmed and unconfirmed balances of a script hash fn script_get_balance(&self, script: &Script) -> Result; + /// Return the confirmed and unconfirmed balances of a list of script hashes + fn scripts_get_balance(&self, scripts: &[&Script]) -> Result>; + /// Verify that a transaction appears in the address script history async fn verify_tx( &self, @@ -207,6 +210,10 @@ impl BitcoinChainService for HybridBitcoinChainService { Ok(self.client.script_get_balance(script)?) } + fn scripts_get_balance(&self, scripts: &[&Script]) -> Result> { + Ok(self.client.batch_script_get_balance(scripts)?) + } + async fn verify_tx( &self, address: &Address, diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index e45c265..8705536 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -142,11 +142,7 @@ impl ChainSwapHandler { let is_monitoring_expired = current_height > monitoring_block_height; if (is_swap_expired && !is_monitoring_expired) || swap.state == RefundPending { - let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?; - let script_pubkey = swap_script - .to_address(self.config.network.as_bitcoin_chain()) - .map_err(|e| anyhow!("Error getting script address: {e:?}"))? - .script_pubkey(); + let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?; let script_balance = self .bitcoin_chain_service .lock() @@ -743,9 +739,9 @@ impl ChainSwapHandler { let swap = self .persister .fetch_chain_swap_by_lockup_address(lockup_address)? - .ok_or(SdkError::Generic { - err: format!("Swap {} not found", lockup_address), - })?; + .ok_or(SdkError::generic(format!( + "Chain Swap with lockup address {lockup_address} not found" + )))?; let refund_tx_id = swap.refund_tx_id.clone(); if let Some(refund_tx_id) = &refund_tx_id { diff --git a/lib/core/src/error.rs b/lib/core/src/error.rs index 6c2e622..a834c59 100644 --- a/lib/core/src/error.rs +++ b/lib/core/src/error.rs @@ -28,31 +28,34 @@ pub enum SdkError { #[error("Service connectivity: {err}")] ServiceConnectivity { err: String }, } +impl SdkError { + pub fn generic>(err: T) -> Self { + Self::Generic { + err: err.as_ref().to_string(), + } + } +} impl From for SdkError { fn from(e: Error) -> Self { - SdkError::Generic { err: e.to_string() } + SdkError::generic(e.to_string()) } } impl From for SdkError { fn from(err: boltz_client::error::Error) -> Self { match err { - boltz_client::error::Error::HTTP(e) => SdkError::Generic { - err: format!("Could not contact servers: {e:?}"), - }, - _ => SdkError::Generic { - err: format!("{err:?}"), - }, + boltz_client::error::Error::HTTP(e) => { + SdkError::generic(format!("Could not contact servers: {e:?}")) + } + _ => SdkError::generic(format!("{err:?}")), } } } impl From for SdkError { fn from(err: secp256k1::Error) -> Self { - SdkError::Generic { - err: format!("{err:?}"), - } + SdkError::generic(format!("{err:?}")) } } diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 18a56d9..343bf6f 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use anyhow::{anyhow, Result}; use boltz_client::{ + bitcoin::ScriptBuf, network::Chain, swaps::boltz::{ CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree, @@ -608,6 +609,27 @@ impl ChainSwap { Ok(swap_script) } + /// Returns the lockup script pubkey for Receive Chain Swaps + pub(crate) fn get_receive_lockup_swap_script_pubkey( + &self, + network: LiquidNetwork, + ) -> SdkResult { + let swap_script = self.get_lockup_swap_script()?.as_bitcoin_script()?; + let script_pubkey = swap_script + .to_address(network.as_bitcoin_chain()) + .map_err(|e| SdkError::generic(format!("Error getting script address: {e:?}")))? + .script_pubkey(); + Ok(script_pubkey) + } + + pub(crate) fn to_refundable(&self, refundable_amount_sat: u64) -> RefundableSwap { + RefundableSwap { + swap_address: self.lockup_address.clone(), + timestamp: self.created_at, + amount_sat: refundable_amount_sat, + } + } + pub(crate) fn from_boltz_struct_to_json( create_response: &CreateChainResponse, expected_swap_id: &str, @@ -681,11 +703,11 @@ impl SendSwap { &self.get_boltz_create_response()?, self.get_refund_keypair()?.public_key().into(), ) - .map_err(|e| SdkError::Generic { - err: format!( + .map_err(|e| { + SdkError::generic(format!( "Failed to create swap script for Send Swap {}: {e:?}", self.id - ), + )) }) } @@ -806,17 +828,9 @@ impl ReceiveSwap { pub struct RefundableSwap { pub swap_address: String, pub timestamp: u32, + /// Amount that is refundable, from all UTXOs pub amount_sat: u64, } -impl From for RefundableSwap { - fn from(swap: ChainSwap) -> Self { - Self { - swap_address: swap.lockup_address, - timestamp: swap.created_at, - amount_sat: swap.payer_amount_sat, - } - } -} /// The payment state of an individual payment. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Hash)] diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 6a88f1d..42cfce1 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -246,9 +246,7 @@ impl LiquidSdk { let mut is_started = self.is_started.write().await; self.shutdown_sender .send(()) - .map_err(|e| SdkError::Generic { - err: format!("Shutdown failed: {e}"), - })?; + .map_err(|e| SdkError::generic(format!("Shutdown failed: {e}")))?; *is_started = false; Ok(()) } @@ -1740,12 +1738,34 @@ impl LiquidSdk { /// List all failed chain swaps that need to be refunded. /// They can be refunded by calling [LiquidSdk::prepare_refund] then [LiquidSdk::refund]. pub async fn list_refundables(&self) -> SdkResult> { - Ok(self - .persister - .list_refundable_chain_swaps()? - .into_iter() - .map(Into::into) - .collect()) + let chain_swaps = self.persister.list_refundable_chain_swaps()?; + + let mut lockup_script_pubkeys = vec![]; + for swap in &chain_swaps { + let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?; + lockup_script_pubkeys.push(script_pubkey); + } + let lockup_scripts: Vec<&boltz_client::bitcoin::Script> = lockup_script_pubkeys + .iter() + .map(|s| s.as_script()) + .collect(); + let scripts_balance = self + .bitcoin_chain_service + .lock() + .await + .scripts_get_balance(&lockup_scripts)?; + + let mut refundables = vec![]; + for (chain_swap, script_balance) in chain_swaps.into_iter().zip(scripts_balance) { + let swap_id = &chain_swap.id; + let refundable_confirmed_sat = script_balance.confirmed; + info!("Incoming Chain Swap {swap_id} is refundable with {refundable_confirmed_sat} confirmed sats"); + + let refundable: RefundableSwap = chain_swap.to_refundable(refundable_confirmed_sat); + refundables.push(refundable); + } + + Ok(refundables) } /// Prepares to refund a failed chain swap by calculating the refund transaction size and absolute fee. @@ -2069,10 +2089,7 @@ impl LiquidSdk { .unwrap_or(self.persister.get_default_backup_path()); ensure_sdk!( backup_path.exists(), - SdkError::Generic { - err: "Backup file does not exist".to_string() - } - .into() + SdkError::generic("Backup file does not exist").into() ); self.persister.restore_from_backup(backup_path) } @@ -2121,12 +2138,10 @@ impl LiquidSdk { }); }; - let preimage_str = preimage - .clone() - .ok_or(SdkError::Generic { + let preimage_str = + preimage.clone().ok_or(LnUrlPayError::Generic { err: "Payment successful but no preimage found".to_string(), - }) - .unwrap(); + })?; let preimage = sha256::Hash::from_str(&preimage_str).map_err(|_| { LnUrlPayError::Generic { diff --git a/lib/core/src/swapper/boltz/bitcoin.rs b/lib/core/src/swapper/boltz/bitcoin.rs index c1d1bdb..65244d5 100644 --- a/lib/core/src/swapper/boltz/bitcoin.rs +++ b/lib/core/src/swapper/boltz/bitcoin.rs @@ -34,21 +34,17 @@ impl BoltzSwapper { ) } Direction::Outgoing => { - return Err(SdkError::Generic { - err: format!( - "Cannot create Bitcoin refund wrapper for outgoing Chain swap {}", - swap.id - ), - }); + return Err(SdkError::generic(format!( + "Cannot create Bitcoin refund wrapper for outgoing Chain swap {}", + swap.id + ))); } }, _ => { - return Err(SdkError::Generic { - err: format!( - "Cannot create Bitcoin refund wrapper for swap {}", - swap.id() - ), - }); + return Err(SdkError::generic(format!( + "Cannot create Bitcoin refund wrapper for swap {}", + swap.id() + ))); } }?; Ok(refund_wrapper) @@ -64,28 +60,21 @@ impl BoltzSwapper { ) -> Result { ensure_sdk!( swap.direction == Direction::Incoming, - SdkError::Generic { - err: "Cannot create BTC refund tx for outgoing Chain swaps.".to_string() - } + SdkError::generic("Cannot create BTC refund tx for outgoing Chain swaps.") ); - let address = Address::from_str(refund_address).map_err(|err| SdkError::Generic { - err: format!("Could not parse address: {err:?}"), - })?; + let address = Address::from_str(refund_address) + .map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?; ensure_sdk!( address.is_valid_for_network(self.config.network.into()), - SdkError::Generic { - err: "Address network validation failed".to_string() - } + SdkError::generic("Address network validation failed") ); let utxo = utxos .first() .and_then(|utxo| utxo.as_bitcoin().cloned()) - .ok_or(SdkError::Generic { - err: "No UTXO found".to_string(), - })?; + .ok_or(SdkError::generic("No UTXO found"))?; let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?; let refund_tx = BtcSwapTx { diff --git a/lib/core/src/swapper/boltz/liquid.rs b/lib/core/src/swapper/boltz/liquid.rs index 7dc5eb6..6374af5 100644 --- a/lib/core/src/swapper/boltz/liquid.rs +++ b/lib/core/src/swapper/boltz/liquid.rs @@ -111,12 +111,10 @@ impl BoltzSwapper { let refund_wrapper = match swap { Swap::Chain(swap) => match swap.direction { Direction::Incoming => { - return Err(SdkError::Generic { - err: format!( - "Cannot create Liquid refund wrapper for incoming Chain swap {}", - swap.id - ), - }); + return Err(SdkError::generic(format!( + "Cannot create Liquid refund wrapper for incoming Chain swap {}", + swap.id + ))); } Direction::Outgoing => { let swap_script = swap.get_lockup_swap_script()?; @@ -140,12 +138,10 @@ impl BoltzSwapper { ) } Swap::Receive(swap) => { - return Err(SdkError::Generic { - err: format!( - "Cannot create Liquid refund wrapper for Receive swap {}", - swap.id - ), - }); + return Err(SdkError::generic(format!( + "Cannot create Liquid refund wrapper for Receive swap {}", + swap.id + ))); } }?; Ok(refund_wrapper) @@ -162,9 +158,7 @@ impl BoltzSwapper { Swap::Chain(swap) => { ensure_sdk!( swap.direction == Direction::Outgoing, - SdkError::Generic { - err: "Cannot create LBTC refund tx for incoming Chain swaps".to_string() - } + SdkError::generic("Cannot create LBTC refund tx for incoming Chain swaps") ); ( @@ -179,25 +173,23 @@ impl BoltzSwapper { Preimage::new(), ), Swap::Receive(_) => { - return Err(SdkError::Generic { - err: "Cannot create LBTC refund tx for Receive swaps.".to_string(), - }); + return Err(SdkError::generic( + "Cannot create LBTC refund tx for Receive swaps.", + )); } }; let swap_id = swap.id(); - let address = Address::from_str(refund_address).map_err(|err| SdkError::Generic { - err: format!("Could not parse address: {err:?}"), - })?; + let address = Address::from_str(refund_address) + .map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?; let genesis_hash = liquid_genesis_hash(&self.liquid_electrum_config)?; - let (funding_outpoint, funding_tx_out) = *utxos - .first() - .and_then(|utxo| utxo.as_liquid()) - .ok_or(SdkError::Generic { - err: "No refundable UTXOs found".to_string(), - })?; + let (funding_outpoint, funding_tx_out) = + *utxos + .first() + .and_then(|utxo| utxo.as_liquid()) + .ok_or(SdkError::generic("No refundable UTXOs found"))?; let refund_tx = LBtcSwapTx { kind: SwapTxKind::Refund, diff --git a/lib/core/src/swapper/boltz/mod.rs b/lib/core/src/swapper/boltz/mod.rs index 56d772b..fb176cc 100644 --- a/lib/core/src/swapper/boltz/mod.rs +++ b/lib/core/src/swapper/boltz/mod.rs @@ -297,12 +297,10 @@ impl Swapper for BoltzSwapper { ), Swap::Send(swap) => (swap.get_refund_keypair()?, Preimage::new()), Swap::Receive(swap) => { - return Err(SdkError::Generic { - err: format!( - "Failed to retrieve refund keypair and preimage for Receive swap {}: invalid swap type", - swap.id - ), - }); + return Err(SdkError::generic(format!( + "Failed to retrieve refund keypair and preimage for Receive swap {}: invalid swap type", + swap.id + ))); } }; diff --git a/lib/core/src/test_utils/chain.rs b/lib/core/src/test_utils/chain.rs index 67924a8..275710a 100644 --- a/lib/core/src/test_utils/chain.rs +++ b/lib/core/src/test_utils/chain.rs @@ -10,6 +10,7 @@ use boltz_client::{ Amount, }; use electrum_client::bitcoin::{consensus::deserialize, OutPoint, Script, TxOut}; +use electrum_client::GetBalanceRes; use lwk_wollet::{ elements::{BlockHash, Txid as ElementsTxid}, History, @@ -188,6 +189,10 @@ impl BitcoinChainService for MockBitcoinChainService { unimplemented!() } + fn scripts_get_balance(&self, _scripts: &[&Script]) -> Result> { + unimplemented!() + } + async fn verify_tx( &self, _address: &boltz_client::Address, diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index 2a02d64..3570a32 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -1047,6 +1047,8 @@ class RefundResponse { class RefundableSwap { final String swapAddress; final int timestamp; + + /// Amount that is refundable, from all UTXOs final BigInt amountSat; const RefundableSwap({