breez_sdk_liquid/
sdk.rs

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