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