breez_sdk_liquid/
sdk.rs

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