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::{
13 absolute,
14 blockdata::constants::WITNESS_SCALE_FACTOR,
15 consensus::serialize,
16 hashes::{sha256, Hash},
17 key::KeyPair,
18 secp256k1::{Message, Secp256k1, SecretKey},
19 sighash::{EcdsaSighashType, SighashCache},
20 Address, AddressType, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut,
21 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 hex::encode(reverse_swap_keys.preimage_hash_bytes()),
325 hex::encode(reverse_swap_keys.public_key()?.serialize()),
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 =
367 Address::from_str(&rs.claim_pubkey)?.require_network(self.config.network.into())?;
368 let redeem_script = ScriptBuf::from_hex(&rs.redeem_script)?;
369
370 match lockup_addr.address_type() {
371 Some(AddressType::P2wsh) => {
372 let confirmed_txs = self
385 .chain_service
386 .address_transactions(lockup_addr.to_string())
387 .await?
388 .into_iter()
389 .filter(|tx| tx.status.confirmed)
390 .collect();
391 debug!("Found confirmed txs for lockup address {lockup_addr}: {confirmed_txs:?}");
392 let utxos = get_utxos(lockup_addr.to_string(), confirmed_txs, true)?;
393
394 let claim_amount_sat = rs.onchain_amount_sat;
396 debug!("Claim tx amount: {claim_amount_sat} sat");
397
398 let tx_out_value = match rs.sat_per_vbyte {
400 Some(claim_tx_feerate) => {
401 claim_amount_sat - Self::calculate_claim_tx_fee(claim_tx_feerate)?
402 }
403 None => rs.receive_amount_sat.ok_or(anyhow!(
404 "Cannot create claim tx: no claim feerate or receive amount found"
405 ))?,
406 };
407 debug!("Tx out amount: {tx_out_value} sat");
408
409 Self::build_claim_tx_inner(
410 SecretKey::from_slice(rs.private_key.as_slice())?,
411 rs.preimage.clone(),
412 utxos,
413 claim_addr,
414 &redeem_script,
415 tx_out_value,
416 )
417 }
418 Some(addr_type) => Err(anyhow!("Unexpected lock address type: {addr_type:?}")),
419 None => Err(anyhow!("Could not determine lock address type")),
420 }
421 }
422
423 fn build_claim_tx_inner(
424 secret_key: SecretKey,
425 preimage: Vec<u8>,
426 utxos: AddressUtxos,
427 claim_addr: Address,
428 redeem_script: &Script,
429 tx_out_value: u64,
430 ) -> Result<Transaction> {
431 let txins: Vec<TxIn> = utxos
432 .confirmed
433 .iter()
434 .map(|utxo| TxIn {
435 previous_output: utxo.out,
436 script_sig: ScriptBuf::new(),
437 sequence: Sequence(0),
438 witness: Witness::default(),
439 })
440 .collect();
441
442 let tx_out: Vec<TxOut> = vec![TxOut {
443 value: tx_out_value,
444 script_pubkey: claim_addr.script_pubkey(),
445 }];
446
447 let mut tx = Transaction {
449 version: 2,
450 lock_time: absolute::LockTime::ZERO,
451 input: txins.clone(),
452 output: tx_out,
453 };
454
455 let claim_script_bytes = redeem_script.to_bytes();
456
457 let scpt = Secp256k1::signing_only();
459 let mut signed_inputs: Vec<TxIn> = Vec::new();
460 for (index, input) in tx.input.iter().enumerate() {
461 let mut signer = SighashCache::new(&tx);
462 let sig = signer.segwit_signature_hash(
463 index,
464 redeem_script,
465 utxos.confirmed[index].value,
466 EcdsaSighashType::All,
467 )?;
468 let msg = Message::from_slice(&sig[..])?;
469 let sig = scpt.sign_ecdsa(&msg, &secret_key);
470
471 let mut sigvec = sig.serialize_der().to_vec();
472 sigvec.push(EcdsaSighashType::All as u8);
473
474 let witness: Vec<Vec<u8>> = vec![sigvec, preimage.clone(), claim_script_bytes.clone()];
475
476 let mut signed_input = input.clone();
477 let w = Witness::from_slice(&witness);
478 signed_input.witness = w;
479 signed_inputs.push(signed_input);
480 }
481 tx.input = signed_inputs;
482
483 Ok(tx)
484 }
485
486 pub(crate) fn calculate_claim_tx_fee(claim_tx_feerate: u32) -> SdkResult<u64> {
487 let tx = build_fake_claim_tx()?;
488
489 let claim_witness_input_size: u32 = 1 + 1 + 8 + 73 + 1 + 32 + 1 + 100;
491 let tx_weight =
492 tx.strippedsize() as u32 * WITNESS_SCALE_FACTOR as u32 + claim_witness_input_size;
493 let fees: u64 = (tx_weight * claim_tx_feerate / WITNESS_SCALE_FACTOR as u32) as u64;
494
495 Ok(fees)
496 }
497
498 async fn get_claim_tx(&self, rsi: &FullReverseSwapInfo) -> Result<Option<OnchainTx>> {
499 let lockup_addr = rsi.get_lockup_address(self.config.network)?;
500 Ok(self
501 .chain_service
502 .address_transactions(lockup_addr.to_string())
503 .await?
504 .into_iter()
505 .find(|tx| {
506 tx.vin
507 .iter()
508 .any(|vin| vin.prevout.scriptpubkey_address == lockup_addr.to_string())
509 && tx
510 .vout
511 .iter()
512 .any(|vout| vout.scriptpubkey_address == rsi.claim_pubkey.clone())
513 }))
514 }
515
516 async fn get_lockup_tx(&self, rsi: &FullReverseSwapInfo) -> Result<Option<OnchainTx>> {
517 let lockup_addr = rsi.get_lockup_address(self.config.network)?;
518 let maybe_lockup_tx = self
519 .chain_service
520 .address_transactions(lockup_addr.to_string())
521 .await?
522 .into_iter()
523 .find(|tx| {
524 trace!("Checking potential lock tx {tx:#?}");
527 tx.vout.iter().any(|vout| {
528 vout.value == rsi.onchain_amount_sat
529 && vout.scriptpubkey_address == lockup_addr.to_string()
530 })
531 });
532
533 Ok(maybe_lockup_tx)
534 }
535
536 async fn get_status_update_for_monitored(
540 &self,
541 rsi: &FullReverseSwapInfo,
542 claim_tx_status: TxStatus,
543 ) -> Result<Option<ReverseSwapStatus>> {
544 let current_status = rsi.cache.status;
545 ensure!(
546 current_status.is_monitored_state(),
547 "Tried to get status for non-monitored reverse swap"
548 );
549
550 let payment_hash_hex = format!("{:x}", &rsi.get_preimage_hash().forward_hex());
551 let payment_status = self.persister.get_payment_by_hash(&payment_hash_hex)?;
552 if let Some(ref payment) = payment_status {
553 if payment.status == PaymentStatus::Failed {
554 warn!("Payment failed for reverse swap {}", rsi.id);
555 return Ok(Some(Cancelled));
556 }
557 }
558
559 let new_status = match ¤t_status {
560 Initial => match payment_status {
561 Some(_) => Some(InProgress),
562 None => match self
563 .reverse_swap_service_api
564 .get_boltz_status(rsi.id.clone())
565 .await?
566 {
567 SwapExpired | LockTxFailed | LockTxRefunded { .. } | InvoiceExpired => {
568 Some(Cancelled)
572 }
573 _ => None,
574 },
575 },
576 InProgress => match claim_tx_status {
577 TxStatus::Unknown => {
578 let block_height = self.chain_service.current_tip().await?;
579 match block_height >= rsi.timeout_block_height {
580 true => {
581 warn!("Reverse swap {} crossed the timeout block height", rsi.id);
582 Some(Cancelled)
583 }
584 false => None,
585 }
586 }
587 TxStatus::Mempool => Some(CompletedSeen),
588 TxStatus::Confirmed => Some(CompletedConfirmed),
589 },
590 CompletedSeen => match claim_tx_status {
591 TxStatus::Confirmed => Some(CompletedConfirmed),
592 _ => None,
593 },
594 _ => None,
595 };
596
597 Ok(new_status)
598 }
599
600 async fn process_monitored_reverse_swaps(&self) -> Result<()> {
604 let monitored = self.list_monitored().await?;
605 debug!("Found {} monitored reverse swaps", monitored.len());
606 self.claim_reverse_swaps(monitored).await
607 }
608
609 async fn claim_reverse_swaps(&self, reverse_swaps: Vec<FullReverseSwapInfo>) -> Result<()> {
610 for rsi in reverse_swaps {
611 debug!("Processing reverse swap {rsi:?}");
612
613 let lockup_tx = self.get_lockup_tx(&rsi).await?;
615 let lock_tx_status = TxStatus::from(&lockup_tx);
616 let claim_tx = self.get_claim_tx(&rsi).await?;
617 let claim_tx_status = TxStatus::from(&claim_tx);
618
619 if let Some(tx) = &claim_tx {
620 info!(
621 "Found claim tx for reverse swap {:?}: {:?}, status: {:?}",
622 rsi.id, tx.txid, claim_tx_status
623 );
624 }
625 if let Some(new_status) = self
627 .get_status_update_for_monitored(&rsi, claim_tx_status)
628 .await?
629 {
630 self.persister
631 .update_reverse_swap_status(&rsi.id, &new_status)?;
632 self.emit_reverse_swap_updated(&rsi.id).await?;
633 }
634
635 let broadcasted_claim_tx = if matches!(lock_tx_status, TxStatus::Confirmed) {
637 info!("Lock tx is confirmed, preparing claim tx");
638 let claim_tx = self.create_claim_tx(&rsi).await?;
639 let claim_tx_broadcast_res = self
640 .chain_service
641 .broadcast_transaction(serialize(&claim_tx))
642 .await;
643 match claim_tx_broadcast_res {
644 Ok(txid) => info!("Claim tx was broadcast with txid {txid}"),
645 Err(e) => error!("Claim tx failed to broadcast: {e}"),
646 };
647 Some(claim_tx)
648 } else {
649 None
650 };
651
652 if rsi.cache.lockup_txid.is_none() {
654 self.persister
655 .update_reverse_swap_lockup_txid(&rsi.id, lockup_tx.map(|tx| tx.txid))?;
656 self.emit_reverse_swap_updated(&rsi.id).await?;
657 }
658 if rsi.cache.claim_txid.is_none() {
659 self.persister.update_reverse_swap_claim_txid(
660 &rsi.id,
661 claim_tx
662 .map(|tx| tx.txid)
663 .or(broadcasted_claim_tx.map(|tx| tx.txid().to_string())),
664 )?;
665 self.emit_reverse_swap_updated(&rsi.id).await?;
666 }
667 }
668
669 Ok(())
670 }
671
672 pub async fn claim_reverse_swap(&self, lockup_address: String) -> ReverseSwapResult<()> {
673 let rsis: Vec<FullReverseSwapInfo> = self
674 .list_monitored()
675 .await?
676 .into_iter()
677 .filter(|rev_swap| {
678 lockup_address
679 == rev_swap
680 .get_lockup_address(self.config.network)
681 .map(|a| a.to_string())
682 .unwrap_or_default()
683 })
684 .collect();
685 match rsis.is_empty() {
686 true => Err(ReverseSwapError::Generic(format!(
687 "Reverse swap address {lockup_address} was not found"
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_string()),
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();
760 let preimage_bytes = sha256::Hash::hash("123".as_bytes())
761 .to_byte_array()
762 .to_vec();
763 let redeem_script = FullReverseSwapInfo::build_expected_reverse_swap_script(
764 sha256::Hash::hash(&preimage_bytes), pk_compressed_bytes, pk_compressed_bytes.to_vec(), 840_000,
768 )?;
769
770 let claim_addr = Address::p2tr(
773 &Secp256k1::new(),
774 keys.public_key().x_only_public_key().0,
775 None,
776 Network::Bitcoin,
777 );
778
779 BTCSendSwap::build_claim_tx_inner(
780 sk,
781 preimage_bytes,
782 AddressUtxos {
783 confirmed: vec![Utxo {
784 out: OutPoint {
785 txid: Txid::all_zeros(),
786 vout: 1,
787 },
788 value: 1_000,
789 block_height: Some(123),
790 }],
791 },
792 claim_addr,
793 &redeem_script,
794 1_000,
795 )
796}
797
798#[cfg(test)]
799mod tests {
800 use anyhow::Result;
801
802 use crate::swap_out::get_service_fee_sat;
803 use crate::test_utils::{MOCK_REVERSE_SWAP_MAX, MOCK_REVERSE_SWAP_MIN};
804 use crate::{PrepareOnchainPaymentRequest, PrepareOnchainPaymentResponse, SwapAmountType};
805
806 #[tokio::test]
807 async fn test_prepare_onchain_payment_in_range() -> Result<()> {
808 let sdk = crate::breez_services::tests::breez_services().await?;
809
810 assert_in_range_prep_payment_response(
812 sdk.prepare_onchain_payment(PrepareOnchainPaymentRequest {
813 amount_sat: MOCK_REVERSE_SWAP_MIN,
814 amount_type: SwapAmountType::Receive,
815 claim_tx_feerate: 1,
816 })
817 .await?,
818 )?;
819
820 assert_in_range_prep_payment_response(
822 sdk.prepare_onchain_payment(PrepareOnchainPaymentRequest {
823 amount_sat: MOCK_REVERSE_SWAP_MIN,
824 amount_type: SwapAmountType::Receive,
825 claim_tx_feerate: 1,
826 })
827 .await?,
828 )?;
829
830 Ok(())
831 }
832
833 #[tokio::test]
834 async fn test_prepare_onchain_payment_out_of_range() -> Result<()> {
835 let sdk = crate::breez_services::tests::breez_services().await?;
836
837 assert!(sdk
839 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
840 amount_sat: MOCK_REVERSE_SWAP_MIN - 1,
841 amount_type: SwapAmountType::Send,
842 claim_tx_feerate: 1,
843 })
844 .await
845 .is_err());
846
847 assert!(sdk
849 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
850 amount_sat: MOCK_REVERSE_SWAP_MAX + 1,
851 amount_type: SwapAmountType::Send,
852 claim_tx_feerate: 1,
853 })
854 .await
855 .is_err());
856
857 assert!(sdk
859 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
860 amount_sat: 0,
861 amount_type: SwapAmountType::Receive,
862 claim_tx_feerate: 1,
863 })
864 .await
865 .is_err());
866
867 assert!(sdk
869 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
870 amount_sat: MOCK_REVERSE_SWAP_MAX,
871 amount_type: SwapAmountType::Receive,
872 claim_tx_feerate: 1,
873 })
874 .await
875 .is_err());
876
877 assert!(sdk
879 .prepare_onchain_payment(PrepareOnchainPaymentRequest {
880 amount_sat: MOCK_REVERSE_SWAP_MIN,
881 amount_type: SwapAmountType::Receive,
882 claim_tx_feerate: 1_000_000,
883 })
884 .await
885 .is_err());
886
887 Ok(())
888 }
889
890 fn assert_in_range_prep_payment_response(res: PrepareOnchainPaymentResponse) -> Result<()> {
894 dbg!(&res);
895
896 let send_amount_sat = res.sender_amount_sat;
897 let receive_amount_sat = res.recipient_amount_sat;
898 let total_fees = res.total_fees;
899 assert_eq!(send_amount_sat - total_fees, receive_amount_sat);
900
901 let service_fees = get_service_fee_sat(send_amount_sat, res.fees_percentage);
902 let expected_total_fees = res.fees_lockup + res.fees_claim + service_fees;
903 assert_eq!(expected_total_fees, total_fees);
904
905 Ok(())
906 }
907}