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