breez_sdk_liquid/
sdk.rs

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