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