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`
pub fn set_logger(logger: Box<dyn Logger>) -> 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<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.
pub fn breez_log_stream(s: StreamSink<LogEntry>) -> 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(())
}

View File

@@ -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<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
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<Vec<GetBalanceRes>> {
Ok(self.client.batch_script_get_balance(scripts)?)
}
async fn verify_tx(
&self,
address: &Address,

View File

@@ -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 {

View File

@@ -28,31 +28,34 @@ pub enum SdkError {
#[error("Service connectivity: {err}")]
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 {
fn from(e: Error) -> Self {
SdkError::Generic { err: e.to_string() }
SdkError::generic(e.to_string())
}
}
impl From<boltz_client::error::Error> 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<secp256k1::Error> for SdkError {
fn from(err: secp256k1::Error) -> Self {
SdkError::Generic {
err: format!("{err:?}"),
}
SdkError::generic(format!("{err:?}"))
}
}

View File

@@ -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<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(
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<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.
#[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;
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<Vec<RefundableSwap>> {
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 {

View File

@@ -34,21 +34,17 @@ impl BoltzSwapper {
)
}
Direction::Outgoing => {
return Err(SdkError::Generic {
err: format!(
return Err(SdkError::generic(format!(
"Cannot create Bitcoin refund wrapper for outgoing Chain swap {}",
swap.id
),
});
)));
}
},
_ => {
return Err(SdkError::Generic {
err: format!(
return Err(SdkError::generic(format!(
"Cannot create Bitcoin refund wrapper for swap {}",
swap.id()
),
});
)));
}
}?;
Ok(refund_wrapper)
@@ -64,28 +60,21 @@ impl BoltzSwapper {
) -> Result<Transaction, SdkError> {
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 {

View File

@@ -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!(
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!(
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
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(),
})?;
.ok_or(SdkError::generic("No refundable UTXOs found"))?;
let refund_tx = LBtcSwapTx {
kind: SwapTxKind::Refund,

View File

@@ -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!(
return Err(SdkError::generic(format!(
"Failed to retrieve refund keypair and preimage for Receive swap {}: invalid swap type",
swap.id
),
});
)));
}
};

View File

@@ -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<Vec<GetBalanceRes>> {
unimplemented!()
}
async fn verify_tx(
&self,
_address: &boltz_client::Address,

View File

@@ -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({