breez_sdk_liquid/
sdk.rs

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