breez_sdk_core/swap_out/
reverseswap.rs

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
34// Estimates based on https://github.com/BoltzExchange/boltz-backend/blob/master/lib/rates/FeeProvider.ts#L31-L42
35pub 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    /// HODL invoice that has to be paid, for the Boltz service to lock up the funds
45    invoice: String,
46
47    /// Redeem script from which the lock address is derived. Can be used to check that the Boltz
48    /// service didn't create an address without an HTLC.
49    redeem_script: String,
50
51    /// Amount of sats which will be locked
52    onchain_amount: u64,
53
54    /// Block height at which the reverse swap will be considered cancelled
55    timeout_block_height: u32,
56
57    /// Address to which the funds will be locked
58    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
80/// This struct is responsible for sending to an onchain address using lightning payments.
81/// It uses internally an implementation of [ReverseSwapServiceAPI] that represents Boltz reverse swapper service.
82pub(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                // Since this relies on the most up-to-date states of the reverse swap HODL invoice payments,
134                // a fresh [BreezServices::sync] *must* be called before this method.
135                // Therefore we specifically call this on the Synced event
136                self.process_monitored_reverse_swaps().await
137            }
138            _ => Ok(()),
139        }
140    }
141
142    /// Validates the reverse swap arguments given by the user
143    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    /// Creates and persists a reverse swap. If the initial payment fails, the reverse swap has the new
185    /// status persisted.
186    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        // Perform validation on the created swap
202        trace!("create_rev_swap v2 request: {req:?}");
203        trace!("create_rev_swap v2 created_rsi: {created_rsi:?}");
204
205        // Validate send_amount
206        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        // Validate onchain_amount
211        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        // Validate claim_fee. If onchain_amount and claim_fee are both valid, receive_amount is also valid.
224        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        // Wait until one of the following happens:
235        // - trying to pay the HODL invoice explicitly fails from Greenlight
236        // - the regular poll of the Breez API detects the status of this reverse swap advanced to LockTxMempool
237        //   (meaning Boltz detected that we paid the HODL invoice)
238        // - the max allowed duration of a payment is reached
239        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                // TODO It doesn't fail when trying to pay more sats than max_payable?
245                match pay_thread_res {
246                    // Paying a HODL invoice does not typically return, so if send_payment() returned, it's an abnormal situation
247                    Ok(Ok(res)) => Err(NodeError::PaymentFailed(format!("Payment of HODL invoice unexpectedly returned: {res:?}"))),
248
249                    // send_payment() returned an error, so we know paying the HODL invoice failed
250                    Ok(Err(e)) => Err(NodeError::PaymentFailed(format!("Failed to pay HODL invoice: {e}"))),
251
252                    // send_payment() has been trying to pay for longer than the payment timeout
253                    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        // The result of the creation call can succeed or fail
262        // We update the rev swap status accordingly, which would otherwise have needed a fully fledged sync() call
263        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    /// Endless loop that periodically polls whether the reverse swap transitioned away from the
283    /// initial status.
284    ///
285    /// The loop returns as soon as the lock tx is seen by Boltz. In other words, it returns as soon as
286    /// the reverse swap status, as reported by Boltz, is [BoltzApiReverseSwapStatus::LockTxMempool]
287    /// or [BoltzApiReverseSwapStatus::LockTxConfirmed]
288    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            // Return when lock tx is seen in the mempool or onchain
301            // Typically we first detect when the lock tx is in the mempool
302            // However, if the tx is broadcast and the block is mined between the iterations of this loop,
303            // we might not see the LockTxMempool state and instead directly get the LockTxConfirmed
304            if let LockTxMempool { .. } | LockTxConfirmed { .. } = reverse_swap_boltz_status {
305                return Ok(());
306            }
307            i += 1;
308        }
309    }
310
311    /// Create a new reverse swap on the remote service provider (Boltz), then validates its redeem script
312    /// before returning it
313    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    /// Builds and signs claim tx
364    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                // We explicitly only get the confirmed onchain transactions
372                //
373                // Otherwise, if we had gotten all txs, we risk a race condition when we try
374                // to re-broadcast the claim tx. On re-broadcast, the claim tx is already in the
375                // mempool, so it would be returned in the list below. This however would mark
376                // the utxos as spent, so this address would have a confirmed amount of 0. When
377                // building the claim tx below and trying to subtract fees from the confirmed amount,
378                // this would lead to creating a tx with a negative amount. This doesn't happen
379                // if we restrict this to confirmed txs, because then the mempool claim tx is not returned.
380                //
381                // If the claim tx is confirmed, we would not try to re-broadcast it, so the race
382                // condition only exists when a re-broadcast is tried and the claim tx is unconfirmed.
383                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                // The amount locked in the claim address
394                let claim_amount_sat = rs.onchain_amount_sat;
395                debug!("Claim tx amount: {claim_amount_sat} sat");
396
397                // Calculate amount sent in a backward compatible way
398                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        // construct the transaction
447        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        // Sign inputs (iterate, even though we only have one input)
457        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        // Based on https://github.com/breez/boltz/blob/master/boltz.go#L32
489        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                // Lockup tx is identified by having a vout matching the expected rev swap amount
524                // going to the lockup address (P2WSH)
525                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    /// Determine the new active status of a monitored reverse swap.
536    ///
537    /// If the status has not changed, it will return [None].
538    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 &current_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                        // We only mark a reverse swap as Cancelled if Boltz also reports it in a cancelled or error state
568                        // We do this to avoid race conditions in the edge-case when a reverse swap status update
569                        // is triggered after creation succeeds, but before the payment is persisted in the DB
570                        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    /// Updates the cached values of monitored reverse swaps in the cache table and executes the
600    /// corresponding next steps for the pending reverse swaps. This includes the blocking
601    /// reverse swaps as well, since the blocking statuses are a subset of the monitored statuses.
602    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            // Look for lockup and claim txs on chain
613            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            // Update cached state when new state is detected
625            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            // (Re-)Broadcast the claim tx for monitored reverse swaps that have a confirmed lockup tx
635            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            // Cache lockup and claim tx txids if not cached yet
652            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    /// Returns the ongoing reverse swaps which have a status that block the creation of new reverse swaps
694    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    /// Returns the reverse swaps for which we expect the status to change, and therefore need
706    /// to be monitored.
707    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    /// See [ReverseSwapServiceAPI::fetch_reverse_swap_fees]
718    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    /// Converts the internal [FullReverseSwapInfo] into the user-facing [ReverseSwapInfo]
725    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
750/// Internal utility to create a fake claim tx: a tx that has the claim tx structure (input,
751/// output, witness, etc) but with random values.
752///
753/// This is used to get the claim tx size, in order to then estimate the claim tx fee, before
754/// knowing the actual claim tx.
755fn 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(), // 32 bytes
763        pk_compressed_bytes.clone(),                  // 33 bytes
764        pk_compressed_bytes,                          // 33 bytes
765        840_000,
766    )?;
767
768    // Use a P2TR output, which is slightly larger than a P2WPKH (native segwit) output
769    // This means we will slightly overpay when claiming to segwit addresses
770    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        // User-specified send amount is within range
809        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        // Derived send amount is within range
819        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        // User-specified send amount is out of range (below min)
836        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        // User-specified send amount is out of range (above max)
846        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        // Derived send amount is out of range (below min: specified receive amount is 0)
856        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        // Derived send amount is out of range (above max)
866        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        // Derived send amount is out of range (above max because the chosen claim tx feerate pushes the send above max)
876        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    /// Validates a [PrepareOnchainPaymentResponse] with all fields set.
889    ///
890    /// This is the case when the requested amount is within the reverse swap range.
891    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}