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.persister.fetch_send_swap_by_invoice(&invoice)? {
2337            Some(swap) => match swap.state {
2338                Created => swap,
2339                TimedOut => {
2340                    self.send_swap_handler.update_swap_info(
2341                        &swap.id,
2342                        PaymentState::Created,
2343                        None,
2344                        None,
2345                        None,
2346                    )?;
2347                    swap
2348                }
2349                Pending => return Err(PaymentError::PaymentInProgress),
2350                Complete => return Err(PaymentError::AlreadyPaid),
2351                RefundPending | Refundable | Failed => {
2352                    return Err(PaymentError::invalid_invoice(
2353                        "Payment has already failed. Please try with another invoice",
2354                    ))
2355                }
2356                WaitingFeeAcceptance => {
2357                    return Err(PaymentError::Generic {
2358                        err: "Send swap payment cannot be in state WaitingFeeAcceptance"
2359                            .to_string(),
2360                    })
2361                }
2362            },
2363            None => {
2364                let keypair = utils::generate_keypair();
2365                let refund_public_key = boltz_client::PublicKey {
2366                    compressed: true,
2367                    inner: keypair.public_key(),
2368                };
2369                let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2370                    url,
2371                    hash_swap_id: Some(true),
2372                    status: Some(vec![
2373                        SubSwapStates::InvoiceFailedToPay,
2374                        SubSwapStates::SwapExpired,
2375                        SubSwapStates::TransactionClaimPending,
2376                        SubSwapStates::TransactionLockupFailed,
2377                    ]),
2378                });
2379                let create_response = self
2380                    .swapper
2381                    .create_send_swap(CreateSubmarineRequest {
2382                        from: "L-BTC".to_string(),
2383                        to: "BTC".to_string(),
2384                        invoice: invoice.to_string(),
2385                        refund_public_key,
2386                        pair_hash: Some(lbtc_pair.hash.clone()),
2387                        referral_id: None,
2388                        webhook,
2389                    })
2390                    .await?;
2391
2392                let swap_id = &create_response.id;
2393                let create_response_json =
2394                    SendSwap::from_boltz_struct_to_json(&create_response, swap_id)?;
2395                let destination_pubkey =
2396                    utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some())?;
2397
2398                let payer_amount_sat = fees_sat + receiver_amount_sat;
2399                let swap = SendSwap {
2400                    id: swap_id.to_string(),
2401                    invoice: invoice.to_string(),
2402                    bolt12_offer,
2403                    payment_hash: Some(payment_hash.to_string()),
2404                    destination_pubkey: Some(destination_pubkey),
2405                    timeout_block_height: create_response.timeout_block_height,
2406                    description,
2407                    preimage: None,
2408                    payer_amount_sat,
2409                    receiver_amount_sat,
2410                    pair_fees_json: serde_json::to_string(&lbtc_pair).map_err(|e| {
2411                        PaymentError::generic(format!("Failed to serialize SubmarinePair: {e:?}"))
2412                    })?,
2413                    create_response_json,
2414                    lockup_tx_id: None,
2415                    refund_address: None,
2416                    refund_tx_id: None,
2417                    created_at: utils::now(),
2418                    state: PaymentState::Created,
2419                    refund_private_key: keypair.display_secret().to_string(),
2420                    metadata: Default::default(),
2421                };
2422                self.persister.insert_or_update_send_swap(&swap)?;
2423                swap
2424            }
2425        };
2426        self.status_stream.track_swap_id(&swap.id)?;
2427
2428        let create_response = swap.get_boltz_create_response()?;
2429        self.send_swap_handler
2430            .try_lockup(&swap, &create_response)
2431            .await?;
2432
2433        self.wait_for_payment_with_timeout(
2434            Swap::Send(swap),
2435            create_response.accept_zero_conf,
2436            timeout_sec,
2437        )
2438        .await
2439        .map(|payment| SendPaymentResponse { payment })
2440    }
2441
2442    /// Fetch the current payment limits for [LiquidSdk::send_payment] and [LiquidSdk::receive_payment].
2443    pub async fn fetch_lightning_limits(
2444        &self,
2445    ) -> Result<LightningPaymentLimitsResponse, PaymentError> {
2446        self.ensure_is_started().await?;
2447
2448        let submarine_pair = self
2449            .swapper
2450            .get_submarine_pairs()
2451            .await?
2452            .ok_or(PaymentError::PairsNotFound)?;
2453        let send_limits = submarine_pair.limits;
2454
2455        let reverse_pair = self
2456            .swapper
2457            .get_reverse_swap_pairs()
2458            .await?
2459            .ok_or(PaymentError::PairsNotFound)?;
2460        let receive_limits = reverse_pair.limits;
2461
2462        let res = LightningPaymentLimitsResponse {
2463            send: Limits {
2464                min_sat: send_limits.minimal_batched.unwrap_or(send_limits.minimal),
2465                max_sat: send_limits.maximal,
2466                max_zero_conf_sat: send_limits.maximal_zero_conf,
2467            },
2468            receive: Limits {
2469                min_sat: receive_limits.minimal,
2470                max_sat: receive_limits.maximal,
2471                max_zero_conf_sat: self.config.zero_conf_max_amount_sat(),
2472            },
2473        };
2474        debug!("fetch_lightning_limits returned: {res:?}");
2475        Ok(res)
2476    }
2477
2478    /// Fetch the current payment limits for [LiquidSdk::pay_onchain] and [LiquidSdk::receive_onchain].
2479    pub async fn fetch_onchain_limits(&self) -> Result<OnchainPaymentLimitsResponse, PaymentError> {
2480        self.ensure_is_started().await?;
2481
2482        let (pair_outgoing, pair_incoming) = self.swapper.get_chain_pairs().await?;
2483        let send_limits = pair_outgoing
2484            .ok_or(PaymentError::PairsNotFound)
2485            .map(|pair| pair.limits)?;
2486        let receive_limits = pair_incoming
2487            .ok_or(PaymentError::PairsNotFound)
2488            .map(|pair| pair.limits)?;
2489
2490        Ok(OnchainPaymentLimitsResponse {
2491            send: Limits {
2492                min_sat: send_limits.minimal,
2493                max_sat: send_limits.maximal,
2494                max_zero_conf_sat: send_limits.maximal_zero_conf,
2495            },
2496            receive: Limits {
2497                min_sat: receive_limits.minimal,
2498                max_sat: receive_limits.maximal,
2499                max_zero_conf_sat: receive_limits.maximal_zero_conf,
2500            },
2501        })
2502    }
2503
2504    /// Prepares to pay to a Bitcoin address via a chain swap.
2505    ///
2506    /// # Arguments
2507    ///
2508    /// * `req` - the [PreparePayOnchainRequest] containing:
2509    ///     * `amount` - which can be of two types: [PayAmount::Drain], which uses all funds,
2510    ///       and [PayAmount::Bitcoin], which sets the amount the receiver should receive
2511    ///     * `fee_rate_sat_per_vbyte` - the optional fee rate of the Bitcoin claim transaction. Defaults to the swapper estimated claim fee
2512    pub async fn prepare_pay_onchain(
2513        &self,
2514        req: &PreparePayOnchainRequest,
2515    ) -> Result<PreparePayOnchainResponse, PaymentError> {
2516        self.ensure_is_started().await?;
2517
2518        let get_info_res = self.get_info().await?;
2519        let pair = self.get_chain_pair(Direction::Outgoing).await?;
2520        let claim_fees_sat = match req.fee_rate_sat_per_vbyte {
2521            Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64,
2522            None => pair.clone().fees.claim_estimate(),
2523        };
2524        let server_fees_sat = pair.fees.server();
2525
2526        info!("Preparing for onchain payment of kind: {:?}", req.amount);
2527        let (payer_amount_sat, receiver_amount_sat, total_fees_sat) = match req.amount {
2528            PayAmount::Bitcoin {
2529                receiver_amount_sat: amount_sat,
2530            } => {
2531                let receiver_amount_sat = amount_sat;
2532
2533                let user_lockup_amount_sat_without_service_fee =
2534                    receiver_amount_sat + claim_fees_sat + server_fees_sat;
2535
2536                // The resulting invoice amount contains the service fee, which is rounded up with ceil()
2537                // Therefore, when calculating the user_lockup amount, we must also round it up with ceil()
2538                let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64
2539                    * 100.0
2540                    / (100.0 - pair.fees.percentage))
2541                    .ceil() as u64;
2542                self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2543
2544                let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
2545
2546                let boltz_fees_sat =
2547                    user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2548                let total_fees_sat =
2549                    boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2550                let payer_amount_sat = receiver_amount_sat + total_fees_sat;
2551
2552                (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2553            }
2554            PayAmount::Drain => {
2555                ensure_sdk!(
2556                    get_info_res.wallet_info.pending_receive_sat == 0
2557                        && get_info_res.wallet_info.pending_send_sat == 0,
2558                    PaymentError::Generic {
2559                        err: "Cannot drain while there are pending payments".to_string(),
2560                    }
2561                );
2562                let payer_amount_sat = get_info_res.wallet_info.balance_sat;
2563                let lockup_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
2564
2565                let user_lockup_amount_sat = payer_amount_sat - lockup_fees_sat;
2566                self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2567
2568                let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
2569                let total_fees_sat =
2570                    boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2571                let receiver_amount_sat = payer_amount_sat - total_fees_sat;
2572
2573                (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2574            }
2575            PayAmount::Asset { .. } => {
2576                return Err(PaymentError::asset_error(
2577                    "Cannot send an asset to a Bitcoin address",
2578                ))
2579            }
2580        };
2581
2582        let res = PreparePayOnchainResponse {
2583            receiver_amount_sat,
2584            claim_fees_sat,
2585            total_fees_sat,
2586        };
2587
2588        ensure_sdk!(
2589            payer_amount_sat <= get_info_res.wallet_info.balance_sat,
2590            PaymentError::InsufficientFunds
2591        );
2592
2593        info!("Prepared onchain payment: {res:?}");
2594        Ok(res)
2595    }
2596
2597    /// Pays to a Bitcoin address via a chain swap.
2598    ///
2599    /// Depending on [Config]'s `payment_timeout_sec`, this function will return:
2600    /// * [PaymentState::Pending] payment - if the payment could be initiated but didn't yet
2601    ///   complete in this time
2602    /// * [PaymentState::Complete] payment - if the payment was successfully completed in this time
2603    ///
2604    /// # Arguments
2605    ///
2606    /// * `req` - the [PayOnchainRequest] containing:
2607    ///     * `address` - the Bitcoin address to pay to
2608    ///     * `prepare_response` - the [PreparePayOnchainResponse] from calling [LiquidSdk::prepare_pay_onchain]
2609    ///
2610    /// # Errors
2611    ///
2612    /// * [PaymentError::PaymentTimeout] - if the payment could not be initiated in this time
2613    pub async fn pay_onchain(
2614        &self,
2615        req: &PayOnchainRequest,
2616    ) -> Result<SendPaymentResponse, PaymentError> {
2617        self.ensure_is_started().await?;
2618        info!("Paying onchain, request = {req:?}");
2619
2620        let timeout_sec = self.config.payment_timeout_sec;
2621
2622        let claim_address = self.validate_bitcoin_address(&req.address).await?;
2623        let balance_sat = self.get_info().await?.wallet_info.balance_sat;
2624        let receiver_amount_sat = req.prepare_response.receiver_amount_sat;
2625        let pair = self.get_chain_pair(Direction::Outgoing).await?;
2626        let claim_fees_sat = req.prepare_response.claim_fees_sat;
2627        let server_fees_sat = pair.fees.server();
2628        let server_lockup_amount_sat = receiver_amount_sat + claim_fees_sat;
2629
2630        let user_lockup_amount_sat_without_service_fee =
2631            receiver_amount_sat + claim_fees_sat + server_fees_sat;
2632
2633        // The resulting invoice amount contains the service fee, which is rounded up with ceil()
2634        // Therefore, when calculating the user_lockup amount, we must also round it up with ceil()
2635        let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64 * 100.0
2636            / (100.0 - pair.fees.percentage))
2637            .ceil() as u64;
2638        let boltz_fee_sat = user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2639        self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2640
2641        let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2642
2643        let lockup_fees_sat = match payer_amount_sat == balance_sat {
2644            true => self.estimate_drain_tx_fee(None, None).await?,
2645            false => self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?,
2646        };
2647
2648        ensure_sdk!(
2649            req.prepare_response.total_fees_sat
2650                == boltz_fee_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat,
2651            PaymentError::InvalidOrExpiredFees
2652        );
2653
2654        ensure_sdk!(
2655            payer_amount_sat <= balance_sat,
2656            PaymentError::InsufficientFunds
2657        );
2658
2659        let preimage = Preimage::new();
2660        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2661
2662        let claim_keypair = utils::generate_keypair();
2663        let claim_public_key = boltz_client::PublicKey {
2664            compressed: true,
2665            inner: claim_keypair.public_key(),
2666        };
2667        let refund_keypair = utils::generate_keypair();
2668        let refund_public_key = boltz_client::PublicKey {
2669            compressed: true,
2670            inner: refund_keypair.public_key(),
2671        };
2672        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2673            url,
2674            hash_swap_id: Some(true),
2675            status: Some(vec![
2676                ChainSwapStates::TransactionFailed,
2677                ChainSwapStates::TransactionLockupFailed,
2678                ChainSwapStates::TransactionServerConfirmed,
2679            ]),
2680        });
2681        let create_response = self
2682            .swapper
2683            .create_chain_swap(CreateChainRequest {
2684                from: "L-BTC".to_string(),
2685                to: "BTC".to_string(),
2686                preimage_hash: preimage.sha256,
2687                claim_public_key: Some(claim_public_key),
2688                refund_public_key: Some(refund_public_key),
2689                user_lock_amount: None,
2690                server_lock_amount: Some(server_lockup_amount_sat),
2691                pair_hash: Some(pair.hash.clone()),
2692                referral_id: None,
2693                webhook,
2694            })
2695            .await?;
2696
2697        let create_response_json =
2698            ChainSwap::from_boltz_struct_to_json(&create_response, &create_response.id)?;
2699        let swap_id = create_response.id;
2700
2701        let accept_zero_conf = server_lockup_amount_sat <= pair.limits.maximal_zero_conf;
2702        let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2703
2704        let swap = ChainSwap {
2705            id: swap_id.clone(),
2706            direction: Direction::Outgoing,
2707            claim_address: Some(claim_address),
2708            lockup_address: create_response.lockup_details.lockup_address,
2709            refund_address: None,
2710            timeout_block_height: create_response.lockup_details.timeout_block_height,
2711            claim_timeout_block_height: create_response.claim_details.timeout_block_height,
2712            preimage: preimage_str,
2713            description: Some("Bitcoin transfer".to_string()),
2714            payer_amount_sat,
2715            actual_payer_amount_sat: None,
2716            receiver_amount_sat,
2717            accepted_receiver_amount_sat: None,
2718            claim_fees_sat,
2719            pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
2720                PaymentError::generic(format!("Failed to serialize outgoing ChainPair: {e:?}"))
2721            })?,
2722            accept_zero_conf,
2723            create_response_json,
2724            claim_private_key: claim_keypair.display_secret().to_string(),
2725            refund_private_key: refund_keypair.display_secret().to_string(),
2726            server_lockup_tx_id: None,
2727            user_lockup_tx_id: None,
2728            claim_tx_id: None,
2729            refund_tx_id: None,
2730            created_at: utils::now(),
2731            state: PaymentState::Created,
2732            auto_accepted_fees: false,
2733            user_lockup_spent: false,
2734            metadata: Default::default(),
2735        };
2736        self.persister.insert_or_update_chain_swap(&swap)?;
2737        self.status_stream.track_swap_id(&swap_id)?;
2738
2739        self.wait_for_payment_with_timeout(Swap::Chain(swap), accept_zero_conf, timeout_sec)
2740            .await
2741            .map(|payment| SendPaymentResponse { payment })
2742    }
2743
2744    async fn wait_for_payment_with_timeout(
2745        &self,
2746        swap: Swap,
2747        accept_zero_conf: bool,
2748        timeout_sec: u64,
2749    ) -> Result<Payment, PaymentError> {
2750        let timeout_fut = tokio::time::sleep(Duration::from_secs(timeout_sec));
2751        tokio::pin!(timeout_fut);
2752
2753        let expected_swap_id = swap.id();
2754        let mut events_stream = self.event_manager.subscribe();
2755        let mut maybe_payment: Option<Payment> = None;
2756
2757        loop {
2758            tokio::select! {
2759                _ = &mut timeout_fut => match maybe_payment {
2760                    Some(payment) => return Ok(payment),
2761                    None => {
2762                        debug!("Timeout occurred without payment, set swap to timed out");
2763                        let update_res = match swap {
2764                            Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None),
2765                            Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
2766                                    swap_id: expected_swap_id.clone(),
2767                                    to_state: TimedOut,
2768                                    ..Default::default()
2769                                }),
2770                            _ => Ok(())
2771                        };
2772                        return match update_res {
2773                            Ok(_) => Err(PaymentError::PaymentTimeout),
2774                            Err(_) => {
2775                                // Not able to transition the payment state to TimedOut, which means the payment
2776                                // state progressed but we didn't see the event before the timeout
2777                                self.persister.get_payment(&expected_swap_id).ok().flatten().ok_or(PaymentError::generic("Payment not found"))
2778                            }
2779                        }
2780                    },
2781                },
2782                event = events_stream.recv() => match event {
2783                    Ok(SdkEvent::PaymentPending { details: payment }) => {
2784                        let maybe_payment_swap_id = payment.details.get_swap_id();
2785                        if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2786                            match accept_zero_conf {
2787                                true => {
2788                                    debug!("Received Send Payment pending event with zero-conf accepted");
2789                                    return Ok(payment)
2790                                }
2791                                false => {
2792                                    debug!("Received Send Payment pending event, waiting for confirmation");
2793                                    maybe_payment = Some(payment);
2794                                }
2795                            }
2796                        };
2797                    },
2798                    Ok(SdkEvent::PaymentSucceeded { details: payment }) => {
2799                        let maybe_payment_swap_id = payment.details.get_swap_id();
2800                        if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2801                            debug!("Received Send Payment succeed event");
2802                            return Ok(payment);
2803                        }
2804                    },
2805                    Ok(event) => debug!("Unhandled event waiting for payment: {event:?}"),
2806                    Err(e) => debug!("Received error waiting for payment: {e:?}"),
2807                }
2808            }
2809        }
2810    }
2811
2812    /// Prepares to receive a Lightning payment via a reverse submarine swap.
2813    ///
2814    /// # Arguments
2815    ///
2816    /// * `req` - the [PrepareReceiveRequest] containing:
2817    ///     * `payment_method` - the supported payment methods; either an invoice, an offer, a Liquid address or a Bitcoin address
2818    ///     * `amount` - The optional amount of type [ReceiveAmount] to be paid.
2819    ///        - [ReceiveAmount::Bitcoin] which sets the amount in satoshi that should be paid
2820    ///        - [ReceiveAmount::Asset] which sets the amount of an asset that should be paid
2821    pub async fn prepare_receive_payment(
2822        &self,
2823        req: &PrepareReceiveRequest,
2824    ) -> Result<PrepareReceiveResponse, PaymentError> {
2825        self.ensure_is_started().await?;
2826
2827        let result = match req.payment_method.clone() {
2828            #[allow(deprecated)]
2829            PaymentMethod::Bolt11Invoice => {
2830                let payer_amount_sat = match req.amount {
2831                    Some(ReceiveAmount::Asset { .. }) => {
2832                        let err = PaymentError::asset_error(
2833                            "Cannot receive an asset for this payment method",
2834                        );
2835                        error!("prepare_receive_payment returned error: {err:?}");
2836                        return Err(err);
2837                    }
2838                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2839                    None => {
2840                        let err = PaymentError::generic(
2841                            "Bitcoin payer amount must be set for this payment method",
2842                        );
2843                        error!("prepare_receive_payment returned error: {err:?}");
2844                        return Err(err);
2845                    }
2846                };
2847                let reverse_pair = self
2848                    .swapper
2849                    .get_reverse_swap_pairs()
2850                    .await?
2851                    .ok_or(PaymentError::PairsNotFound)?;
2852
2853                let fees_sat = reverse_pair.fees.total(payer_amount_sat);
2854
2855                reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
2856                    PaymentError::AmountOutOfRange {
2857                        min: reverse_pair.limits.minimal,
2858                        max: reverse_pair.limits.maximal,
2859                    }
2860                })?;
2861
2862                let min_payer_amount_sat = Some(reverse_pair.limits.minimal);
2863                let max_payer_amount_sat = Some(reverse_pair.limits.maximal);
2864                let swapper_feerate = Some(reverse_pair.fees.percentage);
2865
2866                debug!(
2867                    "Preparing Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat"
2868                );
2869
2870                Ok(PrepareReceiveResponse {
2871                    payment_method: req.payment_method.clone(),
2872                    amount: req.amount.clone(),
2873                    fees_sat,
2874                    min_payer_amount_sat,
2875                    max_payer_amount_sat,
2876                    swapper_feerate,
2877                })
2878            }
2879            PaymentMethod::Bolt12Offer => {
2880                if req.amount.is_some() {
2881                    let err = PaymentError::generic("Amount cannot be set for this payment method");
2882                    error!("prepare_receive_payment returned error: {err:?}");
2883                    return Err(err);
2884                }
2885
2886                let reverse_pair = self
2887                    .swapper
2888                    .get_reverse_swap_pairs()
2889                    .await?
2890                    .ok_or(PaymentError::PairsNotFound)?;
2891
2892                let fees_sat = reverse_pair.fees.total(0);
2893                debug!("Preparing Bolt12Offer Receive Swap with: min fees_sat {fees_sat}");
2894
2895                Ok(PrepareReceiveResponse {
2896                    payment_method: req.payment_method.clone(),
2897                    amount: req.amount.clone(),
2898                    fees_sat,
2899                    min_payer_amount_sat: Some(reverse_pair.limits.minimal),
2900                    max_payer_amount_sat: Some(reverse_pair.limits.maximal),
2901                    swapper_feerate: Some(reverse_pair.fees.percentage),
2902                })
2903            }
2904            PaymentMethod::BitcoinAddress => {
2905                let payer_amount_sat = match req.amount {
2906                    Some(ReceiveAmount::Asset { .. }) => {
2907                        let err = PaymentError::asset_error(
2908                            "Asset cannot be received for this payment method",
2909                        );
2910                        error!("prepare_receive_payment returned error: {err:?}");
2911                        return Err(err);
2912                    }
2913                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2914                    None => None,
2915                };
2916                let pair = self
2917                    .get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)
2918                    .await?;
2919                let claim_fees_sat = pair.fees.claim_estimate();
2920                let server_fees_sat = pair.fees.server();
2921                let service_fees_sat = payer_amount_sat
2922                    .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
2923                    .unwrap_or_default();
2924
2925                let fees_sat = service_fees_sat + claim_fees_sat + server_fees_sat;
2926                debug!("Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}");
2927
2928                Ok(PrepareReceiveResponse {
2929                    payment_method: req.payment_method.clone(),
2930                    amount: req.amount.clone(),
2931                    fees_sat,
2932                    min_payer_amount_sat: Some(pair.limits.minimal),
2933                    max_payer_amount_sat: Some(pair.limits.maximal),
2934                    swapper_feerate: Some(pair.fees.percentage),
2935                })
2936            }
2937            PaymentMethod::LiquidAddress => {
2938                let (asset_id, payer_amount, payer_amount_sat) = match req.amount.clone() {
2939                    Some(ReceiveAmount::Asset {
2940                        payer_amount,
2941                        asset_id,
2942                    }) => (asset_id, payer_amount, None),
2943                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2944                        (self.config.lbtc_asset_id(), None, Some(payer_amount_sat))
2945                    }
2946                    None => (self.config.lbtc_asset_id(), None, None),
2947                };
2948
2949                debug!("Preparing Liquid Receive with: asset_id {asset_id}, amount {payer_amount:?}, amount_sat {payer_amount_sat:?}");
2950
2951                Ok(PrepareReceiveResponse {
2952                    payment_method: req.payment_method.clone(),
2953                    amount: req.amount.clone(),
2954                    fees_sat: 0,
2955                    min_payer_amount_sat: None,
2956                    max_payer_amount_sat: None,
2957                    swapper_feerate: None,
2958                })
2959            }
2960        };
2961        result
2962            .inspect(|res| debug!("prepare_receive_payment returned: {res:?}"))
2963            .inspect_err(|e| error!("prepare_receive_payment returned error: {e:?}"))
2964    }
2965
2966    /// Receive a Lightning payment via a reverse submarine swap, a chain swap or via direct Liquid
2967    /// payment.
2968    ///
2969    /// # Arguments
2970    ///
2971    /// * `req` - the [ReceivePaymentRequest] containing:
2972    ///     * `prepare_response` - the [PrepareReceiveResponse] from calling [LiquidSdk::prepare_receive_payment]
2973    ///     * `description` - the optional payment description
2974    ///     * `description_hash` - optional, whether to pass a custom description hash or to
2975    ///       calculate it from the `description` field
2976    ///     * `payer_note` - the optional payer note, typically included in a LNURL-Pay request
2977    ///
2978    /// # Returns
2979    ///
2980    /// * A [ReceivePaymentResponse] containing:
2981    ///     * `destination` - the final destination to be paid by the payer, either:
2982    ///        - a BIP21 URI (Liquid or Bitcoin)
2983    ///        - a Liquid address
2984    ///        - a BOLT11 invoice
2985    ///        - a BOLT12 offer
2986    pub async fn receive_payment(
2987        &self,
2988        req: &ReceivePaymentRequest,
2989    ) -> Result<ReceivePaymentResponse, PaymentError> {
2990        self.ensure_is_started().await?;
2991
2992        let PrepareReceiveResponse {
2993            payment_method,
2994            amount,
2995            fees_sat,
2996            ..
2997        } = req.prepare_response.clone();
2998
2999        let result = match payment_method {
3000            #[allow(deprecated)]
3001            PaymentMethod::Bolt11Invoice => {
3002                let amount_sat = match amount.clone() {
3003                    Some(ReceiveAmount::Asset { .. }) => {
3004                        let err = PaymentError::asset_error(
3005                            "Asset cannot be received for this payment method",
3006                        );
3007                        error!("receive_payment returned error: {err:?}");
3008                        return Err(err);
3009                    }
3010                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
3011                    None => {
3012                        let err = PaymentError::generic(
3013                            "Bitcoin payer amount must be set for this payment method",
3014                        );
3015                        error!("receive_payment returned error: {err:?}");
3016                        return Err(err);
3017                    }
3018                };
3019
3020                let (description, description_hash) = match (
3021                    req.description.clone(),
3022                    req.description_hash.clone(),
3023                ) {
3024                    (None, Some(description_hash)) => match description_hash {
3025                        DescriptionHash::UseDescription => {
3026                            let err = PaymentError::InvalidDescription { err: "Cannot calculate payment description hash: no description provided".to_string() };
3027                            error!("receive_payment returned error: {err:?}");
3028                            return Err(err);
3029                        }
3030                        DescriptionHash::Custom { hash } => (None, Some(hash)),
3031                    },
3032                    (Some(description), Some(description_hash)) => {
3033                        let calculated_hash = sha256::Hash::hash(description.as_bytes()).to_hex();
3034                        match description_hash {
3035                            DescriptionHash::UseDescription => (None, Some(calculated_hash)),
3036                            DescriptionHash::Custom { hash } => {
3037                                ensure_sdk!(
3038                                    calculated_hash == *hash,
3039                                    PaymentError::InvalidDescription {
3040                                        err: "Payment description hash mismatch".to_string()
3041                                    }
3042                                );
3043                                (None, Some(calculated_hash))
3044                            }
3045                        }
3046                    }
3047                    (description, None) => (description, None),
3048                };
3049                self.create_bolt11_receive_swap(
3050                    amount_sat,
3051                    fees_sat,
3052                    description,
3053                    description_hash,
3054                    req.payer_note.clone(),
3055                )
3056                .await
3057            }
3058            PaymentMethod::Bolt12Offer => {
3059                let description = req.description.clone().unwrap_or("".to_string());
3060                match self
3061                    .persister
3062                    .fetch_bolt12_offer_by_description(&description)?
3063                {
3064                    Some(bolt12_offer) => Ok(ReceivePaymentResponse {
3065                        destination: bolt12_offer.id,
3066                        liquid_expiration_blockheight: None,
3067                        bitcoin_expiration_blockheight: None,
3068                    }),
3069                    None => self.create_bolt12_offer(description).await,
3070                }
3071            }
3072            PaymentMethod::BitcoinAddress => {
3073                let amount_sat = match amount.clone() {
3074                    Some(ReceiveAmount::Asset { .. }) => {
3075                        let err = PaymentError::asset_error(
3076                            "Asset cannot be received for this payment method",
3077                        );
3078                        error!("receive_payment returned error: {err:?}");
3079                        return Err(err);
3080                    }
3081                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
3082                    None => None,
3083                };
3084                self.receive_onchain(amount_sat, fees_sat).await
3085            }
3086            PaymentMethod::LiquidAddress => {
3087                let lbtc_asset_id = self.config.lbtc_asset_id();
3088                let (asset_id, amount, amount_sat) = match amount.clone() {
3089                    Some(ReceiveAmount::Asset {
3090                        asset_id,
3091                        payer_amount,
3092                    }) => (asset_id, payer_amount, None),
3093                    Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
3094                        (lbtc_asset_id.clone(), None, Some(payer_amount_sat))
3095                    }
3096                    None => (lbtc_asset_id.clone(), None, None),
3097                };
3098
3099                let address = self.onchain_wallet.next_unused_address().await?.to_string();
3100                let receive_destination =
3101                    if asset_id.ne(&lbtc_asset_id) || amount.is_some() || amount_sat.is_some() {
3102                        LiquidAddressData {
3103                            address: address.to_string(),
3104                            network: self.config.network.into(),
3105                            amount,
3106                            amount_sat,
3107                            asset_id: Some(asset_id),
3108                            label: None,
3109                            message: req.description.clone(),
3110                        }
3111                        .to_uri()
3112                        .map_err(|e| PaymentError::Generic {
3113                            err: format!("Could not build BIP21 URI: {e:?}"),
3114                        })?
3115                    } else {
3116                        address
3117                    };
3118
3119                Ok(ReceivePaymentResponse {
3120                    destination: receive_destination,
3121                    liquid_expiration_blockheight: None,
3122                    bitcoin_expiration_blockheight: None,
3123                })
3124            }
3125        };
3126        result
3127            .inspect(|res| debug!("receive_payment returned: {res:?}"))
3128            .inspect_err(|e| error!("receive_payment returned error: {e:?}"))
3129    }
3130
3131    async fn create_bolt11_receive_swap(
3132        &self,
3133        payer_amount_sat: u64,
3134        fees_sat: u64,
3135        description: Option<String>,
3136        description_hash: Option<String>,
3137        payer_note: Option<String>,
3138    ) -> Result<ReceivePaymentResponse, PaymentError> {
3139        let reverse_pair = self
3140            .swapper
3141            .get_reverse_swap_pairs()
3142            .await?
3143            .ok_or(PaymentError::PairsNotFound)?;
3144        let new_fees_sat = reverse_pair.fees.total(payer_amount_sat);
3145        ensure_sdk!(fees_sat == new_fees_sat, PaymentError::InvalidOrExpiredFees);
3146
3147        debug!("Creating BOLT11 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
3148
3149        let keypair = utils::generate_keypair();
3150
3151        let preimage = Preimage::new();
3152        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3153        let preimage_hash = preimage.sha256.to_string();
3154
3155        // Address to be used for a BIP-21 direct payment
3156        let mrh_addr = self.onchain_wallet.next_unused_address().await?;
3157        // Signature of the claim public key of the SHA256 hash of the address for the direct payment
3158        let mrh_addr_str = mrh_addr.to_string();
3159        let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
3160
3161        let receiver_amount_sat = payer_amount_sat - fees_sat;
3162        let webhook_claim_status =
3163            match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3164                true => RevSwapStates::TransactionConfirmed,
3165                false => RevSwapStates::TransactionMempool,
3166            };
3167        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3168            url,
3169            hash_swap_id: Some(true),
3170            status: Some(vec![webhook_claim_status]),
3171        });
3172
3173        let v2_req = CreateReverseRequest {
3174            from: "BTC".to_string(),
3175            to: "L-BTC".to_string(),
3176            invoice: None,
3177            invoice_amount: Some(payer_amount_sat),
3178            preimage_hash: Some(preimage.sha256),
3179            claim_public_key: keypair.public_key().into(),
3180            description,
3181            description_hash,
3182            address: Some(mrh_addr_str.clone()),
3183            address_signature: Some(mrh_addr_hash_sig.to_hex()),
3184            referral_id: None,
3185            webhook,
3186        };
3187        let create_response = self.swapper.create_receive_swap(v2_req).await?;
3188        let invoice_str = create_response
3189            .invoice
3190            .clone()
3191            .ok_or(PaymentError::receive_error("Invoice not found"))?;
3192
3193        // Reserve this address until the timeout block height
3194        self.persister.insert_or_update_reserved_address(
3195            &mrh_addr_str,
3196            create_response.timeout_block_height,
3197        )?;
3198
3199        // Check if correct MRH was added to the invoice by Boltz
3200        let (bip21_lbtc_address, _bip21_amount_btc) = self
3201            .swapper
3202            .check_for_mrh(&invoice_str)
3203            .await?
3204            .ok_or(PaymentError::receive_error("Invoice has no MRH"))?;
3205        ensure_sdk!(
3206            bip21_lbtc_address == mrh_addr_str,
3207            PaymentError::receive_error("Invoice has incorrect address in MRH")
3208        );
3209
3210        let swap_id = create_response.id.clone();
3211        let invoice = Bolt11Invoice::from_str(&invoice_str)
3212            .map_err(|err| PaymentError::invalid_invoice(err.to_string()))?;
3213        let payer_amount_sat =
3214            invoice
3215                .amount_milli_satoshis()
3216                .ok_or(PaymentError::invalid_invoice(
3217                    "Invoice does not contain an amount",
3218                ))?
3219                / 1000;
3220        let destination_pubkey = invoice_pubkey(&invoice);
3221
3222        // Double check that the generated invoice includes our data
3223        // https://docs.boltz.exchange/v/api/dont-trust-verify#lightning-invoice-verification
3224        ensure_sdk!(
3225            invoice.payment_hash().to_string() == preimage_hash,
3226            PaymentError::invalid_invoice("Invalid preimage returned by swapper")
3227        );
3228
3229        let create_response_json = ReceiveSwap::from_boltz_struct_to_json(
3230            &create_response,
3231            &swap_id,
3232            Some(&invoice.to_string()),
3233        )?;
3234        let invoice_description = match invoice.description() {
3235            Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
3236            Bolt11InvoiceDescription::Hash(_) => None,
3237        };
3238
3239        self.persister
3240            .insert_or_update_receive_swap(&ReceiveSwap {
3241                id: swap_id.clone(),
3242                preimage: preimage_str,
3243                create_response_json,
3244                claim_private_key: keypair.display_secret().to_string(),
3245                invoice: invoice.to_string(),
3246                bolt12_offer: None,
3247                payment_hash: Some(preimage_hash),
3248                destination_pubkey: Some(destination_pubkey),
3249                timeout_block_height: create_response.timeout_block_height,
3250                description: invoice_description,
3251                payer_note,
3252                payer_amount_sat,
3253                receiver_amount_sat,
3254                pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3255                    PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3256                })?,
3257                claim_fees_sat: reverse_pair.fees.claim_estimate(),
3258                lockup_tx_id: None,
3259                claim_address: None,
3260                claim_tx_id: None,
3261                mrh_address: mrh_addr_str,
3262                mrh_tx_id: None,
3263                created_at: utils::now(),
3264                state: PaymentState::Created,
3265                metadata: Default::default(),
3266            })
3267            .map_err(|e| {
3268                error!("Failed to insert or update receive swap: {e:?}");
3269                PaymentError::PersistError
3270            })?;
3271        self.status_stream.track_swap_id(&swap_id)?;
3272
3273        Ok(ReceivePaymentResponse {
3274            destination: invoice.to_string(),
3275            liquid_expiration_blockheight: Some(create_response.timeout_block_height),
3276            bitcoin_expiration_blockheight: None,
3277        })
3278    }
3279
3280    /// Create a BOLT12 invoice for a given BOLT12 offer and invoice request.
3281    ///
3282    /// # Arguments
3283    ///
3284    /// * `req` - the [CreateBolt12InvoiceRequest] containing:
3285    ///     * `offer` - the BOLT12 offer
3286    ///     * `invoice_request` - the invoice request created from the offer
3287    ///
3288    /// # Returns
3289    ///
3290    /// * A [CreateBolt12InvoiceResponse] containing:
3291    ///     * `invoice` - the BOLT12 invoice
3292    pub async fn create_bolt12_invoice(
3293        &self,
3294        req: &CreateBolt12InvoiceRequest,
3295    ) -> Result<CreateBolt12InvoiceResponse, PaymentError> {
3296        debug!("Started create BOLT12 invoice");
3297        let bolt12_offer =
3298            self.persister
3299                .fetch_bolt12_offer_by_id(&req.offer)?
3300                .ok_or(PaymentError::generic(format!(
3301                    "Bolt12 offer not found: {}",
3302                    req.offer
3303                )))?;
3304        // Get the CLN node public key from the offer
3305        let offer = Offer::try_from(bolt12_offer.clone())?;
3306        let cln_node_public_key = offer
3307            .paths()
3308            .iter()
3309            .find_map(|path| match path.introduction_node().clone() {
3310                IntroductionNode::NodeId(node_id) => Some(node_id),
3311                IntroductionNode::DirectedShortChannelId(_, _) => None,
3312            })
3313            .ok_or(PaymentError::generic(format!(
3314                "No BTC CLN node found: {}",
3315                req.offer
3316            )))?;
3317        let invoice_request = utils::bolt12::decode_invoice_request(&req.invoice_request)?;
3318        let payer_amount_sat = invoice_request
3319            .amount_msats()
3320            .map(|msats| msats / 1_000)
3321            .ok_or(PaymentError::amount_missing(
3322                "Invoice request must contain an amount",
3323            ))?;
3324        // Parellelize the calls to get_bolt12_params and get_reverse_swap_pairs
3325        let (params, maybe_reverse_pair) = tokio::try_join!(
3326            self.swapper.get_bolt12_params(),
3327            self.swapper.get_reverse_swap_pairs()
3328        )?;
3329        let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3330        reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
3331            PaymentError::AmountOutOfRange {
3332                min: reverse_pair.limits.minimal,
3333                max: reverse_pair.limits.maximal,
3334            }
3335        })?;
3336        let fees_sat = reverse_pair.fees.total(payer_amount_sat);
3337        debug!("Creating BOLT12 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
3338
3339        let secp = Secp256k1::new();
3340        let keypair = bolt12_offer.get_keypair()?;
3341        let preimage = Preimage::new();
3342        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3343        let preimage_hash = preimage.sha256.to_byte_array();
3344
3345        // Address to be used for a BIP-21 direct payment
3346        let mrh_addr = self.onchain_wallet.next_unused_address().await?;
3347        // Signature of the claim public key of the SHA256 hash of the address for the direct payment
3348        let mrh_addr_str = mrh_addr.to_string();
3349        let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
3350
3351        let entropy_source = RandomBytes::new(utils::generate_entropy());
3352        let nonce = Nonce::from_entropy_source(&entropy_source);
3353        let payer_note = invoice_request.payer_note().map(|s| s.to_string());
3354        let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
3355            offer_id: Offer::try_from(bolt12_offer)?.id(),
3356            invoice_request: InvoiceRequestFields {
3357                payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
3358                quantity: invoice_request.quantity(),
3359                payer_note_truncated: payer_note.clone().map(UntrustedString),
3360                human_readable_name: invoice_request.offer_from_hrn().clone(),
3361            },
3362        });
3363        let expanded_key = ExpandedKey::new(keypair.secret_key().secret_bytes());
3364        let payee_tlvs = UnauthenticatedReceiveTlvs {
3365            payment_secret: PaymentSecret(utils::generate_entropy()),
3366            payment_constraints: PaymentConstraints {
3367                max_cltv_expiry: 1_000_000,
3368                htlc_minimum_msat: 1,
3369            },
3370            payment_context,
3371        }
3372        .authenticate(nonce, &expanded_key);
3373
3374        // Configure the blinded payment path
3375        let payment_path = BlindedPaymentPath::one_hop(
3376            cln_node_public_key,
3377            payee_tlvs.clone(),
3378            params.min_cltv as u16,
3379            &entropy_source,
3380            &secp,
3381        )
3382        .map_err(|_| {
3383            PaymentError::generic(
3384                "Failed to create BOLT12 invoice: Error creating blinded payment path",
3385            )
3386        })?;
3387
3388        // Create the invoice
3389        let invoice = invoice_request
3390            .respond_with_no_std(
3391                vec![payment_path],
3392                PaymentHash(preimage_hash),
3393                SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| {
3394                    PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3395                })?,
3396            )?
3397            .build()?
3398            .sign(|unsigned_invoice: &UnsignedBolt12Invoice| {
3399                Ok(secp.sign_schnorr_no_aux_rand(unsigned_invoice.as_ref().as_digest(), &keypair))
3400            })
3401            .map_err(|e| {
3402                PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3403            })?;
3404        let invoice_str = encode_invoice(&invoice).map_err(|e| {
3405            PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3406        })?;
3407        debug!("Created BOLT12 invoice: {invoice_str}");
3408
3409        let claim_keypair = utils::generate_keypair();
3410        let receiver_amount_sat = payer_amount_sat - fees_sat;
3411        let webhook_claim_status =
3412            match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3413                true => RevSwapStates::TransactionConfirmed,
3414                false => RevSwapStates::TransactionMempool,
3415            };
3416        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3417            url,
3418            hash_swap_id: Some(true),
3419            status: Some(vec![webhook_claim_status]),
3420        });
3421
3422        let v2_req = CreateReverseRequest {
3423            from: "BTC".to_string(),
3424            to: "L-BTC".to_string(),
3425            invoice: Some(invoice_str.clone()),
3426            invoice_amount: None,
3427            preimage_hash: None,
3428            claim_public_key: claim_keypair.public_key().into(),
3429            description: None,
3430            description_hash: None,
3431            address: Some(mrh_addr_str.clone()),
3432            address_signature: Some(mrh_addr_hash_sig.to_hex()),
3433            referral_id: None,
3434            webhook,
3435        };
3436        let create_response = self.swapper.create_receive_swap(v2_req).await?;
3437
3438        // Reserve this address until the timeout block height
3439        self.persister.insert_or_update_reserved_address(
3440            &mrh_addr_str,
3441            create_response.timeout_block_height,
3442        )?;
3443
3444        let swap_id = create_response.id.clone();
3445        let destination_pubkey = cln_node_public_key.to_hex();
3446        debug!("Created receive swap: {swap_id}");
3447
3448        let create_response_json =
3449            ReceiveSwap::from_boltz_struct_to_json(&create_response, &swap_id, None)?;
3450        let invoice_description = invoice.description().map(|s| s.to_string());
3451
3452        self.persister
3453            .insert_or_update_receive_swap(&ReceiveSwap {
3454                id: swap_id.clone(),
3455                preimage: preimage_str,
3456                create_response_json,
3457                claim_private_key: claim_keypair.display_secret().to_string(),
3458                invoice: invoice_str.clone(),
3459                bolt12_offer: Some(req.offer.clone()),
3460                payment_hash: Some(preimage.sha256.to_string()),
3461                destination_pubkey: Some(destination_pubkey),
3462                timeout_block_height: create_response.timeout_block_height,
3463                description: invoice_description,
3464                payer_note,
3465                payer_amount_sat,
3466                receiver_amount_sat,
3467                pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3468                    PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3469                })?,
3470                claim_fees_sat: reverse_pair.fees.claim_estimate(),
3471                lockup_tx_id: None,
3472                claim_address: None,
3473                claim_tx_id: None,
3474                mrh_address: mrh_addr_str,
3475                mrh_tx_id: None,
3476                created_at: utils::now(),
3477                state: PaymentState::Created,
3478                metadata: Default::default(),
3479            })
3480            .map_err(|e| {
3481                error!("Failed to insert or update receive swap: {e:?}");
3482                PaymentError::PersistError
3483            })?;
3484        self.status_stream.track_swap_id(&swap_id)?;
3485        debug!("Finished create BOLT12 invoice");
3486
3487        Ok(CreateBolt12InvoiceResponse {
3488            invoice: invoice_str,
3489        })
3490    }
3491
3492    async fn create_bolt12_offer(
3493        &self,
3494        description: String,
3495    ) -> Result<ReceivePaymentResponse, PaymentError> {
3496        let webhook_url = self.persister.get_webhook_url()?;
3497        // Parallelize the calls to get_nodes and get_reverse_swap_pairs
3498        let (nodes, maybe_reverse_pair) = tokio::try_join!(
3499            self.swapper.get_nodes(),
3500            self.swapper.get_reverse_swap_pairs()
3501        )?;
3502        let cln_node = nodes
3503            .get_btc_cln_node()
3504            .ok_or(PaymentError::generic("No BTC CLN node found"))?;
3505        debug!("Creating BOLT12 offer for description: {description}");
3506        let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3507        let min_amount_sat = reverse_pair.limits.minimal;
3508        let keypair = utils::generate_keypair();
3509        let entropy_source = RandomBytes::new(utils::generate_entropy());
3510        let secp = Secp256k1::new();
3511        let message_context = MessageContext::Offers(OffersContext::InvoiceRequest {
3512            nonce: Nonce::from_entropy_source(&entropy_source),
3513        });
3514
3515        // Build the offer with a one-hop blinded path to the swapper CLN node
3516        let offer = OfferBuilder::new(keypair.public_key())
3517            .chain(self.config.network.into())
3518            .amount_msats(min_amount_sat * 1_000)
3519            .description(description.clone())
3520            .path(
3521                BlindedMessagePath::one_hop(
3522                    cln_node.public_key,
3523                    message_context,
3524                    &entropy_source,
3525                    &secp,
3526                )
3527                .map_err(|_| {
3528                    PaymentError::generic(
3529                        "Error creating Bolt12 Offer: Could not create a one-hop blinded path",
3530                    )
3531                })?,
3532            )
3533            .build()?;
3534        let offer_str = utils::bolt12::encode_offer(&offer)?;
3535        info!("Created BOLT12 offer: {offer_str}");
3536        self.swapper
3537            .create_bolt12_offer(CreateBolt12OfferRequest {
3538                offer: offer_str.clone(),
3539                url: webhook_url.clone(),
3540            })
3541            .await?;
3542        // Store the bolt12 offer
3543        self.persister.insert_or_update_bolt12_offer(&Bolt12Offer {
3544            id: offer_str.clone(),
3545            description,
3546            private_key: keypair.display_secret().to_string(),
3547            webhook_url,
3548            created_at: utils::now(),
3549        })?;
3550        // Start tracking the offer with the status stream
3551        let subscribe_hash_sig = utils::sign_message_hash("SUBSCRIBE", &keypair)?;
3552        self.status_stream
3553            .track_offer(&offer_str, &subscribe_hash_sig.to_hex())?;
3554
3555        Ok(ReceivePaymentResponse {
3556            destination: offer_str,
3557            liquid_expiration_blockheight: None,
3558            bitcoin_expiration_blockheight: None,
3559        })
3560    }
3561
3562    async fn create_receive_chain_swap(
3563        &self,
3564        user_lockup_amount_sat: Option<u64>,
3565        fees_sat: u64,
3566    ) -> Result<ChainSwap, PaymentError> {
3567        let pair = self
3568            .get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)
3569            .await?;
3570        let claim_fees_sat = pair.fees.claim_estimate();
3571        let server_fees_sat = pair.fees.server();
3572        // Service fees are 0 if this is a zero-amount swap
3573        let service_fees_sat = user_lockup_amount_sat
3574            .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
3575            .unwrap_or_default();
3576
3577        ensure_sdk!(
3578            fees_sat == service_fees_sat + claim_fees_sat + server_fees_sat,
3579            PaymentError::InvalidOrExpiredFees
3580        );
3581
3582        let preimage = Preimage::new();
3583        let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3584
3585        let claim_keypair = utils::generate_keypair();
3586        let claim_public_key = boltz_client::PublicKey {
3587            compressed: true,
3588            inner: claim_keypair.public_key(),
3589        };
3590        let refund_keypair = utils::generate_keypair();
3591        let refund_public_key = boltz_client::PublicKey {
3592            compressed: true,
3593            inner: refund_keypair.public_key(),
3594        };
3595        let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3596            url,
3597            hash_swap_id: Some(true),
3598            status: Some(vec![
3599                ChainSwapStates::TransactionFailed,
3600                ChainSwapStates::TransactionLockupFailed,
3601                ChainSwapStates::TransactionServerConfirmed,
3602            ]),
3603        });
3604        let create_response = self
3605            .swapper
3606            .create_chain_swap(CreateChainRequest {
3607                from: "BTC".to_string(),
3608                to: "L-BTC".to_string(),
3609                preimage_hash: preimage.sha256,
3610                claim_public_key: Some(claim_public_key),
3611                refund_public_key: Some(refund_public_key),
3612                user_lock_amount: user_lockup_amount_sat,
3613                server_lock_amount: None,
3614                pair_hash: Some(pair.hash.clone()),
3615                referral_id: None,
3616                webhook,
3617            })
3618            .await?;
3619
3620        let swap_id = create_response.id.clone();
3621        let create_response_json =
3622            ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?;
3623
3624        let accept_zero_conf = user_lockup_amount_sat
3625            .map(|user_lockup_amount_sat| user_lockup_amount_sat <= pair.limits.maximal_zero_conf)
3626            .unwrap_or(false);
3627        let receiver_amount_sat = user_lockup_amount_sat
3628            .map(|user_lockup_amount_sat| user_lockup_amount_sat - fees_sat)
3629            .unwrap_or(0);
3630
3631        let swap = ChainSwap {
3632            id: swap_id.clone(),
3633            direction: Direction::Incoming,
3634            claim_address: None,
3635            lockup_address: create_response.lockup_details.lockup_address,
3636            refund_address: None,
3637            timeout_block_height: create_response.lockup_details.timeout_block_height,
3638            claim_timeout_block_height: create_response.claim_details.timeout_block_height,
3639            preimage: preimage_str,
3640            description: Some("Bitcoin transfer".to_string()),
3641            payer_amount_sat: user_lockup_amount_sat.unwrap_or(0),
3642            actual_payer_amount_sat: None,
3643            receiver_amount_sat,
3644            accepted_receiver_amount_sat: None,
3645            claim_fees_sat,
3646            pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
3647                PaymentError::generic(format!("Failed to serialize incoming ChainPair: {e:?}"))
3648            })?,
3649            accept_zero_conf,
3650            create_response_json,
3651            claim_private_key: claim_keypair.display_secret().to_string(),
3652            refund_private_key: refund_keypair.display_secret().to_string(),
3653            server_lockup_tx_id: None,
3654            user_lockup_tx_id: None,
3655            claim_tx_id: None,
3656            refund_tx_id: None,
3657            created_at: utils::now(),
3658            state: PaymentState::Created,
3659            auto_accepted_fees: false,
3660            user_lockup_spent: false,
3661            metadata: Default::default(),
3662        };
3663        self.persister.insert_or_update_chain_swap(&swap)?;
3664        self.status_stream.track_swap_id(&swap.id)?;
3665        Ok(swap)
3666    }
3667
3668    /// Receive from a Bitcoin transaction via a chain swap.
3669    ///
3670    /// If no `user_lockup_amount_sat` is specified, this is an amountless swap and `fees_sat` exclude
3671    /// the service fees.
3672    async fn receive_onchain(
3673        &self,
3674        user_lockup_amount_sat: Option<u64>,
3675        fees_sat: u64,
3676    ) -> Result<ReceivePaymentResponse, PaymentError> {
3677        self.ensure_is_started().await?;
3678
3679        let swap = self
3680            .create_receive_chain_swap(user_lockup_amount_sat, fees_sat)
3681            .await?;
3682        let create_response = swap.get_boltz_create_response()?;
3683        let address = create_response.lockup_details.lockup_address;
3684
3685        let amount = create_response.lockup_details.amount as f64 / 100_000_000.0;
3686        let bip21 = create_response.lockup_details.bip21.unwrap_or(format!(
3687            "bitcoin:{address}?amount={amount}&label=Send%20to%20L-BTC%20address"
3688        ));
3689
3690        Ok(ReceivePaymentResponse {
3691            destination: bip21,
3692            liquid_expiration_blockheight: Some(swap.claim_timeout_block_height),
3693            bitcoin_expiration_blockheight: Some(swap.timeout_block_height),
3694        })
3695    }
3696
3697    /// List all failed chain swaps that need to be refunded.
3698    /// They can be refunded by calling [LiquidSdk::prepare_refund] then [LiquidSdk::refund].
3699    pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> {
3700        let chain_swaps = self.persister.list_refundable_chain_swaps()?;
3701
3702        let mut chain_swaps_with_scripts = vec![];
3703        for swap in &chain_swaps {
3704            let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
3705            chain_swaps_with_scripts.push((swap, script_pubkey));
3706        }
3707
3708        let lockup_scripts: Vec<&boltz_client::bitcoin::Script> = chain_swaps_with_scripts
3709            .iter()
3710            .map(|(_, script_pubkey)| script_pubkey.as_script())
3711            .collect();
3712        let scripts_utxos = self
3713            .bitcoin_chain_service
3714            .get_scripts_utxos(&lockup_scripts)
3715            .await?;
3716
3717        let mut script_to_utxos_map = std::collections::HashMap::new();
3718        for script_utxos in scripts_utxos {
3719            if let Some(first_utxo) = script_utxos.first() {
3720                if let Some((_, txo)) = first_utxo.as_bitcoin() {
3721                    let script_pubkey: boltz_client::bitcoin::ScriptBuf = txo.script_pubkey.clone();
3722                    script_to_utxos_map.insert(script_pubkey, script_utxos);
3723                }
3724            }
3725        }
3726
3727        let mut refundables = vec![];
3728
3729        for (chain_swap, script_pubkey) in chain_swaps_with_scripts {
3730            if let Some(script_utxos) = script_to_utxos_map.get(&script_pubkey) {
3731                let swap_id = &chain_swap.id;
3732                let amount_sat: u64 = script_utxos
3733                    .iter()
3734                    .filter_map(|utxo| utxo.as_bitcoin().cloned())
3735                    .map(|(_, txo)| txo.value.to_sat())
3736                    .sum();
3737                info!("Incoming Chain Swap {swap_id} is refundable with {amount_sat} sats");
3738
3739                refundables.push(chain_swap.to_refundable(amount_sat));
3740            }
3741        }
3742
3743        Ok(refundables)
3744    }
3745
3746    /// Prepares to refund a failed chain swap by calculating the refund transaction size and absolute fee.
3747    ///
3748    /// # Arguments
3749    ///
3750    /// * `req` - the [PrepareRefundRequest] containing:
3751    ///     * `swap_address` - the swap address to refund from [RefundableSwap::swap_address]
3752    ///     * `refund_address` - the Bitcoin address to refund to
3753    ///     * `fee_rate_sat_per_vbyte` - the fee rate at which to broadcast the refund transaction
3754    pub async fn prepare_refund(
3755        &self,
3756        req: &PrepareRefundRequest,
3757    ) -> SdkResult<PrepareRefundResponse> {
3758        let refund_address = self
3759            .validate_bitcoin_address(&req.refund_address)
3760            .await
3761            .map_err(|e| SdkError::Generic {
3762                err: format!("Failed to validate refund address: {e}"),
3763            })?;
3764
3765        let (tx_vsize, tx_fee_sat, refund_tx_id) = self
3766            .chain_swap_handler
3767            .prepare_refund(
3768                &req.swap_address,
3769                &refund_address,
3770                req.fee_rate_sat_per_vbyte,
3771            )
3772            .await?;
3773        Ok(PrepareRefundResponse {
3774            tx_vsize,
3775            tx_fee_sat,
3776            last_refund_tx_id: refund_tx_id,
3777        })
3778    }
3779
3780    /// Refund a failed chain swap.
3781    ///
3782    /// # Arguments
3783    ///
3784    /// * `req` - the [RefundRequest] containing:
3785    ///     * `swap_address` - the swap address to refund from [RefundableSwap::swap_address]
3786    ///     * `refund_address` - the Bitcoin address to refund to
3787    ///     * `fee_rate_sat_per_vbyte` - the fee rate at which to broadcast the refund transaction
3788    pub async fn refund(&self, req: &RefundRequest) -> Result<RefundResponse, PaymentError> {
3789        let refund_address = self
3790            .validate_bitcoin_address(&req.refund_address)
3791            .await
3792            .map_err(|e| SdkError::Generic {
3793                err: format!("Failed to validate refund address: {e}"),
3794            })?;
3795
3796        let refund_tx_id = self
3797            .chain_swap_handler
3798            .refund_incoming_swap(
3799                &req.swap_address,
3800                &refund_address,
3801                req.fee_rate_sat_per_vbyte,
3802                true,
3803            )
3804            .or_else(|e| {
3805                warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
3806                self.chain_swap_handler.refund_incoming_swap(
3807                    &req.swap_address,
3808                    &refund_address,
3809                    req.fee_rate_sat_per_vbyte,
3810                    false,
3811                )
3812            })
3813            .await?;
3814
3815        Ok(RefundResponse { refund_tx_id })
3816    }
3817
3818    /// Rescans all expired chain swaps created from calling [LiquidSdk::receive_onchain] to check
3819    /// if there are any confirmed funds available to refund.
3820    ///
3821    /// Since it bypasses the monitoring period, this should be called rarely or when the caller
3822    /// expects there is a very old refundable chain swap. Otherwise, for relatively recent swaps
3823    /// (within last [CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS] blocks = ~14 days), calling this
3824    /// is not necessary as it happens automatically in the background.
3825    pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> {
3826        let t0 = Instant::now();
3827        let mut rescannable_swaps: Vec<Swap> = self
3828            .persister
3829            .list_chain_swaps()?
3830            .into_iter()
3831            .map(Into::into)
3832            .collect();
3833        self.recoverer
3834            .recover_from_onchain(&mut rescannable_swaps, None)
3835            .await?;
3836        let scanned_len = rescannable_swaps.len();
3837        for swap in rescannable_swaps {
3838            let swap_id = &swap.id();
3839            if let Swap::Chain(chain_swap) = swap {
3840                if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
3841                    error!("Error persisting rescanned Chain Swap {swap_id}: {e}");
3842                }
3843            }
3844        }
3845        info!(
3846            "Rescanned {} chain swaps in {} seconds",
3847            scanned_len,
3848            t0.elapsed().as_millis()
3849        );
3850        Ok(())
3851    }
3852
3853    fn validate_buy_bitcoin(&self, amount_sat: u64) -> Result<(), PaymentError> {
3854        ensure_sdk!(
3855            self.config.network == LiquidNetwork::Mainnet,
3856            PaymentError::invalid_network("Can only buy bitcoin on Mainnet")
3857        );
3858        // The Moonpay API defines BTC amounts as having precision = 5, so only 5 decimals are considered
3859        ensure_sdk!(
3860            amount_sat.is_multiple_of(1_000),
3861            PaymentError::generic("Can only buy sat amounts that are multiples of 1000")
3862        );
3863        Ok(())
3864    }
3865
3866    /// Prepares to buy Bitcoin via a chain swap.
3867    ///
3868    /// # Arguments
3869    ///
3870    /// * `req` - the [PrepareBuyBitcoinRequest] containing:
3871    ///     * `provider` - the [BuyBitcoinProvider] to use
3872    ///     * `amount_sat` - the amount in satoshis to buy from the provider
3873    pub async fn prepare_buy_bitcoin(
3874        &self,
3875        req: &PrepareBuyBitcoinRequest,
3876    ) -> Result<PrepareBuyBitcoinResponse, PaymentError> {
3877        self.validate_buy_bitcoin(req.amount_sat)?;
3878
3879        let res = self
3880            .prepare_receive_payment(&PrepareReceiveRequest {
3881                payment_method: PaymentMethod::BitcoinAddress,
3882                amount: Some(ReceiveAmount::Bitcoin {
3883                    payer_amount_sat: req.amount_sat,
3884                }),
3885            })
3886            .await?;
3887
3888        let Some(ReceiveAmount::Bitcoin {
3889            payer_amount_sat: amount_sat,
3890        }) = res.amount
3891        else {
3892            return Err(PaymentError::Generic {
3893                err: format!(
3894                    "Error preparing receive payment, got amount: {:?}",
3895                    res.amount
3896                ),
3897            });
3898        };
3899
3900        Ok(PrepareBuyBitcoinResponse {
3901            provider: req.provider,
3902            amount_sat,
3903            fees_sat: res.fees_sat,
3904        })
3905    }
3906
3907    /// Generate a URL to a third party provider used to buy Bitcoin via a chain swap.
3908    ///
3909    /// # Arguments
3910    ///
3911    /// * `req` - the [BuyBitcoinRequest] containing:
3912    ///     * `prepare_response` - the [PrepareBuyBitcoinResponse] from calling [LiquidSdk::prepare_buy_bitcoin]
3913    ///     * `redirect_url` - the optional redirect URL the provider should redirect to after purchase
3914    pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> {
3915        self.validate_buy_bitcoin(req.prepare_response.amount_sat)?;
3916
3917        let swap = self
3918            .create_receive_chain_swap(
3919                Some(req.prepare_response.amount_sat),
3920                req.prepare_response.fees_sat,
3921            )
3922            .await?;
3923
3924        Ok(self
3925            .buy_bitcoin_service
3926            .buy_bitcoin(
3927                req.prepare_response.provider,
3928                &swap,
3929                req.redirect_url.clone(),
3930            )
3931            .await?)
3932    }
3933
3934    /// Returns a list of swaps that need to be monitored for recovery.
3935    ///
3936    /// If no Bitcoin tip is provided, chain swaps will not be considered.
3937    pub(crate) async fn get_monitored_swaps_list(
3938        &self,
3939        only_receive_swaps: bool,
3940        include_expired_incoming_chain_swaps: bool,
3941        chain_tips: ChainTips,
3942    ) -> Result<Vec<Swap>> {
3943        let receive_swaps = self
3944            .persister
3945            .list_recoverable_receive_swaps()?
3946            .into_iter()
3947            .map(Into::into)
3948            .collect();
3949
3950        if only_receive_swaps {
3951            return Ok(receive_swaps);
3952        }
3953
3954        let send_swaps = self
3955            .persister
3956            .list_recoverable_send_swaps()?
3957            .into_iter()
3958            .map(Into::into)
3959            .collect();
3960
3961        let Some(bitcoin_tip) = chain_tips.bitcoin_tip else {
3962            return Ok([receive_swaps, send_swaps].concat());
3963        };
3964
3965        let final_swap_states: [PaymentState; 2] = [PaymentState::Complete, PaymentState::Failed];
3966
3967        let chain_swaps: Vec<Swap> = self
3968            .persister
3969            .list_chain_swaps()?
3970            .into_iter()
3971            .filter(|swap| match swap.direction {
3972                Direction::Incoming => {
3973                    if include_expired_incoming_chain_swaps {
3974                        bitcoin_tip
3975                            <= swap.timeout_block_height
3976                                + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS
3977                            && chain_tips.liquid_tip
3978                                <= swap.claim_timeout_block_height
3979                                    + CHAIN_SWAP_MONITORING_PERIOD_LIQUID_BLOCKS
3980                    } else {
3981                        bitcoin_tip <= swap.timeout_block_height
3982                            && chain_tips.liquid_tip <= swap.claim_timeout_block_height
3983                    }
3984                }
3985                Direction::Outgoing => {
3986                    !final_swap_states.contains(&swap.state)
3987                        && chain_tips.liquid_tip <= swap.timeout_block_height
3988                        && bitcoin_tip <= swap.claim_timeout_block_height
3989                }
3990            })
3991            .map(Into::into)
3992            .collect();
3993
3994        Ok([receive_swaps, send_swaps, chain_swaps].concat())
3995    }
3996
3997    /// This method fetches the chain tx data (onchain and mempool) using LWK. For every wallet tx,
3998    /// it inserts or updates a corresponding entry in our Payments table.
3999    async fn sync_payments_with_chain_data(
4000        &self,
4001        mut recoverable_swaps: Vec<Swap>,
4002        chain_tips: ChainTips,
4003    ) -> Result<()> {
4004        debug!("LiquidSdk::sync_payments_with_chain_data: start");
4005        debug!(
4006            "LiquidSdk::sync_payments_with_chain_data: called with {} recoverable swaps",
4007            recoverable_swaps.len()
4008        );
4009        let mut wallet_tx_map = self
4010            .recoverer
4011            .recover_from_onchain(&mut recoverable_swaps, Some(chain_tips))
4012            .await?;
4013
4014        let all_wallet_tx_ids: HashSet<String> =
4015            wallet_tx_map.keys().map(|txid| txid.to_string()).collect();
4016
4017        for swap in recoverable_swaps {
4018            let swap_id = &swap.id();
4019
4020            // Update the payment wallet txs before updating the swap so the tx data is pulled into the payment
4021            match swap {
4022                Swap::Receive(receive_swap) => {
4023                    let history_updates = vec![&receive_swap.claim_tx_id, &receive_swap.mrh_tx_id];
4024                    for tx_id in history_updates
4025                        .into_iter()
4026                        .flatten()
4027                        .collect::<Vec<&String>>()
4028                    {
4029                        if let Some(tx) =
4030                            wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4031                        {
4032                            self.persister
4033                                .insert_or_update_payment_with_wallet_tx(&tx)?;
4034                        }
4035                    }
4036                    if let Err(e) = self.receive_swap_handler.update_swap(receive_swap) {
4037                        error!("Error persisting recovered receive swap {swap_id}: {e}");
4038                    }
4039                }
4040                Swap::Send(send_swap) => {
4041                    let history_updates = vec![&send_swap.lockup_tx_id, &send_swap.refund_tx_id];
4042                    for tx_id in history_updates
4043                        .into_iter()
4044                        .flatten()
4045                        .collect::<Vec<&String>>()
4046                    {
4047                        if let Some(tx) =
4048                            wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4049                        {
4050                            self.persister
4051                                .insert_or_update_payment_with_wallet_tx(&tx)?;
4052                        }
4053                    }
4054                    if let Err(e) = self.send_swap_handler.update_swap(send_swap) {
4055                        error!("Error persisting recovered send swap {swap_id}: {e}");
4056                    }
4057                }
4058                Swap::Chain(chain_swap) => {
4059                    let history_updates = match chain_swap.direction {
4060                        Direction::Incoming => vec![&chain_swap.claim_tx_id],
4061                        Direction::Outgoing => {
4062                            vec![&chain_swap.user_lockup_tx_id, &chain_swap.refund_tx_id]
4063                        }
4064                    };
4065                    for tx_id in history_updates
4066                        .into_iter()
4067                        .flatten()
4068                        .collect::<Vec<&String>>()
4069                    {
4070                        if let Some(tx) =
4071                            wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4072                        {
4073                            self.persister
4074                                .insert_or_update_payment_with_wallet_tx(&tx)?;
4075                        }
4076                    }
4077                    if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
4078                        error!("Error persisting recovered Chain Swap {swap_id}: {e}");
4079                    }
4080                }
4081            };
4082        }
4083
4084        let non_swap_wallet_tx_map = wallet_tx_map;
4085
4086        let payments = self
4087            .persister
4088            .get_payments_by_tx_id(&ListPaymentsRequest::default())?;
4089
4090        // We query only these that may need update, should be a fast query.
4091        let unconfirmed_payment_txs_data = self.persister.list_unconfirmed_payment_txs_data()?;
4092        let unconfirmed_txs_by_id: HashMap<String, PaymentTxData> = unconfirmed_payment_txs_data
4093            .into_iter()
4094            .map(|tx| (tx.tx_id.clone(), tx))
4095            .collect::<HashMap<String, PaymentTxData>>();
4096
4097        debug!(
4098            "Found {} unconfirmed payment txs",
4099            unconfirmed_txs_by_id.len()
4100        );
4101        for tx in non_swap_wallet_tx_map.values() {
4102            let tx_id = tx.txid.to_string();
4103            let maybe_payment = payments.get(&tx_id);
4104            let mut updated = false;
4105            match maybe_payment {
4106                // When no payment is found or its a Liquid payment
4107                None
4108                | Some(Payment {
4109                    details: PaymentDetails::Liquid { .. },
4110                    ..
4111                }) => {
4112                    let updated_needed = maybe_payment
4113                        .is_none_or(|payment| payment.status == Pending && tx.height.is_some());
4114                    if updated_needed {
4115                        // An unknown tx which needs inserting or a known Liquid payment tx
4116                        // that was in the mempool, but is now confirmed
4117                        self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
4118                        self.emit_payment_updated(Some(tx_id.clone())).await?;
4119                        updated = true
4120                    }
4121                }
4122
4123                _ => {}
4124            }
4125            if !updated && unconfirmed_txs_by_id.contains_key(&tx_id) && tx.height.is_some() {
4126                // An unconfirmed tx that was not found in the payments table
4127                self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
4128            }
4129        }
4130
4131        let unknown_unconfirmed_txs: Vec<_> = unconfirmed_txs_by_id
4132            .iter()
4133            .filter(|(txid, _)| !all_wallet_tx_ids.contains(*txid))
4134            .map(|(_, tx)| tx)
4135            .collect();
4136
4137        debug!(
4138            "Found {} unknown unconfirmed txs",
4139            unknown_unconfirmed_txs.len()
4140        );
4141        for unknown_unconfirmed_tx in unknown_unconfirmed_txs {
4142            if unknown_unconfirmed_tx.timestamp.is_some_and(|t| {
4143                (utils::now().saturating_sub(t)) > NETWORK_PROPAGATION_GRACE_PERIOD.as_secs() as u32
4144            }) {
4145                self.persister
4146                    .delete_payment_tx_data(&unknown_unconfirmed_tx.tx_id)?;
4147                info!(
4148                    "Found an unknown unconfirmed tx and deleted it. Txid: {}",
4149                    unknown_unconfirmed_tx.tx_id
4150                );
4151            } else {
4152                debug!(
4153                    "Found an unknown unconfirmed tx that was inserted at {:?}. \
4154                Keeping it to allow propagation through the network. Txid: {}",
4155                    unknown_unconfirmed_tx.timestamp, unknown_unconfirmed_tx.tx_id
4156                )
4157            }
4158        }
4159
4160        self.update_wallet_info().await?;
4161        debug!("LiquidSdk::sync_payments_with_chain_data: end");
4162        Ok(())
4163    }
4164
4165    async fn update_wallet_info(&self) -> Result<()> {
4166        let asset_metadata: HashMap<String, AssetMetadata> = self
4167            .persister
4168            .list_asset_metadata()?
4169            .into_iter()
4170            .map(|am| (am.asset_id.clone(), am))
4171            .collect();
4172        let transactions = self.onchain_wallet.transactions().await?;
4173        let tx_ids = transactions
4174            .iter()
4175            .map(|tx| tx.txid.to_string())
4176            .collect::<Vec<_>>();
4177        let asset_balances = transactions
4178            .into_iter()
4179            .fold(BTreeMap::<AssetId, i64>::new(), |mut acc, tx| {
4180                tx.balance.into_iter().for_each(|(asset_id, balance)| {
4181                    // Consider only confirmed unspent outputs (confirmed transactions output reduced by unconfirmed spent outputs)
4182                    if tx.height.is_some() || balance < 0 {
4183                        *acc.entry(asset_id).or_default() += balance;
4184                    }
4185                });
4186                acc
4187            })
4188            .into_iter()
4189            .map(|(asset_id, balance)| {
4190                let asset_id = asset_id.to_hex();
4191                let balance_sat = balance.unsigned_abs();
4192                let maybe_asset_metadata = asset_metadata.get(&asset_id);
4193                AssetBalance {
4194                    asset_id,
4195                    balance_sat,
4196                    name: maybe_asset_metadata.map(|am| am.name.clone()),
4197                    ticker: maybe_asset_metadata.map(|am| am.ticker.clone()),
4198                    balance: maybe_asset_metadata.map(|am| am.amount_from_sat(balance_sat)),
4199                }
4200            })
4201            .collect::<Vec<AssetBalance>>();
4202        let mut balance_sat = asset_balances
4203            .clone()
4204            .into_iter()
4205            .find(|ab| ab.asset_id.eq(&self.config.lbtc_asset_id()))
4206            .map_or(0, |ab| ab.balance_sat);
4207
4208        let mut pending_send_sat = 0;
4209        let mut pending_receive_sat = 0;
4210        let payments = self.persister.get_payments(&ListPaymentsRequest {
4211            states: Some(vec![
4212                PaymentState::Pending,
4213                PaymentState::RefundPending,
4214                PaymentState::WaitingFeeAcceptance,
4215            ]),
4216            ..Default::default()
4217        })?;
4218
4219        for payment in payments {
4220            let is_lbtc_asset_id = payment.details.is_lbtc_asset_id(self.config.network);
4221            match payment.payment_type {
4222                PaymentType::Send => match payment.details.get_refund_tx_amount_sat() {
4223                    Some(refund_tx_amount_sat) => pending_receive_sat += refund_tx_amount_sat,
4224                    None => {
4225                        let total_sat = if is_lbtc_asset_id {
4226                            payment.amount_sat + payment.fees_sat
4227                        } else {
4228                            payment.fees_sat
4229                        };
4230                        if let Some(tx_id) = payment.tx_id {
4231                            if !tx_ids.contains(&tx_id) {
4232                                debug!("Deducting {total_sat} sats from balance");
4233                                balance_sat = balance_sat.saturating_sub(total_sat);
4234                            }
4235                        }
4236                        pending_send_sat += total_sat
4237                    }
4238                },
4239                PaymentType::Receive => {
4240                    if is_lbtc_asset_id && payment.status != RefundPending {
4241                        pending_receive_sat += payment.amount_sat;
4242                    }
4243                }
4244            }
4245        }
4246
4247        debug!("Onchain wallet balance: {balance_sat} sats");
4248        let info_response = WalletInfo {
4249            balance_sat,
4250            pending_send_sat,
4251            pending_receive_sat,
4252            fingerprint: self.onchain_wallet.fingerprint()?,
4253            pubkey: self.onchain_wallet.pubkey()?,
4254            asset_balances,
4255        };
4256        self.persister.set_wallet_info(&info_response)
4257    }
4258
4259    /// Lists the SDK payments in reverse chronological order, from newest to oldest.
4260    /// The payments are determined based on onchain transactions and swaps.
4261    pub async fn list_payments(
4262        &self,
4263        req: &ListPaymentsRequest,
4264    ) -> Result<Vec<Payment>, PaymentError> {
4265        self.ensure_is_started().await?;
4266
4267        Ok(self.persister.get_payments(req)?)
4268    }
4269
4270    /// Retrieves a payment.
4271    ///
4272    /// # Arguments
4273    ///
4274    /// * `req` - the [GetPaymentRequest] containing:
4275    ///     * [GetPaymentRequest::Lightning] - the `payment_hash` of the lightning invoice
4276    ///
4277    /// # Returns
4278    ///
4279    /// Returns an `Option<Payment>` if found, or `None` if no payment matches the given request.
4280    pub async fn get_payment(
4281        &self,
4282        req: &GetPaymentRequest,
4283    ) -> Result<Option<Payment>, PaymentError> {
4284        self.ensure_is_started().await?;
4285
4286        Ok(self.persister.get_payment_by_request(req)?)
4287    }
4288
4289    /// Fetches an up-to-date fees proposal for a [Payment] that is [WaitingFeeAcceptance].
4290    ///
4291    /// Use [LiquidSdk::accept_payment_proposed_fees] to accept the proposed fees and proceed
4292    /// with the payment.
4293    pub async fn fetch_payment_proposed_fees(
4294        &self,
4295        req: &FetchPaymentProposedFeesRequest,
4296    ) -> SdkResult<FetchPaymentProposedFeesResponse> {
4297        let chain_swap =
4298            self.persister
4299                .fetch_chain_swap_by_id(&req.swap_id)?
4300                .ok_or(SdkError::Generic {
4301                    err: format!("Could not find Swap {}", req.swap_id),
4302                })?;
4303
4304        ensure_sdk!(
4305            chain_swap.state == WaitingFeeAcceptance,
4306            SdkError::Generic {
4307                err: "Payment is not WaitingFeeAcceptance".to_string()
4308            }
4309        );
4310
4311        let server_lockup_quote = self
4312            .swapper
4313            .get_zero_amount_chain_swap_quote(&req.swap_id)
4314            .await?;
4315
4316        let actual_payer_amount_sat =
4317            chain_swap
4318                .actual_payer_amount_sat
4319                .ok_or(SdkError::Generic {
4320                    err: "No actual payer amount found when state is WaitingFeeAcceptance"
4321                        .to_string(),
4322                })?;
4323        let fees_sat =
4324            actual_payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat;
4325
4326        Ok(FetchPaymentProposedFeesResponse {
4327            swap_id: req.swap_id.clone(),
4328            fees_sat,
4329            payer_amount_sat: actual_payer_amount_sat,
4330            receiver_amount_sat: actual_payer_amount_sat - fees_sat,
4331        })
4332    }
4333
4334    /// Accepts proposed fees for a [Payment] that is [WaitingFeeAcceptance].
4335    ///
4336    /// Use [LiquidSdk::fetch_payment_proposed_fees] to get an up-to-date fees proposal.
4337    pub async fn accept_payment_proposed_fees(
4338        &self,
4339        req: &AcceptPaymentProposedFeesRequest,
4340    ) -> Result<(), PaymentError> {
4341        let FetchPaymentProposedFeesResponse {
4342            swap_id,
4343            fees_sat,
4344            payer_amount_sat,
4345            ..
4346        } = req.clone().response;
4347
4348        let chain_swap =
4349            self.persister
4350                .fetch_chain_swap_by_id(&swap_id)?
4351                .ok_or(SdkError::Generic {
4352                    err: format!("Could not find Swap {swap_id}"),
4353                })?;
4354
4355        ensure_sdk!(
4356            chain_swap.state == WaitingFeeAcceptance,
4357            PaymentError::Generic {
4358                err: "Payment is not WaitingFeeAcceptance".to_string()
4359            }
4360        );
4361
4362        let server_lockup_quote = self
4363            .swapper
4364            .get_zero_amount_chain_swap_quote(&swap_id)
4365            .await?;
4366
4367        ensure_sdk!(
4368            fees_sat == payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat,
4369            PaymentError::InvalidOrExpiredFees
4370        );
4371
4372        self.persister
4373            .update_accepted_receiver_amount(&swap_id, Some(payer_amount_sat - fees_sat))?;
4374        self.swapper
4375            .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())
4376            .inspect_err(|e| {
4377                error!("Failed to accept zero-amount swap {swap_id} quote: {e} - trying to erase the accepted receiver amount...");
4378                let _ = self
4379                    .persister
4380                    .update_accepted_receiver_amount(&swap_id, None);
4381            }).await?;
4382        self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
4383            swap_id,
4384            to_state: Pending,
4385            ..Default::default()
4386        })
4387    }
4388
4389    /// Empties the Liquid Wallet cache for the [Config::network].
4390    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4391    pub fn empty_wallet_cache(&self) -> Result<()> {
4392        let mut path = PathBuf::from(self.config.working_dir.clone());
4393        path.push(Into::<lwk_wollet::ElementsNetwork>::into(self.config.network).as_str());
4394        path.push("enc_cache");
4395
4396        std::fs::remove_dir_all(&path)?;
4397        std::fs::create_dir_all(path)?;
4398
4399        Ok(())
4400    }
4401
4402    /// Synchronizes the local state with the mempool and onchain data.
4403    pub async fn sync(&self, partial_sync: bool) -> SdkResult<()> {
4404        let blockchain_info = self.get_info().await?.blockchain_info;
4405        let sync_context = self
4406            .get_sync_context(GetSyncContextRequest {
4407                partial_sync: Some(partial_sync),
4408                last_liquid_tip: blockchain_info.liquid_tip,
4409                last_bitcoin_tip: blockchain_info.bitcoin_tip,
4410            })
4411            .await?;
4412
4413        self.sync_inner(
4414            sync_context.recoverable_swaps,
4415            ChainTips {
4416                liquid_tip: sync_context.maybe_liquid_tip.ok_or(SdkError::Generic {
4417                    err: "Liquid tip not available".to_string(),
4418                })?,
4419                bitcoin_tip: sync_context.maybe_bitcoin_tip,
4420            },
4421        )
4422        .await
4423    }
4424
4425    /// Computes the sync context.
4426    ///
4427    /// # Arguments
4428    /// * `partial_sync` - if not provided, this will infer it based on the last known tips.
4429    /// * `last_liquid_tip` - the last known liquid tip
4430    /// * `last_bitcoin_tip` - the last known bitcoin tip
4431    ///
4432    /// # Returns
4433    /// * `maybe_liquid_tip` - the current liquid tip, or `None` if the liquid tip could not be fetched
4434    /// * `maybe_bitcoin_tip` - the current bitcoin tip, or `None` if the bitcoin tip could not be fetched
4435    /// * `recoverable_swaps` - the recoverable swaps, which are built using the last known bitcoin tip. If
4436    ///   the bitcoin tip could not be fetched, this won't include chain swaps. If the liquid tip could not be fetched,
4437    ///   this will be an empty vector.
4438    /// * `is_new_liquid_block` - true if the liquid tip is new
4439    /// * `is_new_bitcoin_block` - true if the bitcoin tip is new
4440    async fn get_sync_context(&self, req: GetSyncContextRequest) -> SdkResult<SyncContext> {
4441        // Get the liquid tip
4442        let t0 = Instant::now();
4443        let liquid_tip = match self.liquid_chain_service.tip().await {
4444            Ok(tip) => Some(tip),
4445            Err(e) => {
4446                error!("Failed to fetch liquid tip: {e}");
4447                None
4448            }
4449        };
4450        let duration_ms = Instant::now().duration_since(t0).as_millis();
4451        if liquid_tip.is_some() {
4452            info!("Fetched liquid tip in ({duration_ms} ms)");
4453        }
4454
4455        let is_new_liquid_block = liquid_tip.is_some_and(|lt| lt > req.last_liquid_tip);
4456
4457        // Get the recoverable swaps assuming full sync if partial sync is not provided
4458        let mut recoverable_swaps = self
4459            .get_monitored_swaps_list(
4460                req.partial_sync.unwrap_or(false),
4461                true,
4462                ChainTips {
4463                    liquid_tip: liquid_tip.unwrap_or(req.last_liquid_tip),
4464                    bitcoin_tip: Some(req.last_bitcoin_tip),
4465                },
4466            )
4467            .await?;
4468
4469        // Only fetch the bitcoin tip if there is a new liquid block and
4470        // there are chain swaps being monitored
4471        let bitcoin_tip = if !is_new_liquid_block {
4472            debug!("No new liquid block, skipping bitcoin tip fetch");
4473            None
4474        } else if recoverable_swaps
4475            .iter()
4476            .any(|s| matches!(s, Swap::Chain(_)))
4477            .not()
4478        {
4479            debug!("No chain swaps being monitored, skipping bitcoin tip fetch");
4480            None
4481        } else {
4482            // Get the bitcoin tip
4483            let t0 = Instant::now();
4484            let bitcoin_tip = match self.bitcoin_chain_service.tip().await {
4485                Ok(tip) => Some(tip),
4486                Err(e) => {
4487                    error!("Failed to fetch bitcoin tip: {e}");
4488                    None
4489                }
4490            };
4491            let duration_ms = Instant::now().duration_since(t0).as_millis();
4492            if bitcoin_tip.is_some() {
4493                info!("Fetched bitcoin tip in ({duration_ms} ms)");
4494            } else {
4495                recoverable_swaps.retain(|s| !matches!(s, Swap::Chain(_)));
4496            }
4497            bitcoin_tip
4498        };
4499
4500        let is_new_bitcoin_block = bitcoin_tip.is_some_and(|bt| bt > req.last_bitcoin_tip);
4501
4502        // Update the recoverable swaps if we previously didn't know if this is a partial sync or not
4503        // No liquid tip means there's no point in returning recoverable swaps
4504        if let Some(liquid_tip) = liquid_tip {
4505            if req.partial_sync.is_none() {
4506                let only_receive_swaps = !is_new_liquid_block && !is_new_bitcoin_block;
4507                let include_expired_incoming_chain_swaps = is_new_bitcoin_block;
4508
4509                recoverable_swaps = self
4510                    .get_monitored_swaps_list(
4511                        only_receive_swaps,
4512                        include_expired_incoming_chain_swaps,
4513                        ChainTips {
4514                            liquid_tip,
4515                            bitcoin_tip,
4516                        },
4517                    )
4518                    .await?;
4519            }
4520        } else {
4521            recoverable_swaps = Vec::new();
4522        }
4523
4524        Ok(SyncContext {
4525            maybe_liquid_tip: liquid_tip,
4526            maybe_bitcoin_tip: bitcoin_tip,
4527            recoverable_swaps,
4528            is_new_liquid_block,
4529            is_new_bitcoin_block,
4530        })
4531    }
4532
4533    async fn sync_inner(
4534        &self,
4535        recoverable_swaps: Vec<Swap>,
4536        chain_tips: ChainTips,
4537    ) -> SdkResult<()> {
4538        debug!(
4539            "LiquidSdk::sync_inner called with {} recoverable swaps",
4540            recoverable_swaps.len()
4541        );
4542        self.ensure_is_started().await?;
4543
4544        let t0 = Instant::now();
4545
4546        self.onchain_wallet.full_scan().await.map_err(|err| {
4547            error!("Failed to scan wallet: {err:?}");
4548            SdkError::generic(err.to_string())
4549        })?;
4550
4551        let is_first_sync = !self
4552            .persister
4553            .get_is_first_sync_complete()?
4554            .unwrap_or(false);
4555        match is_first_sync {
4556            true => {
4557                self.event_manager.pause_notifications();
4558                self.sync_payments_with_chain_data(recoverable_swaps, chain_tips)
4559                    .await?;
4560                self.event_manager.resume_notifications();
4561                self.persister.set_is_first_sync_complete(true)?;
4562            }
4563            false => {
4564                self.sync_payments_with_chain_data(recoverable_swaps, chain_tips)
4565                    .await?;
4566            }
4567        }
4568        let duration_ms = Instant::now().duration_since(t0).as_millis();
4569        info!("Synchronized with mempool and onchain data ({duration_ms} ms)");
4570
4571        self.notify_event_listeners(SdkEvent::Synced).await;
4572        Ok(())
4573    }
4574
4575    /// Backup the local state to the provided backup path.
4576    ///
4577    /// # Arguments
4578    ///
4579    /// * `req` - the [BackupRequest] containing:
4580    ///     * `backup_path` - the optional backup path. Defaults to [Config::working_dir]
4581    pub fn backup(&self, req: BackupRequest) -> Result<()> {
4582        let backup_path = req
4583            .backup_path
4584            .map(PathBuf::from)
4585            .unwrap_or(self.persister.get_default_backup_path());
4586        self.persister.backup(backup_path)
4587    }
4588
4589    /// Restores the local state from the provided backup path.
4590    ///
4591    /// # Arguments
4592    ///
4593    /// * `req` - the [RestoreRequest] containing:
4594    ///     * `backup_path` - the optional backup path. Defaults to [Config::working_dir]
4595    pub fn restore(&self, req: RestoreRequest) -> Result<()> {
4596        let backup_path = req
4597            .backup_path
4598            .map(PathBuf::from)
4599            .unwrap_or(self.persister.get_default_backup_path());
4600        ensure_sdk!(
4601            backup_path.exists(),
4602            SdkError::generic("Backup file does not exist").into()
4603        );
4604        self.persister.restore_from_backup(backup_path)
4605    }
4606
4607    /// Prepares to pay to an LNURL encoded pay request or lightning address.
4608    ///
4609    /// This is the second step of LNURL-pay flow. The first step is [LiquidSdk::parse], which also validates the LNURL
4610    /// destination and generates the [LnUrlPayRequest] payload needed here.
4611    ///
4612    /// This call will validate the `amount_msat` and `comment` parameters of `req` against the parameters
4613    /// of the LNURL endpoint (`req_data`). If they match the endpoint requirements, a [PrepareSendResponse] is
4614    /// prepared for the invoice. If the receiver has encoded a Magic Routing Hint in the invoice, the
4615    /// [PrepareSendResponse]'s `fees_sat` will reflect this.
4616    ///
4617    /// # Arguments
4618    ///
4619    /// * `req` - the [PrepareLnUrlPayRequest] containing:
4620    ///     * `data` - the [LnUrlPayRequestData] returned by [LiquidSdk::parse]
4621    ///     * `amount` - the [PayAmount] to send:
4622    ///        - [PayAmount::Drain] which uses all Bitcoin funds
4623    ///        - [PayAmount::Bitcoin] which sets the amount in satoshi that will be received
4624    ///     * `bip353_address` - a BIP353 address, in case one was used in order to fetch the LNURL
4625    ///       Pay request data. Returned by [parse].
4626    ///     * `comment` - an optional comment LUD-12 to be stored with the payment. The comment is included in the
4627    ///       invoice request sent to the LNURL endpoint.
4628    ///     * `validate_success_action_url` - validates that, if there is a URL success action, the URL domain matches
4629    ///       the LNURL callback domain. Defaults to 'true'.
4630    ///
4631    /// # Returns
4632    /// Returns a [PrepareLnUrlPayResponse] containing:
4633    ///     * `destination` - the destination of the payment
4634    ///     * `amount` - the [PayAmount] to send
4635    ///     * `fees_sat` - the fees in satoshis to send the payment
4636    ///     * `data` - the [LnUrlPayRequestData] returned by [parse]
4637    ///     * `comment` - an optional comment for this payment
4638    ///     * `success_action` - the optional unprocessed LUD-09 success action
4639    pub async fn prepare_lnurl_pay(
4640        &self,
4641        req: PrepareLnUrlPayRequest,
4642    ) -> Result<PrepareLnUrlPayResponse, LnUrlPayError> {
4643        let amount_msat = match req.amount {
4644            PayAmount::Drain => {
4645                let get_info_res = self
4646                    .get_info()
4647                    .await
4648                    .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
4649                ensure_sdk!(
4650                    get_info_res.wallet_info.pending_receive_sat == 0
4651                        && get_info_res.wallet_info.pending_send_sat == 0,
4652                    LnUrlPayError::Generic {
4653                        err: "Cannot drain while there are pending payments".to_string(),
4654                    }
4655                );
4656                let lbtc_pair = self
4657                    .swapper
4658                    .get_submarine_pairs()
4659                    .await?
4660                    .ok_or(PaymentError::PairsNotFound)?;
4661                let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
4662                let drain_amount_sat = get_info_res.wallet_info.balance_sat - drain_fees_sat;
4663                // Get the inverse receiver amount by calculating a dummy amount then increment up to the drain amount
4664                let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
4665                let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
4666                let receiver_amount_sat = utils::increment_receiver_amount_up_to_drain_amount(
4667                    dummy_amount_sat,
4668                    &lbtc_pair,
4669                    drain_amount_sat,
4670                );
4671                lbtc_pair
4672                    .limits
4673                    .within(receiver_amount_sat)
4674                    .map_err(|e| LnUrlPayError::Generic { err: e.message() })?;
4675                // Validate if we can actually drain the wallet with a swap
4676                let pair_fees_sat = lbtc_pair.fees.total(receiver_amount_sat);
4677                ensure_sdk!(
4678                    receiver_amount_sat + pair_fees_sat == drain_amount_sat,
4679                    LnUrlPayError::Generic {
4680                        err: "Cannot drain without leaving a remainder".to_string(),
4681                    }
4682                );
4683
4684                receiver_amount_sat * 1000
4685            }
4686            PayAmount::Bitcoin {
4687                receiver_amount_sat,
4688            } => receiver_amount_sat * 1000,
4689            PayAmount::Asset { .. } => {
4690                return Err(LnUrlPayError::Generic {
4691                    err: "Cannot send an asset to a Bitcoin address".to_string(),
4692                })
4693            }
4694        };
4695
4696        match validate_lnurl_pay(
4697            self.rest_client.as_ref(),
4698            amount_msat,
4699            &req.comment,
4700            &req.data,
4701            self.config.network.into(),
4702            req.validate_success_action_url,
4703        )
4704        .await?
4705        {
4706            ValidatedCallbackResponse::EndpointError { data } => {
4707                Err(LnUrlPayError::Generic { err: data.reason })
4708            }
4709            ValidatedCallbackResponse::EndpointSuccess { data } => {
4710                let prepare_response = self
4711                    .prepare_send_payment(&PrepareSendRequest {
4712                        destination: data.pr.clone(),
4713                        amount: Some(req.amount.clone()),
4714                        disable_mrh: None,
4715                        payment_timeout_sec: None,
4716                    })
4717                    .await?;
4718
4719                let destination = match prepare_response.destination {
4720                    SendDestination::Bolt11 { invoice, .. } => SendDestination::Bolt11 {
4721                        invoice,
4722                        bip353_address: req.bip353_address,
4723                    },
4724                    SendDestination::LiquidAddress { address_data, .. } => {
4725                        SendDestination::LiquidAddress {
4726                            address_data,
4727                            bip353_address: req.bip353_address,
4728                        }
4729                    }
4730                    destination => destination,
4731                };
4732                let fees_sat = prepare_response
4733                    .fees_sat
4734                    .ok_or(PaymentError::InsufficientFunds)?;
4735
4736                Ok(PrepareLnUrlPayResponse {
4737                    destination,
4738                    fees_sat,
4739                    data: req.data,
4740                    amount: req.amount,
4741                    comment: req.comment,
4742                    success_action: data.success_action,
4743                })
4744            }
4745        }
4746    }
4747
4748    /// Pay to an LNURL encoded pay request or lightning address.
4749    ///
4750    /// The final step of LNURL-pay flow, called after preparing the payment with [LiquidSdk::prepare_lnurl_pay].
4751    /// This call sends the payment using the [PrepareLnUrlPayResponse]'s `prepare_send_response` either via
4752    /// Lightning or directly to a Liquid address if a Magic Routing Hint is included in the invoice.
4753    /// Once the payment is made, the [PrepareLnUrlPayResponse]'s `success_action` is processed decrypting
4754    /// the AES data if needed.
4755    ///
4756    /// # Arguments
4757    ///
4758    /// * `req` - the [LnUrlPayRequest] containing:
4759    ///     * `prepare_response` - the [PrepareLnUrlPayResponse] returned by [LiquidSdk::prepare_lnurl_pay]
4760    pub async fn lnurl_pay(
4761        &self,
4762        req: model::LnUrlPayRequest,
4763    ) -> Result<LnUrlPayResult, LnUrlPayError> {
4764        let prepare_response = req.prepare_response;
4765        let mut payment = self
4766            .send_payment(&SendPaymentRequest {
4767                prepare_response: PrepareSendResponse {
4768                    destination: prepare_response.destination.clone(),
4769                    fees_sat: Some(prepare_response.fees_sat),
4770                    estimated_asset_fees: None,
4771                    exchange_amount_sat: None,
4772                    amount: Some(prepare_response.amount),
4773                    disable_mrh: None,
4774                    payment_timeout_sec: None,
4775                },
4776                use_asset_fees: None,
4777                payer_note: prepare_response.comment.clone(),
4778            })
4779            .await?
4780            .payment;
4781
4782        let maybe_sa_processed: Option<SuccessActionProcessed> = match prepare_response
4783            .success_action
4784            .clone()
4785        {
4786            Some(sa) => {
4787                match sa {
4788                    // For AES, we decrypt the contents if the preimage is available
4789                    SuccessAction::Aes { data } => {
4790                        let PaymentDetails::Lightning {
4791                            swap_id, preimage, ..
4792                        } = &payment.details
4793                        else {
4794                            return Err(LnUrlPayError::Generic {
4795                                err: format!("Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.", payment.details),
4796                            });
4797                        };
4798
4799                        match preimage {
4800                            Some(preimage_str) => {
4801                                debug!(
4802                                    "Decrypting AES success action with preimage for Send Swap {swap_id}"
4803                                );
4804                                let preimage =
4805                                    sha256::Hash::from_str(preimage_str).map_err(|_| {
4806                                        LnUrlPayError::Generic {
4807                                            err: "Invalid preimage".to_string(),
4808                                        }
4809                                    })?;
4810                                let preimage_arr = preimage.to_byte_array();
4811                                let result = match (data, &preimage_arr).try_into() {
4812                                    Ok(data) => AesSuccessActionDataResult::Decrypted { data },
4813                                    Err(e) => AesSuccessActionDataResult::ErrorStatus {
4814                                        reason: e.to_string(),
4815                                    },
4816                                };
4817                                Some(SuccessActionProcessed::Aes { result })
4818                            }
4819                            None => {
4820                                debug!("Preimage not yet available to decrypt AES success action for Send Swap {swap_id}");
4821                                None
4822                            }
4823                        }
4824                    }
4825                    SuccessAction::Message { data } => {
4826                        Some(SuccessActionProcessed::Message { data })
4827                    }
4828                    SuccessAction::Url { data } => Some(SuccessActionProcessed::Url { data }),
4829                }
4830            }
4831            None => None,
4832        };
4833
4834        let description = payment
4835            .details
4836            .get_description()
4837            .or_else(|| extract_description_from_metadata(&prepare_response.data));
4838
4839        let lnurl_pay_domain = match prepare_response.data.ln_address {
4840            Some(_) => None,
4841            None => Some(prepare_response.data.domain),
4842        };
4843        if let (Some(tx_id), Some(destination)) =
4844            (payment.tx_id.clone(), payment.destination.clone())
4845        {
4846            self.persister
4847                .insert_or_update_payment_details(PaymentTxDetails {
4848                    tx_id: tx_id.clone(),
4849                    destination,
4850                    description,
4851                    lnurl_info: Some(LnUrlInfo {
4852                        ln_address: prepare_response.data.ln_address,
4853                        lnurl_pay_comment: prepare_response.comment,
4854                        lnurl_pay_domain,
4855                        lnurl_pay_metadata: Some(prepare_response.data.metadata_str),
4856                        lnurl_pay_success_action: maybe_sa_processed.clone(),
4857                        lnurl_pay_unprocessed_success_action: prepare_response.success_action,
4858                        lnurl_withdraw_endpoint: None,
4859                    }),
4860                    ..Default::default()
4861                })?;
4862            // Get the payment with the lnurl_info details
4863            payment = self.persister.get_payment(&tx_id)?.unwrap_or(payment);
4864        }
4865
4866        Ok(LnUrlPayResult::EndpointSuccess {
4867            data: model::LnUrlPaySuccessData {
4868                payment,
4869                success_action: maybe_sa_processed,
4870            },
4871        })
4872    }
4873
4874    /// Second step of LNURL-withdraw. The first step is [LiquidSdk::parse], which also validates the LNURL destination
4875    /// and generates the [LnUrlWithdrawRequest] payload needed here.
4876    ///
4877    /// This call will validate the given `amount_msat` against the parameters
4878    /// of the LNURL endpoint (`data`). If they match the endpoint requirements, the LNURL withdraw
4879    /// request is made. A successful result here means the endpoint started the payment.
4880    pub async fn lnurl_withdraw(
4881        &self,
4882        req: LnUrlWithdrawRequest,
4883    ) -> Result<LnUrlWithdrawResult, LnUrlWithdrawError> {
4884        let prepare_response = self
4885            .prepare_receive_payment(&{
4886                PrepareReceiveRequest {
4887                    payment_method: PaymentMethod::Bolt11Invoice,
4888                    amount: Some(ReceiveAmount::Bitcoin {
4889                        payer_amount_sat: req.amount_msat / 1_000,
4890                    }),
4891                }
4892            })
4893            .await?;
4894        let receive_res = self
4895            .receive_payment(&ReceivePaymentRequest {
4896                prepare_response,
4897                description: req.description.clone(),
4898                description_hash: None,
4899                payer_note: None,
4900            })
4901            .await?;
4902
4903        let Ok(invoice) = parse_invoice(&receive_res.destination) else {
4904            return Err(LnUrlWithdrawError::Generic {
4905                err: "Received unexpected output from receive request".to_string(),
4906            });
4907        };
4908
4909        let res =
4910            validate_lnurl_withdraw(self.rest_client.as_ref(), req.data.clone(), invoice.clone())
4911                .await?;
4912        if let LnUrlWithdrawResult::Ok { data: _ } = res {
4913            if let Some(ReceiveSwap {
4914                claim_tx_id: Some(tx_id),
4915                ..
4916            }) = self
4917                .persister
4918                .fetch_receive_swap_by_invoice(&invoice.bolt11)?
4919            {
4920                self.persister
4921                    .insert_or_update_payment_details(PaymentTxDetails {
4922                        tx_id,
4923                        destination: receive_res.destination,
4924                        description: req.description,
4925                        lnurl_info: Some(LnUrlInfo {
4926                            lnurl_withdraw_endpoint: Some(req.data.callback),
4927                            ..Default::default()
4928                        }),
4929                        ..Default::default()
4930                    })?;
4931            }
4932        }
4933        Ok(res)
4934    }
4935
4936    /// Third and last step of LNURL-auth. The first step is [LiquidSdk::parse], which also validates the LNURL destination
4937    /// and generates the [LnUrlAuthRequestData] payload needed here. The second step is user approval of auth action.
4938    ///
4939    /// This call will sign `k1` of the LNURL endpoint (`req_data`) on `secp256k1` using `linkingPrivKey` and DER-encodes the signature.
4940    /// If they match the endpoint requirements, the LNURL auth request is made. A successful result here means the client signature is verified.
4941    pub async fn lnurl_auth(
4942        &self,
4943        req_data: LnUrlAuthRequestData,
4944    ) -> Result<LnUrlCallbackStatus, LnUrlAuthError> {
4945        Ok(perform_lnurl_auth(
4946            self.rest_client.as_ref(),
4947            &req_data,
4948            &SdkLnurlAuthSigner::new(self.signer.clone()),
4949        )
4950        .await?)
4951    }
4952
4953    /// Register for webhook callbacks at the given `webhook_url`. Each created swap after registering the
4954    /// webhook will include the `webhook_url`.
4955    ///
4956    /// This method should be called every time the application is started and when the `webhook_url` changes.
4957    /// For example, if the `webhook_url` contains a push notification token and the token changes after
4958    /// the application was started, then this method should be called to register for callbacks at
4959    /// the new correct `webhook_url`. To unregister a webhook call [LiquidSdk::unregister_webhook].
4960    pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> {
4961        info!("Registering for webhook notifications");
4962        self.persister.set_webhook_url(webhook_url.clone())?;
4963
4964        // Update all BOLT12 offers where the webhook URL is different
4965        let bolt12_offers = self.persister.list_bolt12_offers()?;
4966        for mut bolt12_offer in bolt12_offers {
4967            if bolt12_offer
4968                .webhook_url
4969                .clone()
4970                .is_none_or(|url| url != webhook_url)
4971            {
4972                let keypair = bolt12_offer.get_keypair()?;
4973                let webhook_url_hash_sig = utils::sign_message_hash(&webhook_url, &keypair)?;
4974                self.swapper
4975                    .update_bolt12_offer(UpdateBolt12OfferRequest {
4976                        offer: bolt12_offer.id.clone(),
4977                        url: Some(webhook_url.clone()),
4978                        signature: webhook_url_hash_sig.to_hex(),
4979                    })
4980                    .await?;
4981                bolt12_offer.webhook_url = Some(webhook_url.clone());
4982                self.persister
4983                    .insert_or_update_bolt12_offer(&bolt12_offer)?;
4984            }
4985        }
4986
4987        Ok(())
4988    }
4989
4990    /// Unregister webhook callbacks. Each swap already created will continue to use the registered
4991    /// `webhook_url` until complete.
4992    ///
4993    /// This can be called when callbacks are no longer needed or the `webhook_url`
4994    /// has changed such that it needs unregistering. For example, the token is valid but the locale changes.
4995    /// To register a webhook call [LiquidSdk::register_webhook].
4996    pub async fn unregister_webhook(&self) -> SdkResult<()> {
4997        info!("Unregistering for webhook notifications");
4998        let maybe_old_webhook_url = self.persister.get_webhook_url()?;
4999
5000        self.persister.remove_webhook_url()?;
5001
5002        // Update all bolt12 offers that were created with the old webhook URL
5003        if let Some(old_webhook_url) = maybe_old_webhook_url {
5004            let bolt12_offers = self
5005                .persister
5006                .list_bolt12_offers_by_webhook_url(&old_webhook_url)?;
5007            for mut bolt12_offer in bolt12_offers {
5008                let keypair = bolt12_offer.get_keypair()?;
5009                let update_hash_sig = utils::sign_message_hash("UPDATE", &keypair)?;
5010                self.swapper
5011                    .update_bolt12_offer(UpdateBolt12OfferRequest {
5012                        offer: bolt12_offer.id.clone(),
5013                        url: None,
5014                        signature: update_hash_sig.to_hex(),
5015                    })
5016                    .await?;
5017                bolt12_offer.webhook_url = None;
5018                self.persister
5019                    .insert_or_update_bolt12_offer(&bolt12_offer)?;
5020            }
5021        }
5022
5023        Ok(())
5024    }
5025
5026    /// Fetch live rates of fiat currencies, sorted by name.
5027    pub async fn fetch_fiat_rates(&self) -> Result<Vec<Rate>, SdkError> {
5028        self.fiat_api.fetch_fiat_rates().await.map_err(Into::into)
5029    }
5030
5031    /// List all supported fiat currencies for which there is a known exchange rate.
5032    /// List is sorted by the canonical name of the currency.
5033    pub async fn list_fiat_currencies(&self) -> Result<Vec<FiatCurrency>, SdkError> {
5034        self.fiat_api
5035            .list_fiat_currencies()
5036            .await
5037            .map_err(Into::into)
5038    }
5039
5040    /// Get the recommended BTC fees based on the configured mempool.space instance.
5041    pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
5042        Ok(self.bitcoin_chain_service.recommended_fees().await?)
5043    }
5044
5045    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
5046    /// Get the full default [Config] for specific [LiquidNetwork].
5047    pub fn default_config(
5048        network: LiquidNetwork,
5049        breez_api_key: Option<String>,
5050    ) -> Result<Config, SdkError> {
5051        let config = match network {
5052            LiquidNetwork::Mainnet => Config::mainnet_esplora(breez_api_key),
5053            LiquidNetwork::Testnet => {
5054                return Err(SdkError::network_not_supported(network));
5055            }
5056            LiquidNetwork::Regtest => Config::regtest_esplora(),
5057        };
5058
5059        Ok(config)
5060    }
5061
5062    /// Parses a string into an [InputType]. See [input_parser::parse].
5063    ///
5064    /// Can optionally be configured to use external input parsers by providing `external_input_parsers` in [Config].
5065    pub async fn parse(&self, input: &str) -> Result<InputType, PaymentError> {
5066        let external_parsers = &self.external_input_parsers;
5067        let input_type =
5068            parse_with_rest_client(self.rest_client.as_ref(), input, Some(external_parsers))
5069                .await
5070                .map_err(|e| PaymentError::generic(e.to_string()))?;
5071
5072        let res = match input_type {
5073            InputType::LiquidAddress { ref address } => match &address.asset_id {
5074                Some(asset_id) if asset_id.ne(&self.config.lbtc_asset_id()) => {
5075                    let asset_metadata = self.persister.get_asset_metadata(asset_id)?.ok_or(
5076                        PaymentError::AssetError {
5077                            err: format!("Asset {asset_id} is not supported"),
5078                        },
5079                    )?;
5080                    let mut address = address.clone();
5081                    address.set_amount_precision(asset_metadata.precision.into());
5082                    InputType::LiquidAddress { address }
5083                }
5084                _ => input_type,
5085            },
5086            _ => input_type,
5087        };
5088        Ok(res)
5089    }
5090
5091    /// Parses a string into an [LNInvoice]. See [invoice::parse_invoice].
5092    pub fn parse_invoice(input: &str) -> Result<LNInvoice, PaymentError> {
5093        parse_invoice(input).map_err(|e| PaymentError::invalid_invoice(e.to_string()))
5094    }
5095
5096    /// Configures a global SDK logger that will log to file and will forward log events to
5097    /// an optional application-specific logger.
5098    ///
5099    /// If called, it should be called before any SDK methods (for example, before `connect`).
5100    ///
5101    /// It must be called only once in the application lifecycle. Alternatively, If the application
5102    /// already uses a globally-registered logger, this method shouldn't be called at all.
5103    ///
5104    /// ### Arguments
5105    ///
5106    /// - `log_dir`: Location where the the SDK log file will be created. The directory must already exist.
5107    ///
5108    /// - `app_logger`: Optional application logger.
5109    ///
5110    /// If the application is to use it's own logger, but would also like the SDK to log SDK-specific
5111    /// log output to a file in the configured `log_dir`, then do not register the
5112    /// app-specific logger as a global logger and instead call this method with the app logger as an arg.
5113    ///
5114    /// ### Errors
5115    ///
5116    /// An error is thrown if the log file cannot be created in the working directory.
5117    ///
5118    /// An error is thrown if a global logger is already configured.
5119    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
5120    pub fn init_logging(log_dir: &str, app_logger: Option<Box<dyn log::Log>>) -> Result<()> {
5121        crate::logger::init_logging(log_dir, app_logger)
5122    }
5123
5124    async fn start_plugin_inner(self: &Arc<Self>, plugin: &Arc<dyn Plugin>) -> SdkResult<()> {
5125        let plugin_id = plugin.id();
5126        let plugin_passphrase = self
5127            .signer
5128            .hmac_sha256(plugin_id.as_bytes().to_vec(), "m/49'/1'/0'/0/0".to_string())
5129            .map_err(|err| {
5130                SdkError::generic(format!("Could not generate plugin passphrase: {err}"))
5131            })?;
5132        let storage = PluginStorage::new(
5133            Arc::downgrade(&self.persister),
5134            &plugin_passphrase,
5135            plugin.id(),
5136        )?;
5137        plugin
5138            .on_start(PluginSdk::new(Arc::downgrade(self)), storage)
5139            .await;
5140        Ok(())
5141    }
5142
5143    pub async fn start_plugin(self: &Arc<Self>, plugin: Arc<dyn Plugin>) -> SdkResult<()> {
5144        let plugin_id = plugin.id();
5145        let mut plugins = self.plugins.lock().await;
5146        if plugins.get(&plugin_id).is_some() {
5147            return Err(SdkError::generic(format!(
5148                "Plugin {plugin_id} is already running"
5149            )));
5150        }
5151        plugins.insert(plugin_id, plugin.clone());
5152        self.start_plugin_inner(&plugin).await?;
5153        Ok(())
5154    }
5155}
5156
5157/// Extracts `description` from `metadata_str`
5158fn extract_description_from_metadata(request_data: &LnUrlPayRequestData) -> Option<String> {
5159    let metadata = request_data.metadata_vec().ok()?;
5160    metadata
5161        .iter()
5162        .find(|item| item.key == "text/plain")
5163        .map(|item| {
5164            info!("Extracted payment description: '{}'", item.value);
5165            item.value.clone()
5166        })
5167}
5168
5169#[cfg(test)]
5170mod tests {
5171    use std::time::Duration;
5172    use std::{str::FromStr, sync::Arc};
5173
5174    use anyhow::{anyhow, Result};
5175    use boltz_client::{
5176        boltz::{self, TransactionInfo},
5177        swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates},
5178        Secp256k1,
5179    };
5180    use lwk_wollet::{bitcoin::Network, hashes::hex::DisplayHex as _};
5181    use sdk_common::{
5182        bitcoin::hashes::hex::ToHex,
5183        lightning_with_bolt12::{
5184            ln::{channelmanager::PaymentId, inbound_payment::ExpandedKey},
5185            offers::{nonce::Nonce, offer::Offer},
5186            sign::RandomBytes,
5187            util::ser::Writeable,
5188        },
5189    };
5190    use tokio_with_wasm::alias as tokio;
5191
5192    use crate::test_utils::swapper::ZeroAmountSwapMockConfig;
5193    use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX;
5194    use crate::utils;
5195    use crate::{
5196        bitcoin, elements,
5197        model::{BtcHistory, Direction, LBtcHistory, PaymentState, Swap},
5198        sdk::LiquidSdk,
5199        test_utils::{
5200            chain::{MockBitcoinChainService, MockLiquidChainService},
5201            chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX},
5202            persist::{create_persister, new_receive_swap, new_send_swap},
5203            sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services},
5204            status_stream::MockStatusStream,
5205            swapper::MockSwapper,
5206        },
5207    };
5208    use crate::{
5209        model::CreateBolt12InvoiceRequest,
5210        test_utils::chain_swap::{
5211            TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX, TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX,
5212            TEST_LIQUID_OUTGOING_USER_LOCKUP_TX,
5213        },
5214    };
5215    use paste::paste;
5216
5217    #[cfg(feature = "browser-tests")]
5218    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
5219
5220    struct NewSwapArgs {
5221        direction: Direction,
5222        accepts_zero_conf: bool,
5223        initial_payment_state: Option<PaymentState>,
5224        receiver_amount_sat: Option<u64>,
5225        user_lockup_tx_id: Option<String>,
5226        zero_amount: bool,
5227        set_actual_payer_amount: bool,
5228    }
5229
5230    impl Default for NewSwapArgs {
5231        fn default() -> Self {
5232            Self {
5233                accepts_zero_conf: false,
5234                initial_payment_state: None,
5235                direction: Direction::Outgoing,
5236                receiver_amount_sat: None,
5237                user_lockup_tx_id: None,
5238                zero_amount: false,
5239                set_actual_payer_amount: false,
5240            }
5241        }
5242    }
5243
5244    impl NewSwapArgs {
5245        pub fn set_direction(mut self, direction: Direction) -> Self {
5246            self.direction = direction;
5247            self
5248        }
5249
5250        pub fn set_accepts_zero_conf(mut self, accepts_zero_conf: bool) -> Self {
5251            self.accepts_zero_conf = accepts_zero_conf;
5252            self
5253        }
5254
5255        pub fn set_receiver_amount_sat(mut self, receiver_amount_sat: Option<u64>) -> Self {
5256            self.receiver_amount_sat = receiver_amount_sat;
5257            self
5258        }
5259
5260        pub fn set_user_lockup_tx_id(mut self, user_lockup_tx_id: Option<String>) -> Self {
5261            self.user_lockup_tx_id = user_lockup_tx_id;
5262            self
5263        }
5264
5265        pub fn set_initial_payment_state(mut self, payment_state: PaymentState) -> Self {
5266            self.initial_payment_state = Some(payment_state);
5267            self
5268        }
5269
5270        pub fn set_zero_amount(mut self, zero_amount: bool) -> Self {
5271            self.zero_amount = zero_amount;
5272            self
5273        }
5274
5275        pub fn set_set_actual_payer_amount(mut self, set_actual_payer_amount: bool) -> Self {
5276            self.set_actual_payer_amount = set_actual_payer_amount;
5277            self
5278        }
5279    }
5280
5281    macro_rules! trigger_swap_update {
5282        (
5283            $type:literal,
5284            $args:expr,
5285            $persister:expr,
5286            $status_stream:expr,
5287            $status:expr,
5288            $transaction:expr,
5289            $zero_conf_rejected:expr
5290        ) => {{
5291            let swap = match $type {
5292                "chain" => {
5293                    let swap = new_chain_swap(
5294                        $args.direction,
5295                        $args.initial_payment_state,
5296                        $args.accepts_zero_conf,
5297                        $args.user_lockup_tx_id,
5298                        $args.zero_amount,
5299                        $args.set_actual_payer_amount,
5300                        $args.receiver_amount_sat,
5301                    );
5302                    $persister.insert_or_update_chain_swap(&swap).unwrap();
5303                    Swap::Chain(swap)
5304                }
5305                "send" => {
5306                    let swap =
5307                        new_send_swap($args.initial_payment_state, $args.receiver_amount_sat);
5308                    $persister.insert_or_update_send_swap(&swap).unwrap();
5309                    Swap::Send(swap)
5310                }
5311                "receive" => {
5312                    let swap =
5313                        new_receive_swap($args.initial_payment_state, $args.receiver_amount_sat);
5314                    $persister.insert_or_update_receive_swap(&swap).unwrap();
5315                    Swap::Receive(swap)
5316                }
5317                _ => panic!(),
5318            };
5319
5320            $status_stream
5321                .clone()
5322                .send_mock_update(boltz::SwapStatus {
5323                    id: swap.id(),
5324                    status: $status.to_string(),
5325                    transaction: $transaction,
5326                    zero_conf_rejected: $zero_conf_rejected,
5327                    ..Default::default()
5328                })
5329                .await
5330                .unwrap();
5331
5332            paste! {
5333                $persister.[<fetch _ $type _swap_by_id>](&swap.id())
5334                    .unwrap()
5335                    .ok_or(anyhow!("Could not retrieve {} swap", $type))
5336                    .unwrap()
5337            }
5338        }};
5339    }
5340
5341    #[sdk_macros::async_test_all]
5342    async fn test_receive_swap_update_tracking() -> Result<()> {
5343        create_persister!(persister);
5344        let swapper = Arc::new(MockSwapper::default());
5345        let status_stream = Arc::new(MockStatusStream::new());
5346        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5347        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5348
5349        let sdk = new_liquid_sdk_with_chain_services(
5350            persister.clone(),
5351            swapper.clone(),
5352            status_stream.clone(),
5353            liquid_chain_service.clone(),
5354            bitcoin_chain_service.clone(),
5355            None,
5356        )
5357        .await?;
5358
5359        LiquidSdk::track_swap_updates(&sdk);
5360
5361        // We spawn a new thread since updates can only be sent when called via async runtimes
5362        tokio::spawn(async move {
5363            // Verify the swap becomes invalid after final states are received
5364            let unrecoverable_states: [RevSwapStates; 4] = [
5365                RevSwapStates::SwapExpired,
5366                RevSwapStates::InvoiceExpired,
5367                RevSwapStates::TransactionFailed,
5368                RevSwapStates::TransactionRefunded,
5369            ];
5370
5371            for status in unrecoverable_states {
5372                let persisted_swap = trigger_swap_update!(
5373                    "receive",
5374                    NewSwapArgs::default(),
5375                    persister,
5376                    status_stream,
5377                    status,
5378                    None,
5379                    None
5380                );
5381                assert_eq!(persisted_swap.state, PaymentState::Failed);
5382            }
5383
5384            // Check that `TransactionMempool` and `TransactionConfirmed` correctly trigger the claim,
5385            // which in turn sets the `claim_tx_id`
5386            for status in [
5387                RevSwapStates::TransactionMempool,
5388                RevSwapStates::TransactionConfirmed,
5389            ] {
5390                let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
5391                let mock_tx_id = mock_tx.txid();
5392                let height = (serde_json::to_string(&status).unwrap()
5393                    == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
5394                    as i32;
5395                liquid_chain_service.set_history(vec![LBtcHistory {
5396                    txid: mock_tx_id,
5397                    height,
5398                }]);
5399
5400                let persisted_swap = trigger_swap_update!(
5401                    "receive",
5402                    NewSwapArgs::default(),
5403                    persister,
5404                    status_stream,
5405                    status,
5406                    Some(TransactionInfo {
5407                        id: mock_tx_id.to_string(),
5408                        hex: Some(
5409                            lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
5410                        ),
5411                        eta: None,
5412                    }),
5413                    None
5414                );
5415                assert!(persisted_swap.claim_tx_id.is_some());
5416            }
5417
5418            // Check that `TransactionMempool` and `TransactionConfirmed` checks the lockup amount
5419            // and doesn't claim if not verified
5420            for status in [
5421                RevSwapStates::TransactionMempool,
5422                RevSwapStates::TransactionConfirmed,
5423            ] {
5424                let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
5425                let mock_tx_id = mock_tx.txid();
5426                let height = (serde_json::to_string(&status).unwrap()
5427                    == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
5428                    as i32;
5429                liquid_chain_service.set_history(vec![LBtcHistory {
5430                    txid: mock_tx_id,
5431                    height,
5432                }]);
5433
5434                let persisted_swap = trigger_swap_update!(
5435                    "receive",
5436                    NewSwapArgs::default().set_receiver_amount_sat(Some(1000)),
5437                    persister,
5438                    status_stream,
5439                    status,
5440                    Some(TransactionInfo {
5441                        id: mock_tx_id.to_string(),
5442                        hex: Some(
5443                            lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
5444                        ),
5445                        eta: None
5446                    }),
5447                    None
5448                );
5449                assert!(persisted_swap.claim_tx_id.is_none());
5450            }
5451        })
5452        .await
5453        .unwrap();
5454
5455        Ok(())
5456    }
5457
5458    #[sdk_macros::async_test_all]
5459    async fn test_send_swap_update_tracking() -> Result<()> {
5460        create_persister!(persister);
5461        let swapper = Arc::new(MockSwapper::default());
5462        let status_stream = Arc::new(MockStatusStream::new());
5463
5464        let sdk = Arc::new(
5465            new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?,
5466        );
5467
5468        LiquidSdk::track_swap_updates(&sdk);
5469
5470        // We spawn a new thread since updates can only be sent when called via async runtimes
5471        tokio::spawn(async move {
5472            // Verify the swap becomes invalid after final states are received
5473            let unrecoverable_states: [SubSwapStates; 3] = [
5474                SubSwapStates::TransactionLockupFailed,
5475                SubSwapStates::InvoiceFailedToPay,
5476                SubSwapStates::SwapExpired,
5477            ];
5478
5479            for status in unrecoverable_states {
5480                let persisted_swap = trigger_swap_update!(
5481                    "send",
5482                    NewSwapArgs::default(),
5483                    persister,
5484                    status_stream,
5485                    status,
5486                    None,
5487                    None
5488                );
5489                assert_eq!(persisted_swap.state, PaymentState::Failed);
5490            }
5491
5492            // Verify that `TransactionClaimPending` correctly sets the state to `Complete`
5493            // and stores the preimage
5494            let persisted_swap = trigger_swap_update!(
5495                "send",
5496                NewSwapArgs::default(),
5497                persister,
5498                status_stream,
5499                SubSwapStates::TransactionClaimPending,
5500                None,
5501                None
5502            );
5503            assert_eq!(persisted_swap.state, PaymentState::Complete);
5504            assert!(persisted_swap.preimage.is_some());
5505        })
5506        .await
5507        .unwrap();
5508
5509        Ok(())
5510    }
5511
5512    #[sdk_macros::async_test_all]
5513    async fn test_chain_swap_update_tracking() -> Result<()> {
5514        create_persister!(persister);
5515        let swapper = Arc::new(MockSwapper::default());
5516        let status_stream = Arc::new(MockStatusStream::new());
5517        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5518        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5519
5520        let sdk = new_liquid_sdk_with_chain_services(
5521            persister.clone(),
5522            swapper.clone(),
5523            status_stream.clone(),
5524            liquid_chain_service.clone(),
5525            bitcoin_chain_service.clone(),
5526            None,
5527        )
5528        .await?;
5529
5530        LiquidSdk::track_swap_updates(&sdk);
5531
5532        // We spawn a new thread since updates can only be sent when called via async runtimes
5533        tokio::spawn(async move {
5534            let trigger_failed: [ChainSwapStates; 3] = [
5535                ChainSwapStates::TransactionFailed,
5536                ChainSwapStates::SwapExpired,
5537                ChainSwapStates::TransactionRefunded,
5538            ];
5539
5540            // Checks that work for both incoming and outgoing chain swaps
5541            for direction in [Direction::Incoming, Direction::Outgoing] {
5542                // Verify the swap becomes invalid after final states are received
5543                for status in &trigger_failed {
5544                    let persisted_swap = trigger_swap_update!(
5545                        "chain",
5546                        NewSwapArgs::default().set_direction(direction),
5547                        persister,
5548                        status_stream,
5549                        status,
5550                        None,
5551                        None
5552                    );
5553                    assert_eq!(persisted_swap.state, PaymentState::Failed);
5554                }
5555
5556                let (mock_user_lockup_tx_hex, mock_user_lockup_tx_id) = match direction {
5557                    Direction::Outgoing => {
5558                        let tx = TEST_LIQUID_OUTGOING_USER_LOCKUP_TX.clone();
5559                        (
5560                            lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5561                            tx.txid().to_string(),
5562                        )
5563                    }
5564                    Direction::Incoming => {
5565                        let tx = TEST_BITCOIN_INCOMING_USER_LOCKUP_TX.clone();
5566                        (
5567                            sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5568                            tx.txid().to_string(),
5569                        )
5570                    }
5571                };
5572
5573                let (mock_server_lockup_tx_hex, mock_server_lockup_tx_id) = match direction {
5574                    Direction::Incoming => {
5575                        let tx = TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX.clone();
5576                        (
5577                            lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5578                            tx.txid().to_string(),
5579                        )
5580                    }
5581                    Direction::Outgoing => {
5582                        let tx = TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX.clone();
5583                        (
5584                            sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5585                            tx.txid().to_string(),
5586                        )
5587                    }
5588                };
5589
5590                // Verify that `TransactionLockupFailed` correctly sets the state as
5591                // `RefundPending`/`Refundable` or as `Failed` depending on whether or not
5592                // `user_lockup_tx_id` is present
5593                for user_lockup_tx_id in &[None, Some(mock_user_lockup_tx_id.clone())] {
5594                    if let Some(user_lockup_tx_id) = user_lockup_tx_id {
5595                        match direction {
5596                            Direction::Incoming => {
5597                                bitcoin_chain_service.set_history(vec![BtcHistory {
5598                                    txid: bitcoin::Txid::from_str(user_lockup_tx_id).unwrap(),
5599                                    height: 0,
5600                                }]);
5601                            }
5602                            Direction::Outgoing => {
5603                                liquid_chain_service.set_history(vec![LBtcHistory {
5604                                    txid: elements::Txid::from_str(user_lockup_tx_id).unwrap(),
5605                                    height: 0,
5606                                }]);
5607                            }
5608                        }
5609                    }
5610                    let persisted_swap = trigger_swap_update!(
5611                        "chain",
5612                        NewSwapArgs::default()
5613                            .set_direction(direction)
5614                            .set_initial_payment_state(PaymentState::Pending)
5615                            .set_user_lockup_tx_id(user_lockup_tx_id.clone()),
5616                        persister,
5617                        status_stream,
5618                        ChainSwapStates::TransactionLockupFailed,
5619                        None,
5620                        None
5621                    );
5622                    let expected_state = if user_lockup_tx_id.is_some() {
5623                        match direction {
5624                            Direction::Incoming => PaymentState::Refundable,
5625                            Direction::Outgoing => PaymentState::RefundPending,
5626                        }
5627                    } else {
5628                        PaymentState::Failed
5629                    };
5630                    assert_eq!(persisted_swap.state, expected_state);
5631                }
5632
5633                // Verify that `TransactionMempool` and `TransactionConfirmed` correctly set
5634                // `user_lockup_tx_id` and `accept_zero_conf`
5635                for status in [
5636                    ChainSwapStates::TransactionMempool,
5637                    ChainSwapStates::TransactionConfirmed,
5638                ] {
5639                    if direction == Direction::Incoming {
5640                        bitcoin_chain_service.set_history(vec![BtcHistory {
5641                            txid: bitcoin::Txid::from_str(&mock_user_lockup_tx_id).unwrap(),
5642                            height: 0,
5643                        }]);
5644                        bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]);
5645                    }
5646                    let persisted_swap = trigger_swap_update!(
5647                        "chain",
5648                        NewSwapArgs::default().set_direction(direction),
5649                        persister,
5650                        status_stream,
5651                        status,
5652                        Some(TransactionInfo {
5653                            id: mock_user_lockup_tx_id.clone(),
5654                            hex: Some(mock_user_lockup_tx_hex.clone()),
5655                            eta: None
5656                        }), // sets `update.transaction`
5657                        Some(true) // sets `update.zero_conf_rejected`
5658                    );
5659                    assert_eq!(
5660                        persisted_swap.user_lockup_tx_id,
5661                        Some(mock_user_lockup_tx_id.clone())
5662                    );
5663                    assert!(!persisted_swap.accept_zero_conf);
5664                }
5665
5666                // Verify that `TransactionServerMempool` correctly:
5667                // 1. Sets the payment as `Pending` and creates `server_lockup_tx_id` when
5668                //    `accepts_zero_conf` is false
5669                // 2. Sets the payment as `Pending` and creates `claim_tx_id` when `accepts_zero_conf`
5670                //    is true
5671                for accepts_zero_conf in [false, true] {
5672                    let persisted_swap = trigger_swap_update!(
5673                        "chain",
5674                        NewSwapArgs::default()
5675                            .set_direction(direction)
5676                            .set_accepts_zero_conf(accepts_zero_conf)
5677                            .set_set_actual_payer_amount(true),
5678                        persister,
5679                        status_stream,
5680                        ChainSwapStates::TransactionServerMempool,
5681                        Some(TransactionInfo {
5682                            id: mock_server_lockup_tx_id.clone(),
5683                            hex: Some(mock_server_lockup_tx_hex.clone()),
5684                            eta: None,
5685                        }),
5686                        None
5687                    );
5688                    match accepts_zero_conf {
5689                        false => {
5690                            assert_eq!(persisted_swap.state, PaymentState::Pending);
5691                            assert!(persisted_swap.server_lockup_tx_id.is_some());
5692                        }
5693                        true => {
5694                            assert_eq!(persisted_swap.state, PaymentState::Pending);
5695                            assert!(persisted_swap.claim_tx_id.is_some());
5696                        }
5697                    };
5698                }
5699
5700                // Verify that `TransactionServerConfirmed` correctly
5701                // sets the payment as `Pending` and creates `claim_tx_id`
5702                let persisted_swap = trigger_swap_update!(
5703                    "chain",
5704                    NewSwapArgs::default()
5705                        .set_direction(direction)
5706                        .set_set_actual_payer_amount(true),
5707                    persister,
5708                    status_stream,
5709                    ChainSwapStates::TransactionServerConfirmed,
5710                    Some(TransactionInfo {
5711                        id: mock_server_lockup_tx_id,
5712                        hex: Some(mock_server_lockup_tx_hex),
5713                        eta: None,
5714                    }),
5715                    None
5716                );
5717                assert_eq!(persisted_swap.state, PaymentState::Pending);
5718                assert!(persisted_swap.claim_tx_id.is_some());
5719            }
5720
5721            // For outgoing payments, verify that `Created` correctly sets the payment as `Pending` and creates
5722            // the `user_lockup_tx_id`
5723            let persisted_swap = trigger_swap_update!(
5724                "chain",
5725                NewSwapArgs::default().set_direction(Direction::Outgoing),
5726                persister,
5727                status_stream,
5728                ChainSwapStates::Created,
5729                None,
5730                None
5731            );
5732            assert_eq!(persisted_swap.state, PaymentState::Pending);
5733            assert!(persisted_swap.user_lockup_tx_id.is_some());
5734        })
5735        .await
5736        .unwrap();
5737
5738        Ok(())
5739    }
5740
5741    #[sdk_macros::async_test_all]
5742    async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> {
5743        let user_lockup_sat = 50_000;
5744
5745        create_persister!(persister);
5746        let swapper = Arc::new(MockSwapper::new());
5747        let status_stream = Arc::new(MockStatusStream::new());
5748        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5749        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5750
5751        let sdk = new_liquid_sdk_with_chain_services(
5752            persister.clone(),
5753            swapper.clone(),
5754            status_stream.clone(),
5755            liquid_chain_service.clone(),
5756            bitcoin_chain_service.clone(),
5757            Some(0),
5758        )
5759        .await?;
5760
5761        LiquidSdk::track_swap_updates(&sdk);
5762
5763        // We spawn a new thread since updates can only be sent when called via async runtimes
5764        tokio::spawn(async move {
5765            // Verify that `TransactionLockupFailed` correctly:
5766            // 1. does not affect state when swapper doesn't increase fees
5767            // 2. triggers a change to WaitingFeeAcceptance when there is a fee increase > 0
5768            for fee_increase in [0, 1] {
5769                swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5770                    user_lockup_sat,
5771                    onchain_fee_increase_sat: fee_increase,
5772                });
5773                bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5774                let persisted_swap = trigger_swap_update!(
5775                    "chain",
5776                    NewSwapArgs::default()
5777                        .set_direction(Direction::Incoming)
5778                        .set_accepts_zero_conf(false)
5779                        .set_zero_amount(true),
5780                    persister,
5781                    status_stream,
5782                    ChainSwapStates::TransactionLockupFailed,
5783                    None,
5784                    None
5785                );
5786                match fee_increase {
5787                    0 => {
5788                        assert_eq!(persisted_swap.state, PaymentState::Created);
5789                    }
5790                    1 => {
5791                        assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5792                    }
5793                    _ => panic!("Unexpected fee_increase"),
5794                }
5795            }
5796        })
5797        .await?;
5798
5799        Ok(())
5800    }
5801
5802    #[sdk_macros::async_test_all]
5803    async fn test_zero_amount_chain_swap_with_leeway() -> Result<()> {
5804        let user_lockup_sat = 50_000;
5805        let onchain_fee_rate_leeway_sat = 500;
5806
5807        create_persister!(persister);
5808        let swapper = Arc::new(MockSwapper::new());
5809        let status_stream = Arc::new(MockStatusStream::new());
5810        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5811        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5812
5813        let sdk = new_liquid_sdk_with_chain_services(
5814            persister.clone(),
5815            swapper.clone(),
5816            status_stream.clone(),
5817            liquid_chain_service.clone(),
5818            bitcoin_chain_service.clone(),
5819            Some(onchain_fee_rate_leeway_sat),
5820        )
5821        .await?;
5822
5823        LiquidSdk::track_swap_updates(&sdk);
5824
5825        // We spawn a new thread since updates can only be sent when called via async runtimes
5826        tokio::spawn(async move {
5827            // Verify that `TransactionLockupFailed` correctly:
5828            // 1. does not affect state when swapper increases fee by up to sat/vbyte leeway * tx size
5829            // 2. triggers a change to WaitingFeeAcceptance when it is any higher
5830            for fee_increase in [onchain_fee_rate_leeway_sat, onchain_fee_rate_leeway_sat + 1] {
5831                swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5832                    user_lockup_sat,
5833                    onchain_fee_increase_sat: fee_increase,
5834                });
5835                bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5836                let persisted_swap = trigger_swap_update!(
5837                    "chain",
5838                    NewSwapArgs::default()
5839                        .set_direction(Direction::Incoming)
5840                        .set_accepts_zero_conf(false)
5841                        .set_zero_amount(true),
5842                    persister,
5843                    status_stream,
5844                    ChainSwapStates::TransactionLockupFailed,
5845                    None,
5846                    None
5847                );
5848                match fee_increase {
5849                    val if val == onchain_fee_rate_leeway_sat => {
5850                        assert_eq!(persisted_swap.state, PaymentState::Created);
5851                    }
5852                    val if val == (onchain_fee_rate_leeway_sat + 1) => {
5853                        assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5854                    }
5855                    _ => panic!("Unexpected fee_increase"),
5856                }
5857            }
5858        })
5859        .await?;
5860
5861        Ok(())
5862    }
5863
5864    #[sdk_macros::async_test_all]
5865    async fn test_background_tasks() -> Result<()> {
5866        create_persister!(persister);
5867        let swapper = Arc::new(MockSwapper::new());
5868        let status_stream = Arc::new(MockStatusStream::new());
5869        let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5870        let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5871
5872        let sdk = new_liquid_sdk_with_chain_services(
5873            persister.clone(),
5874            swapper.clone(),
5875            status_stream.clone(),
5876            liquid_chain_service.clone(),
5877            bitcoin_chain_service.clone(),
5878            None,
5879        )
5880        .await?;
5881
5882        sdk.start().await?;
5883
5884        tokio::time::sleep(Duration::from_secs(3)).await;
5885
5886        sdk.disconnect().await?;
5887
5888        Ok(())
5889    }
5890
5891    #[sdk_macros::async_test_all]
5892    async fn test_create_bolt12_offer() -> Result<()> {
5893        create_persister!(persister);
5894
5895        let swapper = Arc::new(MockSwapper::default());
5896        let status_stream = Arc::new(MockStatusStream::new());
5897        let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5898
5899        // Register a webhook URL
5900        let webhook_url = "https://example.com/webhook";
5901        persister.set_webhook_url(webhook_url.to_string())?;
5902
5903        // Call create_bolt12_offer
5904        let description = "test offer".to_string();
5905        let response = sdk.create_bolt12_offer(description.clone()).await?;
5906
5907        // Verify that the response contains a destination (offer string)
5908        assert!(!response.destination.is_empty());
5909
5910        // Verify the offer was stored in the persister
5911        let offers = persister.list_bolt12_offers_by_webhook_url(webhook_url)?;
5912        assert_eq!(offers.len(), 1);
5913
5914        // Verify the offer details
5915        let offer = &offers[0];
5916        assert_eq!(offer.description, description);
5917        assert_eq!(offer.webhook_url, Some(webhook_url.to_string()));
5918        assert_eq!(offer.id, response.destination);
5919
5920        // Verify the offer has a private key
5921        assert!(!offer.private_key.is_empty());
5922
5923        Ok(())
5924    }
5925
5926    #[sdk_macros::async_test_all]
5927    async fn test_create_bolt12_receive_swap() -> Result<()> {
5928        create_persister!(persister);
5929
5930        let swapper = Arc::new(MockSwapper::default());
5931        let status_stream = Arc::new(MockStatusStream::new());
5932        let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5933
5934        // Register a webhook URL
5935        let webhook_url = "https://example.com/webhook";
5936        persister.set_webhook_url(webhook_url.to_string())?;
5937
5938        // Call create_bolt12_offer
5939        let description = "test offer".to_string();
5940        let response = sdk.create_bolt12_offer(description.clone()).await?;
5941        let offer = persister
5942            .fetch_bolt12_offer_by_id(&response.destination)?
5943            .unwrap();
5944
5945        // Create the invoice request
5946        let expanded_key = ExpandedKey::new([42; 32]);
5947        let entropy_source = RandomBytes::new(utils::generate_entropy());
5948        let nonce = Nonce::from_entropy_source(&entropy_source);
5949        let secp = Secp256k1::new();
5950        let payment_id = PaymentId([1; 32]);
5951        let invoice_request = TryInto::<Offer>::try_into(offer.clone())?
5952            .request_invoice(&expanded_key, nonce, &secp, payment_id)
5953            .unwrap()
5954            .amount_msats(1_000_000)
5955            .unwrap()
5956            .chain(Network::Regtest)
5957            .unwrap()
5958            .build_and_sign()
5959            .unwrap();
5960        let mut buffer = Vec::new();
5961        invoice_request.write(&mut buffer).unwrap();
5962
5963        // Call create_bolt12_receive_swap
5964        let create_res = sdk
5965            .create_bolt12_invoice(&CreateBolt12InvoiceRequest {
5966                offer: offer.id,
5967                invoice_request: buffer.to_hex(),
5968            })
5969            .await
5970            .unwrap();
5971        assert!(create_res.invoice.starts_with("lni"));
5972
5973        Ok(())
5974    }
5975}