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::{
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
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                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    /// 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 =
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                // We explicitly only get the confirmed onchain transactions
373                //
374                // Otherwise, if we had gotten all txs, we risk a race condition when we try
375                // to re-broadcast the claim tx. On re-broadcast, the claim tx is already in the
376                // mempool, so it would be returned in the list below. This however would mark
377                // the utxos as spent, so this address would have a confirmed amount of 0. When
378                // building the claim tx below and trying to subtract fees from the confirmed amount,
379                // this would lead to creating a tx with a negative amount. This doesn't happen
380                // if we restrict this to confirmed txs, because then the mempool claim tx is not returned.
381                //
382                // If the claim tx is confirmed, we would not try to re-broadcast it, so the race
383                // condition only exists when a re-broadcast is tried and the claim tx is unconfirmed.
384                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                // The amount locked in the claim address
395                let claim_amount_sat = rs.onchain_amount_sat;
396                debug!("Claim tx amount: {claim_amount_sat} sat");
397
398                // Calculate amount sent in a backward compatible way
399                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        // construct the transaction
448        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        // Sign inputs (iterate, even though we only have one input)
458        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        // Based on https://github.com/breez/boltz/blob/master/boltz.go#L32
490        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                // Lockup tx is identified by having a vout matching the expected rev swap amount
525                // going to the lockup address (P2WSH)
526                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    /// Determine the new active status of a monitored reverse swap.
537    ///
538    /// If the status has not changed, it will return [None].
539    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 &current_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                        // We only mark a reverse swap as Cancelled if Boltz also reports it in a cancelled or error state
569                        // We do this to avoid race conditions in the edge-case when a reverse swap status update
570                        // is triggered after creation succeeds, but before the payment is persisted in the DB
571                        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    /// Updates the cached values of monitored reverse swaps in the cache table and executes the
601    /// corresponding next steps for the pending reverse swaps. This includes the blocking
602    /// reverse swaps as well, since the blocking statuses are a subset of the monitored statuses.
603    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            // Look for lockup and claim txs on chain
614            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            // Update cached state when new state is detected
626            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            // (Re-)Broadcast the claim tx for monitored reverse swaps that have a confirmed lockup tx
636            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            // Cache lockup and claim tx txids if not cached yet
653            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    /// 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_string()),
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();
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), // 32 bytes
765        pk_compressed_bytes,                 // 33 bytes
766        pk_compressed_bytes.to_vec(),        // 33 bytes
767        840_000,
768    )?;
769
770    // Use a P2TR output, which is slightly larger than a P2WPKH (native segwit) output
771    // This means we will slightly overpay when claiming to segwit addresses
772    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        // User-specified send amount is within range
811        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        // Derived send amount is within range
821        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        // User-specified send amount is out of range (below min)
838        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        // User-specified send amount is out of range (above max)
848        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        // Derived send amount is out of range (below min: specified receive amount is 0)
858        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        // Derived send amount is out of range (above max)
868        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        // Derived send amount is out of range (above max because the chosen claim tx feerate pushes the send above max)
878        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    /// Validates a [PrepareOnchainPaymentResponse] with all fields set.
891    ///
892    /// This is the case when the requested amount is within the reverse swap range.
893    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}