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