mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-17 21:14:22 +01:00
@@ -9,4 +9,5 @@ pub mod logger;
|
||||
pub mod model;
|
||||
pub mod persist;
|
||||
pub mod sdk;
|
||||
pub(crate) mod swapper;
|
||||
pub(crate) mod utils;
|
||||
|
||||
@@ -342,6 +342,25 @@ impl ReceiveSwap {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) fn get_swap_script(&self) -> Result<LBtcSwapScriptV2, PaymentError> {
|
||||
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,
|
||||
|
||||
@@ -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<Persister>,
|
||||
event_manager: Arc<EventManager>,
|
||||
status_stream: Arc<BoltzStatusStream>,
|
||||
swapper: Arc<dyn Swapper>,
|
||||
is_started: RwLock<bool>,
|
||||
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<SubmarinePair, PaymentError> {
|
||||
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<LBtcSwapTxV2, PaymentError> {
|
||||
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<String, PaymentError> {
|
||||
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<String, PaymentError> {
|
||||
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<String, PaymentError> {
|
||||
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<String, PaymentError> {
|
||||
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<PrepareReceiveResponse, PaymentError> {
|
||||
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
|
||||
|
||||
298
lib/core/src/swapper.rs
Normal file
298
lib/core/src/swapper.rs
Normal file
@@ -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<CreateSubmarineResponse, PaymentError>;
|
||||
|
||||
/// Get a submarine pair information
|
||||
fn get_submarine_pairs(&self) -> Result<Option<SubmarinePair>, PaymentError>;
|
||||
|
||||
/// Refund a cooperatively send swap
|
||||
fn refund_send_swap_cooperative(
|
||||
&self,
|
||||
swap: &SendSwap,
|
||||
output_address: &str,
|
||||
broadcast_fees_sat: Amount,
|
||||
) -> Result<String, PaymentError>;
|
||||
|
||||
/// 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<String, PaymentError>;
|
||||
|
||||
/// 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<String, PaymentError>;
|
||||
|
||||
// Create a new receive swap
|
||||
fn create_receive_swap(
|
||||
&self,
|
||||
req: CreateReverseRequest,
|
||||
) -> Result<CreateReverseResponse, PaymentError>;
|
||||
|
||||
// Get a reverse pair information
|
||||
fn get_reverse_swap_pairs(&self) -> Result<Option<ReversePair>, PaymentError>;
|
||||
|
||||
/// Claim receive swap. Here the local swapper is the one that claims.
|
||||
fn claim_receive_swap(
|
||||
&self,
|
||||
swap: &ReceiveSwap,
|
||||
claim_address: String,
|
||||
) -> Result<String, PaymentError>;
|
||||
|
||||
// chain broadcast
|
||||
fn broadcast_tx(&self, chain: Chain, tx_hex: &str) -> Result<Value, PaymentError>;
|
||||
}
|
||||
|
||||
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<LBtcSwapTxV2, PaymentError> {
|
||||
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<CreateSubmarineResponse, PaymentError> {
|
||||
Ok(self.client.post_swap_req(&req)?)
|
||||
}
|
||||
|
||||
/// Get a submarine pair information
|
||||
fn get_submarine_pairs(&self) -> Result<Option<SubmarinePair>, 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<String, PaymentError> {
|
||||
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<String, PaymentError> {
|
||||
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<String, PaymentError> {
|
||||
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<CreateReverseResponse, PaymentError> {
|
||||
Ok(self.client.post_reverse_req(req)?)
|
||||
}
|
||||
|
||||
// Get a reverse pair information
|
||||
fn get_reverse_swap_pairs(&self) -> Result<Option<ReversePair>, 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<String, PaymentError> {
|
||||
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<Value, PaymentError> {
|
||||
Ok(self.client.broadcast_tx(chain, &tx_hex.into())?)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user