breez_sdk_liquid/
sdk.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::ops::Not as _;
3use std::sync::Arc;
4use std::{path::PathBuf, str::FromStr, time::Duration};
5
6use anyhow::{anyhow, ensure, Context as _, Result};
7use boltz_client::swaps::magic_routing::verify_mrh_signature;
8use boltz_client::Secp256k1;
9use boltz_client::{swaps::boltz::*, util::secrets::Preimage};
10use buy::{BuyBitcoinApi, BuyBitcoinService};
11use chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService};
12use chain_swap::ESTIMATED_BTC_CLAIM_TX_VSIZE;
13use futures_util::stream::select_all;
14use futures_util::{StreamExt, TryFutureExt};
15use lnurl::auth::SdkLnurlAuthSigner;
16use log::{debug, error, info, warn};
17use lwk_wollet::bitcoin::base64::Engine as _;
18use lwk_wollet::elements::AssetId;
19use lwk_wollet::elements_miniscript::elements::bitcoin::bip32::Xpub;
20use lwk_wollet::hashes::{sha256, Hash};
21use persist::model::{PaymentTxBalance, PaymentTxDetails};
22use recover::recoverer::Recoverer;
23use sdk_common::bitcoin::hashes::hex::ToHex;
24use sdk_common::input_parser::InputType;
25use sdk_common::lightning_with_bolt12::blinded_path::message::{
26    BlindedMessagePath, MessageContext, OffersContext,
27};
28use sdk_common::lightning_with_bolt12::blinded_path::payment::{
29    BlindedPaymentPath, Bolt12OfferContext, PaymentConstraints, PaymentContext,
30    UnauthenticatedReceiveTlvs,
31};
32use sdk_common::lightning_with_bolt12::blinded_path::IntroductionNode;
33use sdk_common::lightning_with_bolt12::bolt11_invoice::PaymentSecret;
34use sdk_common::lightning_with_bolt12::ln::inbound_payment::ExpandedKey;
35use sdk_common::lightning_with_bolt12::offers::invoice_request::InvoiceRequestFields;
36use sdk_common::lightning_with_bolt12::offers::nonce::Nonce;
37use sdk_common::lightning_with_bolt12::offers::offer::{Offer, OfferBuilder};
38use sdk_common::lightning_with_bolt12::sign::RandomBytes;
39use sdk_common::lightning_with_bolt12::types::payment::PaymentHash;
40use sdk_common::lightning_with_bolt12::util::string::UntrustedString;
41use sdk_common::liquid::LiquidAddressData;
42use sdk_common::prelude::{FiatAPI, FiatCurrency, LnUrlPayError, LnUrlWithdrawError, Rate};
43use side_swap::api::SideSwapService;
44use signer::SdkSigner;
45use swapper::boltz::proxy::BoltzProxyFetcher;
46use tokio::sync::{watch, Mutex, RwLock};
47use tokio_stream::wrappers::BroadcastStream;
48use tokio_with_wasm::alias as tokio;
49use web_time::{Instant, SystemTime, UNIX_EPOCH};
50use x509_parser::parse_x509_certificate;
51
52use crate::chain_swap::ChainSwapHandler;
53use crate::ensure_sdk;
54use crate::error::SdkError;
55use crate::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription};
56use crate::model::PaymentState::*;
57use crate::model::Signer;
58use crate::payjoin::{side_swap::SideSwapPayjoinService, PayjoinService};
59use crate::plugin::{Plugin, PluginSdk, PluginStorage};
60use crate::receive_swap::ReceiveSwapHandler;
61use crate::send_swap::SendSwapHandler;
62use crate::swapper::SubscriptionHandler;
63use crate::swapper::{
64    boltz::BoltzSwapper, Swapper, SwapperStatusStream, SwapperSubscriptionHandler,
65};
66use crate::utils::bolt12::encode_invoice;
67use crate::utils::run_with_shutdown;
68use crate::wallet::{LiquidOnchainWallet, OnchainWallet};
69use crate::{
70    error::{PaymentError, SdkResult},
71    event::EventManager,
72    model::*,
73    persist::Persister,
74    utils, *,
75};
76use sdk_common::lightning_with_bolt12::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice};
77
78use self::sync::client::BreezSyncerClient;
79use self::sync::SyncService;
80
81pub const DEFAULT_DATA_DIR: &str = ".data";
82/// Number of blocks to monitor a swap after its timeout block height (~14 days)
83pub const CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS: u32 = 6 * 24 * 14; // ~blocks/hour * hours/day * n_days
84pub const CHAIN_SWAP_MONITORING_PERIOD_LIQUID_BLOCKS: u32 = 60 * 24 * 14; // ~blocks/hour * hours/day * n_days
85
86/// A list of external input parsers that are used by default.
87/// To opt-out, set `use_default_external_input_parsers` in [Config] to false.
88pub const DEFAULT_EXTERNAL_INPUT_PARSERS: &[(&str, &str, &str)] = &[
89    (
90        "picknpay",
91        "(.*)(za.co.electrum.picknpay)(.*)",
92        "https://cryptoqr.net/.well-known/lnurlp/<input>",
93    ),
94    (
95        "bootleggers",
96        r"(.*)(wigroup\.co|yoyogroup\.co)(.*)",
97        "https://cryptoqr.net/.well-known/lnurlw/<input>",
98    ),
99];
100
101pub(crate) const NETWORK_PROPAGATION_GRACE_PERIOD: Duration = Duration::from_secs(120);
102
103pub struct LiquidSdkBuilder {
104    config: Config,
105    signer: Arc<Box<dyn Signer>>,
106    breez_server: Arc<BreezServer>,
107    bitcoin_chain_service: Option<Arc<dyn BitcoinChainService>>,
108    liquid_chain_service: Option<Arc<dyn LiquidChainService>>,
109    onchain_wallet: Option<Arc<dyn OnchainWallet>>,
110    payjoin_service: Option<Arc<dyn PayjoinService>>,
111    persister: Option<std::sync::Arc<Persister>>,
112    recoverer: Option<Arc<Recoverer>>,
113    rest_client: Option<Arc<dyn RestClient>>,
114    status_stream: Option<Arc<dyn SwapperStatusStream>>,
115    swapper: Option<Arc<dyn Swapper>>,
116    sync_service: Option<Arc<SyncService>>,
117    plugins: Option<HashMap<String, Arc<dyn Plugin>>>,
118}
119
120#[allow(dead_code)]
121impl LiquidSdkBuilder {
122    pub fn new(
123        config: Config,
124        server_url: String,
125        signer: Arc<Box<dyn Signer>>,
126    ) -> Result<LiquidSdkBuilder> {
127        let breez_server = Arc::new(BreezServer::new(server_url, None)?);
128        Ok(LiquidSdkBuilder {
129            config,
130            signer,
131            breez_server,
132            bitcoin_chain_service: None,
133            liquid_chain_service: None,
134            onchain_wallet: None,
135            payjoin_service: None,
136            persister: None,
137            recoverer: None,
138            rest_client: None,
139            status_stream: None,
140            swapper: None,
141            sync_service: None,
142            plugins: None,
143        })
144    }
145
146    pub fn bitcoin_chain_service(
147        &mut self,
148        bitcoin_chain_service: Arc<dyn BitcoinChainService>,
149    ) -> &mut Self {
150        self.bitcoin_chain_service = Some(bitcoin_chain_service.clone());
151        self
152    }
153
154    pub fn liquid_chain_service(
155        &mut self,
156        liquid_chain_service: Arc<dyn LiquidChainService>,
157    ) -> &mut Self {
158        self.liquid_chain_service = Some(liquid_chain_service.clone());
159        self
160    }
161
162    pub fn recoverer(&mut self, recoverer: Arc<Recoverer>) -> &mut Self {
163        self.recoverer = Some(recoverer.clone());
164        self
165    }
166
167    pub fn onchain_wallet(&mut self, onchain_wallet: Arc<dyn OnchainWallet>) -> &mut Self {
168        self.onchain_wallet = Some(onchain_wallet.clone());
169        self
170    }
171
172    pub fn payjoin_service(&mut self, payjoin_service: Arc<dyn PayjoinService>) -> &mut Self {
173        self.payjoin_service = Some(payjoin_service.clone());
174        self
175    }
176
177    pub fn persister(&mut self, persister: std::sync::Arc<Persister>) -> &mut Self {
178        self.persister = Some(persister.clone());
179        self
180    }
181
182    pub fn rest_client(&mut self, rest_client: Arc<dyn RestClient>) -> &mut Self {
183        self.rest_client = Some(rest_client.clone());
184        self
185    }
186
187    pub fn status_stream(&mut self, status_stream: Arc<dyn SwapperStatusStream>) -> &mut Self {
188        self.status_stream = Some(status_stream.clone());
189        self
190    }
191
192    pub fn swapper(&mut self, swapper: Arc<dyn Swapper>) -> &mut Self {
193        self.swapper = Some(swapper.clone());
194        self
195    }
196
197    pub fn sync_service(&mut self, sync_service: Arc<SyncService>) -> &mut Self {
198        self.sync_service = Some(sync_service.clone());
199        self
200    }
201
202    pub fn use_plugin(&mut self, plugin: Arc<dyn Plugin>) -> &mut Self {
203        let plugins = self.plugins.get_or_insert(HashMap::new());
204        plugins.insert(plugin.id(), plugin);
205        self
206    }
207
208    fn get_working_dir(&self) -> Result<String> {
209        let fingerprint_hex: String =
210            Xpub::decode(self.signer.xpub()?.as_slice())?.identifier()[0..4].to_hex();
211        self.config
212            .get_wallet_dir(&self.config.working_dir, &fingerprint_hex)
213    }
214
215    pub async fn build(self) -> Result<Arc<LiquidSdk>> {
216        if let Some(breez_api_key) = &self.config.breez_api_key {
217            LiquidSdk::validate_breez_api_key(breez_api_key)?
218        }
219
220        let persister = match self.persister.clone() {
221            Some(persister) => persister,
222            None => {
223                #[cfg(all(target_family = "wasm", target_os = "unknown"))]
224                return Err(anyhow!(
225                    "Must provide a Wasm-compatible persister on Wasm builds"
226                ));
227                #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
228                std::sync::Arc::new(Persister::new_using_fs(
229                    &self.get_working_dir()?,
230                    self.config.network,
231                    self.config.sync_enabled(),
232                    self.config.asset_metadata.clone(),
233                )?)
234            }
235        };
236
237        let rest_client: Arc<dyn RestClient> = match self.rest_client.clone() {
238            Some(rest_client) => rest_client,
239            None => Arc::new(ReqwestRestClient::new()?),
240        };
241
242        let bitcoin_chain_service: Arc<dyn BitcoinChainService> =
243            match self.bitcoin_chain_service.clone() {
244                Some(bitcoin_chain_service) => bitcoin_chain_service,
245                None => self.config.bitcoin_chain_service(),
246            };
247
248        let liquid_chain_service: Arc<dyn LiquidChainService> =
249            match self.liquid_chain_service.clone() {
250                Some(liquid_chain_service) => liquid_chain_service,
251                None => self.config.liquid_chain_service()?,
252            };
253
254        let onchain_wallet: Arc<dyn OnchainWallet> = match self.onchain_wallet.clone() {
255            Some(onchain_wallet) => onchain_wallet,
256            None => Arc::new(
257                LiquidOnchainWallet::new(
258                    self.config.clone(),
259                    persister.clone(),
260                    self.signer.clone(),
261                )
262                .await?,
263            ),
264        };
265
266        let event_manager = Arc::new(EventManager::new());
267        let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(());
268
269        let (swapper, status_stream): (Arc<dyn Swapper>, Arc<dyn SwapperStatusStream>) =
270            match (self.swapper.clone(), self.status_stream.clone()) {
271                (Some(swapper), Some(status_stream)) => (swapper, status_stream),
272                (maybe_swapper, maybe_status_stream) => {
273                    let proxy_url_fetcher = Arc::new(BoltzProxyFetcher::new(persister.clone()));
274                    let boltz_swapper =
275                        Arc::new(BoltzSwapper::new(self.config.clone(), proxy_url_fetcher)?);
276                    (
277                        maybe_swapper.unwrap_or(boltz_swapper.clone()),
278                        maybe_status_stream.unwrap_or(boltz_swapper),
279                    )
280                }
281            };
282
283        let recoverer = match self.recoverer.clone() {
284            Some(recoverer) => recoverer,
285            None => Arc::new(Recoverer::new(
286                self.signer.slip77_master_blinding_key()?,
287                utils::lbtc_asset_id(self.config.network),
288                swapper.clone(),
289                onchain_wallet.clone(),
290                liquid_chain_service.clone(),
291                bitcoin_chain_service.clone(),
292                persister.clone(),
293            )?),
294        };
295
296        let sync_service = match self.sync_service.clone() {
297            Some(sync_service) => Some(sync_service),
298            None => match self.config.sync_service_url.clone() {
299                Some(sync_service_url) => {
300                    if BREEZ_SYNC_SERVICE_URL == sync_service_url
301                        && self.config.breez_api_key.is_none()
302                    {
303                        anyhow::bail!(
304                            "Cannot start the Breez real-time sync service without providing an API key. See https://sdk-doc-liquid.breez.technology/guide/getting_started.html#api-key",
305                        );
306                    }
307
308                    let syncer_client =
309                        Box::new(BreezSyncerClient::new(self.config.breez_api_key.clone()));
310                    Some(Arc::new(SyncService::new(
311                        sync_service_url,
312                        persister.clone(),
313                        recoverer.clone(),
314                        self.signer.clone(),
315                        syncer_client,
316                    )))
317                }
318                None => None,
319            },
320        };
321
322        let send_swap_handler = SendSwapHandler::new(
323            self.config.clone(),
324            onchain_wallet.clone(),
325            persister.clone(),
326            swapper.clone(),
327            liquid_chain_service.clone(),
328            recoverer.clone(),
329        );
330
331        let receive_swap_handler = ReceiveSwapHandler::new(
332            self.config.clone(),
333            onchain_wallet.clone(),
334            persister.clone(),
335            swapper.clone(),
336            liquid_chain_service.clone(),
337        );
338
339        let chain_swap_handler = Arc::new(ChainSwapHandler::new(
340            self.config.clone(),
341            onchain_wallet.clone(),
342            persister.clone(),
343            swapper.clone(),
344            liquid_chain_service.clone(),
345            bitcoin_chain_service.clone(),
346        )?);
347
348        let payjoin_service = match self.payjoin_service.clone() {
349            Some(payjoin_service) => payjoin_service,
350            None => Arc::new(SideSwapPayjoinService::new(
351                self.config.clone(),
352                self.breez_server.clone(),
353                persister.clone(),
354                onchain_wallet.clone(),
355                rest_client.clone(),
356            )),
357        };
358
359        let buy_bitcoin_service = Arc::new(BuyBitcoinService::new(
360            self.config.clone(),
361            self.breez_server.clone(),
362        ));
363
364        let external_input_parsers = self.config.get_all_external_input_parsers();
365
366        let sdk = Arc::new(LiquidSdk {
367            config: self.config.clone(),
368            onchain_wallet,
369            signer: self.signer.clone(),
370            persister: persister.clone(),
371            rest_client,
372            event_manager,
373            status_stream: status_stream.clone(),
374            swapper,
375            recoverer,
376            bitcoin_chain_service,
377            liquid_chain_service,
378            fiat_api: self.breez_server.clone(),
379            is_started: RwLock::new(false),
380            shutdown_sender,
381            shutdown_receiver,
382            send_swap_handler,
383            receive_swap_handler,
384            sync_service,
385            chain_swap_handler,
386            payjoin_service,
387            buy_bitcoin_service,
388            external_input_parsers,
389            background_task_handles: Mutex::new(vec![]),
390            plugins: Mutex::new(self.plugins.unwrap_or_default()),
391        });
392        Ok(sdk)
393    }
394}
395
396pub struct LiquidSdk {
397    pub(crate) config: Config,
398    pub(crate) onchain_wallet: Arc<dyn OnchainWallet>,
399    pub(crate) signer: Arc<Box<dyn Signer>>,
400    pub(crate) persister: std::sync::Arc<Persister>,
401    pub(crate) rest_client: Arc<dyn RestClient>,
402    pub(crate) event_manager: Arc<EventManager>,
403    pub(crate) status_stream: Arc<dyn SwapperStatusStream>,
404    pub(crate) swapper: Arc<dyn Swapper>,
405    pub(crate) recoverer: Arc<Recoverer>,
406    pub(crate) liquid_chain_service: Arc<dyn LiquidChainService>,
407    pub(crate) bitcoin_chain_service: Arc<dyn BitcoinChainService>,
408    pub(crate) fiat_api: Arc<dyn FiatAPI>,
409    pub(crate) is_started: RwLock<bool>,
410    pub(crate) shutdown_sender: watch::Sender<()>,
411    pub(crate) shutdown_receiver: watch::Receiver<()>,
412    pub(crate) send_swap_handler: SendSwapHandler,
413    pub(crate) sync_service: Option<Arc<SyncService>>,
414    pub(crate) receive_swap_handler: ReceiveSwapHandler,
415    pub(crate) chain_swap_handler: Arc<ChainSwapHandler>,
416    pub(crate) payjoin_service: Arc<dyn PayjoinService>,
417    pub(crate) buy_bitcoin_service: Arc<dyn BuyBitcoinApi>,
418    pub(crate) external_input_parsers: Vec<ExternalInputParser>,
419    pub(crate) background_task_handles: Mutex<Vec<TaskHandle>>,
420    pub(crate) plugins: Mutex<HashMap<String, Arc<dyn Plugin>>>,
421}
422
423impl LiquidSdk {
424    /// Initializes the SDK services and starts the background tasks.
425    /// This must be called to create the [LiquidSdk] instance.
426    ///
427    /// # Arguments
428    ///
429    /// * `req` - the [ConnectRequest] containing:
430    ///     * `config` - the SDK [Config]
431    ///     * `mnemonic` - the optional Liquid wallet mnemonic
432    ///     * `passphrase` - the optional passphrase for the mnemonic
433    ///     * `seed` - the optional Liquid wallet seed
434    /// * `plugins` - the [Plugin]s which should be loaded by the SDK at startup
435    pub async fn connect(req: ConnectRequest) -> Result<Arc<LiquidSdk>> {
436        let signer = Self::default_signer(&req)?;
437
438        Self::connect_with_signer(
439            ConnectWithSignerRequest { config: req.config },
440            Box::new(signer),
441        )
442        .inspect_err(|e| error!("Failed to connect: {e:?}"))
443        .await
444    }
445
446    pub fn default_signer(req: &ConnectRequest) -> Result<SdkSigner> {
447        let is_mainnet = req.config.network == LiquidNetwork::Mainnet;
448        match (&req.mnemonic, &req.seed) {
449            (None, Some(seed)) => Ok(SdkSigner::new_with_seed(seed.clone(), is_mainnet)?),
450            (Some(mnemonic), None) => Ok(SdkSigner::new(
451                mnemonic,
452                req.passphrase.as_ref().unwrap_or(&"".to_string()).as_ref(),
453                is_mainnet,
454            )?),
455            _ => Err(anyhow!("Either `mnemonic` or `seed` must be set")),
456        }
457    }
458
459    pub async fn connect_with_signer(
460        req: ConnectWithSignerRequest,
461        signer: Box<dyn Signer>,
462    ) -> Result<Arc<LiquidSdk>> {
463        let start_ts = Instant::now();
464
465        #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
466        std::fs::create_dir_all(&req.config.working_dir)?;
467
468        let sdk = LiquidSdkBuilder::new(
469            req.config,
470            PRODUCTION_BREEZSERVER_URL.into(),
471            Arc::new(signer),
472        )?
473        .build()
474        .await?;
475        sdk.start().await?;
476
477        let init_time = Instant::now().duration_since(start_ts);
478        utils::log_print_header(init_time);
479
480        Ok(sdk)
481    }
482
483    fn validate_breez_api_key(api_key: &str) -> Result<()> {
484        let api_key_decoded = lwk_wollet::bitcoin::base64::engine::general_purpose::STANDARD
485            .decode(api_key.as_bytes())
486            .map_err(|err| anyhow!("Could not base64 decode the Breez API key: {err:?}"))?;
487        let (_rem, cert) = parse_x509_certificate(&api_key_decoded)
488            .map_err(|err| anyhow!("Invaid certificate for Breez API key: {err:?}"))?;
489
490        let issuer = cert
491            .issuer()
492            .iter_common_name()
493            .next()
494            .and_then(|cn| cn.as_str().ok());
495        match issuer {
496            Some(common_name) => ensure_sdk!(
497                common_name.starts_with("Breez"),
498                anyhow!("Invalid certificate found for Breez API key: issuer mismatch. Please confirm that the certificate's origin is trusted")
499            ),
500            _ => {
501                return Err(anyhow!("Could not parse Breez API key certificate: issuer is invalid or not found."))
502            }
503        }
504
505        Ok(())
506    }
507
508    /// Starts an SDK instance.
509    ///
510    /// Should only be called once per instance.
511    pub async fn start(self: &Arc<LiquidSdk>) -> SdkResult<()> {
512        let mut is_started = self.is_started.write().await;
513        self.persister
514            .update_send_swaps_by_state(Created, TimedOut, Some(true))
515            .inspect_err(|e| error!("Failed to update send swaps by state: {e:?}"))?;
516
517        self.start_background_tasks()
518            .inspect_err(|e| error!("Failed to start background tasks: {e:?}"))
519            .await?;
520        self.start_plugins().await?;
521        *is_started = true;
522        Ok(())
523    }
524
525    async fn start_plugins(self: &Arc<LiquidSdk>) -> SdkResult<()> {
526        for (_, plugin) in self.plugins.lock().await.iter() {
527            self.start_plugin_inner(plugin).await?;
528        }
529        Ok(())
530    }
531
532    /// Starts background tasks.
533    ///
534    /// Internal method. Should only be used as part of [LiquidSdk::start].
535    async fn start_background_tasks(self: &Arc<LiquidSdk>) -> SdkResult<()> {
536        let mut handles = self.background_task_handles.lock().await;
537        let subscription_handler = Box::new(SwapperSubscriptionHandler::new(
538            self.persister.clone(),
539            self.status_stream.clone(),
540        ));
541        self.status_stream
542            .clone()
543            .start(subscription_handler.clone(), self.shutdown_receiver.clone());
544        if let Some(sync_service) = self.sync_service.clone() {
545            handles.push(TaskHandle {
546                name: "sync-reconnect".to_string(),
547                handle: sync_service.start(self.shutdown_receiver.clone()),
548            });
549        }
550        handles.push(TaskHandle {
551            name: "track-new-blocks".to_string(),
552            handle: self.start_track_new_blocks_task(),
553        });
554        handles.push(TaskHandle {
555            name: "track-swap-updates".to_string(),
556            handle: self.track_swap_updates(),
557        });
558        if let Some(handle) = self.track_realtime_sync_events(subscription_handler) {
559            handles.push(TaskHandle {
560                name: "track-realtime-sync-events".to_string(),
561                handle,
562            });
563        }
564        Ok(())
565    }
566
567    async fn ensure_is_started(&self) -> SdkResult<()> {
568        let is_started = self.is_started.read().await;
569        ensure_sdk!(*is_started, SdkError::NotStarted);
570        Ok(())
571    }
572
573    /// Disconnects the [LiquidSdk] instance and stops the background tasks.
574    pub async fn disconnect(&self) -> SdkResult<()> {
575        self.ensure_is_started().await?;
576
577        let mut is_started = self.is_started.write().await;
578        let mut handles: Vec<_> = self
579            .background_task_handles
580            .lock()
581            .await
582            .drain(..)
583            .collect();
584
585        // Send graceful shutdown signal
586        if self.shutdown_sender.send(()).is_ok() {
587            info!("Sent shutdown signal to background tasks - waiting for tasks to shutdown gracefully");
588
589            let graceful_shutdown_result = tokio::time::timeout(
590                Duration::from_secs(5),
591                futures::future::try_join_all(handles.iter_mut().map(|h| &mut h.handle)),
592            )
593            .await;
594
595            match graceful_shutdown_result {
596                Ok(_) => info!("All background tasks completed gracefully"),
597                Err(_) => {
598                    warn!("Some background tasks did not complete within timeout - aborting remaining tasks");
599                }
600            }
601        } else {
602            warn!("Failed to send shutdown signal - aborting tasks");
603        }
604
605        for handle in handles {
606            if !handle.handle.is_finished() {
607                info!("Aborting task: {:?}", handle.name);
608                handle.handle.abort();
609            }
610        }
611        for (_, plugin) in self.plugins.lock().await.iter() {
612            plugin.on_stop().await;
613        }
614
615        #[cfg(all(target_family = "wasm", target_os = "unknown"))]
616        // Clear the database if we're on WASM
617        self.persister.clear_in_memory_db()?;
618
619        *is_started = false;
620        Ok(())
621    }
622
623    fn track_realtime_sync_events(
624        self: &Arc<LiquidSdk>,
625        subscription_handler: Box<dyn SubscriptionHandler>,
626    ) -> Option<tokio::task::JoinHandle<()>> {
627        let cloned = self.clone();
628        let sync_service = cloned.sync_service.clone()?;
629        let track_realtime_sync_events_future = async move {
630            let mut sync_events_receiver = sync_service.subscribe_events();
631            loop {
632                if let Ok(event) = sync_events_receiver.recv().await {
633                    match event {
634                        sync::Event::SyncedCompleted { data } => {
635                            info!(
636                                "Received sync event: pulled {} records, pushed {} records",
637                                data.pulled_records_count, data.pushed_records_count
638                            );
639                            let did_pull_new_records = data.pulled_records_count > 0;
640                            if did_pull_new_records {
641                                subscription_handler.track_subscriptions().await;
642                            }
643                            cloned
644                                .notify_event_listeners(SdkEvent::DataSynced {
645                                    did_pull_new_records,
646                                })
647                                .await
648                        }
649                    }
650                }
651            }
652        };
653
654        let shutdown_receiver = self.shutdown_receiver.clone();
655        info!("Starting track-realtime-sync-events task");
656        Some(tokio::spawn(async move {
657            run_with_shutdown(
658                shutdown_receiver,
659                "Received shutdown signal, exiting real-time sync loop",
660                track_realtime_sync_events_future,
661            )
662            .await
663        }))
664    }
665
666    async fn track_new_blocks(
667        self: &Arc<LiquidSdk>,
668        current_liquid_block: &mut u32,
669        current_bitcoin_block: &mut u32,
670    ) {
671        info!("Track new blocks iteration started");
672
673        let Ok(sync_context) = self
674            .get_sync_context(GetSyncContextRequest {
675                partial_sync: None,
676                last_liquid_tip: *current_liquid_block,
677                last_bitcoin_tip: *current_bitcoin_block,
678            })
679            .await
680        else {
681            error!("Failed to get sync context");
682            return;
683        };
684
685        *current_liquid_block = sync_context
686            .maybe_liquid_tip
687            .unwrap_or(*current_liquid_block);
688        *current_bitcoin_block = sync_context
689            .maybe_bitcoin_tip
690            .unwrap_or(*current_bitcoin_block);
691
692        if let Some(liquid_tip) = sync_context.maybe_liquid_tip {
693            self.persister
694                .update_blockchain_info(liquid_tip, sync_context.maybe_bitcoin_tip)
695                .unwrap_or_else(|err| warn!("Could not update local tips: {err:?}"));
696
697            if let Err(e) = self
698                .sync_inner(
699                    sync_context.recoverable_swaps,
700                    ChainTips {
701                        liquid_tip,
702                        bitcoin_tip: sync_context.maybe_bitcoin_tip,
703                    },
704                )
705                .await
706            {
707                error!("Failed to sync while tracking new blocks: {e}");
708                self.event_manager
709                    .notify(SdkEvent::SyncFailed {
710                        error: e.to_string(),
711                    })
712                    .await;
713            }
714        }
715
716        // Update swap handlers
717        if sync_context.is_new_liquid_block {
718            self.chain_swap_handler
719                .on_liquid_block(*current_liquid_block)
720                .await;
721            self.receive_swap_handler
722                .on_liquid_block(*current_liquid_block)
723                .await;
724            self.send_swap_handler
725                .on_liquid_block(*current_liquid_block)
726                .await;
727        }
728        if sync_context.is_new_bitcoin_block {
729            self.chain_swap_handler
730                .on_bitcoin_block(*current_bitcoin_block)
731                .await;
732            self.receive_swap_handler
733                .on_bitcoin_block(*current_liquid_block)
734                .await;
735            self.send_swap_handler
736                .on_bitcoin_block(*current_bitcoin_block)
737                .await;
738        }
739    }
740
741    fn start_track_new_blocks_task(self: &Arc<LiquidSdk>) -> tokio::task::JoinHandle<()> {
742        let cloned = self.clone();
743
744        let track_new_blocks_future = async move {
745            let last_blockchain_info = cloned
746                .get_info()
747                .await
748                .map(|i| i.blockchain_info)
749                .unwrap_or_default();
750
751            let mut current_liquid_block: u32 = last_blockchain_info.liquid_tip;
752            let mut current_bitcoin_block: u32 = last_blockchain_info.bitcoin_tip;
753            cloned
754                .track_new_blocks(&mut current_liquid_block, &mut current_bitcoin_block)
755                .await;
756            loop {
757                tokio::time::sleep(Duration::from_secs(
758                    cloned.config.onchain_sync_period_sec as u64,
759                ))
760                .await;
761                cloned
762                    .track_new_blocks(&mut current_liquid_block, &mut current_bitcoin_block)
763                    .await;
764            }
765        };
766
767        let shutdown_receiver = self.shutdown_receiver.clone();
768        info!("Starting track-new-blocks task");
769        tokio::spawn(async move {
770            run_with_shutdown(
771                shutdown_receiver,
772                "Received shutdown signal, exiting track blocks loop",
773                track_new_blocks_future,
774            )
775            .await
776        })
777    }
778
779    fn track_swap_updates(self: &Arc<LiquidSdk>) -> tokio::task::JoinHandle<()> {
780        let cloned = self.clone();
781        let track_swap_updates_future = async move {
782            let mut updates_stream = cloned.status_stream.subscribe_swap_updates();
783            let mut invoice_request_stream = cloned.status_stream.subscribe_invoice_requests();
784            let swaps_streams = vec![
785                cloned.send_swap_handler.subscribe_payment_updates(),
786                cloned.receive_swap_handler.subscribe_payment_updates(),
787                cloned.chain_swap_handler.subscribe_payment_updates(),
788            ];
789            let mut combined_swap_streams =
790                select_all(swaps_streams.into_iter().map(BroadcastStream::new));
791            loop {
792                tokio::select! {
793                    payment_id = combined_swap_streams.next() => {
794                      if let Some(payment_id) = payment_id {
795                        match payment_id {
796                            Ok(payment_id) => {
797                              if let Err(e) = cloned.emit_payment_updated(Some(payment_id)).await {
798                                error!("Failed to emit payment update: {e:?}");
799                              }
800                            }
801                            Err(e) => error!("Failed to receive swap state change: {e:?}")
802                        }
803                      }
804                    }
805                    update = updates_stream.recv() => match update {
806                        Ok(update) => {
807                            let id = &update.id;
808                            match cloned.persister.fetch_swap_by_id(id) {
809                                Ok(Swap::Send(_)) => match cloned.send_swap_handler.on_new_status(&update).await {
810                                    Ok(_) => info!("Successfully handled Send Swap {id} update"),
811                                    Err(e) => error!("Failed to handle Send Swap {id} update: {e}")
812                                },
813                                Ok(Swap::Receive(_)) => match cloned.receive_swap_handler.on_new_status(&update).await {
814                                    Ok(_) => info!("Successfully handled Receive Swap {id} update"),
815                                    Err(e) => error!("Failed to handle Receive Swap {id} update: {e}")
816                                },
817                                Ok(Swap::Chain(_)) => match cloned.chain_swap_handler.on_new_status(&update).await {
818                                    Ok(_) => info!("Successfully handled Chain Swap {id} update"),
819                                    Err(e) => error!("Failed to handle Chain Swap {id} update: {e}")
820                                },
821                                _ => {
822                                    error!("Could not find Swap {id}");
823                                }
824                            }
825                        }
826                        Err(e) => error!("Received update stream error: {e:?}"),
827                    },
828                    invoice_request_res = invoice_request_stream.recv() => match invoice_request_res {
829                        Ok(boltz_client::boltz::InvoiceRequest{id, offer, invoice_request}) => {
830                            match cloned.create_bolt12_invoice(&CreateBolt12InvoiceRequest { offer, invoice_request }).await {
831                                Ok(response) => {
832                                    match cloned.status_stream.send_invoice_created(&id, &response.invoice) {
833                                        Ok(_) => info!("Successfully handled invoice request {id}"),
834                                        Err(e) => error!("Failed to handle invoice request {id}: {e}")
835                                    }
836                                },
837                                Err(e) => {
838                                    let error = match e {
839                                        PaymentError::AmountOutOfRange { .. } => e.to_string(),
840                                        PaymentError::AmountMissing { .. } => "Amount missing in invoice request".to_string(),
841                                        _ => "Failed to create invoice".to_string(),
842                                    };
843                                    match cloned.status_stream.send_invoice_error(&id, &error) {
844                                        Ok(_) => info!("Failed to create invoice from request {id}: {e:?}"),
845                                        Err(_) => error!("Failed to create invoice from request {id} and return error: {error}"),
846                                    }
847                                },
848                            };
849                        },
850                        Err(e) => error!("Received invoice request stream error: {e:?}"),
851                    },
852                }
853            }
854        };
855
856        let shutdown_receiver = self.shutdown_receiver.clone();
857        info!("Starting track-swap-updates task");
858        tokio::spawn(async move {
859            run_with_shutdown(
860                shutdown_receiver,
861                "Received shutdown signal, exiting track swap updates loop",
862                track_swap_updates_future,
863            )
864            .await
865        })
866    }
867
868    async fn notify_event_listeners(&self, e: SdkEvent) {
869        self.event_manager.notify(e).await;
870    }
871
872    /// Adds an event listener to the [LiquidSdk] instance, where all [SdkEvent]'s will be emitted to.
873    /// The event listener can be removed be calling [LiquidSdk::remove_event_listener].
874    ///
875    /// # Arguments
876    ///
877    /// * `listener` - The listener which is an implementation of the [EventListener] trait
878    pub async fn add_event_listener(&self, listener: Box<dyn EventListener>) -> SdkResult<String> {
879        Ok(self.event_manager.add(listener).await?)
880    }
881
882    /// Removes an event listener from the [LiquidSdk] instance.
883    ///
884    /// # Arguments
885    ///
886    /// * `id` - the event listener id returned by [LiquidSdk::add_event_listener]
887    pub async fn remove_event_listener(&self, id: String) -> SdkResult<()> {
888        self.event_manager.remove(id).await;
889        Ok(())
890    }
891
892    async fn emit_payment_updated(&self, payment_id: Option<String>) -> Result<()> {
893        if let Some(id) = payment_id {
894            match self.persister.get_payment(&id)? {
895                Some(payment) => {
896                    self.update_wallet_info().await?;
897                    match payment.status {
898                        Complete => {
899                            self.notify_event_listeners(SdkEvent::PaymentSucceeded {
900                                details: payment,
901                            })
902                            .await
903                        }
904                        Pending => {
905                            match &payment.details.get_swap_id() {
906                                Some(swap_id) => match self.persister.fetch_swap_by_id(swap_id)? {
907                                    Swap::Chain(ChainSwap { claim_tx_id, .. }) => {
908                                        if claim_tx_id.is_some() {
909                                            // The claim tx has now been broadcast
910                                            self.notify_event_listeners(
911                                                SdkEvent::PaymentWaitingConfirmation {
912                                                    details: payment,
913                                                },
914                                            )
915                                            .await
916                                        } else {
917                                            // The lockup tx is in the mempool/confirmed
918                                            self.notify_event_listeners(SdkEvent::PaymentPending {
919                                                details: payment,
920                                            })
921                                            .await
922                                        }
923                                    }
924                                    Swap::Receive(ReceiveSwap {
925                                        claim_tx_id,
926                                        mrh_tx_id,
927                                        ..
928                                    }) => {
929                                        if claim_tx_id.is_some() || mrh_tx_id.is_some() {
930                                            // The a claim or mrh tx has now been broadcast
931                                            self.notify_event_listeners(
932                                                SdkEvent::PaymentWaitingConfirmation {
933                                                    details: payment,
934                                                },
935                                            )
936                                            .await
937                                        } else {
938                                            // The lockup tx is in the mempool/confirmed
939                                            self.notify_event_listeners(SdkEvent::PaymentPending {
940                                                details: payment,
941                                            })
942                                            .await
943                                        }
944                                    }
945                                    Swap::Send(_) => {
946                                        // The lockup tx is in the mempool/confirmed
947                                        self.notify_event_listeners(SdkEvent::PaymentPending {
948                                            details: payment,
949                                        })
950                                        .await
951                                    }
952                                },
953                                // Here we probably have a liquid address payment so we emit PaymentWaitingConfirmation
954                                None => {
955                                    self.notify_event_listeners(
956                                        SdkEvent::PaymentWaitingConfirmation { details: payment },
957                                    )
958                                    .await
959                                }
960                            };
961                        }
962                        WaitingFeeAcceptance => {
963                            let swap_id = &payment
964                                .details
965                                .get_swap_id()
966                                .ok_or(anyhow!("Payment WaitingFeeAcceptance must have a swap"))?;
967
968                            ensure!(
969                                matches!(
970                                    self.persister.fetch_swap_by_id(swap_id)?,
971                                    Swap::Chain(ChainSwap { .. })
972                                ),
973                                "Swap in WaitingFeeAcceptance payment must be chain swap"
974                            );
975
976                            self.notify_event_listeners(SdkEvent::PaymentWaitingFeeAcceptance {
977                                details: payment,
978                            })
979                            .await;
980                        }
981                        Refundable => {
982                            self.notify_event_listeners(SdkEvent::PaymentRefundable {
983                                details: payment,
984                            })
985                            .await
986                        }
987                        RefundPending => {
988                            // The swap state has changed to RefundPending
989                            self.notify_event_listeners(SdkEvent::PaymentRefundPending {
990                                details: payment,
991                            })
992                            .await
993                        }
994                        Failed => match payment.payment_type {
995                            PaymentType::Receive => {
996                                self.notify_event_listeners(SdkEvent::PaymentFailed {
997                                    details: payment,
998                                })
999                                .await
1000                            }
1001                            PaymentType::Send => {
1002                                // The refund tx is confirmed
1003                                self.notify_event_listeners(SdkEvent::PaymentRefunded {
1004                                    details: payment,
1005                                })
1006                                .await
1007                            }
1008                        },
1009                        _ => (),
1010                    };
1011                }
1012                None => debug!("Payment not found: {id}"),
1013            }
1014        }
1015        Ok(())
1016    }
1017
1018    /// Get the wallet and blockchain info from local storage
1019    pub async fn get_info(&self) -> SdkResult<GetInfoResponse> {
1020        self.ensure_is_started().await?;
1021        let maybe_info = self.persister.get_info()?;
1022        match maybe_info {
1023            Some(info) => Ok(info),
1024            None => {
1025                self.update_wallet_info().await?;
1026                self.persister.get_info()?.ok_or(SdkError::Generic {
1027                    err: "Info not found".into(),
1028                })
1029            }
1030        }
1031    }
1032
1033    /// Sign given message with the private key. Returns a zbase encoded signature.
1034    pub fn sign_message(&self, req: &SignMessageRequest) -> SdkResult<SignMessageResponse> {
1035        let signature = self.onchain_wallet.sign_message(&req.message)?;
1036        Ok(SignMessageResponse { signature })
1037    }
1038
1039    /// Check whether given message was signed by the given
1040    /// pubkey and the signature (zbase encoded) is valid.
1041    pub fn check_message(&self, req: &CheckMessageRequest) -> SdkResult<CheckMessageResponse> {
1042        let is_valid =
1043            self.onchain_wallet
1044                .check_message(&req.message, &req.pubkey, &req.signature)?;
1045        Ok(CheckMessageResponse { is_valid })
1046    }
1047
1048    async fn validate_bitcoin_address(&self, input: &str) -> Result<String, PaymentError> {
1049        match self.parse(input).await? {
1050            InputType::BitcoinAddress {
1051                address: bitcoin_address_data,
1052                ..
1053            } => match bitcoin_address_data.network == self.config.network.into() {
1054                true => Ok(bitcoin_address_data.address),
1055                false => Err(PaymentError::InvalidNetwork {
1056                    err: format!(
1057                        "Not a {} address",
1058                        Into::<Network>::into(self.config.network)
1059                    ),
1060                }),
1061            },
1062            _ => Err(PaymentError::Generic {
1063                err: "Invalid Bitcoin address".to_string(),
1064            }),
1065        }
1066    }
1067
1068    fn validate_bolt11_invoice(&self, invoice: &str) -> Result<Bolt11Invoice, PaymentError> {
1069        let invoice = invoice
1070            .trim()
1071            .parse::<Bolt11Invoice>()
1072            .map_err(|err| PaymentError::invalid_invoice(err.to_string()))?;
1073
1074        match (invoice.network().to_string().as_str(), self.config.network) {
1075            ("bitcoin", LiquidNetwork::Mainnet) => {}
1076            ("testnet", LiquidNetwork::Testnet) => {}
1077            ("regtest", LiquidNetwork::Regtest) => {}
1078            _ => {
1079                return Err(PaymentError::InvalidNetwork {
1080                    err: "Invoice cannot be paid on the current network".to_string(),
1081                })
1082            }
1083        }
1084
1085        // Verify invoice isn't expired
1086        let invoice_ts_web_time = web_time::SystemTime::UNIX_EPOCH
1087            + invoice
1088                .timestamp()
1089                .duration_since(std::time::SystemTime::UNIX_EPOCH)
1090                .map_err(|_| PaymentError::invalid_invoice("Invalid invoice timestamp"))?;
1091        if let Ok(elapsed_web_time) =
1092            web_time::SystemTime::now().duration_since(invoice_ts_web_time)
1093        {
1094            ensure_sdk!(
1095                elapsed_web_time <= invoice.expiry_time(),
1096                PaymentError::invalid_invoice("Invoice has expired")
1097            )
1098        }
1099
1100        Ok(invoice)
1101    }
1102
1103    fn validate_bolt12_invoice(
1104        &self,
1105        offer: &LNOffer,
1106        user_specified_receiver_amount_sat: u64,
1107        invoice: &str,
1108    ) -> Result<Bolt12Invoice, PaymentError> {
1109        let invoice_parsed = utils::bolt12::decode_invoice(invoice)?;
1110        let invoice_signing_pubkey = invoice_parsed.signing_pubkey().to_hex();
1111
1112        // Check if the invoice is signed by same key as the offer
1113        match &offer.signing_pubkey {
1114            None => {
1115                ensure_sdk!(
1116                    &offer
1117                        .paths
1118                        .iter()
1119                        .filter_map(|path| path.blinded_hops.last())
1120                        .any(|last_hop| &invoice_signing_pubkey == last_hop),
1121                    PaymentError::invalid_invoice(
1122                        "Invalid Bolt12 invoice signing key when using blinded path"
1123                    )
1124                );
1125            }
1126            Some(offer_signing_pubkey) => {
1127                ensure_sdk!(
1128                    offer_signing_pubkey == &invoice_signing_pubkey,
1129                    PaymentError::invalid_invoice("Invalid Bolt12 invoice signing key")
1130                );
1131            }
1132        }
1133
1134        let receiver_amount_sat = invoice_parsed.amount_msats() / 1_000;
1135        ensure_sdk!(
1136            receiver_amount_sat == user_specified_receiver_amount_sat,
1137            PaymentError::invalid_invoice("Invalid Bolt12 invoice amount")
1138        );
1139
1140        Ok(invoice_parsed)
1141    }
1142
1143    /// For submarine swaps (Liquid -> LN), the output amount (invoice amount) is checked if it fits
1144    /// the pair limits. This is unlike all the other swap types, where the input amount is checked.
1145    async fn validate_submarine_pairs(
1146        &self,
1147        receiver_amount_sat: u64,
1148    ) -> Result<SubmarinePair, PaymentError> {
1149        let lbtc_pair = self
1150            .swapper
1151            .get_submarine_pairs()
1152            .await?
1153            .ok_or(PaymentError::PairsNotFound)?;
1154
1155        lbtc_pair.limits.within(receiver_amount_sat)?;
1156
1157        Ok(lbtc_pair)
1158    }
1159
1160    async fn get_chain_pair(&self, direction: Direction) -> Result<ChainPair, PaymentError> {
1161        self.swapper
1162            .get_chain_pair(direction)
1163            .await?
1164            .ok_or(PaymentError::PairsNotFound)
1165    }
1166
1167    /// Validates if the `user_lockup_amount_sat` fits within the limits of this pair
1168    fn validate_user_lockup_amount_for_chain_pair(
1169        &self,
1170        pair: &ChainPair,
1171        user_lockup_amount_sat: u64,
1172    ) -> Result<(), PaymentError> {
1173        pair.limits.within(user_lockup_amount_sat)?;
1174
1175        Ok(())
1176    }
1177
1178    async fn get_and_validate_chain_pair(
1179        &self,
1180        direction: Direction,
1181        user_lockup_amount_sat: Option<u64>,
1182    ) -> Result<ChainPair, PaymentError> {
1183        let pair = self.get_chain_pair(direction).await?;
1184        if let Some(user_lockup_amount_sat) = user_lockup_amount_sat {
1185            self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
1186        }
1187        Ok(pair)
1188    }
1189
1190    /// Estimate the onchain fee for sending the given amount to the given destination address
1191    async fn estimate_onchain_tx_fee(
1192        &self,
1193        amount_sat: u64,
1194        address: &str,
1195        asset_id: &str,
1196    ) -> Result<u64, PaymentError> {
1197        let fee_sat = self
1198            .onchain_wallet
1199            .build_tx(
1200                Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
1201                address,
1202                asset_id,
1203                amount_sat,
1204            )
1205            .await?
1206            .all_fees()
1207            .values()
1208            .sum::<u64>();
1209        info!("Estimated tx fee: {fee_sat} sat");
1210        Ok(fee_sat)
1211    }
1212
1213    fn get_temp_p2tr_addr(&self) -> &str {
1214        // TODO Replace this with own address when LWK supports taproot
1215        //  https://github.com/Blockstream/lwk/issues/31
1216        match self.config.network {
1217            LiquidNetwork::Mainnet => "lq1pqvzxvqhrf54dd4sny4cag7497pe38252qefk46t92frs7us8r80ja9ha8r5me09nn22m4tmdqp5p4wafq3s59cql3v9n45t5trwtxrmxfsyxjnstkctj",
1218            LiquidNetwork::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5",
1219            LiquidNetwork::Regtest => "el1pqtjufhhy2se6lj2t7wufvpqqhnw66v57x2s0uu5dxs4fqlzlvh3hqe87vn83z3qreh8kxn49xe0h0fpe4kjkhl4gv99tdppupk0tdd485q8zegdag97r",
1220        }
1221    }
1222
1223    /// Estimate the lockup tx fee for Send and Chain Send swaps
1224    async fn estimate_lockup_tx_fee(
1225        &self,
1226        user_lockup_amount_sat: u64,
1227    ) -> Result<u64, PaymentError> {
1228        let temp_p2tr_addr = self.get_temp_p2tr_addr();
1229        self.estimate_onchain_tx_fee(
1230            user_lockup_amount_sat,
1231            temp_p2tr_addr,
1232            self.config.lbtc_asset_id().as_str(),
1233        )
1234        .await
1235    }
1236
1237    async fn estimate_drain_tx_fee(
1238        &self,
1239        enforce_amount_sat: Option<u64>,
1240        address: Option<&str>,
1241    ) -> Result<u64, PaymentError> {
1242        let receipent_address = address.unwrap_or(self.get_temp_p2tr_addr());
1243        let fee_sat = self
1244            .onchain_wallet
1245            .build_drain_tx(
1246                Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
1247                receipent_address,
1248                enforce_amount_sat,
1249            )
1250            .await?
1251            .all_fees()
1252            .values()
1253            .sum();
1254        info!("Estimated drain tx fee: {fee_sat} sat");
1255
1256        Ok(fee_sat)
1257    }
1258
1259    async fn estimate_onchain_tx_or_drain_tx_fee(
1260        &self,
1261        amount_sat: u64,
1262        address: &str,
1263        asset_id: &str,
1264    ) -> Result<u64, PaymentError> {
1265        match self
1266            .estimate_onchain_tx_fee(amount_sat, address, asset_id)
1267            .await
1268        {
1269            Ok(fees_sat) => Ok(fees_sat),
1270            Err(PaymentError::InsufficientFunds) if asset_id.eq(&self.config.lbtc_asset_id()) => {
1271                self.estimate_drain_tx_fee(Some(amount_sat), Some(address))
1272                    .await
1273                    .map_err(|_| PaymentError::InsufficientFunds)
1274            }
1275            Err(e) => Err(e),
1276        }
1277    }
1278
1279    async fn estimate_lockup_tx_or_drain_tx_fee(
1280        &self,
1281        amount_sat: u64,
1282    ) -> Result<u64, PaymentError> {
1283        let temp_p2tr_addr = self.get_temp_p2tr_addr();
1284        self.estimate_onchain_tx_or_drain_tx_fee(
1285            amount_sat,
1286            temp_p2tr_addr,
1287            &self.config.lbtc_asset_id(),
1288        )
1289        .await
1290    }
1291
1292    /// Prepares to pay a Lightning invoice via a submarine swap.
1293    ///
1294    /// # Arguments
1295    ///
1296    /// * `req` - the [PrepareSendRequest] containing:
1297    ///     * `destination` - Either a Liquid BIP21 URI/address, a BOLT11 invoice or a BOLT12 offer
1298    ///     * `amount` - The optional amount of type [PayAmount]. Should only be specified
1299    ///       when paying directly onchain or via amount-less BIP21.
1300    ///        - [PayAmount::Drain] which uses all Bitcoin funds
1301    ///        - [PayAmount::Bitcoin] which sets the amount in satoshi that will be received
1302    ///        - [PayAmount::Asset] which sets the amount of an asset that will be received
1303    ///
1304    /// # Returns
1305    /// Returns a [PrepareSendResponse] containing:
1306    ///     * `destination` - the parsed destination, of type [SendDestination]
1307    ///     * `amount` - the optional [PayAmount] to be sent in either Bitcoin or another asset
1308    ///     * `fees_sat` - the optional estimated fee in satoshi. Is set when there is Bitcoin
1309    ///        available to pay fees. When not set, there are asset fees available to pay fees.
1310    ///     * `estimated_asset_fees` - the optional estimated fee in the asset. Is set when
1311    ///        [PayAmount::Asset::estimate_asset_fees] is set to `true`, the Payjoin service accepts
1312    ///        this asset to pay fees and there are funds available in this asset to pay fees.
1313    pub async fn prepare_send_payment(
1314        &self,
1315        req: &PrepareSendRequest,
1316    ) -> Result<PrepareSendResponse, PaymentError> {
1317        self.ensure_is_started().await?;
1318
1319        let use_mrh = match req.disable_mrh {
1320            Some(disable_mrh) => !disable_mrh,
1321            None => self.config.use_magic_routing_hints,
1322        };
1323
1324        let timeout_sec = req
1325            .payment_timeout_sec
1326            .unwrap_or(self.config.payment_timeout_sec);
1327
1328        let get_info_res = self.get_info().await?;
1329        let fees_sat;
1330        let estimated_asset_fees;
1331        let receiver_amount_sat;
1332        let asset_id;
1333        let payment_destination;
1334        let mut validate_funds = true;
1335        let mut exchange_amount_sat = None;
1336
1337        match self.parse(&req.destination).await {
1338            Ok(InputType::LiquidAddress {
1339                address: mut liquid_address_data,
1340            }) => {
1341                let amount = match (
1342                    liquid_address_data.amount,
1343                    liquid_address_data.amount_sat,
1344                    liquid_address_data.asset_id,
1345                    req.amount.clone(),
1346                ) {
1347                    (Some(amount), Some(amount_sat), Some(asset_id), None) => {
1348                        if asset_id.eq(&self.config.lbtc_asset_id()) {
1349                            PayAmount::Bitcoin {
1350                                receiver_amount_sat: amount_sat,
1351                            }
1352                        } else {
1353                            PayAmount::Asset {
1354                                to_asset: asset_id,
1355                                from_asset: None,
1356                                receiver_amount: amount,
1357                                estimate_asset_fees: None,
1358                            }
1359                        }
1360                    }
1361                    (_, Some(amount_sat), None, None) => PayAmount::Bitcoin {
1362                        receiver_amount_sat: amount_sat,
1363                    },
1364                    (_, _, _, Some(amount)) => amount,
1365                    _ => {
1366                        return Err(PaymentError::AmountMissing {
1367                            err: "Amount must be set when paying to a Liquid address".to_string(),
1368                        });
1369                    }
1370                };
1371
1372                ensure_sdk!(
1373                    liquid_address_data.network == self.config.network.into(),
1374                    PaymentError::InvalidNetwork {
1375                        err: format!(
1376                            "Cannot send payment from {} to {}",
1377                            Into::<sdk_common::bitcoin::Network>::into(self.config.network),
1378                            liquid_address_data.network
1379                        )
1380                    }
1381                );
1382
1383                let is_sideswap_payment = amount.is_sideswap_payment();
1384                (
1385                    asset_id,
1386                    receiver_amount_sat,
1387                    fees_sat,
1388                    estimated_asset_fees,
1389                ) = match amount {
1390                    PayAmount::Drain => {
1391                        ensure_sdk!(
1392                            get_info_res.wallet_info.pending_receive_sat == 0
1393                                && get_info_res.wallet_info.pending_send_sat == 0,
1394                            PaymentError::Generic {
1395                                err: "Cannot drain while there are pending payments".to_string(),
1396                            }
1397                        );
1398                        let drain_fees_sat = self
1399                            .estimate_drain_tx_fee(None, Some(&liquid_address_data.address))
1400                            .await?;
1401                        let drain_amount_sat =
1402                            get_info_res.wallet_info.balance_sat - drain_fees_sat;
1403                        info!("Drain amount: {drain_amount_sat} sat");
1404                        (
1405                            self.config.lbtc_asset_id(),
1406                            drain_amount_sat,
1407                            Some(drain_fees_sat),
1408                            None,
1409                        )
1410                    }
1411                    PayAmount::Bitcoin {
1412                        receiver_amount_sat,
1413                    } => {
1414                        let asset_id = self.config.lbtc_asset_id();
1415                        let fees_sat = self
1416                            .estimate_onchain_tx_or_drain_tx_fee(
1417                                receiver_amount_sat,
1418                                &liquid_address_data.address,
1419                                &asset_id,
1420                            )
1421                            .await?;
1422                        (asset_id, receiver_amount_sat, Some(fees_sat), None)
1423                    }
1424                    PayAmount::Asset {
1425                        to_asset,
1426                        from_asset,
1427                        receiver_amount,
1428                        estimate_asset_fees,
1429                    } => {
1430                        let from_asset = from_asset.unwrap_or(to_asset.clone());
1431                        ensure_sdk!(
1432                            self.persister.get_asset_metadata(&from_asset)?.is_some(),
1433                            PaymentError::AssetError {
1434                                err: format!("Asset {from_asset} is not supported"),
1435                            }
1436                        );
1437                        let receiver_asset_metadata = self
1438                            .persister
1439                            .get_asset_metadata(&to_asset)?
1440                            .ok_or(PaymentError::AssetError {
1441                                err: format!("Asset {to_asset} is not supported"),
1442                            })?;
1443                        let receiver_amount_sat =
1444                            receiver_asset_metadata.amount_to_sat(receiver_amount);
1445
1446                        let asset_fees = if estimate_asset_fees.unwrap_or(false) {
1447                            ensure_sdk!(
1448                                !is_sideswap_payment,
1449                                PaymentError::generic("Cannot pay asset fees when executing a payment between two separate assets")
1450                            );
1451                            self.payjoin_service
1452                                .estimate_payjoin_tx_fee(&to_asset, receiver_amount_sat)
1453                                .await
1454                                .inspect_err(|e| debug!("Error estimating payjoin tx: {e}"))
1455                                .ok()
1456                        } else {
1457                            None
1458                        };
1459
1460                        let fees_sat_res = match is_sideswap_payment {
1461                            false => {
1462                                self.estimate_onchain_tx_or_drain_tx_fee(
1463                                    receiver_amount_sat,
1464                                    &liquid_address_data.address,
1465                                    &to_asset,
1466                                )
1467                                .await
1468                            }
1469                            true => {
1470                                let to_asset = AssetId::from_str(&to_asset)?;
1471                                let from_asset = AssetId::from_str(&from_asset)?;
1472                                let swap = SideSwapService::from_sdk(self)
1473                                    .await
1474                                    .get_asset_swap(from_asset, to_asset, receiver_amount_sat)
1475                                    .await?;
1476                                validate_funds = false;
1477                                ensure_sdk!(
1478                                    get_info_res.wallet_info.balance_sat
1479                                        >= swap.payer_amount_sat + swap.fees_sat,
1480                                    PaymentError::InsufficientFunds
1481                                );
1482                                exchange_amount_sat = Some(swap.payer_amount_sat - swap.fees_sat);
1483                                Ok(swap.fees_sat)
1484                            }
1485                        };
1486
1487                        let fees_sat = match (fees_sat_res, asset_fees) {
1488                            (Ok(fees_sat), _) => Some(fees_sat),
1489                            (Err(e), Some(_asset_fees)) => {
1490                                debug!(
1491                                    "Error estimating onchain tx fees, but returning payjoin fees: {e}"
1492                                );
1493                                None
1494                            }
1495                            (Err(e), None) => return Err(e),
1496                        };
1497                        (to_asset, receiver_amount_sat, fees_sat, asset_fees)
1498                    }
1499                };
1500
1501                liquid_address_data.amount_sat = Some(receiver_amount_sat);
1502                liquid_address_data.asset_id = Some(asset_id.clone());
1503                payment_destination = SendDestination::LiquidAddress {
1504                    address_data: liquid_address_data,
1505                    bip353_address: None,
1506                };
1507            }
1508            Ok(InputType::Bolt11 { invoice }) => {
1509                self.ensure_send_is_not_self_transfer(&invoice.bolt11)?;
1510                self.validate_bolt11_invoice(&invoice.bolt11)?;
1511
1512                let invoice_amount_sat = invoice.amount_msat.ok_or(
1513                    PaymentError::amount_missing("Expected invoice with an amount"),
1514                )? / 1000;
1515
1516                if let Some(PayAmount::Bitcoin {
1517                    receiver_amount_sat: amount_sat,
1518                }) = req.amount
1519                {
1520                    ensure_sdk!(
1521                        invoice_amount_sat == amount_sat,
1522                        PaymentError::Generic {
1523                            err: "Receiver amount and invoice amount do not match".to_string()
1524                        }
1525                    );
1526                }
1527
1528                let lbtc_pair = self.validate_submarine_pairs(invoice_amount_sat).await?;
1529                let mrh_address = if use_mrh {
1530                    self.swapper
1531                        .check_for_mrh(&invoice.bolt11)
1532                        .await?
1533                        .map(|(address, _)| address)
1534                } else {
1535                    None
1536                };
1537                asset_id = self.config.lbtc_asset_id();
1538                estimated_asset_fees = None;
1539                (receiver_amount_sat, fees_sat) = match (mrh_address.clone(), req.amount.clone()) {
1540                    (Some(lbtc_address), Some(PayAmount::Drain)) => {
1541                        // The BOLT11 invoice has an MRH and it is requested that the
1542                        // wallet balance is to be drained, so we calculate the fees of
1543                        // a direct Liquid drain transaction
1544                        let drain_fees_sat = self
1545                            .estimate_drain_tx_fee(None, Some(&lbtc_address))
1546                            .await?;
1547                        let drain_amount_sat =
1548                            get_info_res.wallet_info.balance_sat - drain_fees_sat;
1549                        (drain_amount_sat, Some(drain_fees_sat))
1550                    }
1551                    (Some(lbtc_address), _) => {
1552                        // The BOLT11 invoice has an MRH but no drain is requested,
1553                        // so we calculate the fees of a direct Liquid transaction
1554                        let fees_sat = self
1555                            .estimate_onchain_tx_or_drain_tx_fee(
1556                                invoice_amount_sat,
1557                                &lbtc_address,
1558                                &asset_id,
1559                            )
1560                            .await?;
1561                        (invoice_amount_sat, Some(fees_sat))
1562                    }
1563                    (None, _) => {
1564                        // The BOLT11 invoice has no MRH (or MRH is disabled), so we calculate the fees using a swap
1565                        let boltz_fees_total = lbtc_pair.fees.total(invoice_amount_sat);
1566                        let user_lockup_amount_sat = invoice_amount_sat + boltz_fees_total;
1567                        let lockup_fees_sat = self
1568                            .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
1569                            .await?;
1570                        let fees_sat = boltz_fees_total + lockup_fees_sat;
1571                        (invoice_amount_sat, Some(fees_sat))
1572                    }
1573                };
1574
1575                payment_destination = SendDestination::Bolt11 {
1576                    invoice,
1577                    bip353_address: None,
1578                };
1579            }
1580            Ok(InputType::Bolt12Offer {
1581                offer,
1582                bip353_address,
1583            }) => {
1584                asset_id = self.config.lbtc_asset_id();
1585                estimated_asset_fees = None;
1586                (receiver_amount_sat, fees_sat) = match req.amount {
1587                    Some(PayAmount::Drain) => {
1588                        ensure_sdk!(
1589                            get_info_res.wallet_info.pending_receive_sat == 0
1590                                && get_info_res.wallet_info.pending_send_sat == 0,
1591                            PaymentError::Generic {
1592                                err: "Cannot drain while there are pending payments".to_string(),
1593                            }
1594                        );
1595                        let lbtc_pair = self
1596                            .swapper
1597                            .get_submarine_pairs()
1598                            .await?
1599                            .ok_or(PaymentError::PairsNotFound)?;
1600                        let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
1601                        let drain_amount_sat =
1602                            get_info_res.wallet_info.balance_sat - drain_fees_sat;
1603                        // Get the inverse receiver amount by calculating a dummy amount then increment up to the drain amount
1604                        let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
1605                        let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
1606                        let receiver_amount_sat =
1607                            utils::increment_receiver_amount_up_to_drain_amount(
1608                                dummy_amount_sat,
1609                                &lbtc_pair,
1610                                drain_amount_sat,
1611                            );
1612                        lbtc_pair.limits.within(receiver_amount_sat)?;
1613                        // Validate if we can actually drain the wallet with a swap
1614                        let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1615                        ensure_sdk!(
1616                            receiver_amount_sat + boltz_fees_total == drain_amount_sat,
1617                            PaymentError::Generic {
1618                                err: "Cannot drain without leaving a remainder".to_string(),
1619                            }
1620                        );
1621                        let fees_sat = Some(boltz_fees_total + drain_fees_sat);
1622                        info!("Drain amount: {receiver_amount_sat} sat");
1623                        Ok((receiver_amount_sat, fees_sat))
1624                    }
1625                    Some(PayAmount::Bitcoin {
1626                        receiver_amount_sat,
1627                    }) => {
1628                        let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
1629                        let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1630                        let lockup_fees_sat = self
1631                            .estimate_lockup_tx_or_drain_tx_fee(
1632                                receiver_amount_sat + boltz_fees_total,
1633                            )
1634                            .await?;
1635                        let fees_sat = Some(boltz_fees_total + lockup_fees_sat);
1636                        Ok((receiver_amount_sat, fees_sat))
1637                    }
1638                    _ => Err(PaymentError::amount_missing(
1639                        "Expected PayAmount of type Receiver when processing a Bolt12 offer",
1640                    )),
1641                }?;
1642                if let Some(Amount::Bitcoin { amount_msat }) = &offer.min_amount {
1643                    ensure_sdk!(
1644                        receiver_amount_sat >= amount_msat / 1_000,
1645                        PaymentError::invalid_invoice(
1646                            "Invalid receiver amount: below offer minimum"
1647                        )
1648                    );
1649                }
1650
1651                payment_destination = SendDestination::Bolt12 {
1652                    offer,
1653                    receiver_amount_sat,
1654                    bip353_address,
1655                };
1656            }
1657            _ => {
1658                return Err(PaymentError::generic("Destination is not valid"));
1659            }
1660        };
1661
1662        if validate_funds {
1663            get_info_res.wallet_info.validate_sufficient_funds(
1664                self.config.network,
1665                receiver_amount_sat,
1666                fees_sat,
1667                &asset_id,
1668            )?;
1669        }
1670
1671        Ok(PrepareSendResponse {
1672            destination: payment_destination,
1673            fees_sat,
1674            estimated_asset_fees,
1675            amount: req.amount.clone(),
1676            exchange_amount_sat,
1677            disable_mrh: req.disable_mrh,
1678            payment_timeout_sec: Some(timeout_sec),
1679        })
1680    }
1681
1682    fn ensure_send_is_not_self_transfer(&self, invoice: &str) -> Result<(), PaymentError> {
1683        match self.persister.fetch_receive_swap_by_invoice(invoice)? {
1684            None => Ok(()),
1685            Some(_) => Err(PaymentError::SelfTransferNotSupported),
1686        }
1687    }
1688
1689    /// Either pays a Lightning invoice via a submarine swap or sends funds directly to an address.
1690    ///
1691    /// Depending on [Config]'s `payment_timeout_sec`, this function will return:
1692    /// * [PaymentState::Pending] payment - if the payment could be initiated but didn't yet
1693    ///   complete in this time
1694    /// * [PaymentState::Complete] payment - if the payment was successfully completed in this time
1695    ///
1696    /// # Arguments
1697    ///
1698    /// * `req` - A [SendPaymentRequest], containing:
1699    ///     * `prepare_response` - the [PrepareSendResponse] returned by [LiquidSdk::prepare_send_payment]
1700    ///     * `use_asset_fees` - if set to true, the payment will be sent using the SideSwap payjoin service
1701    ///     * `payer_note` - the optional payer note, which is to be included in a BOLT12 invoice request
1702    ///
1703    /// # Errors
1704    ///
1705    /// * [PaymentError::PaymentTimeout] - if the payment could not be initiated in this time
1706    pub async fn send_payment(
1707        &self,
1708        req: &SendPaymentRequest,
1709    ) -> Result<SendPaymentResponse, PaymentError> {
1710        self.ensure_is_started().await?;
1711
1712        let use_mrh = match req.prepare_response.disable_mrh {
1713            Some(disable_mrh) => !disable_mrh,
1714            None => self.config.use_magic_routing_hints,
1715        };
1716
1717        let PrepareSendResponse {
1718            fees_sat,
1719            destination: payment_destination,
1720            amount,
1721            payment_timeout_sec,
1722            ..
1723        } = &req.prepare_response;
1724        let is_drain = matches!(amount, Some(PayAmount::Drain));
1725
1726        let timeout_sec = payment_timeout_sec.unwrap_or(self.config.payment_timeout_sec);
1727
1728        match payment_destination {
1729            SendDestination::LiquidAddress {
1730                address_data: liquid_address_data,
1731                bip353_address,
1732            } => {
1733                let Some(receiver_amount_sat) = liquid_address_data.amount_sat else {
1734                    return Err(PaymentError::AmountMissing {
1735                        err: "Receiver amount must be set when paying to a Liquid address"
1736                            .to_string(),
1737                    });
1738                };
1739                let Some(to_asset) = liquid_address_data.asset_id.clone() else {
1740                    return Err(PaymentError::asset_error(
1741                        "Asset must be set when paying to a Liquid address",
1742                    ));
1743                };
1744
1745                ensure_sdk!(
1746                    liquid_address_data.network == self.config.network.into(),
1747                    PaymentError::InvalidNetwork {
1748                        err: format!(
1749                            "Cannot send payment from {} to {}",
1750                            Into::<sdk_common::bitcoin::Network>::into(self.config.network),
1751                            liquid_address_data.network
1752                        )
1753                    }
1754                );
1755
1756                let asset_pay_fees = req.use_asset_fees.unwrap_or_default();
1757                let mut response = match amount.as_ref().is_some_and(|a| a.is_sideswap_payment()) {
1758                    false => {
1759                        self.pay_liquid(PayLiquidRequest {
1760                            address_data: liquid_address_data.clone(),
1761                            to_asset,
1762                            receiver_amount_sat,
1763                            asset_pay_fees,
1764                            fees_sat: *fees_sat,
1765                        })
1766                        .await
1767                    }
1768                    true => {
1769                        let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1770                        ensure_sdk!(
1771                            !asset_pay_fees,
1772                            PaymentError::generic("Cannot pay asset fees when executing a payment between two separate assets")
1773                        );
1774
1775                        self.pay_sideswap(PaySideSwapRequest {
1776                            address_data: liquid_address_data.clone(),
1777                            to_asset,
1778                            receiver_amount_sat,
1779                            fees_sat,
1780                            amount: amount.clone(),
1781                        })
1782                        .await
1783                    }
1784                }?;
1785
1786                self.insert_payment_details(&None, bip353_address, &mut response)?;
1787                Ok(response)
1788            }
1789            SendDestination::Bolt11 {
1790                invoice,
1791                bip353_address,
1792            } => {
1793                let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1794                let mut response = self
1795                    .pay_bolt11_invoice(&invoice.bolt11, fees_sat, is_drain, use_mrh, timeout_sec)
1796                    .await?;
1797                self.insert_payment_details(&req.payer_note, bip353_address, &mut response)?;
1798                Ok(response)
1799            }
1800            SendDestination::Bolt12 {
1801                offer,
1802                receiver_amount_sat,
1803                bip353_address,
1804            } => {
1805                let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1806                let bolt12_info = self
1807                    .swapper
1808                    .get_bolt12_info(GetBolt12FetchRequest {
1809                        offer: offer.offer.clone(),
1810                        amount: *receiver_amount_sat,
1811                        note: req.payer_note.clone(),
1812                    })
1813                    .await?;
1814                let mut response = self
1815                    .pay_bolt12_invoice(
1816                        offer,
1817                        *receiver_amount_sat,
1818                        bolt12_info,
1819                        fees_sat,
1820                        is_drain,
1821                        use_mrh,
1822                        timeout_sec,
1823                    )
1824                    .await?;
1825                self.insert_payment_details(&req.payer_note, bip353_address, &mut response)?;
1826                Ok(response)
1827            }
1828        }
1829    }
1830
1831    fn insert_payment_details(
1832        &self,
1833        payer_note: &Option<String>,
1834        bip353_address: &Option<String>,
1835        response: &mut SendPaymentResponse,
1836    ) -> Result<()> {
1837        if payer_note.is_some() || bip353_address.is_some() {
1838            if let (Some(tx_id), Some(destination)) =
1839                (&response.payment.tx_id, &response.payment.destination)
1840            {
1841                self.persister
1842                    .insert_or_update_payment_details(PaymentTxDetails {
1843                        tx_id: tx_id.clone(),
1844                        destination: destination.clone(),
1845                        bip353_address: bip353_address.clone(),
1846                        payer_note: payer_note.clone(),
1847                        ..Default::default()
1848                    })?;
1849                // Get the payment with the bip353_address details
1850                if let Some(payment) = self.persister.get_payment(tx_id)? {
1851                    response.payment = payment;
1852                }
1853            }
1854        }
1855        Ok(())
1856    }
1857
1858    async fn pay_bolt11_invoice(
1859        &self,
1860        invoice: &str,
1861        fees_sat: u64,
1862        is_drain: bool,
1863        use_mrh: bool,
1864        timeout_sec: u64,
1865    ) -> Result<SendPaymentResponse, PaymentError> {
1866        self.ensure_send_is_not_self_transfer(invoice)?;
1867        let bolt11_invoice = self.validate_bolt11_invoice(invoice)?;
1868
1869        let amount_sat = bolt11_invoice
1870            .amount_milli_satoshis()
1871            .map(|msat| msat / 1_000)
1872            .ok_or(PaymentError::AmountMissing {
1873                err: "Invoice amount is missing".to_string(),
1874            })?;
1875        let payer_amount_sat = amount_sat + fees_sat;
1876        let get_info_response = self.get_info().await?;
1877        ensure_sdk!(
1878            payer_amount_sat <= get_info_response.wallet_info.balance_sat,
1879            PaymentError::InsufficientFunds
1880        );
1881
1882        let description = match bolt11_invoice.description() {
1883            Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
1884            Bolt11InvoiceDescription::Hash(_) => None,
1885        };
1886
1887        let mrh_address = if use_mrh {
1888            self.swapper
1889                .check_for_mrh(invoice)
1890                .await?
1891                .map(|(address, _)| address)
1892        } else {
1893            None
1894        };
1895
1896        match mrh_address {
1897            // If we find a valid MRH, extract the BIP21 address and pay to it via onchain tx
1898            Some(address) => {
1899                info!("Found MRH for L-BTC address {address}, invoice amount_sat {amount_sat}");
1900                let (amount_sat, fees_sat) = if is_drain {
1901                    let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
1902                    let drain_amount_sat =
1903                        get_info_response.wallet_info.balance_sat - drain_fees_sat;
1904                    info!("Drain amount: {drain_amount_sat} sat");
1905                    (drain_amount_sat, drain_fees_sat)
1906                } else {
1907                    (amount_sat, fees_sat)
1908                };
1909
1910                self.pay_liquid_onchain(
1911                    LiquidAddressData {
1912                        address,
1913                        network: self.config.network.into(),
1914                        asset_id: None,
1915                        amount: None,
1916                        amount_sat: None,
1917                        label: None,
1918                        message: None,
1919                    },
1920                    amount_sat,
1921                    fees_sat,
1922                    false,
1923                )
1924                .await
1925            }
1926
1927            // If no MRH found (or MRH is disabled), perform usual swap
1928            None => {
1929                self.send_payment_via_swap(
1930                    SendPaymentViaSwapRequest {
1931                        invoice: invoice.to_string(),
1932                        bolt12_offer: None,
1933                        payment_hash: bolt11_invoice.payment_hash().to_string(),
1934                        description,
1935                        receiver_amount_sat: amount_sat,
1936                        fees_sat,
1937                    },
1938                    timeout_sec,
1939                )
1940                .await
1941            }
1942        }
1943    }
1944
1945    #[allow(clippy::too_many_arguments)]
1946    async fn pay_bolt12_invoice(
1947        &self,
1948        offer: &LNOffer,
1949        user_specified_receiver_amount_sat: u64,
1950        bolt12_info: GetBolt12FetchResponse,
1951        fees_sat: u64,
1952        is_drain: bool,
1953        use_mrh: bool,
1954        timeout_sec: u64,
1955    ) -> Result<SendPaymentResponse, PaymentError> {
1956        let invoice = self.validate_bolt12_invoice(
1957            offer,
1958            user_specified_receiver_amount_sat,
1959            &bolt12_info.invoice,
1960        )?;
1961
1962        let receiver_amount_sat = invoice.amount_msats() / 1_000;
1963        let payer_amount_sat = receiver_amount_sat + fees_sat;
1964        let get_info_response = self.get_info().await?;
1965        ensure_sdk!(
1966            payer_amount_sat <= get_info_response.wallet_info.balance_sat,
1967            PaymentError::InsufficientFunds
1968        );
1969
1970        match (bolt12_info.magic_routing_hint, use_mrh) {
1971            // If we find a valid MRH, extract the BIP21 address and pay to it via onchain tx
1972            (Some(MagicRoutingHint { bip21, signature }), true) => {
1973                info!(
1974                    "Found MRH for L-BTC address {bip21}, invoice amount_sat {receiver_amount_sat}"
1975                );
1976                let signing_pubkey = invoice.signing_pubkey().to_string();
1977                let (_, address, _, _) = verify_mrh_signature(&bip21, &signing_pubkey, &signature)?;
1978                let (receiver_amount_sat, fees_sat) = if is_drain {
1979                    let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
1980                    let drain_amount_sat =
1981                        get_info_response.wallet_info.balance_sat - drain_fees_sat;
1982                    info!("Drain amount: {drain_amount_sat} sat");
1983                    (drain_amount_sat, drain_fees_sat)
1984                } else {
1985                    (receiver_amount_sat, fees_sat)
1986                };
1987
1988                self.pay_liquid_onchain(
1989                    LiquidAddressData {
1990                        address,
1991                        network: self.config.network.into(),
1992                        asset_id: None,
1993                        amount: None,
1994                        amount_sat: None,
1995                        label: None,
1996                        message: None,
1997                    },
1998                    receiver_amount_sat,
1999                    fees_sat,
2000                    false,
2001                )
2002                .await
2003            }
2004
2005            // If no MRH found (or MRH is disabled), perform usual swap
2006            _ => {
2007                self.send_payment_via_swap(
2008                    SendPaymentViaSwapRequest {
2009                        invoice: bolt12_info.invoice,
2010                        bolt12_offer: Some(offer.offer.clone()),
2011                        payment_hash: invoice.payment_hash().to_string(),
2012                        description: invoice.description().map(|desc| desc.to_string()),
2013                        receiver_amount_sat,
2014                        fees_sat,
2015                    },
2016                    timeout_sec,
2017                )
2018                .await
2019            }
2020        }
2021    }
2022
2023    async fn pay_liquid(&self, req: PayLiquidRequest) -> Result<SendPaymentResponse, PaymentError> {
2024        let PayLiquidRequest {
2025            address_data,
2026            receiver_amount_sat,
2027            to_asset,
2028            fees_sat,
2029            asset_pay_fees,
2030            ..
2031        } = req;
2032
2033        self.get_info()
2034            .await?
2035            .wallet_info
2036            .validate_sufficient_funds(
2037                self.config.network,
2038                receiver_amount_sat,
2039                fees_sat,
2040                &to_asset,
2041            )?;
2042
2043        if asset_pay_fees {
2044            return self
2045                .pay_liquid_payjoin(address_data.clone(), receiver_amount_sat)
2046                .await;
2047        }
2048
2049        let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
2050        self.pay_liquid_onchain(address_data.clone(), receiver_amount_sat, fees_sat, true)
2051            .await
2052    }
2053
2054    /// Performs a Send Payment by doing an onchain tx to a Liquid address
2055    async fn pay_liquid_onchain(
2056        &self,
2057        address_data: LiquidAddressData,
2058        receiver_amount_sat: u64,
2059        fees_sat: u64,
2060        skip_already_paid_check: bool,
2061    ) -> Result<SendPaymentResponse, PaymentError> {
2062        let destination = address_data
2063            .to_uri()
2064            .unwrap_or(address_data.address.clone());
2065        let asset_id = address_data.asset_id.unwrap_or(self.config.lbtc_asset_id());
2066        let payments = self.persister.get_payments(&ListPaymentsRequest {
2067            details: Some(ListPaymentDetails::Liquid {
2068                asset_id: Some(asset_id.clone()),
2069                destination: Some(destination.clone()),
2070            }),
2071            ..Default::default()
2072        })?;
2073        ensure_sdk!(
2074            skip_already_paid_check || payments.is_empty(),
2075            PaymentError::AlreadyPaid
2076        );
2077
2078        let tx = self
2079            .onchain_wallet
2080            .build_tx_or_drain_tx(
2081                Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
2082                &address_data.address,
2083                &asset_id,
2084                receiver_amount_sat,
2085            )
2086            .await?;
2087        let tx_id = tx.txid().to_string();
2088        let tx_fees_sat = tx.all_fees().values().sum::<u64>();
2089        ensure_sdk!(tx_fees_sat <= fees_sat, PaymentError::InvalidOrExpiredFees);
2090
2091        info!(
2092            "Built onchain Liquid tx with receiver_amount_sat = {receiver_amount_sat}, fees_sat = {fees_sat} and txid = {tx_id}"
2093        );
2094
2095        let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
2096
2097        // We insert a pseudo-tx in case LWK fails to pick up the new mempool tx for a while
2098        // This makes the tx known to the SDK (get_info, list_payments) instantly
2099        let tx_data = PaymentTxData {
2100            tx_id: tx_id.clone(),
2101            timestamp: Some(utils::now()),
2102            is_confirmed: false,
2103            fees_sat,
2104            unblinding_data: None,
2105        };
2106        let tx_balance = PaymentTxBalance {
2107            amount: receiver_amount_sat,
2108            asset_id: asset_id.clone(),
2109            payment_type: PaymentType::Send,
2110        };
2111
2112        let description = address_data.message;
2113
2114        self.persister.insert_or_update_payment(
2115            tx_data.clone(),
2116            std::slice::from_ref(&tx_balance),
2117            Some(PaymentTxDetails {
2118                tx_id: tx_id.clone(),
2119                destination: destination.clone(),
2120                description: description.clone(),
2121                ..Default::default()
2122            }),
2123            false,
2124        )?;
2125        self.emit_payment_updated(Some(tx_id)).await?; // Emit Pending event
2126
2127        let asset_info = self
2128            .persister
2129            .get_asset_metadata(&asset_id)?
2130            .map(|ref am| AssetInfo {
2131                name: am.name.clone(),
2132                ticker: am.ticker.clone(),
2133                amount: am.amount_from_sat(receiver_amount_sat),
2134                fees: None,
2135            });
2136        let payment_details = PaymentDetails::Liquid {
2137            asset_id,
2138            destination,
2139            description: description.unwrap_or("Liquid transfer".to_string()),
2140            asset_info,
2141            lnurl_info: None,
2142            bip353_address: None,
2143            payer_note: None,
2144        };
2145
2146        Ok(SendPaymentResponse {
2147            payment: Payment::from_tx_data(tx_data, tx_balance, None, payment_details),
2148        })
2149    }
2150
2151    /// Performs a Liquid send payment via SideSwap
2152    async fn pay_sideswap(
2153        &self,
2154        req: PaySideSwapRequest,
2155    ) -> Result<SendPaymentResponse, PaymentError> {
2156        let PaySideSwapRequest {
2157            address_data,
2158            to_asset,
2159            amount,
2160            receiver_amount_sat,
2161            fees_sat,
2162        } = req;
2163
2164        let from_asset = AssetId::from_str(match amount {
2165            Some(PayAmount::Asset {
2166                from_asset: Some(ref from_asset),
2167                ..
2168            }) => from_asset,
2169            _ => &to_asset,
2170        })?;
2171        let to_asset = AssetId::from_str(&to_asset)?;
2172        let to_address = elements::Address::from_str(&address_data.address).map_err(|err| {
2173            PaymentError::generic(format!("Could not convert destination address: {err}"))
2174        })?;
2175
2176        let sideswap_service = SideSwapService::from_sdk(self).await;
2177
2178        let swap = sideswap_service
2179            .get_asset_swap(from_asset, to_asset, receiver_amount_sat)
2180            .await?;
2181
2182        ensure_sdk!(
2183            swap.fees_sat <= fees_sat,
2184            PaymentError::InvalidOrExpiredFees
2185        );
2186
2187        ensure_sdk!(
2188            self.get_info().await?.wallet_info.balance_sat >= swap.payer_amount_sat,
2189            PaymentError::InsufficientFunds
2190        );
2191
2192        let tx_id = sideswap_service
2193            .execute_swap(to_address.clone(), &swap)
2194            .await?;
2195
2196        // We insert a pseudo-tx in case LWK fails to pick up the new mempool tx for a while
2197        // This makes the tx known to the SDK (get_info, list_payments) instantly
2198        self.persister.insert_or_update_payment(
2199            PaymentTxData {
2200                tx_id: tx_id.clone(),
2201                timestamp: Some(utils::now()),
2202                fees_sat: swap.fees_sat,
2203                is_confirmed: false,
2204                unblinding_data: None,
2205            },
2206            &[PaymentTxBalance {
2207                asset_id: utils::lbtc_asset_id(self.config.network).to_string(),
2208                amount: swap.payer_amount_sat,
2209                payment_type: PaymentType::Send,
2210            }],
2211            Some(PaymentTxDetails {
2212                tx_id: tx_id.clone(),
2213                destination: to_address.to_string(),
2214                description: address_data.message,
2215                ..Default::default()
2216            }),
2217            false,
2218        )?;
2219        self.emit_payment_updated(Some(tx_id.clone())).await?; // Emit Pending event
2220
2221        let payment = self
2222            .persister
2223            .get_payment(&tx_id)?
2224            .context("Payment not found")?;
2225        Ok(SendPaymentResponse { payment })
2226    }
2227
2228    /// Performs a Send Payment by doing a payjoin tx to a Liquid address
2229    async fn pay_liquid_payjoin(
2230        &self,
2231        address_data: LiquidAddressData,
2232        receiver_amount_sat: u64,
2233    ) -> Result<SendPaymentResponse, PaymentError> {
2234        let destination = address_data
2235            .to_uri()
2236            .unwrap_or(address_data.address.clone());
2237        let Some(asset_id) = address_data.asset_id else {
2238            return Err(PaymentError::asset_error(
2239                "Asset must be set when paying to a Liquid address",
2240            ));
2241        };
2242
2243        let (tx, asset_fees) = self
2244            .payjoin_service
2245            .build_payjoin_tx(&address_data.address, &asset_id, receiver_amount_sat)
2246            .await
2247            .inspect_err(|e| error!("Error building payjoin tx: {e}"))?;
2248        let tx_id = tx.txid().to_string();
2249        let fees_sat = tx.all_fees().values().sum::<u64>();
2250
2251        info!(
2252            "Built payjoin Liquid tx with receiver_amount_sat = {receiver_amount_sat}, asset_fees = {asset_fees}, fees_sat = {fees_sat} and txid = {tx_id}"
2253        );
2254
2255        let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
2256
2257        // We insert a pseudo-tx in case LWK fails to pick up the new mempool tx for a while
2258        // This makes the tx known to the SDK (get_info, list_payments) instantly
2259        let tx_data = PaymentTxData {
2260            tx_id: tx_id.clone(),
2261            fees_sat,
2262            timestamp: Some(utils::now()),
2263            is_confirmed: false,
2264            unblinding_data: None,
2265        };
2266        let tx_balance = PaymentTxBalance {
2267            asset_id: asset_id.clone(),
2268            amount: receiver_amount_sat + asset_fees,
2269            payment_type: PaymentType::Send,
2270        };
2271
2272        let description = address_data.message;
2273
2274        self.persister.insert_or_update_payment(
2275            tx_data.clone(),
2276            std::slice::from_ref(&tx_balance),
2277            Some(PaymentTxDetails {
2278                tx_id: tx_id.clone(),
2279                destination: destination.clone(),
2280                description: description.clone(),
2281                asset_fees: Some(asset_fees),
2282                ..Default::default()
2283            }),
2284            false,
2285        )?;
2286        self.emit_payment_updated(Some(tx_id)).await?; // Emit Pending event
2287
2288        let asset_info = self
2289            .persister
2290            .get_asset_metadata(&asset_id)?
2291            .map(|ref am| AssetInfo {
2292                name: am.name.clone(),
2293                ticker: am.ticker.clone(),
2294                amount: am.amount_from_sat(receiver_amount_sat),
2295                fees: Some(am.amount_from_sat(asset_fees)),
2296            });
2297        let payment_details = PaymentDetails::Liquid {
2298            asset_id,
2299            destination,
2300            description: description.unwrap_or("Liquid transfer".to_string()),
2301            asset_info,
2302            lnurl_info: None,
2303            bip353_address: None,
2304            payer_note: None,
2305        };
2306
2307        Ok(SendPaymentResponse {
2308            payment: Payment::from_tx_data(tx_data, tx_balance, None, payment_details),
2309        })
2310    }
2311
2312    /// Performs a Send Payment by doing a swap (create it, fund it, track it, etc).
2313    ///
2314    /// If `bolt12_offer` is set, `invoice` refers to a Bolt12 invoice, otherwise it's a Bolt11 one.
2315    async fn send_payment_via_swap(
2316        &self,
2317        req: SendPaymentViaSwapRequest,
2318        timeout_sec: u64,
2319    ) -> Result<SendPaymentResponse, PaymentError> {
2320        let SendPaymentViaSwapRequest {
2321            invoice,
2322            bolt12_offer,
2323            payment_hash,
2324            description,
2325            receiver_amount_sat,
2326            fees_sat,
2327        } = req;
2328        let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
2329        let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
2330        let user_lockup_amount_sat = receiver_amount_sat + boltz_fees_total;
2331        let lockup_tx_fees_sat = self
2332            .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
2333            .await?;
2334        ensure_sdk!(
2335            fees_sat == boltz_fees_total + lockup_tx_fees_sat,
2336            PaymentError::InvalidOrExpiredFees
2337        );
2338
2339        let swap = match self.persister.fetch_send_swap_by_invoice(&invoice)? {
2340            Some(swap) => match swap.state {
2341                Created => swap,
2342                TimedOut => {
2343                    self.send_swap_handler.update_swap_info(
2344                        &swap.id,
2345                        PaymentState::Created,
2346                        None,
2347                        None,
2348                        None,
2349                    )?;
2350                    swap
2351                }
2352                Pending => return Err(PaymentError::PaymentInProgress),
2353                Complete => return Err(PaymentError::AlreadyPaid),
2354                RefundPending | Refundable | Failed => {
2355                    return Err(PaymentError::invalid_invoice(
2356                        "Payment has already failed. Please try with another invoice",
2357                    ))
2358                }
2359                WaitingFeeAcceptance => {
2360                    return Err(PaymentError::Generic {
2361                        err: "Send swap payment cannot be in state WaitingFeeAcceptance"
2362                            .to_string(),
2363                    })
2364                }
2365            },
2366            None => {
2367                let keypair = utils::generate_keypair();
2368                let refund_public_key = boltz_client::PublicKey {
2369                    compressed: true,
2370                    inner: keypair.public_key(),
2371                };
2372                let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2373                    url,
2374                    hash_swap_id: Some(true),
2375                    status: Some(vec![
2376                        SubSwapStates::InvoiceFailedToPay,
2377                        SubSwapStates::SwapExpired,
2378                        SubSwapStates::TransactionClaimPending,
2379                        SubSwapStates::TransactionLockupFailed,
2380                    ]),
2381                });
2382                let create_response = self
2383                    .swapper
2384                    .create_send_swap(CreateSubmarineRequest {
2385                        from: "L-BTC".to_string(),
2386                        to: "BTC".to_string(),
2387                        invoice: invoice.to_string(),
2388                        refund_public_key,
2389                        pair_hash: Some(lbtc_pair.hash.clone()),
2390                        referral_id: None,
2391                        webhook,
2392                    })
2393                    .await?;
2394
2395                let swap_id = &create_response.id;
2396                let create_response_json =
2397                    SendSwap::from_boltz_struct_to_json(&create_response, swap_id)?;
2398                let destination_pubkey =
2399                    utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some())?;
2400
2401                let payer_amount_sat = fees_sat + receiver_amount_sat;
2402                let swap = SendSwap {
2403                    id: swap_id.to_string(),
2404                    invoice: invoice.to_string(),
2405                    bolt12_offer,
2406                    payment_hash: Some(payment_hash.to_string()),
2407                    destination_pubkey: Some(destination_pubkey),
2408                    timeout_block_height: create_response.timeout_block_height,
2409                    description,
2410                    preimage: None,
2411                    payer_amount_sat,
2412                    receiver_amount_sat,
2413                    pair_fees_json: serde_json::to_string(&lbtc_pair).map_err(|e| {
2414                        PaymentError::generic(format!("Failed to serialize SubmarinePair: {e:?}"))
2415                    })?,
2416                    create_response_json,
2417                    lockup_tx_id: None,
2418                    refund_address: None,
2419                    refund_tx_id: None,
2420                    created_at: utils::now(),
2421                    state: PaymentState::Created,
2422                    refund_private_key: keypair.display_secret().to_string(),
2423                    metadata: Default::default(),
2424                };
2425                self.persister.insert_or_update_send_swap(&swap)?;
2426                swap
2427            }
2428        };
2429        self.status_stream.track_swap_id(&swap.id)?;
2430
2431        let create_response = swap.get_boltz_create_response()?;
2432        self.send_swap_handler
2433            .try_lockup(&swap, &create_response)
2434            .await?;
2435
2436        self.wait_for_payment_with_timeout(
2437            Swap::Send(swap),
2438            create_response.accept_zero_conf,
2439            timeout_sec,
2440        )
2441        .await
2442        .map(|payment| SendPaymentResponse { payment })
2443    }
2444
2445    /// Fetch the current payment limits for [LiquidSdk::send_payment] and [LiquidSdk::receive_payment].
2446    pub async fn fetch_lightning_limits(
2447        &self,
2448    ) -> Result<LightningPaymentLimitsResponse, PaymentError> {
2449        self.ensure_is_started().await?;
2450
2451        let submarine_pair = self
2452            .swapper
2453            .get_submarine_pairs()
2454            .await?
2455            .ok_or(PaymentError::PairsNotFound)?;
2456        let send_limits = submarine_pair.limits;
2457
2458        let reverse_pair = self
2459            .swapper
2460            .get_reverse_swap_pairs()
2461            .await?
2462            .ok_or(PaymentError::PairsNotFound)?;
2463        let receive_limits = reverse_pair.limits;
2464
2465        Ok(LightningPaymentLimitsResponse {
2466            send: Limits {
2467                min_sat: send_limits.minimal_batched.unwrap_or(send_limits.minimal),
2468                max_sat: send_limits.maximal,
2469                max_zero_conf_sat: send_limits.maximal_zero_conf,
2470            },
2471            receive: Limits {
2472                min_sat: receive_limits.minimal,
2473                max_sat: receive_limits.maximal,
2474                max_zero_conf_sat: self.config.zero_conf_max_amount_sat(),
2475            },
2476        })
2477    }
2478
2479    /// Fetch the current payment limits for [LiquidSdk::pay_onchain] and [LiquidSdk::receive_onchain].
2480    pub async fn fetch_onchain_limits(&self) -> Result<OnchainPaymentLimitsResponse, PaymentError> {
2481        self.ensure_is_started().await?;
2482
2483        let (pair_outgoing, pair_incoming) = self.swapper.get_chain_pairs().await?;
2484        let send_limits = pair_outgoing
2485            .ok_or(PaymentError::PairsNotFound)
2486            .map(|pair| pair.limits)?;
2487        let receive_limits = pair_incoming
2488            .ok_or(PaymentError::PairsNotFound)
2489            .map(|pair| pair.limits)?;
2490
2491        Ok(OnchainPaymentLimitsResponse {
2492            send: Limits {
2493                min_sat: send_limits.minimal,
2494                max_sat: send_limits.maximal,
2495                max_zero_conf_sat: send_limits.maximal_zero_conf,
2496            },
2497            receive: Limits {
2498                min_sat: receive_limits.minimal,
2499                max_sat: receive_limits.maximal,
2500                max_zero_conf_sat: receive_limits.maximal_zero_conf,
2501            },
2502        })
2503    }
2504
2505    /// Prepares to pay to a Bitcoin address via a chain swap.
2506    ///
2507    /// # Arguments
2508    ///
2509    /// * `req` - the [PreparePayOnchainRequest] containing:
2510    ///     * `amount` - which can be of two types: [PayAmount::Drain], which uses all funds,
2511    ///       and [PayAmount::Bitcoin], which sets the amount the receiver should receive
2512    ///     * `fee_rate_sat_per_vbyte` - the optional fee rate of the Bitcoin claim transaction. Defaults to the swapper estimated claim fee
2513    pub async fn prepare_pay_onchain(
2514        &self,
2515        req: &PreparePayOnchainRequest,
2516    ) -> Result<PreparePayOnchainResponse, PaymentError> {
2517        self.ensure_is_started().await?;
2518
2519        let get_info_res = self.get_info().await?;
2520        let pair = self.get_chain_pair(Direction::Outgoing).await?;
2521        let claim_fees_sat = match req.fee_rate_sat_per_vbyte {
2522            Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64,
2523            None => pair.clone().fees.claim_estimate(),
2524        };
2525        let server_fees_sat = pair.fees.server();
2526
2527        info!("Preparing for onchain payment of kind: {:?}", req.amount);
2528        let (payer_amount_sat, receiver_amount_sat, total_fees_sat) = match req.amount {
2529            PayAmount::Bitcoin {
2530                receiver_amount_sat: amount_sat,
2531            } => {
2532                let receiver_amount_sat = amount_sat;
2533
2534                let user_lockup_amount_sat_without_service_fee =
2535                    receiver_amount_sat + claim_fees_sat + server_fees_sat;
2536
2537                // The resulting invoice amount contains the service fee, which is rounded up with ceil()
2538                // Therefore, when calculating the user_lockup amount, we must also round it up with ceil()
2539                let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64
2540                    * 100.0
2541                    / (100.0 - pair.fees.percentage))
2542                    .ceil() as u64;
2543                self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2544
2545                let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
2546
2547                let boltz_fees_sat =
2548                    user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2549                let total_fees_sat =
2550                    boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2551                let payer_amount_sat = receiver_amount_sat + total_fees_sat;
2552
2553                (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2554            }
2555            PayAmount::Drain => {
2556                ensure_sdk!(
2557                    get_info_res.wallet_info.pending_receive_sat == 0
2558                        && get_info_res.wallet_info.pending_send_sat == 0,
2559                    PaymentError::Generic {
2560                        err: "Cannot drain while there are pending payments".to_string(),
2561                    }
2562                );
2563                let payer_amount_sat = get_info_res.wallet_info.balance_sat;
2564                let lockup_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
2565
2566                let user_lockup_amount_sat = payer_amount_sat - lockup_fees_sat;
2567                self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2568
2569                let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
2570                let total_fees_sat =
2571                    boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2572                let receiver_amount_sat = payer_amount_sat - total_fees_sat;
2573
2574                (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2575            }
2576            PayAmount::Asset { .. } => {
2577                return Err(PaymentError::asset_error(
2578                    "Cannot send an asset to a Bitcoin address",
2579                ))
2580            }
2581        };
2582
2583        let res = PreparePayOnchainResponse {
2584            receiver_amount_sat,
2585            claim_fees_sat,
2586            total_fees_sat,
2587        };
2588
2589        ensure_sdk!(
2590            payer_amount_sat <= get_info_res.wallet_info.balance_sat,
2591            PaymentError::InsufficientFunds
2592        );
2593
2594        info!("Prepared onchain payment: {res:?}");
2595        Ok(res)
2596    }
2597
2598    /// Pays to a Bitcoin address via a chain swap.
2599    ///
2600    /// Depending on [Config]'s `payment_timeout_sec`, this function will return:
2601    /// * [PaymentState::Pending] payment - if the payment could be initiated but didn't yet
2602    ///   complete in this time
2603    /// * [PaymentState::Complete] payment - if the payment was successfully completed in this time
2604    ///
2605    /// # Arguments
2606    ///
2607    /// * `req` - the [PayOnchainRequest] containing:
2608    ///     * `address` - the Bitcoin address to pay to
2609    ///     * `prepare_response` - the [PreparePayOnchainResponse] from calling [LiquidSdk::prepare_pay_onchain]
2610    ///
2611    /// # Errors
2612    ///
2613    /// * [PaymentError::PaymentTimeout] - if the payment could not be initiated in this time
2614    pub async fn pay_onchain(
2615        &self,
2616        req: &PayOnchainRequest,
2617    ) -> Result<SendPaymentResponse, PaymentError> {
2618        self.ensure_is_started().await?;
2619        info!("Paying onchain, request = {req:?}");
2620
2621        let timeout_sec = self.config.payment_timeout_sec;
2622
2623        let claim_address = self.validate_bitcoin_address(&req.address).await?;
2624        let balance_sat = self.get_info().await?.wallet_info.balance_sat;
2625        let receiver_amount_sat = req.prepare_response.receiver_amount_sat;
2626        let pair = self.get_chain_pair(Direction::Outgoing).await?;
2627        let claim_fees_sat = req.prepare_response.claim_fees_sat;
2628        let server_fees_sat = pair.fees.server();
2629        let server_lockup_amount_sat = receiver_amount_sat + claim_fees_sat;
2630
2631        let user_lockup_amount_sat_without_service_fee =
2632            receiver_amount_sat + claim_fees_sat + server_fees_sat;
2633
2634        // The resulting invoice amount contains the service fee, which is rounded up with ceil()
2635        // Therefore, when calculating the user_lockup amount, we must also round it up with ceil()
2636        let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64 * 100.0
2637            / (100.0 - pair.fees.percentage))
2638            .ceil() as u64;
2639        let boltz_fee_sat = user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2640        self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2641
2642        let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2643
2644        let lockup_fees_sat = match payer_amount_sat == balance_sat {
2645            true => self.estimate_drain_tx_fee(None, None).await?,
2646            false => self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?,
2647        };
2648
2649        ensure_sdk!(
2650            req.prepare_response.total_fees_sat
2651                == boltz_fee_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat,
2652            PaymentError::InvalidOrExpiredFees
2653        );
2654
2655        ensure_sdk!(
2656            payer_amount_sat <= balance_sat,
2657            PaymentError::InsufficientFunds
2658        );
2659
2660        let preimage = Preimage::new();
2661        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2662
2663        let claim_keypair = utils::generate_keypair();
2664        let claim_public_key = boltz_client::PublicKey {
2665            compressed: true,
2666            inner: claim_keypair.public_key(),
2667        };
2668        let refund_keypair = utils::generate_keypair();
2669        let refund_public_key = boltz_client::PublicKey {
2670            compressed: true,
2671            inner: refund_keypair.public_key(),
2672        };
2673        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2674            url,
2675            hash_swap_id: Some(true),
2676            status: Some(vec![
2677                ChainSwapStates::TransactionFailed,
2678                ChainSwapStates::TransactionLockupFailed,
2679                ChainSwapStates::TransactionServerConfirmed,
2680            ]),
2681        });
2682        let create_response = self
2683            .swapper
2684            .create_chain_swap(CreateChainRequest {
2685                from: "L-BTC".to_string(),
2686                to: "BTC".to_string(),
2687                preimage_hash: preimage.sha256,
2688                claim_public_key: Some(claim_public_key),
2689                refund_public_key: Some(refund_public_key),
2690                user_lock_amount: None,
2691                server_lock_amount: Some(server_lockup_amount_sat),
2692                pair_hash: Some(pair.hash.clone()),
2693                referral_id: None,
2694                webhook,
2695            })
2696            .await?;
2697
2698        let create_response_json =
2699            ChainSwap::from_boltz_struct_to_json(&create_response, &create_response.id)?;
2700        let swap_id = create_response.id;
2701
2702        let accept_zero_conf = server_lockup_amount_sat <= pair.limits.maximal_zero_conf;
2703        let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2704
2705        let swap = ChainSwap {
2706            id: swap_id.clone(),
2707            direction: Direction::Outgoing,
2708            claim_address: Some(claim_address),
2709            lockup_address: create_response.lockup_details.lockup_address,
2710            refund_address: None,
2711            timeout_block_height: create_response.lockup_details.timeout_block_height,
2712            claim_timeout_block_height: create_response.claim_details.timeout_block_height,
2713            preimage: preimage_str,
2714            description: Some("Bitcoin transfer".to_string()),
2715            payer_amount_sat,
2716            actual_payer_amount_sat: None,
2717            receiver_amount_sat,
2718            accepted_receiver_amount_sat: None,
2719            claim_fees_sat,
2720            pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
2721                PaymentError::generic(format!("Failed to serialize outgoing ChainPair: {e:?}"))
2722            })?,
2723            accept_zero_conf,
2724            create_response_json,
2725            claim_private_key: claim_keypair.display_secret().to_string(),
2726            refund_private_key: refund_keypair.display_secret().to_string(),
2727            server_lockup_tx_id: None,
2728            user_lockup_tx_id: None,
2729            claim_tx_id: None,
2730            refund_tx_id: None,
2731            created_at: utils::now(),
2732            state: PaymentState::Created,
2733            auto_accepted_fees: false,
2734            metadata: Default::default(),
2735        };
2736        self.persister.insert_or_update_chain_swap(&swap)?;
2737        self.status_stream.track_swap_id(&swap_id)?;
2738
2739        self.wait_for_payment_with_timeout(Swap::Chain(swap), accept_zero_conf, timeout_sec)
2740            .await
2741            .map(|payment| SendPaymentResponse { payment })
2742    }
2743
2744    async fn wait_for_payment_with_timeout(
2745        &self,
2746        swap: Swap,
2747        accept_zero_conf: bool,
2748        timeout_sec: u64,
2749    ) -> Result<Payment, PaymentError> {
2750        let timeout_fut = tokio::time::sleep(Duration::from_secs(timeout_sec));
2751        tokio::pin!(timeout_fut);
2752
2753        let expected_swap_id = swap.id();
2754        let mut events_stream = self.event_manager.subscribe();
2755        let mut maybe_payment: Option<Payment> = None;
2756
2757        loop {
2758            tokio::select! {
2759                _ = &mut timeout_fut => match maybe_payment {
2760                    Some(payment) => return Ok(payment),
2761                    None => {
2762                        debug!("Timeout occurred without payment, set swap to timed out");
2763                        let update_res = match swap {
2764                            Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None),
2765                            Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
2766                                    swap_id: expected_swap_id.clone(),
2767                                    to_state: TimedOut,
2768                                    ..Default::default()
2769                                }),
2770                            _ => Ok(())
2771                        };
2772                        return match update_res {
2773                            Ok(_) => Err(PaymentError::PaymentTimeout),
2774                            Err(_) => {
2775                                // Not able to transition the payment state to TimedOut, which means the payment
2776                                // state progressed but we didn't see the event before the timeout
2777                                self.persister.get_payment(&expected_swap_id).ok().flatten().ok_or(PaymentError::generic("Payment not found"))
2778                            }
2779                        }
2780                    },
2781                },
2782                event = events_stream.recv() => match event {
2783                    Ok(SdkEvent::PaymentPending { details: payment }) => {
2784                        let maybe_payment_swap_id = payment.details.get_swap_id();
2785                        if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2786                            match accept_zero_conf {
2787                                true => {
2788                                    debug!("Received Send Payment pending event with zero-conf accepted");
2789                                    return Ok(payment)
2790                                }
2791                                false => {
2792                                    debug!("Received Send Payment pending event, waiting for confirmation");
2793                                    maybe_payment = Some(payment);
2794                                }
2795                            }
2796                        };
2797                    },
2798                    Ok(SdkEvent::PaymentSucceeded { details: payment }) => {
2799                        let maybe_payment_swap_id = payment.details.get_swap_id();
2800                        if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2801                            debug!("Received Send Payment succeed event");
2802                            return Ok(payment);
2803                        }
2804                    },
2805                    Ok(event) => debug!("Unhandled event waiting for payment: {event:?}"),
2806                    Err(e) => debug!("Received error waiting for payment: {e:?}"),
2807                }
2808            }
2809        }
2810    }
2811
2812    /// Prepares to receive a Lightning payment via a reverse submarine swap.
2813    ///
2814    /// # Arguments
2815    ///
2816    /// * `req` - the [PrepareReceiveRequest] containing:
2817    ///     * `payment_method` - the supported payment methods; either an invoice, an offer, a Liquid address or a Bitcoin address
2818    ///     * `amount` - The optional amount of type [ReceiveAmount] to be paid.
2819    ///        - [ReceiveAmount::Bitcoin] which sets the amount in satoshi that should be paid
2820    ///        - [ReceiveAmount::Asset] which sets the amount of an asset that should be paid
2821    pub async fn prepare_receive_payment(
2822        &self,
2823        req: &PrepareReceiveRequest,
2824    ) -> Result<PrepareReceiveResponse, PaymentError> {
2825        self.ensure_is_started().await?;
2826
2827        match req.payment_method.clone() {
2828            #[allow(deprecated)]
2829            PaymentMethod::Bolt11Invoice => {
2830                let payer_amount_sat = match req.amount {
2831                    Some(ReceiveAmount::Asset { .. }) => {
2832                        return Err(PaymentError::asset_error(
2833                            "Cannot receive an asset for this payment method",
2834                        ));
2835                    }
2836                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2837                    None => {
2838                        return Err(PaymentError::generic(
2839                            "Bitcoin payer amount must be set for this payment method",
2840                        ));
2841                    }
2842                };
2843                let reverse_pair = self
2844                    .swapper
2845                    .get_reverse_swap_pairs()
2846                    .await?
2847                    .ok_or(PaymentError::PairsNotFound)?;
2848
2849                let fees_sat = reverse_pair.fees.total(payer_amount_sat);
2850
2851                reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
2852                    PaymentError::AmountOutOfRange {
2853                        min: reverse_pair.limits.minimal,
2854                        max: reverse_pair.limits.maximal,
2855                    }
2856                })?;
2857
2858                let min_payer_amount_sat = Some(reverse_pair.limits.minimal);
2859                let max_payer_amount_sat = Some(reverse_pair.limits.maximal);
2860                let swapper_feerate = Some(reverse_pair.fees.percentage);
2861
2862                debug!(
2863                    "Preparing Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat"
2864                );
2865
2866                Ok(PrepareReceiveResponse {
2867                    payment_method: req.payment_method.clone(),
2868                    amount: req.amount.clone(),
2869                    fees_sat,
2870                    min_payer_amount_sat,
2871                    max_payer_amount_sat,
2872                    swapper_feerate,
2873                })
2874            }
2875            PaymentMethod::Bolt12Offer => {
2876                if req.amount.is_some() {
2877                    return Err(PaymentError::generic(
2878                        "Amount cannot be set for this payment method",
2879                    ));
2880                }
2881
2882                let reverse_pair = self
2883                    .swapper
2884                    .get_reverse_swap_pairs()
2885                    .await?
2886                    .ok_or(PaymentError::PairsNotFound)?;
2887
2888                let fees_sat = reverse_pair.fees.total(0);
2889                debug!("Preparing Bolt12Offer Receive Swap with: min fees_sat {fees_sat}");
2890
2891                Ok(PrepareReceiveResponse {
2892                    payment_method: req.payment_method.clone(),
2893                    amount: req.amount.clone(),
2894                    fees_sat,
2895                    min_payer_amount_sat: Some(reverse_pair.limits.minimal),
2896                    max_payer_amount_sat: Some(reverse_pair.limits.maximal),
2897                    swapper_feerate: Some(reverse_pair.fees.percentage),
2898                })
2899            }
2900            PaymentMethod::BitcoinAddress => {
2901                let payer_amount_sat = match req.amount {
2902                    Some(ReceiveAmount::Asset { .. }) => {
2903                        return Err(PaymentError::asset_error(
2904                            "Asset cannot be received for this payment method",
2905                        ));
2906                    }
2907                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2908                    None => None,
2909                };
2910                let pair = self
2911                    .get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)
2912                    .await?;
2913                let claim_fees_sat = pair.fees.claim_estimate();
2914                let server_fees_sat = pair.fees.server();
2915                let service_fees_sat = payer_amount_sat
2916                    .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
2917                    .unwrap_or_default();
2918
2919                let fees_sat = service_fees_sat + claim_fees_sat + server_fees_sat;
2920                debug!("Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}");
2921
2922                Ok(PrepareReceiveResponse {
2923                    payment_method: req.payment_method.clone(),
2924                    amount: req.amount.clone(),
2925                    fees_sat,
2926                    min_payer_amount_sat: Some(pair.limits.minimal),
2927                    max_payer_amount_sat: Some(pair.limits.maximal),
2928                    swapper_feerate: Some(pair.fees.percentage),
2929                })
2930            }
2931            PaymentMethod::LiquidAddress => {
2932                let (asset_id, payer_amount, payer_amount_sat) = match req.amount.clone() {
2933                    Some(ReceiveAmount::Asset {
2934                        payer_amount,
2935                        asset_id,
2936                    }) => (asset_id, payer_amount, None),
2937                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2938                        (self.config.lbtc_asset_id(), None, Some(payer_amount_sat))
2939                    }
2940                    None => (self.config.lbtc_asset_id(), None, None),
2941                };
2942
2943                debug!("Preparing Liquid Receive with: asset_id {asset_id}, amount {payer_amount:?}, amount_sat {payer_amount_sat:?}");
2944
2945                Ok(PrepareReceiveResponse {
2946                    payment_method: req.payment_method.clone(),
2947                    amount: req.amount.clone(),
2948                    fees_sat: 0,
2949                    min_payer_amount_sat: None,
2950                    max_payer_amount_sat: None,
2951                    swapper_feerate: None,
2952                })
2953            }
2954        }
2955    }
2956
2957    /// Receive a Lightning payment via a reverse submarine swap, a chain swap or via direct Liquid
2958    /// payment.
2959    ///
2960    /// # Arguments
2961    ///
2962    /// * `req` - the [ReceivePaymentRequest] containing:
2963    ///     * `prepare_response` - the [PrepareReceiveResponse] from calling [LiquidSdk::prepare_receive_payment]
2964    ///     * `description` - the optional payment description
2965    ///     * `description_hash` - optional, whether to pass a custom description hash or to
2966    ///       calculate it from the `description` field
2967    ///     * `payer_note` - the optional payer note, typically included in a LNURL-Pay request
2968    ///
2969    /// # Returns
2970    ///
2971    /// * A [ReceivePaymentResponse] containing:
2972    ///     * `destination` - the final destination to be paid by the payer, either:
2973    ///        - a BIP21 URI (Liquid or Bitcoin)
2974    ///        - a Liquid address
2975    ///        - a BOLT11 invoice
2976    ///        - a BOLT12 offer
2977    pub async fn receive_payment(
2978        &self,
2979        req: &ReceivePaymentRequest,
2980    ) -> Result<ReceivePaymentResponse, PaymentError> {
2981        self.ensure_is_started().await?;
2982
2983        let PrepareReceiveResponse {
2984            payment_method,
2985            amount,
2986            fees_sat,
2987            ..
2988        } = req.prepare_response.clone();
2989
2990        match payment_method {
2991            #[allow(deprecated)]
2992            PaymentMethod::Bolt11Invoice => {
2993                let amount_sat = match amount.clone() {
2994                    Some(ReceiveAmount::Asset { .. }) => {
2995                        return Err(PaymentError::asset_error(
2996                            "Asset cannot be received for this payment method",
2997                        ));
2998                    }
2999                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
3000                    None => {
3001                        return Err(PaymentError::generic(
3002                            "Bitcoin payer amount must be set for this payment method",
3003                        ));
3004                    }
3005                };
3006
3007                let (description, description_hash) =
3008                    match (req.description.clone(), req.description_hash.clone()) {
3009                        (None, Some(description_hash)) => {
3010                            match description_hash {
3011                                DescriptionHash::UseDescription => return Err(
3012                                    PaymentError::InvalidDescription { err: "Cannot calculate payment description hash: no description provided".to_string() 
3013                                }),
3014                                DescriptionHash::Custom { hash } => (None, Some(hash)),
3015                            }
3016                        }
3017                        (Some(description), Some(description_hash)) => {
3018                            let calculated_hash =
3019                                sha256::Hash::hash(description.as_bytes()).to_hex();
3020                            match description_hash {
3021                                DescriptionHash::UseDescription => (None, Some(calculated_hash)),
3022                                DescriptionHash::Custom { hash } => {
3023                                    ensure_sdk!(
3024                                        calculated_hash == *hash,
3025                                        PaymentError::InvalidDescription {
3026                                            err: "Payment description hash mismatch".to_string()
3027                                        }
3028                                    );
3029                                    (None, Some(calculated_hash))
3030                                }
3031                            }
3032                        }
3033                        (description, None) => (description, None),
3034                    };
3035                self.create_bolt11_receive_swap(
3036                    amount_sat,
3037                    fees_sat,
3038                    description,
3039                    description_hash,
3040                    req.payer_note.clone(),
3041                )
3042                .await
3043            }
3044            PaymentMethod::Bolt12Offer => {
3045                let description = req.description.clone().unwrap_or("".to_string());
3046                match self
3047                    .persister
3048                    .fetch_bolt12_offer_by_description(&description)?
3049                {
3050                    Some(bolt12_offer) => Ok(ReceivePaymentResponse {
3051                        destination: bolt12_offer.id,
3052                        liquid_expiration_blockheight: None,
3053                        bitcoin_expiration_blockheight: None,
3054                    }),
3055                    None => self.create_bolt12_offer(description).await,
3056                }
3057            }
3058            PaymentMethod::BitcoinAddress => {
3059                let amount_sat = match amount.clone() {
3060                    Some(ReceiveAmount::Asset { .. }) => {
3061                        return Err(PaymentError::asset_error(
3062                            "Asset cannot be received for this payment method",
3063                        ));
3064                    }
3065                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
3066                    None => None,
3067                };
3068                self.receive_onchain(amount_sat, fees_sat).await
3069            }
3070            PaymentMethod::LiquidAddress => {
3071                let lbtc_asset_id = self.config.lbtc_asset_id();
3072                let (asset_id, amount, amount_sat) = match amount.clone() {
3073                    Some(ReceiveAmount::Asset {
3074                        asset_id,
3075                        payer_amount,
3076                    }) => (asset_id, payer_amount, None),
3077                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
3078                        (lbtc_asset_id.clone(), None, Some(payer_amount_sat))
3079                    }
3080                    None => (lbtc_asset_id.clone(), None, None),
3081                };
3082
3083                let address = self.onchain_wallet.next_unused_address().await?.to_string();
3084                let receive_destination =
3085                    if asset_id.ne(&lbtc_asset_id) || amount.is_some() || amount_sat.is_some() {
3086                        LiquidAddressData {
3087                            address: address.to_string(),
3088                            network: self.config.network.into(),
3089                            amount,
3090                            amount_sat,
3091                            asset_id: Some(asset_id),
3092                            label: None,
3093                            message: req.description.clone(),
3094                        }
3095                        .to_uri()
3096                        .map_err(|e| PaymentError::Generic {
3097                            err: format!("Could not build BIP21 URI: {e:?}"),
3098                        })?
3099                    } else {
3100                        address
3101                    };
3102
3103                Ok(ReceivePaymentResponse {
3104                    destination: receive_destination,
3105                    liquid_expiration_blockheight: None,
3106                    bitcoin_expiration_blockheight: None,
3107                })
3108            }
3109        }
3110    }
3111
3112    async fn create_bolt11_receive_swap(
3113        &self,
3114        payer_amount_sat: u64,
3115        fees_sat: u64,
3116        description: Option<String>,
3117        description_hash: Option<String>,
3118        payer_note: Option<String>,
3119    ) -> Result<ReceivePaymentResponse, PaymentError> {
3120        let reverse_pair = self
3121            .swapper
3122            .get_reverse_swap_pairs()
3123            .await?
3124            .ok_or(PaymentError::PairsNotFound)?;
3125        let new_fees_sat = reverse_pair.fees.total(payer_amount_sat);
3126        ensure_sdk!(fees_sat == new_fees_sat, PaymentError::InvalidOrExpiredFees);
3127
3128        debug!("Creating BOLT11 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
3129
3130        let keypair = utils::generate_keypair();
3131
3132        let preimage = Preimage::new();
3133        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3134        let preimage_hash = preimage.sha256.to_string();
3135
3136        // Address to be used for a BIP-21 direct payment
3137        let mrh_addr = self.onchain_wallet.next_unused_address().await?;
3138        // Signature of the claim public key of the SHA256 hash of the address for the direct payment
3139        let mrh_addr_str = mrh_addr.to_string();
3140        let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
3141
3142        let receiver_amount_sat = payer_amount_sat - fees_sat;
3143        let webhook_claim_status =
3144            match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3145                true => RevSwapStates::TransactionConfirmed,
3146                false => RevSwapStates::TransactionMempool,
3147            };
3148        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3149            url,
3150            hash_swap_id: Some(true),
3151            status: Some(vec![webhook_claim_status]),
3152        });
3153
3154        let v2_req = CreateReverseRequest {
3155            from: "BTC".to_string(),
3156            to: "L-BTC".to_string(),
3157            invoice: None,
3158            invoice_amount: Some(payer_amount_sat),
3159            preimage_hash: Some(preimage.sha256),
3160            claim_public_key: keypair.public_key().into(),
3161            description,
3162            description_hash,
3163            address: Some(mrh_addr_str.clone()),
3164            address_signature: Some(mrh_addr_hash_sig.to_hex()),
3165            referral_id: None,
3166            webhook,
3167        };
3168        let create_response = self.swapper.create_receive_swap(v2_req).await?;
3169        let invoice_str = create_response
3170            .invoice
3171            .clone()
3172            .ok_or(PaymentError::receive_error("Invoice not found"))?;
3173
3174        // Reserve this address until the timeout block height
3175        self.persister.insert_or_update_reserved_address(
3176            &mrh_addr_str,
3177            create_response.timeout_block_height,
3178        )?;
3179
3180        // Check if correct MRH was added to the invoice by Boltz
3181        let (bip21_lbtc_address, _bip21_amount_btc) = self
3182            .swapper
3183            .check_for_mrh(&invoice_str)
3184            .await?
3185            .ok_or(PaymentError::receive_error("Invoice has no MRH"))?;
3186        ensure_sdk!(
3187            bip21_lbtc_address == mrh_addr_str,
3188            PaymentError::receive_error("Invoice has incorrect address in MRH")
3189        );
3190
3191        let swap_id = create_response.id.clone();
3192        let invoice = Bolt11Invoice::from_str(&invoice_str)
3193            .map_err(|err| PaymentError::invalid_invoice(err.to_string()))?;
3194        let payer_amount_sat =
3195            invoice
3196                .amount_milli_satoshis()
3197                .ok_or(PaymentError::invalid_invoice(
3198                    "Invoice does not contain an amount",
3199                ))?
3200                / 1000;
3201        let destination_pubkey = invoice_pubkey(&invoice);
3202
3203        // Double check that the generated invoice includes our data
3204        // https://docs.boltz.exchange/v/api/dont-trust-verify#lightning-invoice-verification
3205        ensure_sdk!(
3206            invoice.payment_hash().to_string() == preimage_hash,
3207            PaymentError::invalid_invoice("Invalid preimage returned by swapper")
3208        );
3209
3210        let create_response_json = ReceiveSwap::from_boltz_struct_to_json(
3211            &create_response,
3212            &swap_id,
3213            Some(&invoice.to_string()),
3214        )?;
3215        let invoice_description = match invoice.description() {
3216            Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
3217            Bolt11InvoiceDescription::Hash(_) => None,
3218        };
3219
3220        self.persister
3221            .insert_or_update_receive_swap(&ReceiveSwap {
3222                id: swap_id.clone(),
3223                preimage: preimage_str,
3224                create_response_json,
3225                claim_private_key: keypair.display_secret().to_string(),
3226                invoice: invoice.to_string(),
3227                bolt12_offer: None,
3228                payment_hash: Some(preimage_hash),
3229                destination_pubkey: Some(destination_pubkey),
3230                timeout_block_height: create_response.timeout_block_height,
3231                description: invoice_description,
3232                payer_note,
3233                payer_amount_sat,
3234                receiver_amount_sat,
3235                pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3236                    PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3237                })?,
3238                claim_fees_sat: reverse_pair.fees.claim_estimate(),
3239                lockup_tx_id: None,
3240                claim_address: None,
3241                claim_tx_id: None,
3242                mrh_address: mrh_addr_str,
3243                mrh_tx_id: None,
3244                created_at: utils::now(),
3245                state: PaymentState::Created,
3246                metadata: Default::default(),
3247            })
3248            .map_err(|_| PaymentError::PersistError)?;
3249        self.status_stream.track_swap_id(&swap_id)?;
3250
3251        Ok(ReceivePaymentResponse {
3252            destination: invoice.to_string(),
3253            liquid_expiration_blockheight: Some(create_response.timeout_block_height),
3254            bitcoin_expiration_blockheight: None,
3255        })
3256    }
3257
3258    /// Create a BOLT12 invoice for a given BOLT12 offer and invoice request.
3259    ///
3260    /// # Arguments
3261    ///
3262    /// * `req` - the [CreateBolt12InvoiceRequest] containing:
3263    ///     * `offer` - the BOLT12 offer
3264    ///     * `invoice_request` - the invoice request created from the offer
3265    ///
3266    /// # Returns
3267    ///
3268    /// * A [CreateBolt12InvoiceResponse] containing:
3269    ///     * `invoice` - the BOLT12 invoice
3270    pub async fn create_bolt12_invoice(
3271        &self,
3272        req: &CreateBolt12InvoiceRequest,
3273    ) -> Result<CreateBolt12InvoiceResponse, PaymentError> {
3274        debug!("Started create BOLT12 invoice");
3275        let bolt12_offer =
3276            self.persister
3277                .fetch_bolt12_offer_by_id(&req.offer)?
3278                .ok_or(PaymentError::generic(format!(
3279                    "Bolt12 offer not found: {}",
3280                    req.offer
3281                )))?;
3282        // Get the CLN node public key from the offer
3283        let offer = Offer::try_from(bolt12_offer.clone())?;
3284        let cln_node_public_key = offer
3285            .paths()
3286            .iter()
3287            .find_map(|path| match path.introduction_node().clone() {
3288                IntroductionNode::NodeId(node_id) => Some(node_id),
3289                IntroductionNode::DirectedShortChannelId(_, _) => None,
3290            })
3291            .ok_or(PaymentError::generic(format!(
3292                "No BTC CLN node found: {}",
3293                req.offer
3294            )))?;
3295        let invoice_request = utils::bolt12::decode_invoice_request(&req.invoice_request)?;
3296        let payer_amount_sat = invoice_request
3297            .amount_msats()
3298            .map(|msats| msats / 1_000)
3299            .ok_or(PaymentError::amount_missing(
3300                "Invoice request must contain an amount",
3301            ))?;
3302        // Parellelize the calls to get_bolt12_params and get_reverse_swap_pairs
3303        let (params, maybe_reverse_pair) = tokio::try_join!(
3304            self.swapper.get_bolt12_params(),
3305            self.swapper.get_reverse_swap_pairs()
3306        )?;
3307        let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3308        reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
3309            PaymentError::AmountOutOfRange {
3310                min: reverse_pair.limits.minimal,
3311                max: reverse_pair.limits.maximal,
3312            }
3313        })?;
3314        let fees_sat = reverse_pair.fees.total(payer_amount_sat);
3315        debug!("Creating BOLT12 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
3316
3317        let secp = Secp256k1::new();
3318        let keypair = bolt12_offer.get_keypair()?;
3319        let preimage = Preimage::new();
3320        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3321        let preimage_hash = preimage.sha256.to_byte_array();
3322
3323        // Address to be used for a BIP-21 direct payment
3324        let mrh_addr = self.onchain_wallet.next_unused_address().await?;
3325        // Signature of the claim public key of the SHA256 hash of the address for the direct payment
3326        let mrh_addr_str = mrh_addr.to_string();
3327        let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
3328
3329        let entropy_source = RandomBytes::new(utils::generate_entropy());
3330        let nonce = Nonce::from_entropy_source(&entropy_source);
3331        let payer_note = invoice_request.payer_note().map(|s| s.to_string());
3332        let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
3333            offer_id: Offer::try_from(bolt12_offer)?.id(),
3334            invoice_request: InvoiceRequestFields {
3335                payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
3336                quantity: invoice_request.quantity(),
3337                payer_note_truncated: payer_note.clone().map(UntrustedString),
3338                human_readable_name: invoice_request.offer_from_hrn().clone(),
3339            },
3340        });
3341        let expanded_key = ExpandedKey::new(keypair.secret_key().secret_bytes());
3342        let payee_tlvs = UnauthenticatedReceiveTlvs {
3343            payment_secret: PaymentSecret(utils::generate_entropy()),
3344            payment_constraints: PaymentConstraints {
3345                max_cltv_expiry: 1_000_000,
3346                htlc_minimum_msat: 1,
3347            },
3348            payment_context,
3349        }
3350        .authenticate(nonce, &expanded_key);
3351
3352        // Configure the blinded payment path
3353        let payment_path = BlindedPaymentPath::one_hop(
3354            cln_node_public_key,
3355            payee_tlvs.clone(),
3356            params.min_cltv as u16,
3357            &entropy_source,
3358            &secp,
3359        )
3360        .map_err(|_| {
3361            PaymentError::generic(
3362                "Failed to create BOLT12 invoice: Error creating blinded payment path",
3363            )
3364        })?;
3365
3366        // Create the invoice
3367        let invoice = invoice_request
3368            .respond_with_no_std(
3369                vec![payment_path],
3370                PaymentHash(preimage_hash),
3371                SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| {
3372                    PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3373                })?,
3374            )?
3375            .build()?
3376            .sign(|unsigned_invoice: &UnsignedBolt12Invoice| {
3377                Ok(secp.sign_schnorr_no_aux_rand(unsigned_invoice.as_ref().as_digest(), &keypair))
3378            })
3379            .map_err(|e| {
3380                PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3381            })?;
3382        let invoice_str = encode_invoice(&invoice).map_err(|e| {
3383            PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3384        })?;
3385        debug!("Created BOLT12 invoice: {invoice_str}");
3386
3387        let claim_keypair = utils::generate_keypair();
3388        let receiver_amount_sat = payer_amount_sat - fees_sat;
3389        let webhook_claim_status =
3390            match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3391                true => RevSwapStates::TransactionConfirmed,
3392                false => RevSwapStates::TransactionMempool,
3393            };
3394        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3395            url,
3396            hash_swap_id: Some(true),
3397            status: Some(vec![webhook_claim_status]),
3398        });
3399
3400        let v2_req = CreateReverseRequest {
3401            from: "BTC".to_string(),
3402            to: "L-BTC".to_string(),
3403            invoice: Some(invoice_str.clone()),
3404            invoice_amount: None,
3405            preimage_hash: None,
3406            claim_public_key: claim_keypair.public_key().into(),
3407            description: None,
3408            description_hash: None,
3409            address: Some(mrh_addr_str.clone()),
3410            address_signature: Some(mrh_addr_hash_sig.to_hex()),
3411            referral_id: None,
3412            webhook,
3413        };
3414        let create_response = self.swapper.create_receive_swap(v2_req).await?;
3415
3416        // Reserve this address until the timeout block height
3417        self.persister.insert_or_update_reserved_address(
3418            &mrh_addr_str,
3419            create_response.timeout_block_height,
3420        )?;
3421
3422        let swap_id = create_response.id.clone();
3423        let destination_pubkey = cln_node_public_key.to_hex();
3424        debug!("Created receive swap: {swap_id}");
3425
3426        let create_response_json =
3427            ReceiveSwap::from_boltz_struct_to_json(&create_response, &swap_id, None)?;
3428        let invoice_description = invoice.description().map(|s| s.to_string());
3429
3430        self.persister
3431            .insert_or_update_receive_swap(&ReceiveSwap {
3432                id: swap_id.clone(),
3433                preimage: preimage_str,
3434                create_response_json,
3435                claim_private_key: claim_keypair.display_secret().to_string(),
3436                invoice: invoice_str.clone(),
3437                bolt12_offer: Some(req.offer.clone()),
3438                payment_hash: Some(preimage.sha256.to_string()),
3439                destination_pubkey: Some(destination_pubkey),
3440                timeout_block_height: create_response.timeout_block_height,
3441                description: invoice_description,
3442                payer_note,
3443                payer_amount_sat,
3444                receiver_amount_sat,
3445                pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3446                    PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3447                })?,
3448                claim_fees_sat: reverse_pair.fees.claim_estimate(),
3449                lockup_tx_id: None,
3450                claim_address: None,
3451                claim_tx_id: None,
3452                mrh_address: mrh_addr_str,
3453                mrh_tx_id: None,
3454                created_at: utils::now(),
3455                state: PaymentState::Created,
3456                metadata: Default::default(),
3457            })
3458            .map_err(|_| PaymentError::PersistError)?;
3459        self.status_stream.track_swap_id(&swap_id)?;
3460        debug!("Finished create BOLT12 invoice");
3461
3462        Ok(CreateBolt12InvoiceResponse {
3463            invoice: invoice_str,
3464        })
3465    }
3466
3467    async fn create_bolt12_offer(
3468        &self,
3469        description: String,
3470    ) -> Result<ReceivePaymentResponse, PaymentError> {
3471        let webhook_url = self.persister.get_webhook_url()?;
3472        // Parallelize the calls to get_nodes and get_reverse_swap_pairs
3473        let (nodes, maybe_reverse_pair) = tokio::try_join!(
3474            self.swapper.get_nodes(),
3475            self.swapper.get_reverse_swap_pairs()
3476        )?;
3477        let cln_node = nodes
3478            .get_btc_cln_node()
3479            .ok_or(PaymentError::generic("No BTC CLN node found"))?;
3480        debug!("Creating BOLT12 offer for description: {description}");
3481        let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3482        let min_amount_sat = reverse_pair.limits.minimal;
3483        let keypair = utils::generate_keypair();
3484        let entropy_source = RandomBytes::new(utils::generate_entropy());
3485        let secp = Secp256k1::new();
3486        let message_context = MessageContext::Offers(OffersContext::InvoiceRequest {
3487            nonce: Nonce::from_entropy_source(&entropy_source),
3488        });
3489
3490        // Build the offer with a one-hop blinded path to the swapper CLN node
3491        let offer = OfferBuilder::new(keypair.public_key())
3492            .chain(self.config.network.into())
3493            .amount_msats(min_amount_sat * 1_000)
3494            .description(description.clone())
3495            .path(
3496                BlindedMessagePath::one_hop(
3497                    cln_node.public_key,
3498                    message_context,
3499                    &entropy_source,
3500                    &secp,
3501                )
3502                .map_err(|_| {
3503                    PaymentError::generic(
3504                        "Error creating Bolt12 Offer: Could not create a one-hop blinded path",
3505                    )
3506                })?,
3507            )
3508            .build()?;
3509        let offer_str = utils::bolt12::encode_offer(&offer)?;
3510        info!("Created BOLT12 offer: {offer_str}");
3511        self.swapper
3512            .create_bolt12_offer(CreateBolt12OfferRequest {
3513                offer: offer_str.clone(),
3514                url: webhook_url.clone(),
3515            })
3516            .await?;
3517        // Store the bolt12 offer
3518        self.persister.insert_or_update_bolt12_offer(&Bolt12Offer {
3519            id: offer_str.clone(),
3520            description,
3521            private_key: keypair.display_secret().to_string(),
3522            webhook_url,
3523            created_at: utils::now(),
3524        })?;
3525        // Start tracking the offer with the status stream
3526        let subscribe_hash_sig = utils::sign_message_hash("SUBSCRIBE", &keypair)?;
3527        self.status_stream
3528            .track_offer(&offer_str, &subscribe_hash_sig.to_hex())?;
3529
3530        Ok(ReceivePaymentResponse {
3531            destination: offer_str,
3532            liquid_expiration_blockheight: None,
3533            bitcoin_expiration_blockheight: None,
3534        })
3535    }
3536
3537    async fn create_receive_chain_swap(
3538        &self,
3539        user_lockup_amount_sat: Option<u64>,
3540        fees_sat: u64,
3541    ) -> Result<ChainSwap, PaymentError> {
3542        let pair = self
3543            .get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)
3544            .await?;
3545        let claim_fees_sat = pair.fees.claim_estimate();
3546        let server_fees_sat = pair.fees.server();
3547        // Service fees are 0 if this is a zero-amount swap
3548        let service_fees_sat = user_lockup_amount_sat
3549            .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
3550            .unwrap_or_default();
3551
3552        ensure_sdk!(
3553            fees_sat == service_fees_sat + claim_fees_sat + server_fees_sat,
3554            PaymentError::InvalidOrExpiredFees
3555        );
3556
3557        let preimage = Preimage::new();
3558        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3559
3560        let claim_keypair = utils::generate_keypair();
3561        let claim_public_key = boltz_client::PublicKey {
3562            compressed: true,
3563            inner: claim_keypair.public_key(),
3564        };
3565        let refund_keypair = utils::generate_keypair();
3566        let refund_public_key = boltz_client::PublicKey {
3567            compressed: true,
3568            inner: refund_keypair.public_key(),
3569        };
3570        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3571            url,
3572            hash_swap_id: Some(true),
3573            status: Some(vec![
3574                ChainSwapStates::TransactionFailed,
3575                ChainSwapStates::TransactionLockupFailed,
3576                ChainSwapStates::TransactionServerConfirmed,
3577            ]),
3578        });
3579        let create_response = self
3580            .swapper
3581            .create_chain_swap(CreateChainRequest {
3582                from: "BTC".to_string(),
3583                to: "L-BTC".to_string(),
3584                preimage_hash: preimage.sha256,
3585                claim_public_key: Some(claim_public_key),
3586                refund_public_key: Some(refund_public_key),
3587                user_lock_amount: user_lockup_amount_sat,
3588                server_lock_amount: None,
3589                pair_hash: Some(pair.hash.clone()),
3590                referral_id: None,
3591                webhook,
3592            })
3593            .await?;
3594
3595        let swap_id = create_response.id.clone();
3596        let create_response_json =
3597            ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?;
3598
3599        let accept_zero_conf = user_lockup_amount_sat
3600            .map(|user_lockup_amount_sat| user_lockup_amount_sat <= pair.limits.maximal_zero_conf)
3601            .unwrap_or(false);
3602        let receiver_amount_sat = user_lockup_amount_sat
3603            .map(|user_lockup_amount_sat| user_lockup_amount_sat - fees_sat)
3604            .unwrap_or(0);
3605
3606        let swap = ChainSwap {
3607            id: swap_id.clone(),
3608            direction: Direction::Incoming,
3609            claim_address: None,
3610            lockup_address: create_response.lockup_details.lockup_address,
3611            refund_address: None,
3612            timeout_block_height: create_response.lockup_details.timeout_block_height,
3613            claim_timeout_block_height: create_response.claim_details.timeout_block_height,
3614            preimage: preimage_str,
3615            description: Some("Bitcoin transfer".to_string()),
3616            payer_amount_sat: user_lockup_amount_sat.unwrap_or(0),
3617            actual_payer_amount_sat: None,
3618            receiver_amount_sat,
3619            accepted_receiver_amount_sat: None,
3620            claim_fees_sat,
3621            pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
3622                PaymentError::generic(format!("Failed to serialize incoming ChainPair: {e:?}"))
3623            })?,
3624            accept_zero_conf,
3625            create_response_json,
3626            claim_private_key: claim_keypair.display_secret().to_string(),
3627            refund_private_key: refund_keypair.display_secret().to_string(),
3628            server_lockup_tx_id: None,
3629            user_lockup_tx_id: None,
3630            claim_tx_id: None,
3631            refund_tx_id: None,
3632            created_at: utils::now(),
3633            state: PaymentState::Created,
3634            auto_accepted_fees: false,
3635            metadata: Default::default(),
3636        };
3637        self.persister.insert_or_update_chain_swap(&swap)?;
3638        self.status_stream.track_swap_id(&swap.id)?;
3639        Ok(swap)
3640    }
3641
3642    /// Receive from a Bitcoin transaction via a chain swap.
3643    ///
3644    /// If no `user_lockup_amount_sat` is specified, this is an amountless swap and `fees_sat` exclude
3645    /// the service fees.
3646    async fn receive_onchain(
3647        &self,
3648        user_lockup_amount_sat: Option<u64>,
3649        fees_sat: u64,
3650    ) -> Result<ReceivePaymentResponse, PaymentError> {
3651        self.ensure_is_started().await?;
3652
3653        let swap = self
3654            .create_receive_chain_swap(user_lockup_amount_sat, fees_sat)
3655            .await?;
3656        let create_response = swap.get_boltz_create_response()?;
3657        let address = create_response.lockup_details.lockup_address;
3658
3659        let amount = create_response.lockup_details.amount as f64 / 100_000_000.0;
3660        let bip21 = create_response.lockup_details.bip21.unwrap_or(format!(
3661            "bitcoin:{address}?amount={amount}&label=Send%20to%20L-BTC%20address"
3662        ));
3663
3664        Ok(ReceivePaymentResponse {
3665            destination: bip21,
3666            liquid_expiration_blockheight: Some(swap.claim_timeout_block_height),
3667            bitcoin_expiration_blockheight: Some(swap.timeout_block_height),
3668        })
3669    }
3670
3671    /// List all failed chain swaps that need to be refunded.
3672    /// They can be refunded by calling [LiquidSdk::prepare_refund] then [LiquidSdk::refund].
3673    pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> {
3674        let chain_swaps = self.persister.list_refundable_chain_swaps()?;
3675
3676        let mut chain_swaps_with_scripts = vec![];
3677        for swap in &chain_swaps {
3678            let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
3679            chain_swaps_with_scripts.push((swap, script_pubkey));
3680        }
3681
3682        let lockup_scripts: Vec<&boltz_client::bitcoin::Script> = chain_swaps_with_scripts
3683            .iter()
3684            .map(|(_, script_pubkey)| script_pubkey.as_script())
3685            .collect();
3686        let scripts_utxos = self
3687            .bitcoin_chain_service
3688            .get_scripts_utxos(&lockup_scripts)
3689            .await?;
3690
3691        let mut script_to_utxos_map = std::collections::HashMap::new();
3692        for script_utxos in scripts_utxos {
3693            if let Some(first_utxo) = script_utxos.first() {
3694                if let Some((_, txo)) = first_utxo.as_bitcoin() {
3695                    let script_pubkey: boltz_client::bitcoin::ScriptBuf = txo.script_pubkey.clone();
3696                    script_to_utxos_map.insert(script_pubkey, script_utxos);
3697                }
3698            }
3699        }
3700
3701        let mut refundables = vec![];
3702
3703        for (chain_swap, script_pubkey) in chain_swaps_with_scripts {
3704            if let Some(script_utxos) = script_to_utxos_map.get(&script_pubkey) {
3705                let swap_id = &chain_swap.id;
3706                let amount_sat: u64 = script_utxos
3707                    .iter()
3708                    .filter_map(|utxo| utxo.as_bitcoin().cloned())
3709                    .map(|(_, txo)| txo.value.to_sat())
3710                    .sum();
3711                info!("Incoming Chain Swap {swap_id} is refundable with {amount_sat} sats");
3712
3713                refundables.push(chain_swap.to_refundable(amount_sat));
3714            }
3715        }
3716
3717        Ok(refundables)
3718    }
3719
3720    /// Prepares to refund a failed chain swap by calculating the refund transaction size and absolute fee.
3721    ///
3722    /// # Arguments
3723    ///
3724    /// * `req` - the [PrepareRefundRequest] containing:
3725    ///     * `swap_address` - the swap address to refund from [RefundableSwap::swap_address]
3726    ///     * `refund_address` - the Bitcoin address to refund to
3727    ///     * `fee_rate_sat_per_vbyte` - the fee rate at which to broadcast the refund transaction
3728    pub async fn prepare_refund(
3729        &self,
3730        req: &PrepareRefundRequest,
3731    ) -> SdkResult<PrepareRefundResponse> {
3732        let refund_address = self
3733            .validate_bitcoin_address(&req.refund_address)
3734            .await
3735            .map_err(|e| SdkError::Generic {
3736                err: format!("Failed to validate refund address: {e}"),
3737            })?;
3738
3739        let (tx_vsize, tx_fee_sat, refund_tx_id) = self
3740            .chain_swap_handler
3741            .prepare_refund(
3742                &req.swap_address,
3743                &refund_address,
3744                req.fee_rate_sat_per_vbyte,
3745            )
3746            .await?;
3747        Ok(PrepareRefundResponse {
3748            tx_vsize,
3749            tx_fee_sat,
3750            last_refund_tx_id: refund_tx_id,
3751        })
3752    }
3753
3754    /// Refund a failed chain swap.
3755    ///
3756    /// # Arguments
3757    ///
3758    /// * `req` - the [RefundRequest] containing:
3759    ///     * `swap_address` - the swap address to refund from [RefundableSwap::swap_address]
3760    ///     * `refund_address` - the Bitcoin address to refund to
3761    ///     * `fee_rate_sat_per_vbyte` - the fee rate at which to broadcast the refund transaction
3762    pub async fn refund(&self, req: &RefundRequest) -> Result<RefundResponse, PaymentError> {
3763        let refund_address = self
3764            .validate_bitcoin_address(&req.refund_address)
3765            .await
3766            .map_err(|e| SdkError::Generic {
3767                err: format!("Failed to validate refund address: {e}"),
3768            })?;
3769
3770        let refund_tx_id = self
3771            .chain_swap_handler
3772            .refund_incoming_swap(
3773                &req.swap_address,
3774                &refund_address,
3775                req.fee_rate_sat_per_vbyte,
3776                true,
3777            )
3778            .or_else(|e| {
3779                warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
3780                self.chain_swap_handler.refund_incoming_swap(
3781                    &req.swap_address,
3782                    &refund_address,
3783                    req.fee_rate_sat_per_vbyte,
3784                    false,
3785                )
3786            })
3787            .await?;
3788
3789        Ok(RefundResponse { refund_tx_id })
3790    }
3791
3792    /// Rescans all expired chain swaps created from calling [LiquidSdk::receive_onchain] to check
3793    /// if there are any confirmed funds available to refund.
3794    ///
3795    /// Since it bypasses the monitoring period, this should be called rarely or when the caller
3796    /// expects there is a very old refundable chain swap. Otherwise, for relatively recent swaps
3797    /// (within last [CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS] blocks = ~14 days), calling this
3798    /// is not necessary as it happens automatically in the background.
3799    pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> {
3800        let t0 = Instant::now();
3801        let mut rescannable_swaps: Vec<Swap> = self
3802            .persister
3803            .list_chain_swaps()?
3804            .into_iter()
3805            .map(Into::into)
3806            .collect();
3807        self.recoverer
3808            .recover_from_onchain(&mut rescannable_swaps, None)
3809            .await?;
3810        let scanned_len = rescannable_swaps.len();
3811        for swap in rescannable_swaps {
3812            let swap_id = &swap.id();
3813            if let Swap::Chain(chain_swap) = swap {
3814                if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
3815                    error!("Error persisting rescanned Chain Swap {swap_id}: {e}");
3816                }
3817            }
3818        }
3819        info!(
3820            "Rescanned {} chain swaps in {} seconds",
3821            scanned_len,
3822            t0.elapsed().as_millis()
3823        );
3824        Ok(())
3825    }
3826
3827    fn validate_buy_bitcoin(&self, amount_sat: u64) -> Result<(), PaymentError> {
3828        ensure_sdk!(
3829            self.config.network == LiquidNetwork::Mainnet,
3830            PaymentError::invalid_network("Can only buy bitcoin on Mainnet")
3831        );
3832        // The Moonpay API defines BTC amounts as having precision = 5, so only 5 decimals are considered
3833        ensure_sdk!(
3834            amount_sat.is_multiple_of(1_000),
3835            PaymentError::generic("Can only buy sat amounts that are multiples of 1000")
3836        );
3837        Ok(())
3838    }
3839
3840    /// Prepares to buy Bitcoin via a chain swap.
3841    ///
3842    /// # Arguments
3843    ///
3844    /// * `req` - the [PrepareBuyBitcoinRequest] containing:
3845    ///     * `provider` - the [BuyBitcoinProvider] to use
3846    ///     * `amount_sat` - the amount in satoshis to buy from the provider
3847    pub async fn prepare_buy_bitcoin(
3848        &self,
3849        req: &PrepareBuyBitcoinRequest,
3850    ) -> Result<PrepareBuyBitcoinResponse, PaymentError> {
3851        self.validate_buy_bitcoin(req.amount_sat)?;
3852
3853        let res = self
3854            .prepare_receive_payment(&PrepareReceiveRequest {
3855                payment_method: PaymentMethod::BitcoinAddress,
3856                amount: Some(ReceiveAmount::Bitcoin {
3857                    payer_amount_sat: req.amount_sat,
3858                }),
3859            })
3860            .await?;
3861
3862        let Some(ReceiveAmount::Bitcoin {
3863            payer_amount_sat: amount_sat,
3864        }) = res.amount
3865        else {
3866            return Err(PaymentError::Generic {
3867                err: format!(
3868                    "Error preparing receive payment, got amount: {:?}",
3869                    res.amount
3870                ),
3871            });
3872        };
3873
3874        Ok(PrepareBuyBitcoinResponse {
3875            provider: req.provider,
3876            amount_sat,
3877            fees_sat: res.fees_sat,
3878        })
3879    }
3880
3881    /// Generate a URL to a third party provider used to buy Bitcoin via a chain swap.
3882    ///
3883    /// # Arguments
3884    ///
3885    /// * `req` - the [BuyBitcoinRequest] containing:
3886    ///     * `prepare_response` - the [PrepareBuyBitcoinResponse] from calling [LiquidSdk::prepare_buy_bitcoin]
3887    ///     * `redirect_url` - the optional redirect URL the provider should redirect to after purchase
3888    pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> {
3889        self.validate_buy_bitcoin(req.prepare_response.amount_sat)?;
3890
3891        let swap = self
3892            .create_receive_chain_swap(
3893                Some(req.prepare_response.amount_sat),
3894                req.prepare_response.fees_sat,
3895            )
3896            .await?;
3897
3898        Ok(self
3899            .buy_bitcoin_service
3900            .buy_bitcoin(
3901                req.prepare_response.provider,
3902                &swap,
3903                req.redirect_url.clone(),
3904            )
3905            .await?)
3906    }
3907
3908    /// Returns a list of swaps that need to be monitored for recovery.
3909    ///
3910    /// If no Bitcoin tip is provided, chain swaps will not be considered.
3911    pub(crate) async fn get_monitored_swaps_list(
3912        &self,
3913        only_receive_swaps: bool,
3914        include_expired_incoming_chain_swaps: bool,
3915        chain_tips: ChainTips,
3916    ) -> Result<Vec<Swap>> {
3917        let receive_swaps = self
3918            .persister
3919            .list_recoverable_receive_swaps()?
3920            .into_iter()
3921            .map(Into::into)
3922            .collect();
3923
3924        if only_receive_swaps {
3925            return Ok(receive_swaps);
3926        }
3927
3928        let send_swaps = self
3929            .persister
3930            .list_recoverable_send_swaps()?
3931            .into_iter()
3932            .map(Into::into)
3933            .collect();
3934
3935        let Some(bitcoin_tip) = chain_tips.bitcoin_tip else {
3936            return Ok([receive_swaps, send_swaps].concat());
3937        };
3938
3939        let final_swap_states: [PaymentState; 2] = [PaymentState::Complete, PaymentState::Failed];
3940
3941        let chain_swaps: Vec<Swap> = self
3942            .persister
3943            .list_chain_swaps()?
3944            .into_iter()
3945            .filter(|swap| match swap.direction {
3946                Direction::Incoming => {
3947                    if include_expired_incoming_chain_swaps {
3948                        bitcoin_tip
3949                            <= swap.timeout_block_height
3950                                + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS
3951                            && chain_tips.liquid_tip
3952                                <= swap.claim_timeout_block_height
3953                                    + CHAIN_SWAP_MONITORING_PERIOD_LIQUID_BLOCKS
3954                    } else {
3955                        bitcoin_tip <= swap.timeout_block_height
3956                            && chain_tips.liquid_tip <= swap.claim_timeout_block_height
3957                    }
3958                }
3959                Direction::Outgoing => {
3960                    !final_swap_states.contains(&swap.state)
3961                        && chain_tips.liquid_tip <= swap.timeout_block_height
3962                        && bitcoin_tip <= swap.claim_timeout_block_height
3963                }
3964            })
3965            .map(Into::into)
3966            .collect();
3967
3968        Ok([receive_swaps, send_swaps, chain_swaps].concat())
3969    }
3970
3971    /// This method fetches the chain tx data (onchain and mempool) using LWK. For every wallet tx,
3972    /// it inserts or updates a corresponding entry in our Payments table.
3973    async fn sync_payments_with_chain_data(
3974        &self,
3975        mut recoverable_swaps: Vec<Swap>,
3976        chain_tips: ChainTips,
3977    ) -> Result<()> {
3978        debug!("LiquidSdk::sync_payments_with_chain_data: start");
3979        debug!(
3980            "LiquidSdk::sync_payments_with_chain_data: called with {} recoverable swaps",
3981            recoverable_swaps.len()
3982        );
3983        let mut wallet_tx_map = self
3984            .recoverer
3985            .recover_from_onchain(&mut recoverable_swaps, Some(chain_tips))
3986            .await?;
3987
3988        let all_wallet_tx_ids: HashSet<String> =
3989            wallet_tx_map.keys().map(|txid| txid.to_string()).collect();
3990
3991        for swap in recoverable_swaps {
3992            let swap_id = &swap.id();
3993
3994            // Update the payment wallet txs before updating the swap so the tx data is pulled into the payment
3995            match swap {
3996                Swap::Receive(receive_swap) => {
3997                    let history_updates = vec![&receive_swap.claim_tx_id, &receive_swap.mrh_tx_id];
3998                    for tx_id in history_updates
3999                        .into_iter()
4000                        .flatten()
4001                        .collect::<Vec<&String>>()
4002                    {
4003                        if let Some(tx) =
4004                            wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4005                        {
4006                            self.persister
4007                                .insert_or_update_payment_with_wallet_tx(&tx)?;
4008                        }
4009                    }
4010                    if let Err(e) = self.receive_swap_handler.update_swap(receive_swap) {
4011                        error!("Error persisting recovered receive swap {swap_id}: {e}");
4012                    }
4013                }
4014                Swap::Send(send_swap) => {
4015                    let history_updates = vec![&send_swap.lockup_tx_id, &send_swap.refund_tx_id];
4016                    for tx_id in history_updates
4017                        .into_iter()
4018                        .flatten()
4019                        .collect::<Vec<&String>>()
4020                    {
4021                        if let Some(tx) =
4022                            wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4023                        {
4024                            self.persister
4025                                .insert_or_update_payment_with_wallet_tx(&tx)?;
4026                        }
4027                    }
4028                    if let Err(e) = self.send_swap_handler.update_swap(send_swap) {
4029                        error!("Error persisting recovered send swap {swap_id}: {e}");
4030                    }
4031                }
4032                Swap::Chain(chain_swap) => {
4033                    let history_updates = match chain_swap.direction {
4034                        Direction::Incoming => vec![&chain_swap.claim_tx_id],
4035                        Direction::Outgoing => {
4036                            vec![&chain_swap.user_lockup_tx_id, &chain_swap.refund_tx_id]
4037                        }
4038                    };
4039                    for tx_id in history_updates
4040                        .into_iter()
4041                        .flatten()
4042                        .collect::<Vec<&String>>()
4043                    {
4044                        if let Some(tx) =
4045                            wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4046                        {
4047                            self.persister
4048                                .insert_or_update_payment_with_wallet_tx(&tx)?;
4049                        }
4050                    }
4051                    if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
4052                        error!("Error persisting recovered Chain Swap {swap_id}: {e}");
4053                    }
4054                }
4055            };
4056        }
4057
4058        let non_swap_wallet_tx_map = wallet_tx_map;
4059
4060        let payments = self
4061            .persister
4062            .get_payments_by_tx_id(&ListPaymentsRequest::default())?;
4063
4064        // We query only these that may need update, should be a fast query.
4065        let unconfirmed_payment_txs_data = self.persister.list_unconfirmed_payment_txs_data()?;
4066        let unconfirmed_txs_by_id: HashMap<String, PaymentTxData> = unconfirmed_payment_txs_data
4067            .into_iter()
4068            .map(|tx| (tx.tx_id.clone(), tx))
4069            .collect::<HashMap<String, PaymentTxData>>();
4070
4071        debug!(
4072            "Found {} unconfirmed payment txs",
4073            unconfirmed_txs_by_id.len()
4074        );
4075        for tx in non_swap_wallet_tx_map.values() {
4076            let tx_id = tx.txid.to_string();
4077            let maybe_payment = payments.get(&tx_id);
4078            let mut updated = false;
4079            match maybe_payment {
4080                // When no payment is found or its a Liquid payment
4081                None
4082                | Some(Payment {
4083                    details: PaymentDetails::Liquid { .. },
4084                    ..
4085                }) => {
4086                    let updated_needed = maybe_payment
4087                        .is_none_or(|payment| payment.status == Pending && tx.height.is_some());
4088                    if updated_needed {
4089                        // An unknown tx which needs inserting or a known Liquid payment tx
4090                        // that was in the mempool, but is now confirmed
4091                        self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
4092                        self.emit_payment_updated(Some(tx_id.clone())).await?;
4093                        updated = true
4094                    }
4095                }
4096
4097                _ => {}
4098            }
4099            if !updated && unconfirmed_txs_by_id.contains_key(&tx_id) && tx.height.is_some() {
4100                // An unconfirmed tx that was not found in the payments table
4101                self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
4102            }
4103        }
4104
4105        let unknown_unconfirmed_txs: Vec<_> = unconfirmed_txs_by_id
4106            .iter()
4107            .filter(|(txid, _)| !all_wallet_tx_ids.contains(*txid))
4108            .map(|(_, tx)| tx)
4109            .collect();
4110
4111        debug!(
4112            "Found {} unknown unconfirmed txs",
4113            unknown_unconfirmed_txs.len()
4114        );
4115        for unknown_unconfirmed_tx in unknown_unconfirmed_txs {
4116            if unknown_unconfirmed_tx.timestamp.is_some_and(|t| {
4117                (utils::now().saturating_sub(t)) > NETWORK_PROPAGATION_GRACE_PERIOD.as_secs() as u32
4118            }) {
4119                self.persister
4120                    .delete_payment_tx_data(&unknown_unconfirmed_tx.tx_id)?;
4121                info!(
4122                    "Found an unknown unconfirmed tx and deleted it. Txid: {}",
4123                    unknown_unconfirmed_tx.tx_id
4124                );
4125            } else {
4126                debug!(
4127                    "Found an unknown unconfirmed tx that was inserted at {:?}. \
4128                Keeping it to allow propagation through the network. Txid: {}",
4129                    unknown_unconfirmed_tx.timestamp, unknown_unconfirmed_tx.tx_id
4130                )
4131            }
4132        }
4133
4134        self.update_wallet_info().await?;
4135        debug!("LiquidSdk::sync_payments_with_chain_data: end");
4136        Ok(())
4137    }
4138
4139    async fn update_wallet_info(&self) -> Result<()> {
4140        let asset_metadata: HashMap<String, AssetMetadata> = self
4141            .persister
4142            .list_asset_metadata()?
4143            .into_iter()
4144            .map(|am| (am.asset_id.clone(), am))
4145            .collect();
4146        let transactions = self.onchain_wallet.transactions().await?;
4147        let tx_ids = transactions
4148            .iter()
4149            .map(|tx| tx.txid.to_string())
4150            .collect::<Vec<_>>();
4151        let asset_balances = transactions
4152            .into_iter()
4153            .fold(BTreeMap::<AssetId, i64>::new(), |mut acc, tx| {
4154                tx.balance.into_iter().for_each(|(asset_id, balance)| {
4155                    // Consider only confirmed unspent outputs (confirmed transactions output reduced by unconfirmed spent outputs)
4156                    if tx.height.is_some() || balance < 0 {
4157                        *acc.entry(asset_id).or_default() += balance;
4158                    }
4159                });
4160                acc
4161            })
4162            .into_iter()
4163            .map(|(asset_id, balance)| {
4164                let asset_id = asset_id.to_hex();
4165                let balance_sat = balance.unsigned_abs();
4166                let maybe_asset_metadata = asset_metadata.get(&asset_id);
4167                AssetBalance {
4168                    asset_id,
4169                    balance_sat,
4170                    name: maybe_asset_metadata.map(|am| am.name.clone()),
4171                    ticker: maybe_asset_metadata.map(|am| am.ticker.clone()),
4172                    balance: maybe_asset_metadata.map(|am| am.amount_from_sat(balance_sat)),
4173                }
4174            })
4175            .collect::<Vec<AssetBalance>>();
4176        let mut balance_sat = asset_balances
4177            .clone()
4178            .into_iter()
4179            .find(|ab| ab.asset_id.eq(&self.config.lbtc_asset_id()))
4180            .map_or(0, |ab| ab.balance_sat);
4181
4182        let mut pending_send_sat = 0;
4183        let mut pending_receive_sat = 0;
4184        let payments = self.persister.get_payments(&ListPaymentsRequest {
4185            states: Some(vec![
4186                PaymentState::Pending,
4187                PaymentState::RefundPending,
4188                PaymentState::WaitingFeeAcceptance,
4189            ]),
4190            ..Default::default()
4191        })?;
4192
4193        for payment in payments {
4194            let is_lbtc_asset_id = payment.details.is_lbtc_asset_id(self.config.network);
4195            match payment.payment_type {
4196                PaymentType::Send => match payment.details.get_refund_tx_amount_sat() {
4197                    Some(refund_tx_amount_sat) => pending_receive_sat += refund_tx_amount_sat,
4198                    None => {
4199                        let total_sat = if is_lbtc_asset_id {
4200                            payment.amount_sat + payment.fees_sat
4201                        } else {
4202                            payment.fees_sat
4203                        };
4204                        if let Some(tx_id) = payment.tx_id {
4205                            if !tx_ids.contains(&tx_id) {
4206                                debug!("Deducting {total_sat} sats from balance");
4207                                balance_sat = balance_sat.saturating_sub(total_sat);
4208                            }
4209                        }
4210                        pending_send_sat += total_sat
4211                    }
4212                },
4213                PaymentType::Receive => {
4214                    if is_lbtc_asset_id && payment.status != RefundPending {
4215                        pending_receive_sat += payment.amount_sat;
4216                    }
4217                }
4218            }
4219        }
4220
4221        debug!("Onchain wallet balance: {balance_sat} sats");
4222        let info_response = WalletInfo {
4223            balance_sat,
4224            pending_send_sat,
4225            pending_receive_sat,
4226            fingerprint: self.onchain_wallet.fingerprint()?,
4227            pubkey: self.onchain_wallet.pubkey()?,
4228            asset_balances,
4229        };
4230        self.persister.set_wallet_info(&info_response)
4231    }
4232
4233    /// Lists the SDK payments in reverse chronological order, from newest to oldest.
4234    /// The payments are determined based on onchain transactions and swaps.
4235    pub async fn list_payments(
4236        &self,
4237        req: &ListPaymentsRequest,
4238    ) -> Result<Vec<Payment>, PaymentError> {
4239        self.ensure_is_started().await?;
4240
4241        Ok(self.persister.get_payments(req)?)
4242    }
4243
4244    /// Retrieves a payment.
4245    ///
4246    /// # Arguments
4247    ///
4248    /// * `req` - the [GetPaymentRequest] containing:
4249    ///     * [GetPaymentRequest::Lightning] - the `payment_hash` of the lightning invoice
4250    ///
4251    /// # Returns
4252    ///
4253    /// Returns an `Option<Payment>` if found, or `None` if no payment matches the given request.
4254    pub async fn get_payment(
4255        &self,
4256        req: &GetPaymentRequest,
4257    ) -> Result<Option<Payment>, PaymentError> {
4258        self.ensure_is_started().await?;
4259
4260        Ok(self.persister.get_payment_by_request(req)?)
4261    }
4262
4263    /// Fetches an up-to-date fees proposal for a [Payment] that is [WaitingFeeAcceptance].
4264    ///
4265    /// Use [LiquidSdk::accept_payment_proposed_fees] to accept the proposed fees and proceed
4266    /// with the payment.
4267    pub async fn fetch_payment_proposed_fees(
4268        &self,
4269        req: &FetchPaymentProposedFeesRequest,
4270    ) -> SdkResult<FetchPaymentProposedFeesResponse> {
4271        let chain_swap =
4272            self.persister
4273                .fetch_chain_swap_by_id(&req.swap_id)?
4274                .ok_or(SdkError::Generic {
4275                    err: format!("Could not find Swap {}", req.swap_id),
4276                })?;
4277
4278        ensure_sdk!(
4279            chain_swap.state == WaitingFeeAcceptance,
4280            SdkError::Generic {
4281                err: "Payment is not WaitingFeeAcceptance".to_string()
4282            }
4283        );
4284
4285        let server_lockup_quote = self
4286            .swapper
4287            .get_zero_amount_chain_swap_quote(&req.swap_id)
4288            .await?;
4289
4290        let actual_payer_amount_sat =
4291            chain_swap
4292                .actual_payer_amount_sat
4293                .ok_or(SdkError::Generic {
4294                    err: "No actual payer amount found when state is WaitingFeeAcceptance"
4295                        .to_string(),
4296                })?;
4297        let fees_sat =
4298            actual_payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat;
4299
4300        Ok(FetchPaymentProposedFeesResponse {
4301            swap_id: req.swap_id.clone(),
4302            fees_sat,
4303            payer_amount_sat: actual_payer_amount_sat,
4304            receiver_amount_sat: actual_payer_amount_sat - fees_sat,
4305        })
4306    }
4307
4308    /// Accepts proposed fees for a [Payment] that is [WaitingFeeAcceptance].
4309    ///
4310    /// Use [LiquidSdk::fetch_payment_proposed_fees] to get an up-to-date fees proposal.
4311    pub async fn accept_payment_proposed_fees(
4312        &self,
4313        req: &AcceptPaymentProposedFeesRequest,
4314    ) -> Result<(), PaymentError> {
4315        let FetchPaymentProposedFeesResponse {
4316            swap_id,
4317            fees_sat,
4318            payer_amount_sat,
4319            ..
4320        } = req.clone().response;
4321
4322        let chain_swap =
4323            self.persister
4324                .fetch_chain_swap_by_id(&swap_id)?
4325                .ok_or(SdkError::Generic {
4326                    err: format!("Could not find Swap {swap_id}"),
4327                })?;
4328
4329        ensure_sdk!(
4330            chain_swap.state == WaitingFeeAcceptance,
4331            PaymentError::Generic {
4332                err: "Payment is not WaitingFeeAcceptance".to_string()
4333            }
4334        );
4335
4336        let server_lockup_quote = self
4337            .swapper
4338            .get_zero_amount_chain_swap_quote(&swap_id)
4339            .await?;
4340
4341        ensure_sdk!(
4342            fees_sat == payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat,
4343            PaymentError::InvalidOrExpiredFees
4344        );
4345
4346        self.persister
4347            .update_accepted_receiver_amount(&swap_id, Some(payer_amount_sat - fees_sat))?;
4348        self.swapper
4349            .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())
4350            .inspect_err(|e| {
4351                error!("Failed to accept zero-amount swap {swap_id} quote: {e} - trying to erase the accepted receiver amount...");
4352                let _ = self
4353                    .persister
4354                    .update_accepted_receiver_amount(&swap_id, None);
4355            }).await?;
4356        self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
4357            swap_id,
4358            to_state: Pending,
4359            ..Default::default()
4360        })
4361    }
4362
4363    /// Empties the Liquid Wallet cache for the [Config::network].
4364    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4365    pub fn empty_wallet_cache(&self) -> Result<()> {
4366        let mut path = PathBuf::from(self.config.working_dir.clone());
4367        path.push(Into::<lwk_wollet::ElementsNetwork>::into(self.config.network).as_str());
4368        path.push("enc_cache");
4369
4370        std::fs::remove_dir_all(&path)?;
4371        std::fs::create_dir_all(path)?;
4372
4373        Ok(())
4374    }
4375
4376    /// Synchronizes the local state with the mempool and onchain data.
4377    pub async fn sync(&self, partial_sync: bool) -> SdkResult<()> {
4378        let blockchain_info = self.get_info().await?.blockchain_info;
4379        let sync_context = self
4380            .get_sync_context(GetSyncContextRequest {
4381                partial_sync: Some(partial_sync),
4382                last_liquid_tip: blockchain_info.liquid_tip,
4383                last_bitcoin_tip: blockchain_info.bitcoin_tip,
4384            })
4385            .await?;
4386
4387        self.sync_inner(
4388            sync_context.recoverable_swaps,
4389            ChainTips {
4390                liquid_tip: sync_context.maybe_liquid_tip.ok_or(SdkError::Generic {
4391                    err: "Liquid tip not available".to_string(),
4392                })?,
4393                bitcoin_tip: sync_context.maybe_bitcoin_tip,
4394            },
4395        )
4396        .await
4397    }
4398
4399    /// Computes the sync context.
4400    ///
4401    /// # Arguments
4402    /// * `partial_sync` - if not provided, this will infer it based on the last known tips.
4403    /// * `last_liquid_tip` - the last known liquid tip
4404    /// * `last_bitcoin_tip` - the last known bitcoin tip
4405    ///
4406    /// # Returns
4407    /// * `maybe_liquid_tip` - the current liquid tip, or `None` if the liquid tip could not be fetched
4408    /// * `maybe_bitcoin_tip` - the current bitcoin tip, or `None` if the bitcoin tip could not be fetched
4409    /// * `recoverable_swaps` - the recoverable swaps, which are built using the last known bitcoin tip. If
4410    ///   the bitcoin tip could not be fetched, this won't include chain swaps. If the liquid tip could not be fetched,
4411    ///   this will be an empty vector.
4412    /// * `is_new_liquid_block` - true if the liquid tip is new
4413    /// * `is_new_bitcoin_block` - true if the bitcoin tip is new
4414    async fn get_sync_context(&self, req: GetSyncContextRequest) -> SdkResult<SyncContext> {
4415        // Get the liquid tip
4416        let t0 = Instant::now();
4417        let liquid_tip = match self.liquid_chain_service.tip().await {
4418            Ok(tip) => Some(tip),
4419            Err(e) => {
4420                error!("Failed to fetch liquid tip: {e}");
4421                None
4422            }
4423        };
4424        let duration_ms = Instant::now().duration_since(t0).as_millis();
4425        if liquid_tip.is_some() {
4426            info!("Fetched liquid tip in ({duration_ms} ms)");
4427        }
4428
4429        let is_new_liquid_block = liquid_tip.is_some_and(|lt| lt > req.last_liquid_tip);
4430
4431        // Get the recoverable swaps assuming full sync if partial sync is not provided
4432        let mut recoverable_swaps = self
4433            .get_monitored_swaps_list(
4434                req.partial_sync.unwrap_or(false),
4435                true,
4436                ChainTips {
4437                    liquid_tip: liquid_tip.unwrap_or(req.last_liquid_tip),
4438                    bitcoin_tip: Some(req.last_bitcoin_tip),
4439                },
4440            )
4441            .await?;
4442
4443        // Only fetch the bitcoin tip if there is a new liquid block and
4444        // there are chain swaps being monitored
4445        let bitcoin_tip = if !is_new_liquid_block {
4446            debug!("No new liquid block, skipping bitcoin tip fetch");
4447            None
4448        } else if recoverable_swaps
4449            .iter()
4450            .any(|s| matches!(s, Swap::Chain(_)))
4451            .not()
4452        {
4453            debug!("No chain swaps being monitored, skipping bitcoin tip fetch");
4454            None
4455        } else {
4456            // Get the bitcoin tip
4457            let t0 = Instant::now();
4458            let bitcoin_tip = match self.bitcoin_chain_service.tip().await {
4459                Ok(tip) => Some(tip),
4460                Err(e) => {
4461                    error!("Failed to fetch bitcoin tip: {e}");
4462                    None
4463                }
4464            };
4465            let duration_ms = Instant::now().duration_since(t0).as_millis();
4466            if bitcoin_tip.is_some() {
4467                info!("Fetched bitcoin tip in ({duration_ms} ms)");
4468            } else {
4469                recoverable_swaps.retain(|s| !matches!(s, Swap::Chain(_)));
4470            }
4471            bitcoin_tip
4472        };
4473
4474        let is_new_bitcoin_block = bitcoin_tip.is_some_and(|bt| bt > req.last_bitcoin_tip);
4475
4476        // Update the recoverable swaps if we previously didn't know if this is a partial sync or not
4477        // No liquid tip means there's no point in returning recoverable swaps
4478        if let Some(liquid_tip) = liquid_tip {
4479            if req.partial_sync.is_none() {
4480                let only_receive_swaps = !is_new_liquid_block && !is_new_bitcoin_block;
4481                let include_expired_incoming_chain_swaps = is_new_bitcoin_block;
4482
4483                recoverable_swaps = self
4484                    .get_monitored_swaps_list(
4485                        only_receive_swaps,
4486                        include_expired_incoming_chain_swaps,
4487                        ChainTips {
4488                            liquid_tip,
4489                            bitcoin_tip,
4490                        },
4491                    )
4492                    .await?;
4493            }
4494        } else {
4495            recoverable_swaps = Vec::new();
4496        }
4497
4498        Ok(SyncContext {
4499            maybe_liquid_tip: liquid_tip,
4500            maybe_bitcoin_tip: bitcoin_tip,
4501            recoverable_swaps,
4502            is_new_liquid_block,
4503            is_new_bitcoin_block,
4504        })
4505    }
4506
4507    async fn sync_inner(
4508        &self,
4509        recoverable_swaps: Vec<Swap>,
4510        chain_tips: ChainTips,
4511    ) -> SdkResult<()> {
4512        debug!(
4513            "LiquidSdk::sync_inner called with {} recoverable swaps",
4514            recoverable_swaps.len()
4515        );
4516        self.ensure_is_started().await?;
4517
4518        let t0 = Instant::now();
4519
4520        self.onchain_wallet.full_scan().await.map_err(|err| {
4521            error!("Failed to scan wallet: {err:?}");
4522            SdkError::generic(err.to_string())
4523        })?;
4524
4525        let is_first_sync = !self
4526            .persister
4527            .get_is_first_sync_complete()?
4528            .unwrap_or(false);
4529        match is_first_sync {
4530            true => {
4531                self.event_manager.pause_notifications();
4532                self.sync_payments_with_chain_data(recoverable_swaps, chain_tips)
4533                    .await?;
4534                self.event_manager.resume_notifications();
4535                self.persister.set_is_first_sync_complete(true)?;
4536            }
4537            false => {
4538                self.sync_payments_with_chain_data(recoverable_swaps, chain_tips)
4539                    .await?;
4540            }
4541        }
4542        let duration_ms = Instant::now().duration_since(t0).as_millis();
4543        info!("Synchronized with mempool and onchain data ({duration_ms} ms)");
4544
4545        self.notify_event_listeners(SdkEvent::Synced).await;
4546        Ok(())
4547    }
4548
4549    /// Backup the local state to the provided backup path.
4550    ///
4551    /// # Arguments
4552    ///
4553    /// * `req` - the [BackupRequest] containing:
4554    ///     * `backup_path` - the optional backup path. Defaults to [Config::working_dir]
4555    pub fn backup(&self, req: BackupRequest) -> Result<()> {
4556        let backup_path = req
4557            .backup_path
4558            .map(PathBuf::from)
4559            .unwrap_or(self.persister.get_default_backup_path());
4560        self.persister.backup(backup_path)
4561    }
4562
4563    /// Restores the local state from the provided backup path.
4564    ///
4565    /// # Arguments
4566    ///
4567    /// * `req` - the [RestoreRequest] containing:
4568    ///     * `backup_path` - the optional backup path. Defaults to [Config::working_dir]
4569    pub fn restore(&self, req: RestoreRequest) -> Result<()> {
4570        let backup_path = req
4571            .backup_path
4572            .map(PathBuf::from)
4573            .unwrap_or(self.persister.get_default_backup_path());
4574        ensure_sdk!(
4575            backup_path.exists(),
4576            SdkError::generic("Backup file does not exist").into()
4577        );
4578        self.persister.restore_from_backup(backup_path)
4579    }
4580
4581    /// Prepares to pay to an LNURL encoded pay request or lightning address.
4582    ///
4583    /// This is the second step of LNURL-pay flow. The first step is [LiquidSdk::parse], which also validates the LNURL
4584    /// destination and generates the [LnUrlPayRequest] payload needed here.
4585    ///
4586    /// This call will validate the `amount_msat` and `comment` parameters of `req` against the parameters
4587    /// of the LNURL endpoint (`req_data`). If they match the endpoint requirements, a [PrepareSendResponse] is
4588    /// prepared for the invoice. If the receiver has encoded a Magic Routing Hint in the invoice, the
4589    /// [PrepareSendResponse]'s `fees_sat` will reflect this.
4590    ///
4591    /// # Arguments
4592    ///
4593    /// * `req` - the [PrepareLnUrlPayRequest] containing:
4594    ///     * `data` - the [LnUrlPayRequestData] returned by [LiquidSdk::parse]
4595    ///     * `amount` - the [PayAmount] to send:
4596    ///        - [PayAmount::Drain] which uses all Bitcoin funds
4597    ///        - [PayAmount::Bitcoin] which sets the amount in satoshi that will be received
4598    ///     * `bip353_address` - a BIP353 address, in case one was used in order to fetch the LNURL
4599    ///       Pay request data. Returned by [parse].
4600    ///     * `comment` - an optional comment LUD-12 to be stored with the payment. The comment is included in the
4601    ///       invoice request sent to the LNURL endpoint.
4602    ///     * `validate_success_action_url` - validates that, if there is a URL success action, the URL domain matches
4603    ///       the LNURL callback domain. Defaults to 'true'.
4604    ///
4605    /// # Returns
4606    /// Returns a [PrepareLnUrlPayResponse] containing:
4607    ///     * `destination` - the destination of the payment
4608    ///     * `amount` - the [PayAmount] to send
4609    ///     * `fees_sat` - the fees in satoshis to send the payment
4610    ///     * `data` - the [LnUrlPayRequestData] returned by [parse]
4611    ///     * `comment` - an optional comment for this payment
4612    ///     * `success_action` - the optional unprocessed LUD-09 success action
4613    pub async fn prepare_lnurl_pay(
4614        &self,
4615        req: PrepareLnUrlPayRequest,
4616    ) -> Result<PrepareLnUrlPayResponse, LnUrlPayError> {
4617        let amount_msat = match req.amount {
4618            PayAmount::Drain => {
4619                let get_info_res = self
4620                    .get_info()
4621                    .await
4622                    .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
4623                ensure_sdk!(
4624                    get_info_res.wallet_info.pending_receive_sat == 0
4625                        && get_info_res.wallet_info.pending_send_sat == 0,
4626                    LnUrlPayError::Generic {
4627                        err: "Cannot drain while there are pending payments".to_string(),
4628                    }
4629                );
4630                let lbtc_pair = self
4631                    .swapper
4632                    .get_submarine_pairs()
4633                    .await?
4634                    .ok_or(PaymentError::PairsNotFound)?;
4635                let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
4636                let drain_amount_sat = get_info_res.wallet_info.balance_sat - drain_fees_sat;
4637                // Get the inverse receiver amount by calculating a dummy amount then increment up to the drain amount
4638                let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
4639                let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
4640                let receiver_amount_sat = utils::increment_receiver_amount_up_to_drain_amount(
4641                    dummy_amount_sat,
4642                    &lbtc_pair,
4643                    drain_amount_sat,
4644                );
4645                lbtc_pair
4646                    .limits
4647                    .within(receiver_amount_sat)
4648                    .map_err(|e| LnUrlPayError::Generic { err: e.message() })?;
4649                // Validate if we can actually drain the wallet with a swap
4650                let pair_fees_sat = lbtc_pair.fees.total(receiver_amount_sat);
4651                ensure_sdk!(
4652                    receiver_amount_sat + pair_fees_sat == drain_amount_sat,
4653                    LnUrlPayError::Generic {
4654                        err: "Cannot drain without leaving a remainder".to_string(),
4655                    }
4656                );
4657
4658                receiver_amount_sat * 1000
4659            }
4660            PayAmount::Bitcoin {
4661                receiver_amount_sat,
4662            } => receiver_amount_sat * 1000,
4663            PayAmount::Asset { .. } => {
4664                return Err(LnUrlPayError::Generic {
4665                    err: "Cannot send an asset to a Bitcoin address".to_string(),
4666                })
4667            }
4668        };
4669
4670        match validate_lnurl_pay(
4671            self.rest_client.as_ref(),
4672            amount_msat,
4673            &req.comment,
4674            &req.data,
4675            self.config.network.into(),
4676            req.validate_success_action_url,
4677        )
4678        .await?
4679        {
4680            ValidatedCallbackResponse::EndpointError { data } => {
4681                Err(LnUrlPayError::Generic { err: data.reason })
4682            }
4683            ValidatedCallbackResponse::EndpointSuccess { data } => {
4684                let prepare_response = self
4685                    .prepare_send_payment(&PrepareSendRequest {
4686                        destination: data.pr.clone(),
4687                        amount: Some(req.amount.clone()),
4688                        disable_mrh: None,
4689                        payment_timeout_sec: None,
4690                    })
4691                    .await?;
4692
4693                let destination = match prepare_response.destination {
4694                    SendDestination::Bolt11 { invoice, .. } => SendDestination::Bolt11 {
4695                        invoice,
4696                        bip353_address: req.bip353_address,
4697                    },
4698                    SendDestination::LiquidAddress { address_data, .. } => {
4699                        SendDestination::LiquidAddress {
4700                            address_data,
4701                            bip353_address: req.bip353_address,
4702                        }
4703                    }
4704                    destination => destination,
4705                };
4706                let fees_sat = prepare_response
4707                    .fees_sat
4708                    .ok_or(PaymentError::InsufficientFunds)?;
4709
4710                Ok(PrepareLnUrlPayResponse {
4711                    destination,
4712                    fees_sat,
4713                    data: req.data,
4714                    amount: req.amount,
4715                    comment: req.comment,
4716                    success_action: data.success_action,
4717                })
4718            }
4719        }
4720    }
4721
4722    /// Pay to an LNURL encoded pay request or lightning address.
4723    ///
4724    /// The final step of LNURL-pay flow, called after preparing the payment with [LiquidSdk::prepare_lnurl_pay].
4725    /// This call sends the payment using the [PrepareLnUrlPayResponse]'s `prepare_send_response` either via
4726    /// Lightning or directly to a Liquid address if a Magic Routing Hint is included in the invoice.
4727    /// Once the payment is made, the [PrepareLnUrlPayResponse]'s `success_action` is processed decrypting
4728    /// the AES data if needed.
4729    ///
4730    /// # Arguments
4731    ///
4732    /// * `req` - the [LnUrlPayRequest] containing:
4733    ///     * `prepare_response` - the [PrepareLnUrlPayResponse] returned by [LiquidSdk::prepare_lnurl_pay]
4734    pub async fn lnurl_pay(
4735        &self,
4736        req: model::LnUrlPayRequest,
4737    ) -> Result<LnUrlPayResult, LnUrlPayError> {
4738        let prepare_response = req.prepare_response;
4739        let mut payment = self
4740            .send_payment(&SendPaymentRequest {
4741                prepare_response: PrepareSendResponse {
4742                    destination: prepare_response.destination.clone(),
4743                    fees_sat: Some(prepare_response.fees_sat),
4744                    estimated_asset_fees: None,
4745                    exchange_amount_sat: None,
4746                    amount: Some(prepare_response.amount),
4747                    disable_mrh: None,
4748                    payment_timeout_sec: None,
4749                },
4750                use_asset_fees: None,
4751                payer_note: prepare_response.comment.clone(),
4752            })
4753            .await?
4754            .payment;
4755
4756        let maybe_sa_processed: Option<SuccessActionProcessed> = match prepare_response
4757            .success_action
4758            .clone()
4759        {
4760            Some(sa) => {
4761                match sa {
4762                    // For AES, we decrypt the contents if the preimage is available
4763                    SuccessAction::Aes { data } => {
4764                        let PaymentDetails::Lightning {
4765                            swap_id, preimage, ..
4766                        } = &payment.details
4767                        else {
4768                            return Err(LnUrlPayError::Generic {
4769                                err: format!("Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.", payment.details),
4770                            });
4771                        };
4772
4773                        match preimage {
4774                            Some(preimage_str) => {
4775                                debug!(
4776                                    "Decrypting AES success action with preimage for Send Swap {swap_id}"
4777                                );
4778                                let preimage =
4779                                    sha256::Hash::from_str(preimage_str).map_err(|_| {
4780                                        LnUrlPayError::Generic {
4781                                            err: "Invalid preimage".to_string(),
4782                                        }
4783                                    })?;
4784                                let preimage_arr = preimage.to_byte_array();
4785                                let result = match (data, &preimage_arr).try_into() {
4786                                    Ok(data) => AesSuccessActionDataResult::Decrypted { data },
4787                                    Err(e) => AesSuccessActionDataResult::ErrorStatus {
4788                                        reason: e.to_string(),
4789                                    },
4790                                };
4791                                Some(SuccessActionProcessed::Aes { result })
4792                            }
4793                            None => {
4794                                debug!("Preimage not yet available to decrypt AES success action for Send Swap {swap_id}");
4795                                None
4796                            }
4797                        }
4798                    }
4799                    SuccessAction::Message { data } => {
4800                        Some(SuccessActionProcessed::Message { data })
4801                    }
4802                    SuccessAction::Url { data } => Some(SuccessActionProcessed::Url { data }),
4803                }
4804            }
4805            None => None,
4806        };
4807
4808        let description = payment
4809            .details
4810            .get_description()
4811            .or_else(|| extract_description_from_metadata(&prepare_response.data));
4812
4813        let lnurl_pay_domain = match prepare_response.data.ln_address {
4814            Some(_) => None,
4815            None => Some(prepare_response.data.domain),
4816        };
4817        if let (Some(tx_id), Some(destination)) =
4818            (payment.tx_id.clone(), payment.destination.clone())
4819        {
4820            self.persister
4821                .insert_or_update_payment_details(PaymentTxDetails {
4822                    tx_id: tx_id.clone(),
4823                    destination,
4824                    description,
4825                    lnurl_info: Some(LnUrlInfo {
4826                        ln_address: prepare_response.data.ln_address,
4827                        lnurl_pay_comment: prepare_response.comment,
4828                        lnurl_pay_domain,
4829                        lnurl_pay_metadata: Some(prepare_response.data.metadata_str),
4830                        lnurl_pay_success_action: maybe_sa_processed.clone(),
4831                        lnurl_pay_unprocessed_success_action: prepare_response.success_action,
4832                        lnurl_withdraw_endpoint: None,
4833                    }),
4834                    ..Default::default()
4835                })?;
4836            // Get the payment with the lnurl_info details
4837            payment = self.persister.get_payment(&tx_id)?.unwrap_or(payment);
4838        }
4839
4840        Ok(LnUrlPayResult::EndpointSuccess {
4841            data: model::LnUrlPaySuccessData {
4842                payment,
4843                success_action: maybe_sa_processed,
4844            },
4845        })
4846    }
4847
4848    /// Second step of LNURL-withdraw. The first step is [LiquidSdk::parse], which also validates the LNURL destination
4849    /// and generates the [LnUrlWithdrawRequest] payload needed here.
4850    ///
4851    /// This call will validate the given `amount_msat` against the parameters
4852    /// of the LNURL endpoint (`data`). If they match the endpoint requirements, the LNURL withdraw
4853    /// request is made. A successful result here means the endpoint started the payment.
4854    pub async fn lnurl_withdraw(
4855        &self,
4856        req: LnUrlWithdrawRequest,
4857    ) -> Result<LnUrlWithdrawResult, LnUrlWithdrawError> {
4858        let prepare_response = self
4859            .prepare_receive_payment(&{
4860                PrepareReceiveRequest {
4861                    payment_method: PaymentMethod::Bolt11Invoice,
4862                    amount: Some(ReceiveAmount::Bitcoin {
4863                        payer_amount_sat: req.amount_msat / 1_000,
4864                    }),
4865                }
4866            })
4867            .await?;
4868        let receive_res = self
4869            .receive_payment(&ReceivePaymentRequest {
4870                prepare_response,
4871                description: req.description.clone(),
4872                description_hash: None,
4873                payer_note: None,
4874            })
4875            .await?;
4876
4877        let Ok(invoice) = parse_invoice(&receive_res.destination) else {
4878            return Err(LnUrlWithdrawError::Generic {
4879                err: "Received unexpected output from receive request".to_string(),
4880            });
4881        };
4882
4883        let res =
4884            validate_lnurl_withdraw(self.rest_client.as_ref(), req.data.clone(), invoice.clone())
4885                .await?;
4886        if let LnUrlWithdrawResult::Ok { data: _ } = res {
4887            if let Some(ReceiveSwap {
4888                claim_tx_id: Some(tx_id),
4889                ..
4890            }) = self
4891                .persister
4892                .fetch_receive_swap_by_invoice(&invoice.bolt11)?
4893            {
4894                self.persister
4895                    .insert_or_update_payment_details(PaymentTxDetails {
4896                        tx_id,
4897                        destination: receive_res.destination,
4898                        description: req.description,
4899                        lnurl_info: Some(LnUrlInfo {
4900                            lnurl_withdraw_endpoint: Some(req.data.callback),
4901                            ..Default::default()
4902                        }),
4903                        ..Default::default()
4904                    })?;
4905            }
4906        }
4907        Ok(res)
4908    }
4909
4910    /// Third and last step of LNURL-auth. The first step is [LiquidSdk::parse], which also validates the LNURL destination
4911    /// and generates the [LnUrlAuthRequestData] payload needed here. The second step is user approval of auth action.
4912    ///
4913    /// This call will sign `k1` of the LNURL endpoint (`req_data`) on `secp256k1` using `linkingPrivKey` and DER-encodes the signature.
4914    /// If they match the endpoint requirements, the LNURL auth request is made. A successful result here means the client signature is verified.
4915    pub async fn lnurl_auth(
4916        &self,
4917        req_data: LnUrlAuthRequestData,
4918    ) -> Result<LnUrlCallbackStatus, LnUrlAuthError> {
4919        Ok(perform_lnurl_auth(
4920            self.rest_client.as_ref(),
4921            &req_data,
4922            &SdkLnurlAuthSigner::new(self.signer.clone()),
4923        )
4924        .await?)
4925    }
4926
4927    /// Register for webhook callbacks at the given `webhook_url`. Each created swap after registering the
4928    /// webhook will include the `webhook_url`.
4929    ///
4930    /// This method should be called every time the application is started and when the `webhook_url` changes.
4931    /// For example, if the `webhook_url` contains a push notification token and the token changes after
4932    /// the application was started, then this method should be called to register for callbacks at
4933    /// the new correct `webhook_url`. To unregister a webhook call [LiquidSdk::unregister_webhook].
4934    pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> {
4935        info!("Registering for webhook notifications");
4936        self.persister.set_webhook_url(webhook_url.clone())?;
4937
4938        // Update all BOLT12 offers where the webhook URL is different
4939        let bolt12_offers = self.persister.list_bolt12_offers()?;
4940        for mut bolt12_offer in bolt12_offers {
4941            if bolt12_offer
4942                .webhook_url
4943                .clone()
4944                .is_none_or(|url| url != webhook_url)
4945            {
4946                let keypair = bolt12_offer.get_keypair()?;
4947                let webhook_url_hash_sig = utils::sign_message_hash(&webhook_url, &keypair)?;
4948                self.swapper
4949                    .update_bolt12_offer(UpdateBolt12OfferRequest {
4950                        offer: bolt12_offer.id.clone(),
4951                        url: Some(webhook_url.clone()),
4952                        signature: webhook_url_hash_sig.to_hex(),
4953                    })
4954                    .await?;
4955                bolt12_offer.webhook_url = Some(webhook_url.clone());
4956                self.persister
4957                    .insert_or_update_bolt12_offer(&bolt12_offer)?;
4958            }
4959        }
4960
4961        Ok(())
4962    }
4963
4964    /// Unregister webhook callbacks. Each swap already created will continue to use the registered
4965    /// `webhook_url` until complete.
4966    ///
4967    /// This can be called when callbacks are no longer needed or the `webhook_url`
4968    /// has changed such that it needs unregistering. For example, the token is valid but the locale changes.
4969    /// To register a webhook call [LiquidSdk::register_webhook].
4970    pub async fn unregister_webhook(&self) -> SdkResult<()> {
4971        info!("Unregistering for webhook notifications");
4972        let maybe_old_webhook_url = self.persister.get_webhook_url()?;
4973
4974        self.persister.remove_webhook_url()?;
4975
4976        // Update all bolt12 offers that were created with the old webhook URL
4977        if let Some(old_webhook_url) = maybe_old_webhook_url {
4978            let bolt12_offers = self
4979                .persister
4980                .list_bolt12_offers_by_webhook_url(&old_webhook_url)?;
4981            for mut bolt12_offer in bolt12_offers {
4982                let keypair = bolt12_offer.get_keypair()?;
4983                let update_hash_sig = utils::sign_message_hash("UPDATE", &keypair)?;
4984                self.swapper
4985                    .update_bolt12_offer(UpdateBolt12OfferRequest {
4986                        offer: bolt12_offer.id.clone(),
4987                        url: None,
4988                        signature: update_hash_sig.to_hex(),
4989                    })
4990                    .await?;
4991                bolt12_offer.webhook_url = None;
4992                self.persister
4993                    .insert_or_update_bolt12_offer(&bolt12_offer)?;
4994            }
4995        }
4996
4997        Ok(())
4998    }
4999
5000    /// Fetch live rates of fiat currencies, sorted by name.
5001    pub async fn fetch_fiat_rates(&self) -> Result<Vec<Rate>, SdkError> {
5002        self.fiat_api.fetch_fiat_rates().await.map_err(Into::into)
5003    }
5004
5005    /// List all supported fiat currencies for which there is a known exchange rate.
5006    /// List is sorted by the canonical name of the currency.
5007    pub async fn list_fiat_currencies(&self) -> Result<Vec<FiatCurrency>, SdkError> {
5008        self.fiat_api
5009            .list_fiat_currencies()
5010            .await
5011            .map_err(Into::into)
5012    }
5013
5014    /// Get the recommended BTC fees based on the configured mempool.space instance.
5015    pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
5016        Ok(self.bitcoin_chain_service.recommended_fees().await?)
5017    }
5018
5019    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
5020    /// Get the full default [Config] for specific [LiquidNetwork].
5021    pub fn default_config(
5022        network: LiquidNetwork,
5023        breez_api_key: Option<String>,
5024    ) -> Result<Config, SdkError> {
5025        let config = match network {
5026            LiquidNetwork::Mainnet => Config::mainnet_esplora(breez_api_key),
5027            LiquidNetwork::Testnet => Config::testnet_esplora(breez_api_key),
5028            LiquidNetwork::Regtest => Config::regtest_esplora(),
5029        };
5030
5031        Ok(config)
5032    }
5033
5034    /// Parses a string into an [InputType]. See [input_parser::parse].
5035    ///
5036    /// Can optionally be configured to use external input parsers by providing `external_input_parsers` in [Config].
5037    pub async fn parse(&self, input: &str) -> Result<InputType, PaymentError> {
5038        let external_parsers = &self.external_input_parsers;
5039        let input_type =
5040            parse_with_rest_client(self.rest_client.as_ref(), input, Some(external_parsers))
5041                .await
5042                .map_err(|e| PaymentError::generic(e.to_string()))?;
5043
5044        let res = match input_type {
5045            InputType::LiquidAddress { ref address } => match &address.asset_id {
5046                Some(asset_id) if asset_id.ne(&self.config.lbtc_asset_id()) => {
5047                    let asset_metadata = self.persister.get_asset_metadata(asset_id)?.ok_or(
5048                        PaymentError::AssetError {
5049                            err: format!("Asset {asset_id} is not supported"),
5050                        },
5051                    )?;
5052                    let mut address = address.clone();
5053                    address.set_amount_precision(asset_metadata.precision.into());
5054                    InputType::LiquidAddress { address }
5055                }
5056                _ => input_type,
5057            },
5058            _ => input_type,
5059        };
5060        Ok(res)
5061    }
5062
5063    /// Parses a string into an [LNInvoice]. See [invoice::parse_invoice].
5064    pub fn parse_invoice(input: &str) -> Result<LNInvoice, PaymentError> {
5065        parse_invoice(input).map_err(|e| PaymentError::invalid_invoice(e.to_string()))
5066    }
5067
5068    /// Configures a global SDK logger that will log to file and will forward log events to
5069    /// an optional application-specific logger.
5070    ///
5071    /// If called, it should be called before any SDK methods (for example, before `connect`).
5072    ///
5073    /// It must be called only once in the application lifecycle. Alternatively, If the application
5074    /// already uses a globally-registered logger, this method shouldn't be called at all.
5075    ///
5076    /// ### Arguments
5077    ///
5078    /// - `log_dir`: Location where the the SDK log file will be created. The directory must already exist.
5079    ///
5080    /// - `app_logger`: Optional application logger.
5081    ///
5082    /// If the application is to use it's own logger, but would also like the SDK to log SDK-specific
5083    /// log output to a file in the configured `log_dir`, then do not register the
5084    /// app-specific logger as a global logger and instead call this method with the app logger as an arg.
5085    ///
5086    /// ### Errors
5087    ///
5088    /// An error is thrown if the log file cannot be created in the working directory.
5089    ///
5090    /// An error is thrown if a global logger is already configured.
5091    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
5092    pub fn init_logging(log_dir: &str, app_logger: Option<Box<dyn log::Log>>) -> Result<()> {
5093        crate::logger::init_logging(log_dir, app_logger)
5094    }
5095
5096    async fn start_plugin_inner(self: &Arc<Self>, plugin: &Arc<dyn Plugin>) -> SdkResult<()> {
5097        let plugin_id = plugin.id();
5098        let plugin_passphrase = self
5099            .signer
5100            .hmac_sha256(plugin_id.as_bytes().to_vec(), "m/49'/1'/0'/0/0".to_string())
5101            .map_err(|err| {
5102                SdkError::generic(format!("Could not generate plugin passphrase: {err}"))
5103            })?;
5104        let storage = PluginStorage::new(
5105            Arc::downgrade(&self.persister),
5106            &plugin_passphrase,
5107            plugin.id(),
5108        )?;
5109        plugin
5110            .on_start(PluginSdk::new(Arc::downgrade(self)), storage)
5111            .await;
5112        Ok(())
5113    }
5114
5115    pub async fn start_plugin(self: &Arc<Self>, plugin: Arc<dyn Plugin>) -> SdkResult<()> {
5116        let plugin_id = plugin.id();
5117        let mut plugins = self.plugins.lock().await;
5118        if plugins.get(&plugin_id).is_some() {
5119            return Err(SdkError::generic(format!(
5120                "Plugin {plugin_id} is already running"
5121            )));
5122        }
5123        plugins.insert(plugin_id, plugin.clone());
5124        self.start_plugin_inner(&plugin).await?;
5125        Ok(())
5126    }
5127}
5128
5129/// Extracts `description` from `metadata_str`
5130fn extract_description_from_metadata(request_data: &LnUrlPayRequestData) -> Option<String> {
5131    let metadata = request_data.metadata_vec().ok()?;
5132    metadata
5133        .iter()
5134        .find(|item| item.key == "text/plain")
5135        .map(|item| {
5136            info!("Extracted payment description: '{}'", item.value);
5137            item.value.clone()
5138        })
5139}
5140
5141#[cfg(test)]
5142mod tests {
5143    use std::time::Duration;
5144    use std::{str::FromStr, sync::Arc};
5145
5146    use anyhow::{anyhow, Result};
5147    use boltz_client::{
5148        boltz::{self, TransactionInfo},
5149        swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates},
5150        Secp256k1,
5151    };
5152    use lwk_wollet::{bitcoin::Network, hashes::hex::DisplayHex as _};
5153    use sdk_common::{
5154        bitcoin::hashes::hex::ToHex,
5155        lightning_with_bolt12::{
5156            ln::{channelmanager::PaymentId, inbound_payment::ExpandedKey},
5157            offers::{nonce::Nonce, offer::Offer},
5158            sign::RandomBytes,
5159            util::ser::Writeable,
5160        },
5161    };
5162    use tokio_with_wasm::alias as tokio;
5163
5164    use crate::test_utils::swapper::ZeroAmountSwapMockConfig;
5165    use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX;
5166    use crate::utils;
5167    use crate::{
5168        bitcoin, elements,
5169        model::{BtcHistory, Direction, LBtcHistory, PaymentState, Swap},
5170        sdk::LiquidSdk,
5171        test_utils::{
5172            chain::{MockBitcoinChainService, MockLiquidChainService},
5173            chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX},
5174            persist::{create_persister, new_receive_swap, new_send_swap},
5175            sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services},
5176            status_stream::MockStatusStream,
5177            swapper::MockSwapper,
5178        },
5179    };
5180    use crate::{
5181        model::CreateBolt12InvoiceRequest,
5182        test_utils::chain_swap::{
5183            TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX, TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX,
5184            TEST_LIQUID_OUTGOING_USER_LOCKUP_TX,
5185        },
5186    };
5187    use paste::paste;
5188
5189    #[cfg(feature = "browser-tests")]
5190    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
5191
5192    struct NewSwapArgs {
5193        direction: Direction,
5194        accepts_zero_conf: bool,
5195        initial_payment_state: Option<PaymentState>,
5196        receiver_amount_sat: Option<u64>,
5197        user_lockup_tx_id: Option<String>,
5198        zero_amount: bool,
5199        set_actual_payer_amount: bool,
5200    }
5201
5202    impl Default for NewSwapArgs {
5203        fn default() -> Self {
5204            Self {
5205                accepts_zero_conf: false,
5206                initial_payment_state: None,
5207                direction: Direction::Outgoing,
5208                receiver_amount_sat: None,
5209                user_lockup_tx_id: None,
5210                zero_amount: false,
5211                set_actual_payer_amount: false,
5212            }
5213        }
5214    }
5215
5216    impl NewSwapArgs {
5217        pub fn set_direction(mut self, direction: Direction) -> Self {
5218            self.direction = direction;
5219            self
5220        }
5221
5222        pub fn set_accepts_zero_conf(mut self, accepts_zero_conf: bool) -> Self {
5223            self.accepts_zero_conf = accepts_zero_conf;
5224            self
5225        }
5226
5227        pub fn set_receiver_amount_sat(mut self, receiver_amount_sat: Option<u64>) -> Self {
5228            self.receiver_amount_sat = receiver_amount_sat;
5229            self
5230        }
5231
5232        pub fn set_user_lockup_tx_id(mut self, user_lockup_tx_id: Option<String>) -> Self {
5233            self.user_lockup_tx_id = user_lockup_tx_id;
5234            self
5235        }
5236
5237        pub fn set_initial_payment_state(mut self, payment_state: PaymentState) -> Self {
5238            self.initial_payment_state = Some(payment_state);
5239            self
5240        }
5241
5242        pub fn set_zero_amount(mut self, zero_amount: bool) -> Self {
5243            self.zero_amount = zero_amount;
5244            self
5245        }
5246
5247        pub fn set_set_actual_payer_amount(mut self, set_actual_payer_amount: bool) -> Self {
5248            self.set_actual_payer_amount = set_actual_payer_amount;
5249            self
5250        }
5251    }
5252
5253    macro_rules! trigger_swap_update {
5254        (
5255            $type:literal,
5256            $args:expr,
5257            $persister:expr,
5258            $status_stream:expr,
5259            $status:expr,
5260            $transaction:expr,
5261            $zero_conf_rejected:expr
5262        ) => {{
5263            let swap = match $type {
5264                "chain" => {
5265                    let swap = new_chain_swap(
5266                        $args.direction,
5267                        $args.initial_payment_state,
5268                        $args.accepts_zero_conf,
5269                        $args.user_lockup_tx_id,
5270                        $args.zero_amount,
5271                        $args.set_actual_payer_amount,
5272                        $args.receiver_amount_sat,
5273                    );
5274                    $persister.insert_or_update_chain_swap(&swap).unwrap();
5275                    Swap::Chain(swap)
5276                }
5277                "send" => {
5278                    let swap =
5279                        new_send_swap($args.initial_payment_state, $args.receiver_amount_sat);
5280                    $persister.insert_or_update_send_swap(&swap).unwrap();
5281                    Swap::Send(swap)
5282                }
5283                "receive" => {
5284                    let swap =
5285                        new_receive_swap($args.initial_payment_state, $args.receiver_amount_sat);
5286                    $persister.insert_or_update_receive_swap(&swap).unwrap();
5287                    Swap::Receive(swap)
5288                }
5289                _ => panic!(),
5290            };
5291
5292            $status_stream
5293                .clone()
5294                .send_mock_update(boltz::SwapStatus {
5295                    id: swap.id(),
5296                    status: $status.to_string(),
5297                    transaction: $transaction,
5298                    zero_conf_rejected: $zero_conf_rejected,
5299                    ..Default::default()
5300                })
5301                .await
5302                .unwrap();
5303
5304            paste! {
5305                $persister.[<fetch _ $type _swap_by_id>](&swap.id())
5306                    .unwrap()
5307                    .ok_or(anyhow!("Could not retrieve {} swap", $type))
5308                    .unwrap()
5309            }
5310        }};
5311    }
5312
5313    #[sdk_macros::async_test_all]
5314    async fn test_receive_swap_update_tracking() -> Result<()> {
5315        create_persister!(persister);
5316        let swapper = Arc::new(MockSwapper::default());
5317        let status_stream = Arc::new(MockStatusStream::new());
5318        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5319        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5320
5321        let sdk = new_liquid_sdk_with_chain_services(
5322            persister.clone(),
5323            swapper.clone(),
5324            status_stream.clone(),
5325            liquid_chain_service.clone(),
5326            bitcoin_chain_service.clone(),
5327            None,
5328        )
5329        .await?;
5330
5331        LiquidSdk::track_swap_updates(&sdk);
5332
5333        // We spawn a new thread since updates can only be sent when called via async runtimes
5334        tokio::spawn(async move {
5335            // Verify the swap becomes invalid after final states are received
5336            let unrecoverable_states: [RevSwapStates; 4] = [
5337                RevSwapStates::SwapExpired,
5338                RevSwapStates::InvoiceExpired,
5339                RevSwapStates::TransactionFailed,
5340                RevSwapStates::TransactionRefunded,
5341            ];
5342
5343            for status in unrecoverable_states {
5344                let persisted_swap = trigger_swap_update!(
5345                    "receive",
5346                    NewSwapArgs::default(),
5347                    persister,
5348                    status_stream,
5349                    status,
5350                    None,
5351                    None
5352                );
5353                assert_eq!(persisted_swap.state, PaymentState::Failed);
5354            }
5355
5356            // Check that `TransactionMempool` and `TransactionConfirmed` correctly trigger the claim,
5357            // which in turn sets the `claim_tx_id`
5358            for status in [
5359                RevSwapStates::TransactionMempool,
5360                RevSwapStates::TransactionConfirmed,
5361            ] {
5362                let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
5363                let mock_tx_id = mock_tx.txid();
5364                let height = (serde_json::to_string(&status).unwrap()
5365                    == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
5366                    as i32;
5367                liquid_chain_service.set_history(vec![LBtcHistory {
5368                    txid: mock_tx_id,
5369                    height,
5370                }]);
5371
5372                let persisted_swap = trigger_swap_update!(
5373                    "receive",
5374                    NewSwapArgs::default(),
5375                    persister,
5376                    status_stream,
5377                    status,
5378                    Some(TransactionInfo {
5379                        id: mock_tx_id.to_string(),
5380                        hex: Some(
5381                            lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
5382                        ),
5383                        eta: None,
5384                    }),
5385                    None
5386                );
5387                assert!(persisted_swap.claim_tx_id.is_some());
5388            }
5389
5390            // Check that `TransactionMempool` and `TransactionConfirmed` checks the lockup amount
5391            // and doesn't claim if not verified
5392            for status in [
5393                RevSwapStates::TransactionMempool,
5394                RevSwapStates::TransactionConfirmed,
5395            ] {
5396                let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
5397                let mock_tx_id = mock_tx.txid();
5398                let height = (serde_json::to_string(&status).unwrap()
5399                    == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
5400                    as i32;
5401                liquid_chain_service.set_history(vec![LBtcHistory {
5402                    txid: mock_tx_id,
5403                    height,
5404                }]);
5405
5406                let persisted_swap = trigger_swap_update!(
5407                    "receive",
5408                    NewSwapArgs::default().set_receiver_amount_sat(Some(1000)),
5409                    persister,
5410                    status_stream,
5411                    status,
5412                    Some(TransactionInfo {
5413                        id: mock_tx_id.to_string(),
5414                        hex: Some(
5415                            lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
5416                        ),
5417                        eta: None
5418                    }),
5419                    None
5420                );
5421                assert!(persisted_swap.claim_tx_id.is_none());
5422            }
5423        })
5424        .await
5425        .unwrap();
5426
5427        Ok(())
5428    }
5429
5430    #[sdk_macros::async_test_all]
5431    async fn test_send_swap_update_tracking() -> Result<()> {
5432        create_persister!(persister);
5433        let swapper = Arc::new(MockSwapper::default());
5434        let status_stream = Arc::new(MockStatusStream::new());
5435
5436        let sdk = Arc::new(
5437            new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?,
5438        );
5439
5440        LiquidSdk::track_swap_updates(&sdk);
5441
5442        // We spawn a new thread since updates can only be sent when called via async runtimes
5443        tokio::spawn(async move {
5444            // Verify the swap becomes invalid after final states are received
5445            let unrecoverable_states: [SubSwapStates; 3] = [
5446                SubSwapStates::TransactionLockupFailed,
5447                SubSwapStates::InvoiceFailedToPay,
5448                SubSwapStates::SwapExpired,
5449            ];
5450
5451            for status in unrecoverable_states {
5452                let persisted_swap = trigger_swap_update!(
5453                    "send",
5454                    NewSwapArgs::default(),
5455                    persister,
5456                    status_stream,
5457                    status,
5458                    None,
5459                    None
5460                );
5461                assert_eq!(persisted_swap.state, PaymentState::Failed);
5462            }
5463
5464            // Verify that `TransactionClaimPending` correctly sets the state to `Complete`
5465            // and stores the preimage
5466            let persisted_swap = trigger_swap_update!(
5467                "send",
5468                NewSwapArgs::default(),
5469                persister,
5470                status_stream,
5471                SubSwapStates::TransactionClaimPending,
5472                None,
5473                None
5474            );
5475            assert_eq!(persisted_swap.state, PaymentState::Complete);
5476            assert!(persisted_swap.preimage.is_some());
5477        })
5478        .await
5479        .unwrap();
5480
5481        Ok(())
5482    }
5483
5484    #[sdk_macros::async_test_all]
5485    async fn test_chain_swap_update_tracking() -> Result<()> {
5486        create_persister!(persister);
5487        let swapper = Arc::new(MockSwapper::default());
5488        let status_stream = Arc::new(MockStatusStream::new());
5489        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5490        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5491
5492        let sdk = new_liquid_sdk_with_chain_services(
5493            persister.clone(),
5494            swapper.clone(),
5495            status_stream.clone(),
5496            liquid_chain_service.clone(),
5497            bitcoin_chain_service.clone(),
5498            None,
5499        )
5500        .await?;
5501
5502        LiquidSdk::track_swap_updates(&sdk);
5503
5504        // We spawn a new thread since updates can only be sent when called via async runtimes
5505        tokio::spawn(async move {
5506            let trigger_failed: [ChainSwapStates; 3] = [
5507                ChainSwapStates::TransactionFailed,
5508                ChainSwapStates::SwapExpired,
5509                ChainSwapStates::TransactionRefunded,
5510            ];
5511
5512            // Checks that work for both incoming and outgoing chain swaps
5513            for direction in [Direction::Incoming, Direction::Outgoing] {
5514                // Verify the swap becomes invalid after final states are received
5515                for status in &trigger_failed {
5516                    let persisted_swap = trigger_swap_update!(
5517                        "chain",
5518                        NewSwapArgs::default().set_direction(direction),
5519                        persister,
5520                        status_stream,
5521                        status,
5522                        None,
5523                        None
5524                    );
5525                    assert_eq!(persisted_swap.state, PaymentState::Failed);
5526                }
5527
5528                let (mock_user_lockup_tx_hex, mock_user_lockup_tx_id) = match direction {
5529                    Direction::Outgoing => {
5530                        let tx = TEST_LIQUID_OUTGOING_USER_LOCKUP_TX.clone();
5531                        (
5532                            lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5533                            tx.txid().to_string(),
5534                        )
5535                    }
5536                    Direction::Incoming => {
5537                        let tx = TEST_BITCOIN_INCOMING_USER_LOCKUP_TX.clone();
5538                        (
5539                            sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5540                            tx.txid().to_string(),
5541                        )
5542                    }
5543                };
5544
5545                let (mock_server_lockup_tx_hex, mock_server_lockup_tx_id) = match direction {
5546                    Direction::Incoming => {
5547                        let tx = TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX.clone();
5548                        (
5549                            lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5550                            tx.txid().to_string(),
5551                        )
5552                    }
5553                    Direction::Outgoing => {
5554                        let tx = TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX.clone();
5555                        (
5556                            sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5557                            tx.txid().to_string(),
5558                        )
5559                    }
5560                };
5561
5562                // Verify that `TransactionLockupFailed` correctly sets the state as
5563                // `RefundPending`/`Refundable` or as `Failed` depending on whether or not
5564                // `user_lockup_tx_id` is present
5565                for user_lockup_tx_id in &[None, Some(mock_user_lockup_tx_id.clone())] {
5566                    if let Some(user_lockup_tx_id) = user_lockup_tx_id {
5567                        match direction {
5568                            Direction::Incoming => {
5569                                bitcoin_chain_service.set_history(vec![BtcHistory {
5570                                    txid: bitcoin::Txid::from_str(user_lockup_tx_id).unwrap(),
5571                                    height: 0,
5572                                }]);
5573                            }
5574                            Direction::Outgoing => {
5575                                liquid_chain_service.set_history(vec![LBtcHistory {
5576                                    txid: elements::Txid::from_str(user_lockup_tx_id).unwrap(),
5577                                    height: 0,
5578                                }]);
5579                            }
5580                        }
5581                    }
5582                    let persisted_swap = trigger_swap_update!(
5583                        "chain",
5584                        NewSwapArgs::default()
5585                            .set_direction(direction)
5586                            .set_initial_payment_state(PaymentState::Pending)
5587                            .set_user_lockup_tx_id(user_lockup_tx_id.clone()),
5588                        persister,
5589                        status_stream,
5590                        ChainSwapStates::TransactionLockupFailed,
5591                        None,
5592                        None
5593                    );
5594                    let expected_state = if user_lockup_tx_id.is_some() {
5595                        match direction {
5596                            Direction::Incoming => PaymentState::Refundable,
5597                            Direction::Outgoing => PaymentState::RefundPending,
5598                        }
5599                    } else {
5600                        PaymentState::Failed
5601                    };
5602                    assert_eq!(persisted_swap.state, expected_state);
5603                }
5604
5605                // Verify that `TransactionMempool` and `TransactionConfirmed` correctly set
5606                // `user_lockup_tx_id` and `accept_zero_conf`
5607                for status in [
5608                    ChainSwapStates::TransactionMempool,
5609                    ChainSwapStates::TransactionConfirmed,
5610                ] {
5611                    if direction == Direction::Incoming {
5612                        bitcoin_chain_service.set_history(vec![BtcHistory {
5613                            txid: bitcoin::Txid::from_str(&mock_user_lockup_tx_id).unwrap(),
5614                            height: 0,
5615                        }]);
5616                        bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]);
5617                    }
5618                    let persisted_swap = trigger_swap_update!(
5619                        "chain",
5620                        NewSwapArgs::default().set_direction(direction),
5621                        persister,
5622                        status_stream,
5623                        status,
5624                        Some(TransactionInfo {
5625                            id: mock_user_lockup_tx_id.clone(),
5626                            hex: Some(mock_user_lockup_tx_hex.clone()),
5627                            eta: None
5628                        }), // sets `update.transaction`
5629                        Some(true) // sets `update.zero_conf_rejected`
5630                    );
5631                    assert_eq!(
5632                        persisted_swap.user_lockup_tx_id,
5633                        Some(mock_user_lockup_tx_id.clone())
5634                    );
5635                    assert!(!persisted_swap.accept_zero_conf);
5636                }
5637
5638                // Verify that `TransactionServerMempool` correctly:
5639                // 1. Sets the payment as `Pending` and creates `server_lockup_tx_id` when
5640                //    `accepts_zero_conf` is false
5641                // 2. Sets the payment as `Pending` and creates `claim_tx_id` when `accepts_zero_conf`
5642                //    is true
5643                for accepts_zero_conf in [false, true] {
5644                    let persisted_swap = trigger_swap_update!(
5645                        "chain",
5646                        NewSwapArgs::default()
5647                            .set_direction(direction)
5648                            .set_accepts_zero_conf(accepts_zero_conf)
5649                            .set_set_actual_payer_amount(true),
5650                        persister,
5651                        status_stream,
5652                        ChainSwapStates::TransactionServerMempool,
5653                        Some(TransactionInfo {
5654                            id: mock_server_lockup_tx_id.clone(),
5655                            hex: Some(mock_server_lockup_tx_hex.clone()),
5656                            eta: None,
5657                        }),
5658                        None
5659                    );
5660                    match accepts_zero_conf {
5661                        false => {
5662                            assert_eq!(persisted_swap.state, PaymentState::Pending);
5663                            assert!(persisted_swap.server_lockup_tx_id.is_some());
5664                        }
5665                        true => {
5666                            assert_eq!(persisted_swap.state, PaymentState::Pending);
5667                            assert!(persisted_swap.claim_tx_id.is_some());
5668                        }
5669                    };
5670                }
5671
5672                // Verify that `TransactionServerConfirmed` correctly
5673                // sets the payment as `Pending` and creates `claim_tx_id`
5674                let persisted_swap = trigger_swap_update!(
5675                    "chain",
5676                    NewSwapArgs::default()
5677                        .set_direction(direction)
5678                        .set_set_actual_payer_amount(true),
5679                    persister,
5680                    status_stream,
5681                    ChainSwapStates::TransactionServerConfirmed,
5682                    Some(TransactionInfo {
5683                        id: mock_server_lockup_tx_id,
5684                        hex: Some(mock_server_lockup_tx_hex),
5685                        eta: None,
5686                    }),
5687                    None
5688                );
5689                assert_eq!(persisted_swap.state, PaymentState::Pending);
5690                assert!(persisted_swap.claim_tx_id.is_some());
5691            }
5692
5693            // For outgoing payments, verify that `Created` correctly sets the payment as `Pending` and creates
5694            // the `user_lockup_tx_id`
5695            let persisted_swap = trigger_swap_update!(
5696                "chain",
5697                NewSwapArgs::default().set_direction(Direction::Outgoing),
5698                persister,
5699                status_stream,
5700                ChainSwapStates::Created,
5701                None,
5702                None
5703            );
5704            assert_eq!(persisted_swap.state, PaymentState::Pending);
5705            assert!(persisted_swap.user_lockup_tx_id.is_some());
5706        })
5707        .await
5708        .unwrap();
5709
5710        Ok(())
5711    }
5712
5713    #[sdk_macros::async_test_all]
5714    async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> {
5715        let user_lockup_sat = 50_000;
5716
5717        create_persister!(persister);
5718        let swapper = Arc::new(MockSwapper::new());
5719        let status_stream = Arc::new(MockStatusStream::new());
5720        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5721        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5722
5723        let sdk = new_liquid_sdk_with_chain_services(
5724            persister.clone(),
5725            swapper.clone(),
5726            status_stream.clone(),
5727            liquid_chain_service.clone(),
5728            bitcoin_chain_service.clone(),
5729            Some(0),
5730        )
5731        .await?;
5732
5733        LiquidSdk::track_swap_updates(&sdk);
5734
5735        // We spawn a new thread since updates can only be sent when called via async runtimes
5736        tokio::spawn(async move {
5737            // Verify that `TransactionLockupFailed` correctly:
5738            // 1. does not affect state when swapper doesn't increase fees
5739            // 2. triggers a change to WaitingFeeAcceptance when there is a fee increase > 0
5740            for fee_increase in [0, 1] {
5741                swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5742                    user_lockup_sat,
5743                    onchain_fee_increase_sat: fee_increase,
5744                });
5745                bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5746                let persisted_swap = trigger_swap_update!(
5747                    "chain",
5748                    NewSwapArgs::default()
5749                        .set_direction(Direction::Incoming)
5750                        .set_accepts_zero_conf(false)
5751                        .set_zero_amount(true),
5752                    persister,
5753                    status_stream,
5754                    ChainSwapStates::TransactionLockupFailed,
5755                    None,
5756                    None
5757                );
5758                match fee_increase {
5759                    0 => {
5760                        assert_eq!(persisted_swap.state, PaymentState::Created);
5761                    }
5762                    1 => {
5763                        assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5764                    }
5765                    _ => panic!("Unexpected fee_increase"),
5766                }
5767            }
5768        })
5769        .await?;
5770
5771        Ok(())
5772    }
5773
5774    #[sdk_macros::async_test_all]
5775    async fn test_zero_amount_chain_swap_with_leeway() -> Result<()> {
5776        let user_lockup_sat = 50_000;
5777        let onchain_fee_rate_leeway_sat = 500;
5778
5779        create_persister!(persister);
5780        let swapper = Arc::new(MockSwapper::new());
5781        let status_stream = Arc::new(MockStatusStream::new());
5782        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5783        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5784
5785        let sdk = new_liquid_sdk_with_chain_services(
5786            persister.clone(),
5787            swapper.clone(),
5788            status_stream.clone(),
5789            liquid_chain_service.clone(),
5790            bitcoin_chain_service.clone(),
5791            Some(onchain_fee_rate_leeway_sat),
5792        )
5793        .await?;
5794
5795        LiquidSdk::track_swap_updates(&sdk);
5796
5797        // We spawn a new thread since updates can only be sent when called via async runtimes
5798        tokio::spawn(async move {
5799            // Verify that `TransactionLockupFailed` correctly:
5800            // 1. does not affect state when swapper increases fee by up to sat/vbyte leeway * tx size
5801            // 2. triggers a change to WaitingFeeAcceptance when it is any higher
5802            for fee_increase in [onchain_fee_rate_leeway_sat, onchain_fee_rate_leeway_sat + 1] {
5803                swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5804                    user_lockup_sat,
5805                    onchain_fee_increase_sat: fee_increase,
5806                });
5807                bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5808                let persisted_swap = trigger_swap_update!(
5809                    "chain",
5810                    NewSwapArgs::default()
5811                        .set_direction(Direction::Incoming)
5812                        .set_accepts_zero_conf(false)
5813                        .set_zero_amount(true),
5814                    persister,
5815                    status_stream,
5816                    ChainSwapStates::TransactionLockupFailed,
5817                    None,
5818                    None
5819                );
5820                match fee_increase {
5821                    val if val == onchain_fee_rate_leeway_sat => {
5822                        assert_eq!(persisted_swap.state, PaymentState::Created);
5823                    }
5824                    val if val == (onchain_fee_rate_leeway_sat + 1) => {
5825                        assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5826                    }
5827                    _ => panic!("Unexpected fee_increase"),
5828                }
5829            }
5830        })
5831        .await?;
5832
5833        Ok(())
5834    }
5835
5836    #[sdk_macros::async_test_all]
5837    async fn test_background_tasks() -> Result<()> {
5838        create_persister!(persister);
5839        let swapper = Arc::new(MockSwapper::new());
5840        let status_stream = Arc::new(MockStatusStream::new());
5841        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5842        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5843
5844        let sdk = new_liquid_sdk_with_chain_services(
5845            persister.clone(),
5846            swapper.clone(),
5847            status_stream.clone(),
5848            liquid_chain_service.clone(),
5849            bitcoin_chain_service.clone(),
5850            None,
5851        )
5852        .await?;
5853
5854        sdk.start().await?;
5855
5856        tokio::time::sleep(Duration::from_secs(3)).await;
5857
5858        sdk.disconnect().await?;
5859
5860        Ok(())
5861    }
5862
5863    #[sdk_macros::async_test_all]
5864    async fn test_create_bolt12_offer() -> Result<()> {
5865        create_persister!(persister);
5866
5867        let swapper = Arc::new(MockSwapper::default());
5868        let status_stream = Arc::new(MockStatusStream::new());
5869        let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5870
5871        // Register a webhook URL
5872        let webhook_url = "https://example.com/webhook";
5873        persister.set_webhook_url(webhook_url.to_string())?;
5874
5875        // Call create_bolt12_offer
5876        let description = "test offer".to_string();
5877        let response = sdk.create_bolt12_offer(description.clone()).await?;
5878
5879        // Verify that the response contains a destination (offer string)
5880        assert!(!response.destination.is_empty());
5881
5882        // Verify the offer was stored in the persister
5883        let offers = persister.list_bolt12_offers_by_webhook_url(webhook_url)?;
5884        assert_eq!(offers.len(), 1);
5885
5886        // Verify the offer details
5887        let offer = &offers[0];
5888        assert_eq!(offer.description, description);
5889        assert_eq!(offer.webhook_url, Some(webhook_url.to_string()));
5890        assert_eq!(offer.id, response.destination);
5891
5892        // Verify the offer has a private key
5893        assert!(!offer.private_key.is_empty());
5894
5895        Ok(())
5896    }
5897
5898    #[sdk_macros::async_test_all]
5899    async fn test_create_bolt12_receive_swap() -> Result<()> {
5900        create_persister!(persister);
5901
5902        let swapper = Arc::new(MockSwapper::default());
5903        let status_stream = Arc::new(MockStatusStream::new());
5904        let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5905
5906        // Register a webhook URL
5907        let webhook_url = "https://example.com/webhook";
5908        persister.set_webhook_url(webhook_url.to_string())?;
5909
5910        // Call create_bolt12_offer
5911        let description = "test offer".to_string();
5912        let response = sdk.create_bolt12_offer(description.clone()).await?;
5913        let offer = persister
5914            .fetch_bolt12_offer_by_id(&response.destination)?
5915            .unwrap();
5916
5917        // Create the invoice request
5918        let expanded_key = ExpandedKey::new([42; 32]);
5919        let entropy_source = RandomBytes::new(utils::generate_entropy());
5920        let nonce = Nonce::from_entropy_source(&entropy_source);
5921        let secp = Secp256k1::new();
5922        let payment_id = PaymentId([1; 32]);
5923        let invoice_request = TryInto::<Offer>::try_into(offer.clone())?
5924            .request_invoice(&expanded_key, nonce, &secp, payment_id)
5925            .unwrap()
5926            .amount_msats(1_000_000)
5927            .unwrap()
5928            .chain(Network::Testnet)
5929            .unwrap()
5930            .build_and_sign()
5931            .unwrap();
5932        let mut buffer = Vec::new();
5933        invoice_request.write(&mut buffer).unwrap();
5934
5935        // Call create_bolt12_receive_swap
5936        let create_res = sdk
5937            .create_bolt12_invoice(&CreateBolt12InvoiceRequest {
5938                offer: offer.id,
5939                invoice_request: buffer.to_hex(),
5940            })
5941            .await
5942            .unwrap();
5943        assert!(create_res.invoice.starts_with("lni"));
5944
5945        Ok(())
5946    }
5947}