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