Skip to main content

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