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