diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index 645ed37..0a2a2fc 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -9,4 +9,5 @@ pub mod logger; pub mod model; pub mod persist; pub mod sdk; +pub(crate) mod swapper; pub(crate) mod utils; diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 1540abe..b1a9cd1 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -342,6 +342,25 @@ impl ReceiveSwap { Ok(res) } + pub(crate) fn get_swap_script(&self) -> Result { + let keypair = self.get_claim_keypair()?; + let create_response = + self.get_boltz_create_response() + .map_err(|e| PaymentError::Generic { + err: format!( + "Failed to create swap script for Receive Swap {}: {e:?}", + self.id + ), + })?; + LBtcSwapScriptV2::reverse_from_swap_resp(&create_response, keypair.public_key().into()) + .map_err(|e| PaymentError::Generic { + err: format!( + "Failed to create swap script for Receive Swap {}: {e:?}", + self.id + ), + }) + } + pub(crate) fn from_boltz_struct_to_json( create_response: &CreateReverseResponse, expected_swap_id: &str, diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 94de82b..3563e84 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -9,26 +9,23 @@ use std::{ use anyhow::{anyhow, Result}; use boltz_client::lightning_invoice::Bolt11InvoiceDescription; -use boltz_client::network::Chain; use boltz_client::swaps::boltzv2; use boltz_client::ToHex; use boltz_client::{ swaps::{ boltz::{RevSwapStates, SubSwapStates}, - boltzv2::*, - liquidv2::LBtcSwapTxV2, + boltzv2::*, }, util::secrets::Preimage, - Amount, Bolt11Invoice, ElementsAddress, Keypair, LBtcSwapScriptV2, + Amount, Bolt11Invoice, ElementsAddress, }; use log::{debug, error, info, warn}; use lwk_common::{singlesig_desc, Signer, Singlesig}; use lwk_signer::{AnySigner, SwSigner}; use lwk_wollet::bitcoin::Witness; -use lwk_wollet::elements::LockTime; use lwk_wollet::hashes::{sha256, Hash}; use lwk_wollet::{ - elements::{Address, Transaction}, + elements::{Address, Transaction, LockTime}, BlockchainBackend, ElementsNetwork, FsPersister, Wollet as LwkWollet, WolletDescriptor, }; use tokio::sync::{watch, Mutex, RwLock}; @@ -36,6 +33,7 @@ use tokio::time::MissedTickBehavior; use crate::error::LiquidSdkError; use crate::model::PaymentState::*; +use crate::swapper::{BoltzSwapper, Swapper}; use crate::{ boltz_status_stream::BoltzStatusStream, ensure_sdk, @@ -62,6 +60,7 @@ pub struct LiquidSdk { persister: Arc, event_manager: Arc, status_stream: Arc, + swapper: Arc, is_started: RwLock, shutdown_sender: watch::Sender<()>, shutdown_receiver: watch::Receiver<()>, @@ -104,6 +103,8 @@ impl LiquidSdk { let status_stream = Arc::new(BoltzStatusStream::new(&config.boltz_url, persister.clone())); let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(()); + let swapper = Arc::new(BoltzSwapper::new(config.clone())); + let sdk = Arc::new(LiquidSdk { config, lwk_wollet: Arc::new(Mutex::new(lwk_wollet)), @@ -111,6 +112,7 @@ impl LiquidSdk { persister, event_manager, status_stream, + swapper, is_started: RwLock::new(false), shutdown_sender, shutdown_receiver, @@ -198,19 +200,28 @@ impl LiquidSdk { tokio::select! { update = updates_stream.recv() => match update { Ok(boltzv2::Update { id, status }) => { - let _ = cloned.sync().await; - - if let Ok(_) = cloned.try_handle_send_swap_boltz_status(&status, &id).await - { - info!("Handled send swap update"); - } else if let Ok(_) = cloned - .try_handle_receive_swap_boltz_status(&status, &id) - .await - { - info!("Handled receive swap update"); - } else { - warn!("Unhandled swap {id}: {status}") - } + let _ = cloned.sync().await; + match cloned.persister.fetch_send_swap_by_id(&id) { + Ok(Some(_)) => { + match cloned.try_handle_send_swap_boltz_status(&status, &id).await { + Ok(_) => info!("Succesfully handled Send Swap {id} update"), + Err(e) => error!("Failed to handle Send Swap {id} update: {e}") + } + } + _ => { + match cloned.persister.fetch_receive_swap(&id) { + Ok(Some(_)) => { + match cloned.try_handle_receive_swap_boltz_status(&status, &id).await { + Ok(_) => info!("Succesfully handled Receive Swap {id} update"), + Err(e) => error!("Failed to handle Receive Swap {id} update: {e}") + } + } + _ => { + error!("Could not find Swap {id}"); + } + } + } + } } Err(e) => error!("Received stream error: {e:?}"), }, @@ -510,14 +521,20 @@ impl LiquidSdk { Some(claim_tx_id) => { warn!("Claim tx for Receive Swap {id} was already broadcast: txid {claim_tx_id}") } - None => match self.try_claim(&receive_swap).await { - Ok(()) => {} + None => { + self.try_handle_receive_swap_update(&receive_swap.id, Pending, None) + .await?; + + match self.try_claim(&receive_swap).await { + Ok(_) => {} Err(err) => match err { PaymentError::AlreadyClaimed => warn!("Funds already claimed for Receive Swap {id}"), _ => error!("Claim for Receive Swap {id} failed: {err}") } - }, + + } } + } Ok(()) } @@ -551,7 +568,7 @@ impl LiquidSdk { ongoing_send_swap.state, ongoing_send_swap.lockup_tx_id.clone(), ) { - (PaymentState::Created, None) => { + (PaymentState::Created, None) | (PaymentState::TimedOut, None) => { let create_response = ongoing_send_swap.get_boltz_create_response()?; let lockup_tx_id = self.lockup_funds(id, &create_response).await?; @@ -587,19 +604,12 @@ impl LiquidSdk { // Boltz has detected the lockup in the mempool, we can speed up // the claim by doing so cooperatively Ok(SubSwapStates::TransactionClaimPending) => { - let keypair = ongoing_send_swap.get_refund_keypair()?; - let swap_script = ongoing_send_swap.get_swap_script().map_err(|e| { - anyhow!("Could not rebuild refund details for Send Swap {id}: {e:?}") - })?; - - self.cooperate_send_swap_claim( - id, - &swap_script, - &ongoing_send_swap.invoice, - &keypair, - ) - .await - .map_err(|e| anyhow!("Could not post claim details. Err: {e:?}"))?; + self.cooperate_send_swap_claim(&ongoing_send_swap) + .await + .map_err(|e| { + error!("Could not cooperate Send Swap {id} claim: {e}"); + anyhow!("Could not post claim details. Err: {e:?}") + })?; Ok(()) } @@ -610,6 +620,8 @@ impl LiquidSdk { self.get_preimage_from_script_path_claim_spend(&ongoing_send_swap)?; self.validate_send_swap_preimage(id, &ongoing_send_swap.invoice, &preimage) .await?; + self.try_handle_send_swap_update(id, Complete, Some(&preimage), None, None) + .await?; Ok(()) } @@ -718,11 +730,7 @@ impl LiquidSdk { pending_receive_sat, pubkey: self.lwk_signer.xpub().public_key.to_string(), }) - } - - pub(crate) fn boltz_client_v2(&self) -> BoltzApiClientV2 { - BoltzApiClientV2::new(&self.config.boltz_url) - } + } async fn build_tx( &self, @@ -767,12 +775,12 @@ impl LiquidSdk { } fn validate_submarine_pairs( - client: &BoltzApiClientV2, + &self, receiver_amount_sat: u64, ) -> Result { - let lbtc_pair = client + let lbtc_pair = self + .swapper .get_submarine_pairs()? - .get_lbtc_to_btc_pair() .ok_or(PaymentError::PairsNotFound)?; lbtc_pair.limits.within(receiver_amount_sat)?; @@ -816,8 +824,7 @@ impl LiquidSdk { .ok_or(PaymentError::AmountOutOfRange)? / 1000; - let client = self.boltz_client_v2(); - let lbtc_pair = Self::validate_submarine_pairs(&client, receiver_amount_sat)?; + let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat)?; let broadcast_fees_sat = self .get_broadcast_fee_estimation(receiver_amount_sat) @@ -840,52 +847,10 @@ impl LiquidSdk { .ok_or(PaymentError::InvalidPreimage) } - async fn new_refund_tx( - &self, - swap_id: &str, - swap_script: &LBtcSwapScriptV2, - ) -> Result { - let output_address = self.next_unused_address().await?.to_string(); - let network_config = self.config.get_electrum_config(); - Ok(LBtcSwapTxV2::new_refund( - swap_script.clone(), - &output_address, - &network_config, - self.config.clone().boltz_url, - swap_id.to_string(), - )?) - } - - async fn try_refund_cooperative( - &self, - swap: &SendSwap, - refund_tx: &LBtcSwapTxV2, - broadcast_fees_sat: Amount, - is_lowball: Option<(&BoltzApiClientV2, Chain)>, - ) -> Result { - info!("Initiating cooperative refund for Send Swap {}", &swap.id); - let tx = refund_tx.sign_refund( - &swap.get_refund_keypair()?, - broadcast_fees_sat, - Some((&self.boltz_client_v2(), &swap.id)), - )?; - - let refund_tx_id = - refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?; - info!( - "Successfully broadcast cooperative refund for Send Swap {}", - &swap.id - ); - Ok(refund_tx_id.clone()) - } - async fn try_refund_non_cooperative( &self, swap: &SendSwap, - swap_script: &LBtcSwapScriptV2, - refund_tx: LBtcSwapTxV2, broadcast_fees_sat: Amount, - is_lowball: Option<(&BoltzApiClientV2, Chain)>, ) -> Result { info!( "Initiating non-cooperative refund for Send Swap {}", @@ -893,59 +858,36 @@ impl LiquidSdk { ); let current_height = self.lwk_wollet.lock().await.tip().height(); - let locktime_from_height = - LockTime::from_height(current_height).map_err(|e| PaymentError::Generic { - err: format!("Cannot convert current block height to lock time: {e:?}"), - })?; + let output_address = self.next_unused_address().await?.to_string(); + let refund_tx_id = self.swapper.refund_send_swap_non_cooperative( + swap, + broadcast_fees_sat, + &output_address, + current_height, + )?; - info!("locktime info: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap_script.locktime); - match utils::is_locktime_expired(locktime_from_height, swap_script.locktime) { - true => { - let tx = refund_tx.sign_refund(&swap.get_refund_keypair()?, broadcast_fees_sat, None)?; - let refund_tx_id = - refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?; - info!( - "Successfully broadcast non-cooperative refund for Send Swap {}", - swap.id - ); - Ok(refund_tx_id) - } - false => Err(PaymentError::Generic { - err: format!( - "Cannot refund non-cooperatively. Lock time not elapsed yet. Current tip: {:?}. Script lock time: {:?}", - locktime_from_height, swap_script.locktime - ) - }) - } + info!( + "Successfully broadcast non-cooperative refund for Send Swap {}, tx: {}", + swap.id, refund_tx_id + ); + Ok(refund_tx_id) } - async fn try_refund(&self, swap: &SendSwap) -> Result { - let swap_script = swap.get_swap_script()?; - let refund_tx = self.new_refund_tx(&swap.id, &swap_script).await?; + async fn try_refund(&self, swap: &SendSwap) -> Result { let amount_sat = get_invoice_amount!(swap.invoice); let broadcast_fees_sat = Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat).await?); - let client = self.boltz_client_v2(); - let is_lowball = match self.config.network { - Network::Mainnet => None, - Network::Testnet => Some((&client, boltz_client::network::Chain::LiquidTestnet)), - }; - match self - .try_refund_cooperative(swap, &refund_tx, broadcast_fees_sat, is_lowball) - .await - { + let output_address = self.next_unused_address().await?.to_string(); + let refund_res = + self.swapper + .refund_send_swap_cooperative(swap, &output_address, broadcast_fees_sat); + match refund_res { Ok(res) => Ok(res), Err(e) => { warn!("Cooperative refund failed: {:?}", e); - self.try_refund_non_cooperative( - swap, - &swap_script, - refund_tx, - broadcast_fees_sat, - is_lowball, - ) - .await + self.try_refund_non_cooperative(swap, broadcast_fees_sat) + .await } } } @@ -959,33 +901,19 @@ impl LiquidSdk { ) -> Result<(), PaymentError> { Self::verify_payment_hash(preimage, invoice)?; info!("Preimage is valid for Send Swap {swap_id}"); - self.try_handle_send_swap_update(swap_id, Complete, Some(preimage), None, None) - .await + Ok(()) } /// Interact with Boltz to assist in them doing a cooperative claim - async fn cooperate_send_swap_claim( - &self, - swap_id: &str, - swap_script: &LBtcSwapScriptV2, - invoice: &str, - keypair: &Keypair, - ) -> Result<(), PaymentError> { - debug!("Claim is pending for Send Swap {swap_id}. Initiating cooperative claim"); - let client = self.boltz_client_v2(); - let refund_tx = self.new_refund_tx(swap_id, swap_script).await?; - - let claim_tx_response = client.get_claim_tx_details(&swap_id.to_string())?; - debug!("Received claim tx details: {:?}", &claim_tx_response); - - self.validate_send_swap_preimage(swap_id, invoice, &claim_tx_response.preimage) + async fn cooperate_send_swap_claim(&self, send_swap: &SendSwap) -> Result<(), PaymentError> { + debug!( + "Claim is pending for Send Swap {}. Initiating cooperative claim", + &send_swap.id + ); + let output_address = self.next_unused_address().await?.to_string(); + let preimage = self.swapper.claim_send_swap_cooperative(send_swap, &output_address)?; + self.try_handle_send_swap_update(&send_swap.id, Complete, Some(&preimage), None, None) .await?; - - let (partial_sig, pub_nonce) = - refund_tx.submarine_partial_sig(keypair, &claim_tx_response)?; - - client.post_claim_tx_details(&swap_id.to_string(), pub_nonce, partial_sig)?; - debug!("Successfully sent claim details for Send Swap {swap_id}"); Ok(()) } @@ -1032,8 +960,7 @@ impl LiquidSdk { self.validate_invoice(&req.invoice)?; let receiver_amount_sat = get_invoice_amount!(req.invoice); - let client = self.boltz_client_v2(); - let lbtc_pair = Self::validate_submarine_pairs(&client, receiver_amount_sat)?; + let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat)?; let broadcast_fees_sat = self .get_broadcast_fee_estimation(receiver_amount_sat) .await?; @@ -1050,8 +977,7 @@ impl LiquidSdk { compressed: true, inner: keypair.public_key(), }; - - let create_response = client.post_swap_req(&CreateSubmarineRequest { + let create_response = self.swapper.create_send_swap(CreateSubmarineRequest { from: "L-BTC".to_string(), to: "BTC".to_string(), invoice: req.invoice.to_string(), @@ -1144,45 +1070,10 @@ impl LiquidSdk { ongoing_receive_swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed ); - let swap_id = &ongoing_receive_swap.id; - debug!("Trying to claim Receive Swap {swap_id}",); - - self.try_handle_receive_swap_update(swap_id, Pending, None) - .await?; - - let keypair = ongoing_receive_swap.get_claim_keypair()?; - let create_response = ongoing_receive_swap.get_boltz_create_response()?; - let swap_script = LBtcSwapScriptV2::reverse_from_swap_resp( - &create_response, - keypair.public_key().into(), - )?; - - let claim_address = self.next_unused_address().await?.to_string(); - let claim_tx_wrapper = LBtcSwapTxV2::new_claim( - swap_script, - claim_address, - &self.config.get_electrum_config(), - self.config.clone().boltz_url, - ongoing_receive_swap.id.clone(), - )?; - - let claim_tx = claim_tx_wrapper.sign_claim( - &keypair, - &Preimage::from_str(&ongoing_receive_swap.preimage)?, - Amount::from_sat(ongoing_receive_swap.claim_fees_sat), - // Enable cooperative claim (Some) or not (None) - Some((&self.boltz_client_v2(), swap_id.clone())), - // None - )?; - - let claim_tx_id = claim_tx_wrapper.broadcast( - &claim_tx, - &self.config.get_electrum_config(), - Some((&self.boltz_client_v2(), self.config.network.into())), - )?; - info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}"); - debug!("Claim Tx {:?}", claim_tx); + let claim_address = self.next_unused_address().await?.to_string(); + let claim_tx_id = self.swapper.claim_receive_swap(ongoing_receive_swap, claim_address)?; + info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {}", swap_id); // We insert a pseudo-claim-tx in case LWK fails to pick up the new mempool tx for a while // This makes the tx known to the SDK (get_info, list_payments) instantly @@ -1205,12 +1096,7 @@ impl LiquidSdk { req: &PrepareReceiveRequest, ) -> Result { self.ensure_is_started().await?; - - let reverse_pair = self - .boltz_client_v2() - .get_reverse_pairs()? - .get_btc_to_lbtc_pair() - .ok_or(PaymentError::PairsNotFound)?; + let reverse_pair = self.swapper.get_reverse_swap_pairs()?.ok_or(PaymentError::PairsNotFound)?; let payer_amount_sat = req.payer_amount_sat; let fees_sat = reverse_pair.fees.total(req.payer_amount_sat); @@ -1239,11 +1125,7 @@ impl LiquidSdk { let payer_amount_sat = req.payer_amount_sat; let fees_sat = req.fees_sat; - let reverse_pair = self - .boltz_client_v2() - .get_reverse_pairs()? - .get_btc_to_lbtc_pair() - .ok_or(PaymentError::PairsNotFound)?; + let reverse_pair = self.swapper.get_reverse_swap_pairs()?.ok_or(PaymentError::PairsNotFound)?; let new_fees_sat = reverse_pair.fees.total(req.payer_amount_sat); ensure_sdk!(fees_sat == new_fees_sat, PaymentError::InvalidOrExpiredFees); @@ -1265,7 +1147,7 @@ impl LiquidSdk { address_signature: None, referral_id: None, }; - let create_response = self.boltz_client_v2().post_reverse_req(v2_req)?; + let create_response = self.swapper.create_receive_swap(v2_req)?; let swap_id = create_response.id.clone(); let invoice = Bolt11Invoice::from_str(&create_response.invoice) @@ -1405,6 +1287,7 @@ impl LiquidSdk { .ok_or_else(|| anyhow!("Found no input for claim tx"))?; let script_witness_bytes = input.clone().witness.script_witness; + info!("Found Send Swap {id} claim tx witness: {script_witness_bytes:?}"); let script_witness = Witness::from(script_witness_bytes); let preimage_bytes = script_witness diff --git a/lib/core/src/swapper.rs b/lib/core/src/swapper.rs new file mode 100644 index 0000000..405d37c --- /dev/null +++ b/lib/core/src/swapper.rs @@ -0,0 +1,298 @@ +use std::str::FromStr; + +use anyhow::Result; +use boltz_client::network::Chain; +use boltz_client::swaps::boltzv2::{ + BoltzApiClientV2, CreateReverseRequest, CreateReverseResponse, CreateSubmarineRequest, + CreateSubmarineResponse, ReversePair, SubmarinePair, +}; + +use boltz_client::error::Error; + +use boltz_client::util::secrets::Preimage; +use boltz_client::{Amount, Bolt11Invoice, LBtcSwapTxV2}; +use log::{debug, info}; +use lwk_wollet::elements::LockTime; +use serde_json::Value; + +use crate::error::PaymentError; +use crate::model::{Config, Network, ReceiveSwap, SendSwap}; +use crate::utils; + +pub trait Swapper: Send + Sync { + /// Create a new send swap + fn create_send_swap( + &self, + req: CreateSubmarineRequest, + ) -> Result; + + /// Get a submarine pair information + fn get_submarine_pairs(&self) -> Result, PaymentError>; + + /// Refund a cooperatively send swap + fn refund_send_swap_cooperative( + &self, + swap: &SendSwap, + output_address: &str, + broadcast_fees_sat: Amount, + ) -> Result; + + /// Refund non-cooperatively send swap + fn refund_send_swap_non_cooperative( + &self, + swap: &SendSwap, + broadcast_fees_sat: Amount, + output_address: &str, + current_height: u32, + ) -> Result; + + /// Claim send swap cooperatively. Here the remote swapper is the one that claims. + /// We are helping to use key spend path for cheaper fees. + fn claim_send_swap_cooperative( + &self, + swap: &SendSwap, + output_address: &str, + ) -> Result; + + // Create a new receive swap + fn create_receive_swap( + &self, + req: CreateReverseRequest, + ) -> Result; + + // Get a reverse pair information + fn get_reverse_swap_pairs(&self) -> Result, PaymentError>; + + /// Claim receive swap. Here the local swapper is the one that claims. + fn claim_receive_swap( + &self, + swap: &ReceiveSwap, + claim_address: String, + ) -> Result; + + // chain broadcast + fn broadcast_tx(&self, chain: Chain, tx_hex: &str) -> Result; +} + +pub struct BoltzSwapper { + client: BoltzApiClientV2, + config: Config, +} + +impl BoltzSwapper { + pub fn new(config: Config) -> BoltzSwapper { + BoltzSwapper { + client: BoltzApiClientV2::new(&config.boltz_url), + config, + } + } + + fn new_refund_tx( + &self, + swap: &SendSwap, + output_address: &String, + ) -> Result { + let swap_script = swap.get_swap_script()?; + + Ok(LBtcSwapTxV2::new_refund( + swap_script.clone(), + output_address, + &self.config.get_electrum_config(), + self.config.boltz_url.clone(), + swap.id.to_string(), + )?) + } + + fn validate_send_swap_preimage( + &self, + swap_id: &str, + invoice: &str, + preimage: &str, + ) -> Result<(), PaymentError> { + Self::verify_payment_hash(preimage, invoice)?; + info!("Preimage is valid for Send Swap {swap_id}"); + Ok(()) + } + + fn verify_payment_hash(preimage: &str, invoice: &str) -> Result<(), PaymentError> { + let preimage = Preimage::from_str(preimage)?; + let preimage_hash = preimage.sha256.to_string(); + let invoice = + Bolt11Invoice::from_str(invoice).map_err(|e| Error::Generic(e.to_string()))?; + let invoice_payment_hash = invoice.payment_hash(); + + (invoice_payment_hash.to_string() == preimage_hash) + .then_some(()) + .ok_or(PaymentError::InvalidPreimage) + } +} + +impl Swapper for BoltzSwapper { + /// Create a new send swap + fn create_send_swap( + &self, + req: CreateSubmarineRequest, + ) -> Result { + Ok(self.client.post_swap_req(&req)?) + } + + /// Get a submarine pair information + fn get_submarine_pairs(&self) -> Result, PaymentError> { + Ok(self.client.get_submarine_pairs()?.get_lbtc_to_btc_pair()) + } + + /// Refund a cooperatively send swap + fn refund_send_swap_cooperative( + &self, + swap: &SendSwap, + output_address: &str, + broadcast_fees_sat: Amount, + ) -> Result { + info!("Initiating cooperative refund for Send Swap {}", &swap.id); + let refund_tx = self.new_refund_tx(swap, &output_address.into())?; + + let cooperative = Some((&self.client, &swap.id)); + let tx = refund_tx.sign_refund( + &swap + .get_refund_keypair() + .map_err(|e| Error::Generic(e.to_string()))?, + broadcast_fees_sat, + cooperative, + )?; + let is_lowball = match self.config.network { + Network::Mainnet => None, + Network::Testnet => Some((&self.client, boltz_client::network::Chain::LiquidTestnet)), + }; + let refund_tx_id = + refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?; + info!( + "Successfully broadcast cooperative refund for Send Swap {}", + &swap.id + ); + Ok(refund_tx_id.clone()) + } + + /// Refund non-cooperatively send swap + fn refund_send_swap_non_cooperative( + &self, + swap: &SendSwap, + broadcast_fees_sat: Amount, + output_address: &str, + current_height: u32, + ) -> Result { + let swap_script = swap.get_swap_script()?; + let locktime_from_height = + LockTime::from_height(current_height).map_err(|e| PaymentError::Generic { + err: format!("Cannot convert current block height to lock time: {e:?}"), + })?; + + info!("locktime info: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap_script.locktime); + if !utils::is_locktime_expired(locktime_from_height, swap_script.locktime) { + return Err(PaymentError::Generic { + err: format!( + "Cannot refund non-cooperatively. Lock time not elapsed yet. Current tip: {:?}. Script lock time: {:?}", + locktime_from_height, swap_script.locktime + ) + }); + } + + let refund_tx = self.new_refund_tx(swap, &output_address.into())?; + let tx = refund_tx.sign_refund( + &swap + .get_refund_keypair() + .map_err(|e| Error::Generic(e.to_string()))?, + broadcast_fees_sat, + None, + )?; + let is_lowball = match self.config.network { + Network::Mainnet => None, + Network::Testnet => Some((&self.client, boltz_client::network::Chain::LiquidTestnet)), + }; + let refund_tx_id = + refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?; + info!( + "Successfully broadcast non-cooperative refund for swap-in {}", + swap.id + ); + Ok(refund_tx_id) + } + + /// Claim send swap cooperatively. Here the remote swapper is the one that claims. + /// We are helping to use key spend path for cheaper fees. + fn claim_send_swap_cooperative( + &self, + swap: &SendSwap, + output_address: &str, + ) -> Result { + let swap_id = &swap.id; + let keypair = swap.get_refund_keypair()?; + let refund_tx = self.new_refund_tx(swap, &output_address.into())?; + + let claim_tx_response = self.client.get_claim_tx_details(&swap_id.to_string())?; + info!("Received claim tx details: {:?}", &claim_tx_response); + + self.validate_send_swap_preimage(swap_id, &swap.invoice, &claim_tx_response.preimage)?; + + let (partial_sig, pub_nonce) = + refund_tx.submarine_partial_sig(&keypair, &claim_tx_response)?; + + self.client + .post_claim_tx_details(&swap_id.to_string(), pub_nonce, partial_sig)?; + info!("Successfully sent claim details for swap-in {swap_id}"); + Ok(claim_tx_response.preimage) + } + + // Create a new receive swap + fn create_receive_swap( + &self, + req: CreateReverseRequest, + ) -> Result { + Ok(self.client.post_reverse_req(req)?) + } + + // Get a reverse pair information + fn get_reverse_swap_pairs(&self) -> Result, PaymentError> { + Ok(self.client.get_reverse_pairs()?.get_btc_to_lbtc_pair()) + } + + /// Claim receive swap. Here the local swapper is the one that claims. + fn claim_receive_swap( + &self, + swap: &ReceiveSwap, + claim_address: String, + ) -> Result { + let swap_script = swap.get_swap_script()?; + let swap_id = &swap.id; + let claim_tx_wrapper = LBtcSwapTxV2::new_claim( + swap_script, + claim_address, + &self.config.get_electrum_config(), + self.config.boltz_url.clone(), + swap.id.clone(), + )?; + + let cooperative = Some((&self.client, swap.id.clone())); + let claim_tx = claim_tx_wrapper.sign_claim( + &swap.get_claim_keypair()?, + &Preimage::from_str(&swap.preimage)?, + Amount::from_sat(swap.claim_fees_sat), + // Enable cooperative claim (Some) or not (None) + cooperative, + // None + )?; + + let claim_tx_id = claim_tx_wrapper.broadcast( + &claim_tx, + &self.config.get_electrum_config(), + Some((&self.client, self.config.network.into())), + )?; + info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}"); + debug!("Claim Tx {:?}", claim_tx); + Ok(claim_tx_id) + } + + // chain broadcast + fn broadcast_tx(&self, chain: Chain, tx_hex: &str) -> Result { + Ok(self.client.broadcast_tx(chain, &tx_hex.into())?) + } +}