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