mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-17 04:54:20 +01:00
Handle send swap minimal_batched (#863)
* Handle send swap minimal_batched * Simplify batch handling * Update recoverer handling of batched send swap * Use the preimage to determine if the swap is complete * Use the preimage endpoint when the swap is batched * Try cooperative claim before fetching preimage
This commit is contained in:
@@ -61,10 +61,13 @@ impl SendSwapHandler {
|
||||
let mut recovered_data = Self::recover_onchain_data(&context.tx_map, &swap_id, history)?;
|
||||
|
||||
// Recover preimage if needed
|
||||
if let (Some(claim_tx_id), None) = (&recovered_data.claim_tx_id, &send_swap.preimage) {
|
||||
if recovered_data.lockup_tx_id.is_some() && send_swap.preimage.is_none() {
|
||||
// We can attempt to recover the preimage cooperatively after we know the
|
||||
// lockup tx was broadcast. If we cannot recover it cooperatively,
|
||||
// we can try to recover it from the claim tx.
|
||||
match Self::recover_preimage(
|
||||
context,
|
||||
claim_tx_id.txid,
|
||||
recovered_data.claim_tx_id.clone(),
|
||||
&swap_id,
|
||||
context.swapper.clone(),
|
||||
)
|
||||
@@ -130,7 +133,9 @@ impl SendSwapHandler {
|
||||
// Update state based on recovered data
|
||||
let timeout_block_height = send_swap.timeout_block_height as u32;
|
||||
let is_expired = current_block_height >= timeout_block_height;
|
||||
if let Some(new_state) = recovered_data.derive_partial_state(is_expired) {
|
||||
if let Some(new_state) =
|
||||
recovered_data.derive_partial_state(send_swap.preimage.clone(), is_expired)
|
||||
{
|
||||
send_swap.state = new_state;
|
||||
}
|
||||
|
||||
@@ -181,30 +186,35 @@ impl SendSwapHandler {
|
||||
})
|
||||
}
|
||||
|
||||
/// Tries to recover the preimage for a send swap from its claim tx
|
||||
/// Tries to recover the preimage for a send swap
|
||||
async fn recover_preimage(
|
||||
context: &RecoveryContext,
|
||||
claim_tx_id: Txid,
|
||||
claim_tx_id: Option<LBtcHistory>,
|
||||
swap_id: &str,
|
||||
swapper: Arc<dyn Swapper>,
|
||||
) -> Result<Option<String>> {
|
||||
// Try cooperative first
|
||||
if let Ok(preimage) = swapper.get_submarine_preimage(swap_id).await {
|
||||
log::debug!("Found Send Swap {swap_id} preimage cooperatively: {preimage}");
|
||||
log::debug!("Fetched Send Swap {swap_id} preimage cooperatively: {preimage}");
|
||||
return Ok(Some(preimage));
|
||||
}
|
||||
warn!("Could not recover Send swap {swap_id} preimage cooperatively");
|
||||
let claim_txs = context
|
||||
.liquid_chain_service
|
||||
.get_transactions(&[claim_tx_id])
|
||||
.await?;
|
||||
|
||||
match claim_txs.is_empty() {
|
||||
false => Self::extract_preimage_from_claim_tx(&claim_txs[0], swap_id).map(Some),
|
||||
true => {
|
||||
warn!("Could not recover Send swap {swap_id} preimage non cooperatively");
|
||||
Ok(None)
|
||||
match claim_tx_id {
|
||||
// If we have a claim tx id, we can try to recover the preimage from the tx
|
||||
Some(claim_tx_id) => {
|
||||
let claim_txs = context
|
||||
.liquid_chain_service
|
||||
.get_transactions(&[claim_tx_id.txid])
|
||||
.await?;
|
||||
match claim_txs.is_empty() {
|
||||
false => Self::extract_preimage_from_claim_tx(&claim_txs[0], swap_id).map(Some),
|
||||
true => {
|
||||
warn!("Could not recover Send swap {swap_id} preimage non cooperatively");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,9 +255,13 @@ pub(crate) struct RecoveredOnchainDataSend {
|
||||
}
|
||||
|
||||
impl RecoveredOnchainDataSend {
|
||||
pub(crate) fn derive_partial_state(&self, is_expired: bool) -> Option<PaymentState> {
|
||||
pub(crate) fn derive_partial_state(
|
||||
&self,
|
||||
preimage: Option<String>,
|
||||
is_expired: bool,
|
||||
) -> Option<PaymentState> {
|
||||
match &self.lockup_tx_id {
|
||||
Some(_) => match &self.claim_tx_id {
|
||||
Some(_) => match preimage {
|
||||
Some(_) => Some(PaymentState::Complete),
|
||||
None => match &self.refund_tx_id {
|
||||
Some(refund_tx_id) => match refund_tx_id.confirmed() {
|
||||
|
||||
@@ -12,6 +12,7 @@ mod test {
|
||||
|
||||
#[sdk_macros::test_all]
|
||||
fn test_derive_partial_state_with_lockup_and_claim() {
|
||||
let swap_preimage = Some("cccc".to_string());
|
||||
let recovered_data = RecoveredOnchainDataSend {
|
||||
lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
|
||||
claim_tx_id: Some(create_lbtc_history_txid("2222", 101)),
|
||||
@@ -21,11 +22,11 @@ mod test {
|
||||
|
||||
// When there's a lockup and claim tx, it should always be Complete, regardless of timeout
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(false),
|
||||
recovered_data.derive_partial_state(swap_preimage.clone(), false),
|
||||
Some(PaymentState::Complete)
|
||||
);
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(true),
|
||||
recovered_data.derive_partial_state(swap_preimage, true),
|
||||
Some(PaymentState::Complete)
|
||||
);
|
||||
}
|
||||
@@ -33,6 +34,7 @@ mod test {
|
||||
#[sdk_macros::test_all]
|
||||
fn test_derive_partial_state_with_lockup_and_refund() {
|
||||
// Test with confirmed refund
|
||||
let no_swap_preimage = None;
|
||||
let recovered_data = RecoveredOnchainDataSend {
|
||||
lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
|
||||
claim_tx_id: None,
|
||||
@@ -42,11 +44,11 @@ mod test {
|
||||
|
||||
// When there's a lockup and confirmed refund tx, it should be Failed
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(false),
|
||||
recovered_data.derive_partial_state(no_swap_preimage.clone(), false),
|
||||
Some(PaymentState::Failed)
|
||||
);
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(true),
|
||||
recovered_data.derive_partial_state(no_swap_preimage.clone(), true),
|
||||
Some(PaymentState::Failed)
|
||||
);
|
||||
|
||||
@@ -60,17 +62,19 @@ mod test {
|
||||
|
||||
// When there's a lockup and unconfirmed refund tx, it should be RefundPending
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(false),
|
||||
recovered_data.derive_partial_state(no_swap_preimage.clone(), false),
|
||||
Some(PaymentState::RefundPending)
|
||||
);
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(true),
|
||||
recovered_data.derive_partial_state(no_swap_preimage, true),
|
||||
Some(PaymentState::RefundPending)
|
||||
);
|
||||
}
|
||||
|
||||
#[sdk_macros::test_all]
|
||||
fn test_derive_partial_state_with_lockup_only() {
|
||||
let no_swap_preimage = None;
|
||||
let swap_preimage = Some("cccc".to_string());
|
||||
let recovered_data = RecoveredOnchainDataSend {
|
||||
lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
|
||||
claim_tx_id: None,
|
||||
@@ -80,19 +84,30 @@ mod test {
|
||||
|
||||
// Not expired yet - should be Pending
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(false),
|
||||
recovered_data.derive_partial_state(no_swap_preimage.clone(), false),
|
||||
Some(PaymentState::Pending)
|
||||
);
|
||||
// Not expired yet - should be Complete
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(swap_preimage.clone(), false),
|
||||
Some(PaymentState::Complete)
|
||||
);
|
||||
|
||||
// Expired - should be RefundPending
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(true),
|
||||
recovered_data.derive_partial_state(no_swap_preimage, true),
|
||||
Some(PaymentState::RefundPending)
|
||||
);
|
||||
// Expired - should be Complete
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(swap_preimage, true),
|
||||
Some(PaymentState::Complete)
|
||||
);
|
||||
}
|
||||
|
||||
#[sdk_macros::test_all]
|
||||
fn test_derive_partial_state_with_no_txs() {
|
||||
let no_swap_preimage = None;
|
||||
let recovered_data = RecoveredOnchainDataSend {
|
||||
lockup_tx_id: None,
|
||||
claim_tx_id: None,
|
||||
@@ -101,11 +116,14 @@ mod test {
|
||||
};
|
||||
|
||||
// Not expired yet - should return None because we can't determine the state
|
||||
assert_eq!(recovered_data.derive_partial_state(false), None);
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(no_swap_preimage.clone(), false),
|
||||
None
|
||||
);
|
||||
|
||||
// Expired - should be Failed
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(true),
|
||||
recovered_data.derive_partial_state(no_swap_preimage, true),
|
||||
Some(PaymentState::Failed)
|
||||
);
|
||||
}
|
||||
@@ -113,6 +131,7 @@ mod test {
|
||||
#[sdk_macros::test_all]
|
||||
fn test_derive_partial_state_with_lockup_claim_refund() {
|
||||
// This is an edge case where both claim and refund txs exist
|
||||
let swap_preimage = Some("cccc".to_string());
|
||||
let recovered_data = RecoveredOnchainDataSend {
|
||||
lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
|
||||
claim_tx_id: Some(create_lbtc_history_txid("2222", 101)),
|
||||
@@ -122,11 +141,11 @@ mod test {
|
||||
|
||||
// Complete state should take precedence over refund
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(false),
|
||||
recovered_data.derive_partial_state(swap_preimage.clone(), false),
|
||||
Some(PaymentState::Complete)
|
||||
);
|
||||
assert_eq!(
|
||||
recovered_data.derive_partial_state(true),
|
||||
recovered_data.derive_partial_state(swap_preimage, true),
|
||||
Some(PaymentState::Complete)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::chain::liquid::MockLiquidChainService;
|
||||
use crate::error::PaymentError;
|
||||
use crate::prelude::*;
|
||||
use crate::recover::handlers::tests::{
|
||||
create_empty_lbtc_transaction, create_mock_lbtc_wallet_tx,
|
||||
@@ -84,7 +85,14 @@ mod test {
|
||||
#[sdk_macros::async_test_all]
|
||||
async fn test_recover_with_refund_tx() {
|
||||
// Setup mock data
|
||||
let (mut send_swap, recovery_context) = setup_test_data();
|
||||
let (mut send_swap, mut recovery_context) = setup_test_data();
|
||||
|
||||
// Setup mock swapper
|
||||
let mut swapper = MockSwapper::new();
|
||||
swapper
|
||||
.expect_get_submarine_preimage()
|
||||
.returning(move |_| Err(PaymentError::generic("No preimage available")));
|
||||
recovery_context.swapper = Arc::new(swapper);
|
||||
|
||||
// Create a lockup tx
|
||||
let swap_script = send_swap.get_swap_script().unwrap();
|
||||
@@ -127,7 +135,15 @@ mod test {
|
||||
#[sdk_macros::async_test_all]
|
||||
async fn test_recover_with_lockup_only() {
|
||||
// Setup mock data
|
||||
let (mut send_swap, recovery_context) = setup_test_data();
|
||||
let (mut send_swap, mut recovery_context) = setup_test_data();
|
||||
|
||||
// Setup mock swapper
|
||||
let preimage = "49666c97f6cea07fa5780c22ece1f0c9957caf1e3c37b9037b4f64dc6d09be7f"; // base64 of "somepreimage1234567890"
|
||||
let mut swapper = MockSwapper::new();
|
||||
swapper
|
||||
.expect_get_submarine_preimage()
|
||||
.returning(move |_| Ok(preimage.to_string()));
|
||||
recovery_context.swapper = Arc::new(swapper);
|
||||
|
||||
// Create a lockup tx
|
||||
let swap_script = send_swap.get_swap_script().unwrap();
|
||||
@@ -151,7 +167,7 @@ mod test {
|
||||
|
||||
// Verify results
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(send_swap.state, PaymentState::Pending); // Not expired -> Pending
|
||||
assert_eq!(send_swap.state, PaymentState::Complete); // Not expired -> Complete
|
||||
assert_eq!(send_swap.lockup_tx_id, Some(lockup_tx_id.to_string()));
|
||||
assert_eq!(send_swap.refund_tx_id, None);
|
||||
}
|
||||
@@ -162,6 +178,13 @@ mod test {
|
||||
// Setup mock data
|
||||
let (mut send_swap, mut recovery_context) = setup_test_data();
|
||||
|
||||
// Setup mock swapper
|
||||
let mut swapper = MockSwapper::new();
|
||||
swapper
|
||||
.expect_get_submarine_preimage()
|
||||
.returning(move |_| Err(PaymentError::generic("Swap expired")));
|
||||
recovery_context.swapper = Arc::new(swapper);
|
||||
|
||||
// Set tip height to make swap expired
|
||||
recovery_context.liquid_tip_height = send_swap.timeout_block_height as u32 + 10;
|
||||
|
||||
@@ -196,7 +219,14 @@ mod test {
|
||||
#[sdk_macros::async_test_all]
|
||||
async fn test_recover_with_unconfirmed_refund() {
|
||||
// Setup mock data
|
||||
let (mut send_swap, recovery_context) = setup_test_data();
|
||||
let (mut send_swap, mut recovery_context) = setup_test_data();
|
||||
|
||||
// Setup mock swapper
|
||||
let mut swapper = MockSwapper::new();
|
||||
swapper
|
||||
.expect_get_submarine_preimage()
|
||||
.returning(move |_| Err(PaymentError::generic("No preimage available")));
|
||||
recovery_context.swapper = Arc::new(swapper);
|
||||
|
||||
// Create a lockup tx
|
||||
let swap_script = send_swap.get_swap_script().unwrap();
|
||||
@@ -300,7 +330,7 @@ mod test {
|
||||
preimage: None,
|
||||
payer_amount_sat: 100000,
|
||||
receiver_amount_sat: 95000,
|
||||
pair_fees_json: r#"{"id":"BTC/BTC","rate":0.997,"limits":{"maximal":2000000,"minimal":10000,"maximalZeroConf":50000},"fees":{"percentage":0.5,"miner":200}}"#.to_string(),
|
||||
pair_fees_json: r#"{"hash":"BTC/BTC","rate":0.997,"limits":{"maximal":2000000,"minimal":10000,"maximalZeroConf":50000,"minimalBatched":21},"fees":{"percentage":0.5,"minerFees":200}}"#.to_string(),
|
||||
create_response_json: r#"{"accept_zero_conf":true,"address":"lq1pqg8hsjkptr8u7l35ctx5yn4dpwufkjxt7d24zuj5ddahnn7jaduh8r6celry8kn9xrkgwchrrx2madlemf0u27pnmjar4d4k5wvtem8kfl7ru56w94sv","bip21":"liquidnetwork:lq1pqg8hsjkptr8u7l35ctx5yn4dpwufkjxt7d24zuj5ddahnn7jaduh8r6celry8kn9xrkgwchrrx2madlemf0u27pnmjar4d4k5wvtem8kfl7ru56w94sv?amount=0.00001015&label=Send%20to%20BTC%20lightning&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d","claim_public_key":"0381b8583fe95488b961d12836102b1869b241972e571bd44a933d273b12a0d123","expected_amount":1015,"referral_id":"breez-sdk","swap_tree":{"claim_leaf":{"output":"a914cea2d1aa5af00fb688727b0054de58ecf45e948f882081b8583fe95488b961d12836102b1869b241972e571bd44a933d273b12a0d123ac","version":196},"refund_leaf":{"output":"20a668381222ff9076ca6d5f5b098b501331f07d5065f1dc0e0f217cc493359e69ad03b12432b1","version":196}},"timeout_block_height":3286193,"blinding_key":"73332603e5d438ddb3b12c16c7271c9f98658c77257cbb06639d05773aa1fec3"}"#.to_string(),
|
||||
lockup_tx_id: None,
|
||||
refund_address: None,
|
||||
|
||||
@@ -2000,7 +2000,7 @@ impl LiquidSdk {
|
||||
|
||||
Ok(LightningPaymentLimitsResponse {
|
||||
send: Limits {
|
||||
min_sat: send_limits.minimal,
|
||||
min_sat: send_limits.minimal_batched.unwrap_or(send_limits.minimal),
|
||||
max_sat: send_limits.maximal,
|
||||
max_zero_conf_sat: send_limits.maximal_zero_conf,
|
||||
},
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use boltz_client::boltz::SubmarineClaimTxResponse;
|
||||
use boltz_client::swaps::boltz;
|
||||
use boltz_client::swaps::{boltz::CreateSubmarineResponse, boltz::SubSwapStates};
|
||||
use futures_util::TryFutureExt;
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{debug, info, warn};
|
||||
use lwk_wollet::elements::{LockTime, Transaction};
|
||||
use lwk_wollet::hashes::{sha256, Hash};
|
||||
use sdk_common::prelude::{AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed};
|
||||
@@ -93,14 +94,35 @@ impl SendSwapHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Boltz has detected the lockup in the mempool, we can speed up
|
||||
// the claim by doing so cooperatively
|
||||
// Boltz has detected the lockup in the mempool. If the swap is not to be batched
|
||||
// we can speed up the claim by doing so cooperatively.
|
||||
SubSwapStates::TransactionClaimPending => {
|
||||
if swap.metadata.is_local {
|
||||
self.cooperate_claim(&swap).await.map_err(|e| {
|
||||
error!("Could not cooperate Send Swap {id} claim: {e}");
|
||||
anyhow!("Could not post claim details. Err: {e:?}")
|
||||
})?;
|
||||
let preimage = match self.swapper.get_send_claim_tx_details(&swap).await {
|
||||
Ok(claim_tx_response) => {
|
||||
match self.cooperate_claim(&swap, claim_tx_response.clone()).await {
|
||||
Ok(_) => Some(claim_tx_response.preimage),
|
||||
Err(e) => {
|
||||
warn!("Could not cooperate Send Swap {id} claim: {e:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not get claim tx details for Send Swap {id}: {e:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
let preimage = match preimage {
|
||||
Some(preimage) => preimage,
|
||||
None => {
|
||||
let preimage = self.swapper.get_submarine_preimage(&swap.id).await?;
|
||||
utils::verify_payment_hash(&preimage, &swap.invoice)?;
|
||||
info!("Fetched Send Swap {id} preimage cooperatively");
|
||||
preimage
|
||||
}
|
||||
};
|
||||
self.update_swap_info(&swap.id, Complete, Some(&preimage), None, None)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -359,7 +381,11 @@ impl SendSwapHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cooperate_claim(&self, send_swap: &SendSwap) -> Result<(), PaymentError> {
|
||||
async fn cooperate_claim(
|
||||
&self,
|
||||
send_swap: &SendSwap,
|
||||
claim_tx_response: SubmarineClaimTxResponse,
|
||||
) -> Result<(), PaymentError> {
|
||||
debug!(
|
||||
"Claim is pending for Send Swap {}. Initiating cooperative claim",
|
||||
&send_swap.id
|
||||
@@ -375,16 +401,8 @@ impl SendSwapHandler {
|
||||
}
|
||||
};
|
||||
|
||||
let claim_tx_details = self.swapper.get_send_claim_tx_details(send_swap).await?;
|
||||
self.update_swap_info(
|
||||
&send_swap.id,
|
||||
Complete,
|
||||
Some(&claim_tx_details.preimage),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
self.swapper
|
||||
.claim_send_swap_cooperative(send_swap, claim_tx_details, &refund_address)
|
||||
.claim_send_swap_cooperative(send_swap, claim_tx_response, &refund_address)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -304,8 +304,6 @@ impl<P: ProxyUrlFetcher> Swapper for BoltzSwapper<P> {
|
||||
.new_lbtc_refund_wrapper(&Swap::Send(swap.clone()), refund_address)
|
||||
.await?;
|
||||
|
||||
self.validate_send_swap_preimage(swap_id, &swap.invoice, &claim_tx_response.preimage)?;
|
||||
|
||||
let (partial_sig, pub_nonce) = refund_tx_wrapper.partial_sign(
|
||||
&keypair,
|
||||
&claim_tx_response.pub_nonce,
|
||||
@@ -317,7 +315,7 @@ impl<P: ProxyUrlFetcher> Swapper for BoltzSwapper<P> {
|
||||
.inner
|
||||
.post_submarine_claim_tx_details(&swap_id.to_string(), pub_nonce, partial_sig)
|
||||
.await?;
|
||||
info!("Successfully sent claim details for swap-in {swap_id}");
|
||||
info!("Successfully cooperatively claimed Send Swap {swap_id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ pub fn new_send_swap(
|
||||
"limits": {
|
||||
"maximal": 25000000,
|
||||
"minimal": 1000,
|
||||
"maximalZeroConf": 100000
|
||||
"maximalZeroConf": 100000,
|
||||
"minimalBatched": 21
|
||||
},
|
||||
"fees": {
|
||||
"percentage": 0.1,
|
||||
|
||||
@@ -191,7 +191,7 @@ impl Swapper for MockSwapper {
|
||||
maximal: 25_000_000,
|
||||
minimal: 1_000,
|
||||
maximal_zero_conf: 250_000,
|
||||
minimal_batched: None,
|
||||
minimal_batched: Some(21),
|
||||
},
|
||||
fees: SubmarineFees {
|
||||
percentage: 0.1,
|
||||
|
||||
Submodule regtest/boltz updated: 7d0123c68e...9036dcb31e
Reference in New Issue
Block a user