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