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