list-refundables: show refundable amount, not swap amount (#516)

* list-refundables: show refundable amount, not swap amount

* Rename chainswap fn for clarity

get_lockup_swap_script_pubkey only applies to Receive Chain Swaps, so it was renamed to get_receive_lockup_swap_script_pubkey.

* list_refundables: batch calls to fetch balance from chain service

* Simplify conversion from Chain Swap to RefundableSwap

* Fix MockBitcoinChainService

* Re-generate flutter bindings

* Add utility for creating SdkError::Generic with &str or String

* Chain Swap getter for swap script pk: throw SdkError instead of anyhow::Error

* Update RefundableSwap comment

Co-authored-by: Ross Savage <551697+dangeross@users.noreply.github.com>

* Re-generate dart files

---------

Co-authored-by: Ross Savage <551697+dangeross@users.noreply.github.com>
This commit is contained in:
ok300
2024-10-07 17:56:02 +02:00
committed by GitHub
parent 950d4243e6
commit 046e7ab1c8
12 changed files with 128 additions and 112 deletions

View File

@@ -44,10 +44,7 @@ impl log::Log for UniffiBindingLogger {
/// If used, this must be called before `connect` /// If used, this must be called before `connect`
pub fn set_logger(logger: Box<dyn Logger>) -> Result<(), SdkError> { pub fn set_logger(logger: Box<dyn Logger>) -> Result<(), SdkError> {
UniffiBindingLogger::init(logger).map_err(|_| SdkError::Generic { UniffiBindingLogger::init(logger).map_err(|_| SdkError::generic("Logger already created"))
err: "Logger already created".into(),
})?;
Ok(())
} }
pub fn connect(req: ConnectRequest) -> Result<Arc<BindingLiquidSdk>, SdkError> { pub fn connect(req: ConnectRequest) -> Result<Arc<BindingLiquidSdk>, SdkError> {

View File

@@ -54,9 +54,7 @@ pub async fn connect(req: ConnectRequest) -> Result<BindingLiquidSdk, SdkError>
/// If used, this must be called before `connect`. It can only be called once. /// If used, this must be called before `connect`. It can only be called once.
pub fn breez_log_stream(s: StreamSink<LogEntry>) -> Result<()> { pub fn breez_log_stream(s: StreamSink<LogEntry>) -> Result<()> {
DartBindingLogger::init(s).map_err(|_| SdkError::Generic { DartBindingLogger::init(s).map_err(|_| SdkError::generic("Log stream already created"))?;
err: "Log stream already created".into(),
})?;
Ok(()) Ok(())
} }

View File

@@ -53,6 +53,9 @@ pub trait BitcoinChainService: Send + Sync {
/// Return the confirmed and unconfirmed balances of a script hash /// Return the confirmed and unconfirmed balances of a script hash
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes>; fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes>;
/// Return the confirmed and unconfirmed balances of a list of script hashes
fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<GetBalanceRes>>;
/// Verify that a transaction appears in the address script history /// Verify that a transaction appears in the address script history
async fn verify_tx( async fn verify_tx(
&self, &self,
@@ -207,6 +210,10 @@ impl BitcoinChainService for HybridBitcoinChainService {
Ok(self.client.script_get_balance(script)?) Ok(self.client.script_get_balance(script)?)
} }
fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<GetBalanceRes>> {
Ok(self.client.batch_script_get_balance(scripts)?)
}
async fn verify_tx( async fn verify_tx(
&self, &self,
address: &Address, address: &Address,

View File

@@ -142,11 +142,7 @@ impl ChainSwapHandler {
let is_monitoring_expired = current_height > monitoring_block_height; let is_monitoring_expired = current_height > monitoring_block_height;
if (is_swap_expired && !is_monitoring_expired) || swap.state == RefundPending { 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.get_receive_lockup_swap_script_pubkey(self.config.network)?;
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_balance = self let script_balance = self
.bitcoin_chain_service .bitcoin_chain_service
.lock() .lock()
@@ -743,9 +739,9 @@ impl ChainSwapHandler {
let swap = self let swap = self
.persister .persister
.fetch_chain_swap_by_lockup_address(lockup_address)? .fetch_chain_swap_by_lockup_address(lockup_address)?
.ok_or(SdkError::Generic { .ok_or(SdkError::generic(format!(
err: format!("Swap {} not found", lockup_address), "Chain Swap with lockup address {lockup_address} not found"
})?; )))?;
let refund_tx_id = swap.refund_tx_id.clone(); let refund_tx_id = swap.refund_tx_id.clone();
if let Some(refund_tx_id) = &refund_tx_id { if let Some(refund_tx_id) = &refund_tx_id {

View File

@@ -28,31 +28,34 @@ pub enum SdkError {
#[error("Service connectivity: {err}")] #[error("Service connectivity: {err}")]
ServiceConnectivity { err: String }, ServiceConnectivity { err: String },
} }
impl SdkError {
pub fn generic<T: AsRef<str>>(err: T) -> Self {
Self::Generic {
err: err.as_ref().to_string(),
}
}
}
impl From<anyhow::Error> for SdkError { impl From<anyhow::Error> for SdkError {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
SdkError::Generic { err: e.to_string() } SdkError::generic(e.to_string())
} }
} }
impl From<boltz_client::error::Error> for SdkError { impl From<boltz_client::error::Error> for SdkError {
fn from(err: boltz_client::error::Error) -> Self { fn from(err: boltz_client::error::Error) -> Self {
match err { match err {
boltz_client::error::Error::HTTP(e) => SdkError::Generic { boltz_client::error::Error::HTTP(e) => {
err: format!("Could not contact servers: {e:?}"), SdkError::generic(format!("Could not contact servers: {e:?}"))
}, }
_ => SdkError::Generic { _ => SdkError::generic(format!("{err:?}")),
err: format!("{err:?}"),
},
} }
} }
} }
impl From<secp256k1::Error> for SdkError { impl From<secp256k1::Error> for SdkError {
fn from(err: secp256k1::Error) -> Self { fn from(err: secp256k1::Error) -> Self {
SdkError::Generic { SdkError::generic(format!("{err:?}"))
err: format!("{err:?}"),
}
} }
} }

View File

@@ -3,6 +3,7 @@ use std::path::PathBuf;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use boltz_client::{ use boltz_client::{
bitcoin::ScriptBuf,
network::Chain, network::Chain,
swaps::boltz::{ swaps::boltz::{
CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree, CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree,
@@ -608,6 +609,27 @@ impl ChainSwap {
Ok(swap_script) 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<ScriptBuf> {
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( pub(crate) fn from_boltz_struct_to_json(
create_response: &CreateChainResponse, create_response: &CreateChainResponse,
expected_swap_id: &str, expected_swap_id: &str,
@@ -681,11 +703,11 @@ impl SendSwap {
&self.get_boltz_create_response()?, &self.get_boltz_create_response()?,
self.get_refund_keypair()?.public_key().into(), self.get_refund_keypair()?.public_key().into(),
) )
.map_err(|e| SdkError::Generic { .map_err(|e| {
err: format!( SdkError::generic(format!(
"Failed to create swap script for Send Swap {}: {e:?}", "Failed to create swap script for Send Swap {}: {e:?}",
self.id self.id
), ))
}) })
} }
@@ -806,17 +828,9 @@ impl ReceiveSwap {
pub struct RefundableSwap { pub struct RefundableSwap {
pub swap_address: String, pub swap_address: String,
pub timestamp: u32, pub timestamp: u32,
/// Amount that is refundable, from all UTXOs
pub amount_sat: u64, pub amount_sat: u64,
} }
impl From<ChainSwap> 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. /// The payment state of an individual payment.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Hash)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Hash)]

View File

@@ -246,9 +246,7 @@ impl LiquidSdk {
let mut is_started = self.is_started.write().await; let mut is_started = self.is_started.write().await;
self.shutdown_sender self.shutdown_sender
.send(()) .send(())
.map_err(|e| SdkError::Generic { .map_err(|e| SdkError::generic(format!("Shutdown failed: {e}")))?;
err: format!("Shutdown failed: {e}"),
})?;
*is_started = false; *is_started = false;
Ok(()) Ok(())
} }
@@ -1740,12 +1738,34 @@ impl LiquidSdk {
/// List all failed chain swaps that need to be refunded. /// List all failed chain swaps that need to be refunded.
/// They can be refunded by calling [LiquidSdk::prepare_refund] then [LiquidSdk::refund]. /// They can be refunded by calling [LiquidSdk::prepare_refund] then [LiquidSdk::refund].
pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> { pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> {
Ok(self let chain_swaps = self.persister.list_refundable_chain_swaps()?;
.persister
.list_refundable_chain_swaps()? let mut lockup_script_pubkeys = vec![];
.into_iter() for swap in &chain_swaps {
.map(Into::into) let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
.collect()) 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. /// 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()); .unwrap_or(self.persister.get_default_backup_path());
ensure_sdk!( ensure_sdk!(
backup_path.exists(), backup_path.exists(),
SdkError::Generic { SdkError::generic("Backup file does not exist").into()
err: "Backup file does not exist".to_string()
}
.into()
); );
self.persister.restore_from_backup(backup_path) self.persister.restore_from_backup(backup_path)
} }
@@ -2121,12 +2138,10 @@ impl LiquidSdk {
}); });
}; };
let preimage_str = preimage let preimage_str =
.clone() preimage.clone().ok_or(LnUrlPayError::Generic {
.ok_or(SdkError::Generic {
err: "Payment successful but no preimage found".to_string(), err: "Payment successful but no preimage found".to_string(),
}) })?;
.unwrap();
let preimage = let preimage =
sha256::Hash::from_str(&preimage_str).map_err(|_| { sha256::Hash::from_str(&preimage_str).map_err(|_| {
LnUrlPayError::Generic { LnUrlPayError::Generic {

View File

@@ -34,21 +34,17 @@ impl BoltzSwapper {
) )
} }
Direction::Outgoing => { Direction::Outgoing => {
return Err(SdkError::Generic { return Err(SdkError::generic(format!(
err: format!( "Cannot create Bitcoin refund wrapper for outgoing Chain swap {}",
"Cannot create Bitcoin refund wrapper for outgoing Chain swap {}", swap.id
swap.id )));
),
});
} }
}, },
_ => { _ => {
return Err(SdkError::Generic { return Err(SdkError::generic(format!(
err: format!( "Cannot create Bitcoin refund wrapper for swap {}",
"Cannot create Bitcoin refund wrapper for swap {}", swap.id()
swap.id() )));
),
});
} }
}?; }?;
Ok(refund_wrapper) Ok(refund_wrapper)
@@ -64,28 +60,21 @@ impl BoltzSwapper {
) -> Result<Transaction, SdkError> { ) -> Result<Transaction, SdkError> {
ensure_sdk!( ensure_sdk!(
swap.direction == Direction::Incoming, swap.direction == Direction::Incoming,
SdkError::Generic { SdkError::generic("Cannot create BTC refund tx for outgoing Chain swaps.")
err: "Cannot create BTC refund tx for outgoing Chain swaps.".to_string()
}
); );
let address = Address::from_str(refund_address).map_err(|err| SdkError::Generic { let address = Address::from_str(refund_address)
err: format!("Could not parse address: {err:?}"), .map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?;
})?;
ensure_sdk!( ensure_sdk!(
address.is_valid_for_network(self.config.network.into()), address.is_valid_for_network(self.config.network.into()),
SdkError::Generic { SdkError::generic("Address network validation failed")
err: "Address network validation failed".to_string()
}
); );
let utxo = utxos let utxo = utxos
.first() .first()
.and_then(|utxo| utxo.as_bitcoin().cloned()) .and_then(|utxo| utxo.as_bitcoin().cloned())
.ok_or(SdkError::Generic { .ok_or(SdkError::generic("No UTXO found"))?;
err: "No UTXO found".to_string(),
})?;
let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?; let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?;
let refund_tx = BtcSwapTx { let refund_tx = BtcSwapTx {

View File

@@ -111,12 +111,10 @@ impl BoltzSwapper {
let refund_wrapper = match swap { let refund_wrapper = match swap {
Swap::Chain(swap) => match swap.direction { Swap::Chain(swap) => match swap.direction {
Direction::Incoming => { Direction::Incoming => {
return Err(SdkError::Generic { return Err(SdkError::generic(format!(
err: format!( "Cannot create Liquid refund wrapper for incoming Chain swap {}",
"Cannot create Liquid refund wrapper for incoming Chain swap {}", swap.id
swap.id )));
),
});
} }
Direction::Outgoing => { Direction::Outgoing => {
let swap_script = swap.get_lockup_swap_script()?; let swap_script = swap.get_lockup_swap_script()?;
@@ -140,12 +138,10 @@ impl BoltzSwapper {
) )
} }
Swap::Receive(swap) => { Swap::Receive(swap) => {
return Err(SdkError::Generic { return Err(SdkError::generic(format!(
err: format!( "Cannot create Liquid refund wrapper for Receive swap {}",
"Cannot create Liquid refund wrapper for Receive swap {}", swap.id
swap.id )));
),
});
} }
}?; }?;
Ok(refund_wrapper) Ok(refund_wrapper)
@@ -162,9 +158,7 @@ impl BoltzSwapper {
Swap::Chain(swap) => { Swap::Chain(swap) => {
ensure_sdk!( ensure_sdk!(
swap.direction == Direction::Outgoing, swap.direction == Direction::Outgoing,
SdkError::Generic { SdkError::generic("Cannot create LBTC refund tx for incoming Chain swaps")
err: "Cannot create LBTC refund tx for incoming Chain swaps".to_string()
}
); );
( (
@@ -179,25 +173,23 @@ impl BoltzSwapper {
Preimage::new(), Preimage::new(),
), ),
Swap::Receive(_) => { Swap::Receive(_) => {
return Err(SdkError::Generic { return Err(SdkError::generic(
err: "Cannot create LBTC refund tx for Receive swaps.".to_string(), "Cannot create LBTC refund tx for Receive swaps.",
}); ));
} }
}; };
let swap_id = swap.id(); let swap_id = swap.id();
let address = Address::from_str(refund_address).map_err(|err| SdkError::Generic { let address = Address::from_str(refund_address)
err: format!("Could not parse address: {err:?}"), .map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?;
})?;
let genesis_hash = liquid_genesis_hash(&self.liquid_electrum_config)?; let genesis_hash = liquid_genesis_hash(&self.liquid_electrum_config)?;
let (funding_outpoint, funding_tx_out) = *utxos let (funding_outpoint, funding_tx_out) =
.first() *utxos
.and_then(|utxo| utxo.as_liquid()) .first()
.ok_or(SdkError::Generic { .and_then(|utxo| utxo.as_liquid())
err: "No refundable UTXOs found".to_string(), .ok_or(SdkError::generic("No refundable UTXOs found"))?;
})?;
let refund_tx = LBtcSwapTx { let refund_tx = LBtcSwapTx {
kind: SwapTxKind::Refund, kind: SwapTxKind::Refund,

View File

@@ -297,12 +297,10 @@ impl Swapper for BoltzSwapper {
), ),
Swap::Send(swap) => (swap.get_refund_keypair()?, Preimage::new()), Swap::Send(swap) => (swap.get_refund_keypair()?, Preimage::new()),
Swap::Receive(swap) => { Swap::Receive(swap) => {
return Err(SdkError::Generic { return Err(SdkError::generic(format!(
err: format!( "Failed to retrieve refund keypair and preimage for Receive swap {}: invalid swap type",
"Failed to retrieve refund keypair and preimage for Receive swap {}: invalid swap type", swap.id
swap.id )));
),
});
} }
}; };

View File

@@ -10,6 +10,7 @@ use boltz_client::{
Amount, Amount,
}; };
use electrum_client::bitcoin::{consensus::deserialize, OutPoint, Script, TxOut}; use electrum_client::bitcoin::{consensus::deserialize, OutPoint, Script, TxOut};
use electrum_client::GetBalanceRes;
use lwk_wollet::{ use lwk_wollet::{
elements::{BlockHash, Txid as ElementsTxid}, elements::{BlockHash, Txid as ElementsTxid},
History, History,
@@ -188,6 +189,10 @@ impl BitcoinChainService for MockBitcoinChainService {
unimplemented!() unimplemented!()
} }
fn scripts_get_balance(&self, _scripts: &[&Script]) -> Result<Vec<GetBalanceRes>> {
unimplemented!()
}
async fn verify_tx( async fn verify_tx(
&self, &self,
_address: &boltz_client::Address, _address: &boltz_client::Address,

View File

@@ -1047,6 +1047,8 @@ class RefundResponse {
class RefundableSwap { class RefundableSwap {
final String swapAddress; final String swapAddress;
final int timestamp; final int timestamp;
/// Amount that is refundable, from all UTXOs
final BigInt amountSat; final BigInt amountSat;
const RefundableSwap({ const RefundableSwap({