1use std::str::FromStr;
2use std::sync::Arc;
3
4use anyhow::{anyhow, ensure, Result};
5use rand::thread_rng;
6use serde::{Deserialize, Serialize};
7use tokio::sync::broadcast;
8use tokio::time::{sleep, Duration};
9
10use super::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReverseSwapStatus::*};
11use super::error::{ReverseSwapError, ReverseSwapResult};
12use crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
13use crate::bitcoin::consensus::serialize;
14use crate::bitcoin::hashes::hex::{FromHex, ToHex};
15use crate::bitcoin::hashes::{sha256, Hash};
16use crate::bitcoin::psbt::serialize::Serialize as PsbtSerialize;
17use crate::bitcoin::secp256k1::{Message, Secp256k1, SecretKey};
18use crate::bitcoin::util::sighash::SighashCache;
19use crate::bitcoin::{
20 Address, AddressType, EcdsaSighashType, KeyPair, Network, OutPoint, Script, Sequence,
21 Transaction, TxIn, TxOut, Txid, Witness,
22};
23use crate::chain::{get_utxos, AddressUtxos, ChainService, OnchainTx, Utxo};
24use crate::error::SdkResult;
25use crate::models::{ReverseSwapServiceAPI, ReverseSwapperRoutingAPI};
26use crate::node_api::{NodeAPI, NodeError};
27use crate::swap_in::create_swap_keys;
28use crate::{
29 ensure_sdk, BreezEvent, Config, FullReverseSwapInfo, PayOnchainRequest, PaymentStatus,
30 ReverseSwapInfo, ReverseSwapInfoCached, ReverseSwapPairInfo, ReverseSwapStatus,
31 ReverseSwapStatus::*, RouteHintHop,
32};
33
34pub const ESTIMATED_CLAIM_TX_VSIZE: u64 = 138;
36pub const ESTIMATED_LOCKUP_TX_VSIZE: u64 = 153;
37pub(crate) const MAX_PAYMENT_PATH_HOPS: u32 = 3;
38
39#[derive(Clone, Serialize, Deserialize, Debug)]
40#[serde(rename_all = "camelCase")]
41pub struct CreateReverseSwapResponse {
42 id: String,
43
44 invoice: String,
46
47 redeem_script: String,
50
51 onchain_amount: u64,
53
54 timeout_block_height: u32,
56
57 lockup_address: String,
59}
60
61#[derive(Debug)]
62enum TxStatus {
63 Unknown,
64 Mempool,
65 Confirmed,
66}
67
68impl From<&Option<OnchainTx>> for TxStatus {
69 fn from(value: &Option<OnchainTx>) -> Self {
70 match value {
71 None => TxStatus::Unknown,
72 Some(tx) => match tx.status.block_height {
73 Some(_) => TxStatus::Confirmed,
74 None => TxStatus::Mempool,
75 },
76 }
77 }
78}
79
80pub(crate) struct BTCSendSwap {
83 config: Config,
84 pub(crate) reverse_swapper_api: Arc<dyn ReverseSwapperRoutingAPI>,
85 pub(crate) reverse_swap_service_api: Arc<dyn ReverseSwapServiceAPI>,
86 persister: Arc<crate::persist::db::SqliteStorage>,
87 chain_service: Arc<dyn ChainService>,
88 node_api: Arc<dyn NodeAPI>,
89 status_changes_notifier: broadcast::Sender<BreezEvent>,
90}
91
92impl BTCSendSwap {
93 pub(crate) fn new(
94 config: Config,
95 reverse_swapper_api: Arc<dyn ReverseSwapperRoutingAPI>,
96 reverse_swap_service_api: Arc<dyn ReverseSwapServiceAPI>,
97 persister: Arc<crate::persist::db::SqliteStorage>,
98 chain_service: Arc<dyn ChainService>,
99 node_api: Arc<dyn NodeAPI>,
100 ) -> Self {
101 let (status_changes_notifier, _) = broadcast::channel::<BreezEvent>(100);
102 Self {
103 config,
104 reverse_swapper_api,
105 reverse_swap_service_api,
106 persister,
107 chain_service,
108 node_api,
109 status_changes_notifier,
110 }
111 }
112
113 pub(crate) fn subscribe_status_changes(&self) -> broadcast::Receiver<BreezEvent> {
114 self.status_changes_notifier.subscribe()
115 }
116
117 async fn emit_reverse_swap_updated(&self, id: &str) -> Result<()> {
118 let full_rsi = self
119 .persister
120 .get_reverse_swap(id)?
121 .ok_or_else(|| anyhow!(format!("reverse swap {} was not found", id)))?;
122 self.status_changes_notifier
123 .send(BreezEvent::ReverseSwapUpdated {
124 details: self.convert_reverse_swap_info(full_rsi).await?,
125 })
126 .map_err(anyhow::Error::msg)?;
127 Ok(())
128 }
129
130 pub(crate) async fn on_event(&self, e: BreezEvent) -> Result<()> {
131 match e {
132 BreezEvent::Synced => {
133 self.process_monitored_reverse_swaps().await
137 }
138 _ => Ok(()),
139 }
140 }
141
142 fn validate_recipient_address(claim_pubkey: &str) -> ReverseSwapResult<()> {
144 Address::from_str(claim_pubkey)
145 .map(|_| ())
146 .map_err(|e| ReverseSwapError::InvalidDestinationAddress(e.to_string()))
147 }
148
149 pub(crate) fn validate_claim_tx_fee(claim_fee: u64) -> ReverseSwapResult<()> {
150 let min_claim_fee = Self::calculate_claim_tx_fee(1)?;
151 ensure_sdk!(
152 claim_fee >= min_claim_fee,
153 ReverseSwapError::ClaimFeerateTooLow
154 );
155 Ok(())
156 }
157
158 pub(crate) async fn last_hop_for_payment(&self) -> ReverseSwapResult<RouteHintHop> {
159 let reverse_routing_node = self
160 .reverse_swapper_api
161 .fetch_reverse_routing_node()
162 .await?;
163 let routing_hints = self
164 .reverse_swap_service_api
165 .get_route_hints(hex::encode(reverse_routing_node.clone()))
166 .await?;
167 routing_hints
168 .first()
169 .ok_or_else(|| {
170 ReverseSwapError::RouteNotFound(format!(
171 "No route hints found for reverse routing node {reverse_routing_node:?}"
172 ))
173 })?
174 .hops
175 .first()
176 .ok_or_else(|| {
177 ReverseSwapError::RouteNotFound(format!(
178 "No hops found for reverse routing node {reverse_routing_node:?}"
179 ))
180 })
181 .cloned()
182 }
183
184 pub(crate) async fn create_reverse_swap(
187 &self,
188 req: PayOnchainRequest,
189 ) -> ReverseSwapResult<FullReverseSwapInfo> {
190 Self::validate_recipient_address(&req.recipient_address)?;
191
192 let routing_node = self
193 .reverse_swapper_api
194 .fetch_reverse_routing_node()
195 .await
196 .map(hex::encode)?;
197 let created_rsi = self
198 .create_and_validate_rev_swap_on_remote(req.clone(), routing_node)
199 .await?;
200
201 trace!("create_rev_swap v2 request: {req:?}");
203 trace!("create_rev_swap v2 created_rsi: {created_rsi:?}");
204
205 let request_send_amount_sat = req.prepare_res.sender_amount_sat;
207 let request_send_amount_msat = request_send_amount_sat * 1_000;
208 created_rsi.validate_invoice_amount(request_send_amount_msat)?;
209
210 let lockup_fee_sat = req.prepare_res.fees_lockup;
212 let service_fee_sat = super::get_service_fee_sat(
213 req.prepare_res.sender_amount_sat,
214 req.prepare_res.fees_percentage,
215 );
216 trace!("create_rev_swap v2 service_fee_sat: {service_fee_sat} sat");
217 let expected_onchain_amount = request_send_amount_sat - service_fee_sat - lockup_fee_sat;
218 ensure_sdk!(
219 created_rsi.onchain_amount_sat == expected_onchain_amount,
220 ReverseSwapError::generic("Unexpected onchain amount (lockup fee or service fee)")
221 );
222
223 ensure_sdk!(
225 created_rsi.onchain_amount_sat > req.prepare_res.recipient_amount_sat,
226 ReverseSwapError::generic("Unexpected receive amount")
227 );
228 let claim_fee = created_rsi.onchain_amount_sat - req.prepare_res.recipient_amount_sat;
229 Self::validate_claim_tx_fee(claim_fee)?;
230
231 self.persister.insert_reverse_swap(&created_rsi)?;
232 info!("Created and persisted reverse swap {}", created_rsi.id);
233
234 let res = tokio::select! {
240 pay_thread_res = tokio::time::timeout(
241 Duration::from_secs(self.config.payment_timeout_sec as u64),
242 self.node_api.send_pay(created_rsi.invoice.clone(), MAX_PAYMENT_PATH_HOPS)
243 ) => {
244 match pay_thread_res {
246 Ok(Ok(res)) => Err(NodeError::PaymentFailed(format!("Payment of HODL invoice unexpectedly returned: {res:?}"))),
248
249 Ok(Err(e)) => Err(NodeError::PaymentFailed(format!("Failed to pay HODL invoice: {e}"))),
251
252 Err(e) => Err(NodeError::PaymentTimeout(format!("Trying to pay the HODL invoice timed out: {e}")))
254 }
255 },
256 paid_invoice_res = self.poll_initial_boltz_status_transition(&created_rsi.id) => {
257 paid_invoice_res.map(|_| created_rsi.clone()).map_err(|e| NodeError::Generic(e.to_string()))
258 }
259 };
260
261 match res {
264 Ok(_) => {
265 let lockup_txid = self.get_lockup_tx(&created_rsi).await?.map(|tx| tx.txid);
266 self.persister
267 .update_reverse_swap_status(&created_rsi.id, &InProgress)?;
268 self.persister
269 .update_reverse_swap_lockup_txid(&created_rsi.id, lockup_txid)?;
270 self.emit_reverse_swap_updated(&created_rsi.id).await?
271 }
272 Err(_) => {
273 self.persister
274 .update_reverse_swap_status(&created_rsi.id, &Cancelled)?;
275 self.emit_reverse_swap_updated(&created_rsi.id).await?
276 }
277 }
278
279 Ok(res?)
280 }
281
282 async fn poll_initial_boltz_status_transition(&self, id: &str) -> Result<()> {
289 let mut i = 0;
290 loop {
291 sleep(Duration::from_secs(5)).await;
292
293 info!("Checking Boltz status for reverse swap {id}, attempt {i}");
294 let reverse_swap_boltz_status = self
295 .reverse_swap_service_api
296 .get_boltz_status(id.into())
297 .await?;
298 info!("Got Boltz status {reverse_swap_boltz_status:?}");
299
300 if let LockTxMempool { .. } | LockTxConfirmed { .. } = reverse_swap_boltz_status {
305 return Ok(());
306 }
307 i += 1;
308 }
309 }
310
311 async fn create_and_validate_rev_swap_on_remote(
314 &self,
315 req: PayOnchainRequest,
316 routing_node: String,
317 ) -> ReverseSwapResult<FullReverseSwapInfo> {
318 let reverse_swap_keys = create_swap_keys()?;
319
320 let boltz_response = self
321 .reverse_swap_service_api
322 .create_reverse_swap_on_remote(
323 req.prepare_res.sender_amount_sat,
324 reverse_swap_keys.preimage_hash_bytes().to_hex(),
325 reverse_swap_keys.public_key()?.to_hex(),
326 req.prepare_res.fees_hash,
327 routing_node,
328 )
329 .await?;
330 match boltz_response {
331 BoltzApiCreateReverseSwapResponse::BoltzApiSuccess(response) => {
332 let res = FullReverseSwapInfo {
333 created_at_block_height: self.chain_service.current_tip().await?,
334 claim_pubkey: req.recipient_address,
335 invoice: response.invoice,
336 preimage: reverse_swap_keys.preimage,
337 private_key: reverse_swap_keys.priv_key,
338 timeout_block_height: response.timeout_block_height,
339 id: response.id,
340 onchain_amount_sat: response.onchain_amount,
341 sat_per_vbyte: None,
342 receive_amount_sat: Some(req.prepare_res.recipient_amount_sat),
343 redeem_script: response.redeem_script,
344 cache: ReverseSwapInfoCached {
345 status: Initial,
346 lockup_txid: None,
347 claim_txid: None,
348 },
349 };
350
351 res.validate_invoice(req.prepare_res.sender_amount_sat * 1_000)?;
352 res.validate_redeem_script(response.lockup_address, self.config.network)?;
353 Ok(res)
354 }
355 BoltzApiCreateReverseSwapResponse::BoltzApiError { error } => {
356 Err(ReverseSwapError::ServiceConnectivity(format!(
357 "(Boltz) Failed to create reverse swap: {error}"
358 )))
359 }
360 }
361 }
362
363 async fn create_claim_tx(&self, rs: &FullReverseSwapInfo) -> Result<Transaction> {
365 let lockup_addr = rs.get_lockup_address(self.config.network)?;
366 let claim_addr = Address::from_str(&rs.claim_pubkey)?;
367 let redeem_script = Script::from_hex(&rs.redeem_script)?;
368
369 match lockup_addr.address_type() {
370 Some(AddressType::P2wsh) => {
371 let confirmed_txs = self
384 .chain_service
385 .address_transactions(lockup_addr.to_string())
386 .await?
387 .into_iter()
388 .filter(|tx| tx.status.confirmed)
389 .collect();
390 debug!("Found confirmed txs for lockup address {lockup_addr}: {confirmed_txs:?}");
391 let utxos = get_utxos(lockup_addr.to_string(), confirmed_txs, true)?;
392
393 let claim_amount_sat = rs.onchain_amount_sat;
395 debug!("Claim tx amount: {claim_amount_sat} sat");
396
397 let tx_out_value = match rs.sat_per_vbyte {
399 Some(claim_tx_feerate) => {
400 claim_amount_sat - Self::calculate_claim_tx_fee(claim_tx_feerate)?
401 }
402 None => rs.receive_amount_sat.ok_or(anyhow!(
403 "Cannot create claim tx: no claim feerate or receive amount found"
404 ))?,
405 };
406 debug!("Tx out amount: {tx_out_value} sat");
407
408 Self::build_claim_tx_inner(
409 SecretKey::from_slice(rs.private_key.as_slice())?,
410 rs.preimage.clone(),
411 utxos,
412 claim_addr,
413 redeem_script,
414 tx_out_value,
415 )
416 }
417 Some(addr_type) => Err(anyhow!("Unexpected lock address type: {addr_type:?}")),
418 None => Err(anyhow!("Could not determine lock address type")),
419 }
420 }
421
422 fn build_claim_tx_inner(
423 secret_key: SecretKey,
424 preimage: Vec<u8>,
425 utxos: AddressUtxos,
426 claim_addr: Address,
427 redeem_script: Script,
428 tx_out_value: u64,
429 ) -> Result<Transaction> {
430 let txins: Vec<TxIn> = utxos
431 .confirmed
432 .iter()
433 .map(|utxo| TxIn {
434 previous_output: utxo.out,
435 script_sig: Script::new(),
436 sequence: Sequence(0),
437 witness: Witness::default(),
438 })
439 .collect();
440
441 let tx_out: Vec<TxOut> = vec![TxOut {
442 value: tx_out_value,
443 script_pubkey: claim_addr.script_pubkey(),
444 }];
445
446 let mut tx = Transaction {
448 version: 2,
449 lock_time: crate::bitcoin::PackedLockTime(0),
450 input: txins.clone(),
451 output: tx_out,
452 };
453
454 let claim_script_bytes = PsbtSerialize::serialize(&redeem_script);
455
456 let scpt = Secp256k1::signing_only();
458 let mut signed_inputs: Vec<TxIn> = Vec::new();
459 for (index, input) in tx.input.iter().enumerate() {
460 let mut signer = SighashCache::new(&tx);
461 let sig = signer.segwit_signature_hash(
462 index,
463 &redeem_script,
464 utxos.confirmed[index].value,
465 EcdsaSighashType::All,
466 )?;
467 let msg = Message::from_slice(&sig[..])?;
468 let sig = scpt.sign_ecdsa(&msg, &secret_key);
469
470 let mut sigvec = sig.serialize_der().to_vec();
471 sigvec.push(EcdsaSighashType::All as u8);
472
473 let witness: Vec<Vec<u8>> = vec![sigvec, preimage.clone(), claim_script_bytes.clone()];
474
475 let mut signed_input = input.clone();
476 let w = Witness::from_vec(witness);
477 signed_input.witness = w;
478 signed_inputs.push(signed_input);
479 }
480 tx.input = signed_inputs;
481
482 Ok(tx)
483 }
484
485 pub(crate) fn calculate_claim_tx_fee(claim_tx_feerate: u32) -> SdkResult<u64> {
486 let tx = build_fake_claim_tx()?;
487
488 let claim_witness_input_size: u32 = 1 + 1 + 8 + 73 + 1 + 32 + 1 + 100;
490 let tx_weight =
491 tx.strippedsize() as u32 * WITNESS_SCALE_FACTOR as u32 + claim_witness_input_size;
492 let fees: u64 = (tx_weight * claim_tx_feerate / WITNESS_SCALE_FACTOR as u32) as u64;
493
494 Ok(fees)
495 }
496
497 async fn get_claim_tx(&self, rsi: &FullReverseSwapInfo) -> Result<Option<OnchainTx>> {
498 let lockup_addr = rsi.get_lockup_address(self.config.network)?;
499 Ok(self
500 .chain_service
501 .address_transactions(lockup_addr.to_string())
502 .await?
503 .into_iter()
504 .find(|tx| {
505 tx.vin
506 .iter()
507 .any(|vin| vin.prevout.scriptpubkey_address == lockup_addr.to_string())
508 && tx
509 .vout
510 .iter()
511 .any(|vout| vout.scriptpubkey_address == rsi.claim_pubkey.clone())
512 }))
513 }
514
515 async fn get_lockup_tx(&self, rsi: &FullReverseSwapInfo) -> Result<Option<OnchainTx>> {
516 let lockup_addr = rsi.get_lockup_address(self.config.network)?;
517 let maybe_lockup_tx = self
518 .chain_service
519 .address_transactions(lockup_addr.to_string())
520 .await?
521 .into_iter()
522 .find(|tx| {
523 trace!("Checking potential lock tx {tx:#?}");
526 tx.vout.iter().any(|vout| {
527 vout.value == rsi.onchain_amount_sat
528 && vout.scriptpubkey_address == lockup_addr.to_string()
529 })
530 });
531
532 Ok(maybe_lockup_tx)
533 }
534
535 async fn get_status_update_for_monitored(
539 &self,
540 rsi: &FullReverseSwapInfo,
541 claim_tx_status: TxStatus,
542 ) -> Result<Option<ReverseSwapStatus>> {
543 let current_status = rsi.cache.status;
544 ensure!(
545 current_status.is_monitored_state(),
546 "Tried to get status for non-monitored reverse swap"
547 );
548
549 let payment_hash_hex = &rsi.get_preimage_hash().to_hex();
550 let payment_status = self.persister.get_payment_by_hash(payment_hash_hex)?;
551 if let Some(ref payment) = payment_status {
552 if payment.status == PaymentStatus::Failed {
553 warn!("Payment failed for reverse swap {}", rsi.id);
554 return Ok(Some(Cancelled));
555 }
556 }
557
558 let new_status = match ¤t_status {
559 Initial => match payment_status {
560 Some(_) => Some(InProgress),
561 None => match self
562 .reverse_swap_service_api
563 .get_boltz_status(rsi.id.clone())
564 .await?
565 {
566 SwapExpired | LockTxFailed | LockTxRefunded { .. } | InvoiceExpired => {
567 Some(Cancelled)
571 }
572 _ => None,
573 },
574 },
575 InProgress => match claim_tx_status {
576 TxStatus::Unknown => {
577 let block_height = self.chain_service.current_tip().await?;
578 match block_height >= rsi.timeout_block_height {
579 true => {
580 warn!("Reverse swap {} crossed the timeout block height", rsi.id);
581 Some(Cancelled)
582 }
583 false => None,
584 }
585 }
586 TxStatus::Mempool => Some(CompletedSeen),
587 TxStatus::Confirmed => Some(CompletedConfirmed),
588 },
589 CompletedSeen => match claim_tx_status {
590 TxStatus::Confirmed => Some(CompletedConfirmed),
591 _ => None,
592 },
593 _ => None,
594 };
595
596 Ok(new_status)
597 }
598
599 async fn process_monitored_reverse_swaps(&self) -> Result<()> {
603 let monitored = self.list_monitored().await?;
604 debug!("Found {} monitored reverse swaps", monitored.len());
605 self.claim_reverse_swaps(monitored).await
606 }
607
608 async fn claim_reverse_swaps(&self, reverse_swaps: Vec<FullReverseSwapInfo>) -> Result<()> {
609 for rsi in reverse_swaps {
610 debug!("Processing reverse swap {rsi:?}");
611
612 let lockup_tx = self.get_lockup_tx(&rsi).await?;
614 let lock_tx_status = TxStatus::from(&lockup_tx);
615 let claim_tx = self.get_claim_tx(&rsi).await?;
616 let claim_tx_status = TxStatus::from(&claim_tx);
617
618 if let Some(tx) = &claim_tx {
619 info!(
620 "Found claim tx for reverse swap {:?}: {:?}, status: {:?}",
621 rsi.id, tx.txid, claim_tx_status
622 );
623 }
624 if let Some(new_status) = self
626 .get_status_update_for_monitored(&rsi, claim_tx_status)
627 .await?
628 {
629 self.persister
630 .update_reverse_swap_status(&rsi.id, &new_status)?;
631 self.emit_reverse_swap_updated(&rsi.id).await?;
632 }
633
634 let broadcasted_claim_tx = if matches!(lock_tx_status, TxStatus::Confirmed) {
636 info!("Lock tx is confirmed, preparing claim tx");
637 let claim_tx = self.create_claim_tx(&rsi).await?;
638 let claim_tx_broadcast_res = self
639 .chain_service
640 .broadcast_transaction(serialize(&claim_tx))
641 .await;
642 match claim_tx_broadcast_res {
643 Ok(txid) => info!("Claim tx was broadcast with txid {txid}"),
644 Err(e) => error!("Claim tx failed to broadcast: {e}"),
645 };
646 Some(claim_tx)
647 } else {
648 None
649 };
650
651 if rsi.cache.lockup_txid.is_none() {
653 self.persister
654 .update_reverse_swap_lockup_txid(&rsi.id, lockup_tx.map(|tx| tx.txid))?;
655 self.emit_reverse_swap_updated(&rsi.id).await?;
656 }
657 if rsi.cache.claim_txid.is_none() {
658 self.persister.update_reverse_swap_claim_txid(
659 &rsi.id,
660 claim_tx
661 .map(|tx| tx.txid)
662 .or(broadcasted_claim_tx.map(|tx| tx.txid().to_string())),
663 )?;
664 self.emit_reverse_swap_updated(&rsi.id).await?;
665 }
666 }
667
668 Ok(())
669 }
670
671 pub async fn claim_reverse_swap(&self, lockup_address: String) -> ReverseSwapResult<()> {
672 let rsis: Vec<FullReverseSwapInfo> = self
673 .list_monitored()
674 .await?
675 .into_iter()
676 .filter(|rev_swap| {
677 lockup_address
678 == rev_swap
679 .get_lockup_address(self.config.network)
680 .map(|a| a.to_string())
681 .unwrap_or_default()
682 })
683 .collect();
684 match rsis.is_empty() {
685 true => Err(ReverseSwapError::Generic(format!(
686 "Reverse swap address {} was not found",
687 lockup_address
688 ))),
689 false => Ok(self.claim_reverse_swaps(rsis).await?),
690 }
691 }
692
693 pub async fn list_blocking(&self) -> Result<Vec<FullReverseSwapInfo>> {
695 let mut matching_reverse_swaps = vec![];
696 for rs in self.persister.list_reverse_swaps()? {
697 debug!("Reverse swap {} has status {:?}", rs.id, rs.cache.status);
698 if rs.cache.status.is_blocking_state() {
699 matching_reverse_swaps.push(rs);
700 }
701 }
702 Ok(matching_reverse_swaps)
703 }
704
705 pub async fn list_monitored(&self) -> Result<Vec<FullReverseSwapInfo>> {
708 let mut matching_reverse_swaps = vec![];
709 for rs in self.persister.list_reverse_swaps()? {
710 if rs.cache.status.is_monitored_state() {
711 matching_reverse_swaps.push(rs);
712 }
713 }
714 Ok(matching_reverse_swaps)
715 }
716
717 pub(crate) async fn fetch_reverse_swap_fees(&self) -> ReverseSwapResult<ReverseSwapPairInfo> {
719 self.reverse_swap_service_api
720 .fetch_reverse_swap_fees()
721 .await
722 }
723
724 pub(crate) async fn convert_reverse_swap_info(
726 &self,
727 full_rsi: FullReverseSwapInfo,
728 ) -> Result<ReverseSwapInfo> {
729 Ok(ReverseSwapInfo {
730 id: full_rsi.id.clone(),
731 claim_pubkey: full_rsi.claim_pubkey.clone(),
732 lockup_txid: self
733 .get_lockup_tx(&full_rsi)
734 .await?
735 .map(|lockup_tx| lockup_tx.txid),
736 claim_txid: match full_rsi.cache.status {
737 CompletedSeen | CompletedConfirmed => self
738 .create_claim_tx(&full_rsi)
739 .await
740 .ok()
741 .map(|claim_tx| claim_tx.txid().to_hex()),
742 _ => None,
743 },
744 onchain_amount_sat: full_rsi.onchain_amount_sat,
745 status: full_rsi.cache.status,
746 })
747 }
748}
749
750fn build_fake_claim_tx() -> Result<Transaction> {
756 let keys = KeyPair::new(&Secp256k1::new(), &mut thread_rng());
757
758 let sk = keys.secret_key();
759 let pk_compressed_bytes = keys.public_key().serialize().to_vec();
760 let preimage_bytes = sha256::Hash::hash("123".as_bytes()).to_vec();
761 let redeem_script = FullReverseSwapInfo::build_expected_reverse_swap_script(
762 sha256::Hash::hash(&preimage_bytes).to_vec(), pk_compressed_bytes.clone(), pk_compressed_bytes, 840_000,
766 )?;
767
768 let claim_addr = Address::p2tr(
771 &Secp256k1::new(),
772 keys.public_key().x_only_public_key().0,
773 None,
774 Network::Bitcoin,
775 );
776
777 BTCSendSwap::build_claim_tx_inner(
778 sk,
779 preimage_bytes,
780 AddressUtxos {
781 confirmed: vec![Utxo {
782 out: OutPoint {
783 txid: Txid::all_zeros(),
784 vout: 1,
785 },
786 value: 1_000,
787 block_height: Some(123),
788 }],
789 },
790 claim_addr,
791 redeem_script,
792 1_000,
793 )
794}
795
796#[cfg(test)]
797mod tests {
798 use anyhow::Result;
799
800 use crate::swap_out::get_service_fee_sat;
801 use crate::test_utils::{MOCK_REVERSE_SWAP_MAX, MOCK_REVERSE_SWAP_MIN};
802 use crate::{PrepareOnchainPaymentRequest, PrepareOnchainPaymentResponse, SwapAmountType};
803
804 #[tokio::test]
805 async fn test_prepare_onchain_payment_in_range() -> Result<()> {
806 let sdk = crate::breez_services::tests::breez_services().await?;
807
808 assert_in_range_prep_payment_response(
810 sdk.prepare_onchain_payment(PrepareOnchainPaymentRequest {
811 amount_sat: MOCK_REVERSE_SWAP_MIN,
812 amount_type: SwapAmountType::Receive,
813 claim_tx_feerate: 1,
814 })
815 .await?,
816 )?;
817
818 assert_in_range_prep_payment_response(
820 sdk.prepare_onchain_payment(PrepareOnchainPaymentRequest {
821 amount_sat: MOCK_REVERSE_SWAP_MIN,
822 amount_type: SwapAmountType::Receive,
823 claim_tx_feerate: 1,
824 })
825 .await?,
826 )?;
827
828 Ok(())
829 }
830
831 #[tokio::test]
832 async fn test_prepare_onchain_payment_out_of_range() -> Result<()> {
833 let sdk = crate::breez_services::tests::breez_services().await?;
834
835 assert!(sdk
837 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
838 amount_sat: MOCK_REVERSE_SWAP_MIN - 1,
839 amount_type: SwapAmountType::Send,
840 claim_tx_feerate: 1,
841 })
842 .await
843 .is_err());
844
845 assert!(sdk
847 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
848 amount_sat: MOCK_REVERSE_SWAP_MAX + 1,
849 amount_type: SwapAmountType::Send,
850 claim_tx_feerate: 1,
851 })
852 .await
853 .is_err());
854
855 assert!(sdk
857 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
858 amount_sat: 0,
859 amount_type: SwapAmountType::Receive,
860 claim_tx_feerate: 1,
861 })
862 .await
863 .is_err());
864
865 assert!(sdk
867 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
868 amount_sat: MOCK_REVERSE_SWAP_MAX,
869 amount_type: SwapAmountType::Receive,
870 claim_tx_feerate: 1,
871 })
872 .await
873 .is_err());
874
875 assert!(sdk
877 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
878 amount_sat: MOCK_REVERSE_SWAP_MIN,
879 amount_type: SwapAmountType::Receive,
880 claim_tx_feerate: 1_000_000,
881 })
882 .await
883 .is_err());
884
885 Ok(())
886 }
887
888 fn assert_in_range_prep_payment_response(res: PrepareOnchainPaymentResponse) -> Result<()> {
892 dbg!(&res);
893
894 let send_amount_sat = res.sender_amount_sat;
895 let receive_amount_sat = res.recipient_amount_sat;
896 let total_fees = res.total_fees;
897 assert_eq!(send_amount_sat - total_fees, receive_amount_sat);
898
899 let service_fees = get_service_fee_sat(send_amount_sat, res.fees_percentage);
900 let expected_total_fees = res.fees_lockup + res.fees_claim + service_fees;
901 assert_eq!(expected_total_fees, total_fees);
902
903 Ok(())
904 }
905}