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 swap.check_sufficient_balance(&get_info_res.wallet_info)?;
1483 exchange_amount_sat = Some(swap.payer_amount_sat - swap.fees_sat);
1484 Ok(swap.fees_sat)
1485 }
1486 };
1487
1488 let fees_sat = match (fees_sat_res, asset_fees) {
1489 (Ok(fees_sat), _) => Some(fees_sat),
1490 (Err(e), Some(_asset_fees)) => {
1491 debug!(
1492 "Error estimating onchain tx fees, but returning payjoin fees: {e}"
1493 );
1494 None
1495 }
1496 (Err(e), None) => return Err(e),
1497 };
1498 (to_asset, receiver_amount_sat, fees_sat, asset_fees)
1499 }
1500 };
1501
1502 liquid_address_data.amount_sat = Some(receiver_amount_sat);
1503 liquid_address_data.asset_id = Some(asset_id.clone());
1504 payment_destination = SendDestination::LiquidAddress {
1505 address_data: liquid_address_data,
1506 bip353_address: None,
1507 };
1508 }
1509 Ok(InputType::Bolt11 { invoice }) => {
1510 self.ensure_send_is_not_self_transfer(&invoice.bolt11)?;
1511 self.validate_bolt11_invoice(&invoice.bolt11)?;
1512
1513 let invoice_amount_sat = invoice.amount_msat.ok_or(
1514 PaymentError::amount_missing("Expected invoice with an amount"),
1515 )? / 1000;
1516
1517 if let Some(PayAmount::Bitcoin {
1518 receiver_amount_sat: amount_sat,
1519 }) = req.amount
1520 {
1521 ensure_sdk!(
1522 invoice_amount_sat == amount_sat,
1523 PaymentError::Generic {
1524 err: "Receiver amount and invoice amount do not match".to_string()
1525 }
1526 );
1527 }
1528
1529 let lbtc_pair = self.validate_submarine_pairs(invoice_amount_sat).await?;
1530 let mrh_address = if use_mrh {
1531 self.swapper
1532 .check_for_mrh(&invoice.bolt11)
1533 .await?
1534 .map(|(address, _)| address)
1535 } else {
1536 None
1537 };
1538 asset_id = self.config.lbtc_asset_id();
1539 estimated_asset_fees = None;
1540 (receiver_amount_sat, fees_sat) = match (mrh_address.clone(), req.amount.clone()) {
1541 (Some(lbtc_address), Some(PayAmount::Drain)) => {
1542 let drain_fees_sat = self
1546 .estimate_drain_tx_fee(None, Some(&lbtc_address))
1547 .await?;
1548 let drain_amount_sat =
1549 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1550 (drain_amount_sat, Some(drain_fees_sat))
1551 }
1552 (Some(lbtc_address), _) => {
1553 let fees_sat = self
1556 .estimate_onchain_tx_or_drain_tx_fee(
1557 invoice_amount_sat,
1558 &lbtc_address,
1559 &asset_id,
1560 )
1561 .await?;
1562 (invoice_amount_sat, Some(fees_sat))
1563 }
1564 (None, _) => {
1565 let boltz_fees_total = lbtc_pair.fees.total(invoice_amount_sat);
1567 let user_lockup_amount_sat = invoice_amount_sat + boltz_fees_total;
1568 let lockup_fees_sat = self
1569 .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
1570 .await?;
1571 let fees_sat = boltz_fees_total + lockup_fees_sat;
1572 (invoice_amount_sat, Some(fees_sat))
1573 }
1574 };
1575
1576 payment_destination = SendDestination::Bolt11 {
1577 invoice,
1578 bip353_address: None,
1579 };
1580 }
1581 Ok(InputType::Bolt12Offer {
1582 offer,
1583 bip353_address,
1584 }) => {
1585 asset_id = self.config.lbtc_asset_id();
1586 estimated_asset_fees = None;
1587 (receiver_amount_sat, fees_sat) = match req.amount {
1588 Some(PayAmount::Drain) => {
1589 ensure_sdk!(
1590 get_info_res.wallet_info.pending_receive_sat == 0
1591 && get_info_res.wallet_info.pending_send_sat == 0,
1592 PaymentError::Generic {
1593 err: "Cannot drain while there are pending payments".to_string(),
1594 }
1595 );
1596 let lbtc_pair = self
1597 .swapper
1598 .get_submarine_pairs()
1599 .await?
1600 .ok_or(PaymentError::PairsNotFound)?;
1601 let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
1602 let drain_amount_sat =
1603 get_info_res.wallet_info.balance_sat - drain_fees_sat;
1604 let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
1606 let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
1607 let receiver_amount_sat =
1608 utils::increment_receiver_amount_up_to_drain_amount(
1609 dummy_amount_sat,
1610 &lbtc_pair,
1611 drain_amount_sat,
1612 );
1613 lbtc_pair.limits.within(receiver_amount_sat)?;
1614 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1616 ensure_sdk!(
1617 receiver_amount_sat + boltz_fees_total == drain_amount_sat,
1618 PaymentError::Generic {
1619 err: "Cannot drain without leaving a remainder".to_string(),
1620 }
1621 );
1622 let fees_sat = Some(boltz_fees_total + drain_fees_sat);
1623 info!("Drain amount: {receiver_amount_sat} sat");
1624 Ok((receiver_amount_sat, fees_sat))
1625 }
1626 Some(PayAmount::Bitcoin {
1627 receiver_amount_sat,
1628 }) => {
1629 let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
1630 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
1631 let lockup_fees_sat = self
1632 .estimate_lockup_tx_or_drain_tx_fee(
1633 receiver_amount_sat + boltz_fees_total,
1634 )
1635 .await?;
1636 let fees_sat = Some(boltz_fees_total + lockup_fees_sat);
1637 Ok((receiver_amount_sat, fees_sat))
1638 }
1639 _ => Err(PaymentError::amount_missing(
1640 "Expected PayAmount of type Receiver when processing a Bolt12 offer",
1641 )),
1642 }?;
1643 if let Some(Amount::Bitcoin { amount_msat }) = &offer.min_amount {
1644 ensure_sdk!(
1645 receiver_amount_sat >= amount_msat / 1_000,
1646 PaymentError::invalid_invoice(
1647 "Invalid receiver amount: below offer minimum"
1648 )
1649 );
1650 }
1651
1652 payment_destination = SendDestination::Bolt12 {
1653 offer,
1654 receiver_amount_sat,
1655 bip353_address,
1656 };
1657 }
1658 _ => {
1659 return Err(PaymentError::generic("Destination is not valid"));
1660 }
1661 };
1662
1663 if validate_funds {
1664 get_info_res.wallet_info.validate_sufficient_funds(
1665 self.config.network,
1666 receiver_amount_sat,
1667 fees_sat,
1668 &asset_id,
1669 )?;
1670 }
1671
1672 Ok(PrepareSendResponse {
1673 destination: payment_destination,
1674 fees_sat,
1675 estimated_asset_fees,
1676 amount: req.amount.clone(),
1677 exchange_amount_sat,
1678 disable_mrh: req.disable_mrh,
1679 payment_timeout_sec: Some(timeout_sec),
1680 })
1681 }
1682
1683 fn ensure_send_is_not_self_transfer(&self, invoice: &str) -> Result<(), PaymentError> {
1684 match self.persister.fetch_receive_swap_by_invoice(invoice)? {
1685 None => Ok(()),
1686 Some(_) => Err(PaymentError::SelfTransferNotSupported),
1687 }
1688 }
1689
1690 pub async fn send_payment(
1708 &self,
1709 req: &SendPaymentRequest,
1710 ) -> Result<SendPaymentResponse, PaymentError> {
1711 self.ensure_is_started().await?;
1712
1713 let use_mrh = match req.prepare_response.disable_mrh {
1714 Some(disable_mrh) => !disable_mrh,
1715 None => self.config.use_magic_routing_hints,
1716 };
1717
1718 let PrepareSendResponse {
1719 fees_sat,
1720 destination: payment_destination,
1721 amount,
1722 payment_timeout_sec,
1723 ..
1724 } = &req.prepare_response;
1725 let is_drain = matches!(amount, Some(PayAmount::Drain));
1726
1727 let timeout_sec = payment_timeout_sec.unwrap_or(self.config.payment_timeout_sec);
1728
1729 match payment_destination {
1730 SendDestination::LiquidAddress {
1731 address_data: liquid_address_data,
1732 bip353_address,
1733 } => {
1734 let Some(receiver_amount_sat) = liquid_address_data.amount_sat else {
1735 return Err(PaymentError::AmountMissing {
1736 err: "Receiver amount must be set when paying to a Liquid address"
1737 .to_string(),
1738 });
1739 };
1740 let Some(to_asset) = liquid_address_data.asset_id.clone() else {
1741 return Err(PaymentError::asset_error(
1742 "Asset must be set when paying to a Liquid address",
1743 ));
1744 };
1745
1746 ensure_sdk!(
1747 liquid_address_data.network == self.config.network.into(),
1748 PaymentError::InvalidNetwork {
1749 err: format!(
1750 "Cannot send payment from {} to {}",
1751 Into::<sdk_common::bitcoin::Network>::into(self.config.network),
1752 liquid_address_data.network
1753 )
1754 }
1755 );
1756
1757 let asset_pay_fees = req.use_asset_fees.unwrap_or_default();
1758 let mut response = match amount.as_ref().is_some_and(|a| a.is_sideswap_payment()) {
1759 false => {
1760 self.pay_liquid(PayLiquidRequest {
1761 address_data: liquid_address_data.clone(),
1762 to_asset,
1763 receiver_amount_sat,
1764 asset_pay_fees,
1765 fees_sat: *fees_sat,
1766 })
1767 .await
1768 }
1769 true => {
1770 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1771 ensure_sdk!(
1772 !asset_pay_fees,
1773 PaymentError::generic("Cannot pay asset fees when executing a payment between two separate assets")
1774 );
1775
1776 self.pay_sideswap(PaySideSwapRequest {
1777 address_data: liquid_address_data.clone(),
1778 to_asset,
1779 receiver_amount_sat,
1780 fees_sat,
1781 amount: amount.clone(),
1782 })
1783 .await
1784 }
1785 }?;
1786
1787 self.insert_payment_details(&None, bip353_address, &mut response)?;
1788 Ok(response)
1789 }
1790 SendDestination::Bolt11 {
1791 invoice,
1792 bip353_address,
1793 } => {
1794 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1795 let mut response = self
1796 .pay_bolt11_invoice(&invoice.bolt11, fees_sat, is_drain, use_mrh, timeout_sec)
1797 .await?;
1798 self.insert_payment_details(&req.payer_note, bip353_address, &mut response)?;
1799 Ok(response)
1800 }
1801 SendDestination::Bolt12 {
1802 offer,
1803 receiver_amount_sat,
1804 bip353_address,
1805 } => {
1806 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
1807 let bolt12_info = self
1808 .swapper
1809 .get_bolt12_info(GetBolt12FetchRequest {
1810 offer: offer.offer.clone(),
1811 amount: *receiver_amount_sat,
1812 note: req.payer_note.clone(),
1813 })
1814 .await?;
1815 let mut response = self
1816 .pay_bolt12_invoice(
1817 offer,
1818 *receiver_amount_sat,
1819 bolt12_info,
1820 fees_sat,
1821 is_drain,
1822 use_mrh,
1823 timeout_sec,
1824 )
1825 .await?;
1826 self.insert_payment_details(&req.payer_note, bip353_address, &mut response)?;
1827 Ok(response)
1828 }
1829 }
1830 }
1831
1832 fn insert_payment_details(
1833 &self,
1834 payer_note: &Option<String>,
1835 bip353_address: &Option<String>,
1836 response: &mut SendPaymentResponse,
1837 ) -> Result<()> {
1838 if payer_note.is_some() || bip353_address.is_some() {
1839 if let (Some(tx_id), Some(destination)) =
1840 (&response.payment.tx_id, &response.payment.destination)
1841 {
1842 self.persister
1843 .insert_or_update_payment_details(PaymentTxDetails {
1844 tx_id: tx_id.clone(),
1845 destination: destination.clone(),
1846 bip353_address: bip353_address.clone(),
1847 payer_note: payer_note.clone(),
1848 ..Default::default()
1849 })?;
1850 if let Some(payment) = self.persister.get_payment(tx_id)? {
1852 response.payment = payment;
1853 }
1854 }
1855 }
1856 Ok(())
1857 }
1858
1859 async fn pay_bolt11_invoice(
1860 &self,
1861 invoice: &str,
1862 fees_sat: u64,
1863 is_drain: bool,
1864 use_mrh: bool,
1865 timeout_sec: u64,
1866 ) -> Result<SendPaymentResponse, PaymentError> {
1867 self.ensure_send_is_not_self_transfer(invoice)?;
1868 let bolt11_invoice = self.validate_bolt11_invoice(invoice)?;
1869
1870 let amount_sat = bolt11_invoice
1871 .amount_milli_satoshis()
1872 .map(|msat| msat / 1_000)
1873 .ok_or(PaymentError::AmountMissing {
1874 err: "Invoice amount is missing".to_string(),
1875 })?;
1876 let payer_amount_sat = amount_sat + fees_sat;
1877 let get_info_response = self.get_info().await?;
1878 ensure_sdk!(
1879 payer_amount_sat <= get_info_response.wallet_info.balance_sat,
1880 PaymentError::InsufficientFunds
1881 );
1882
1883 let description = match bolt11_invoice.description() {
1884 Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
1885 Bolt11InvoiceDescription::Hash(_) => None,
1886 };
1887
1888 let mrh_address = if use_mrh {
1889 self.swapper
1890 .check_for_mrh(invoice)
1891 .await?
1892 .map(|(address, _)| address)
1893 } else {
1894 None
1895 };
1896
1897 match mrh_address {
1898 Some(address) => {
1900 info!("Found MRH for L-BTC address {address}, invoice amount_sat {amount_sat}");
1901 let (amount_sat, fees_sat) = if is_drain {
1902 let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
1903 let drain_amount_sat =
1904 get_info_response.wallet_info.balance_sat - drain_fees_sat;
1905 info!("Drain amount: {drain_amount_sat} sat");
1906 (drain_amount_sat, drain_fees_sat)
1907 } else {
1908 (amount_sat, fees_sat)
1909 };
1910
1911 self.pay_liquid_onchain(
1912 LiquidAddressData {
1913 address,
1914 network: self.config.network.into(),
1915 asset_id: None,
1916 amount: None,
1917 amount_sat: None,
1918 label: None,
1919 message: None,
1920 },
1921 amount_sat,
1922 fees_sat,
1923 false,
1924 )
1925 .await
1926 }
1927
1928 None => {
1930 self.send_payment_via_swap(
1931 SendPaymentViaSwapRequest {
1932 invoice: invoice.to_string(),
1933 bolt12_offer: None,
1934 payment_hash: bolt11_invoice.payment_hash().to_string(),
1935 description,
1936 receiver_amount_sat: amount_sat,
1937 fees_sat,
1938 },
1939 timeout_sec,
1940 )
1941 .await
1942 }
1943 }
1944 }
1945
1946 #[allow(clippy::too_many_arguments)]
1947 async fn pay_bolt12_invoice(
1948 &self,
1949 offer: &LNOffer,
1950 user_specified_receiver_amount_sat: u64,
1951 bolt12_info: GetBolt12FetchResponse,
1952 fees_sat: u64,
1953 is_drain: bool,
1954 use_mrh: bool,
1955 timeout_sec: u64,
1956 ) -> Result<SendPaymentResponse, PaymentError> {
1957 let invoice = self.validate_bolt12_invoice(
1958 offer,
1959 user_specified_receiver_amount_sat,
1960 &bolt12_info.invoice,
1961 )?;
1962
1963 let receiver_amount_sat = invoice.amount_msats() / 1_000;
1964 let payer_amount_sat = receiver_amount_sat + fees_sat;
1965 let get_info_response = self.get_info().await?;
1966 ensure_sdk!(
1967 payer_amount_sat <= get_info_response.wallet_info.balance_sat,
1968 PaymentError::InsufficientFunds
1969 );
1970
1971 match (bolt12_info.magic_routing_hint, use_mrh) {
1972 (Some(MagicRoutingHint { bip21, signature }), true) => {
1974 info!(
1975 "Found MRH for L-BTC address {bip21}, invoice amount_sat {receiver_amount_sat}"
1976 );
1977 let signing_pubkey = invoice.signing_pubkey().to_string();
1978 let (_, address, _, _) = verify_mrh_signature(&bip21, &signing_pubkey, &signature)?;
1979 let (receiver_amount_sat, fees_sat) = if is_drain {
1980 let drain_fees_sat = self.estimate_drain_tx_fee(None, Some(&address)).await?;
1981 let drain_amount_sat =
1982 get_info_response.wallet_info.balance_sat - drain_fees_sat;
1983 info!("Drain amount: {drain_amount_sat} sat");
1984 (drain_amount_sat, drain_fees_sat)
1985 } else {
1986 (receiver_amount_sat, fees_sat)
1987 };
1988
1989 self.pay_liquid_onchain(
1990 LiquidAddressData {
1991 address,
1992 network: self.config.network.into(),
1993 asset_id: None,
1994 amount: None,
1995 amount_sat: None,
1996 label: None,
1997 message: None,
1998 },
1999 receiver_amount_sat,
2000 fees_sat,
2001 false,
2002 )
2003 .await
2004 }
2005
2006 _ => {
2008 self.send_payment_via_swap(
2009 SendPaymentViaSwapRequest {
2010 invoice: bolt12_info.invoice,
2011 bolt12_offer: Some(offer.offer.clone()),
2012 payment_hash: invoice.payment_hash().to_string(),
2013 description: invoice.description().map(|desc| desc.to_string()),
2014 receiver_amount_sat,
2015 fees_sat,
2016 },
2017 timeout_sec,
2018 )
2019 .await
2020 }
2021 }
2022 }
2023
2024 async fn pay_liquid(&self, req: PayLiquidRequest) -> Result<SendPaymentResponse, PaymentError> {
2025 let PayLiquidRequest {
2026 address_data,
2027 receiver_amount_sat,
2028 to_asset,
2029 fees_sat,
2030 asset_pay_fees,
2031 ..
2032 } = req;
2033
2034 self.get_info()
2035 .await?
2036 .wallet_info
2037 .validate_sufficient_funds(
2038 self.config.network,
2039 receiver_amount_sat,
2040 fees_sat,
2041 &to_asset,
2042 )?;
2043
2044 if asset_pay_fees {
2045 return self
2046 .pay_liquid_payjoin(address_data.clone(), receiver_amount_sat)
2047 .await;
2048 }
2049
2050 let fees_sat = fees_sat.ok_or(PaymentError::InsufficientFunds)?;
2051 self.pay_liquid_onchain(address_data.clone(), receiver_amount_sat, fees_sat, true)
2052 .await
2053 }
2054
2055 async fn pay_liquid_onchain(
2057 &self,
2058 address_data: LiquidAddressData,
2059 receiver_amount_sat: u64,
2060 fees_sat: u64,
2061 skip_already_paid_check: bool,
2062 ) -> Result<SendPaymentResponse, PaymentError> {
2063 let destination = address_data
2064 .to_uri()
2065 .unwrap_or(address_data.address.clone());
2066 let asset_id = address_data.asset_id.unwrap_or(self.config.lbtc_asset_id());
2067 let payments = self.persister.get_payments(&ListPaymentsRequest {
2068 details: Some(ListPaymentDetails::Liquid {
2069 asset_id: Some(asset_id.clone()),
2070 destination: Some(destination.clone()),
2071 }),
2072 ..Default::default()
2073 })?;
2074 ensure_sdk!(
2075 skip_already_paid_check || payments.is_empty(),
2076 PaymentError::AlreadyPaid
2077 );
2078
2079 let tx = self
2080 .onchain_wallet
2081 .build_tx_or_drain_tx(
2082 Some(LIQUID_FEE_RATE_MSAT_PER_VBYTE),
2083 &address_data.address,
2084 &asset_id,
2085 receiver_amount_sat,
2086 )
2087 .await?;
2088 let tx_id = tx.txid().to_string();
2089 let tx_fees_sat = tx.all_fees().values().sum::<u64>();
2090 ensure_sdk!(tx_fees_sat <= fees_sat, PaymentError::InvalidOrExpiredFees);
2091
2092 info!(
2093 "Built onchain Liquid tx with receiver_amount_sat = {receiver_amount_sat}, fees_sat = {fees_sat} and txid = {tx_id}"
2094 );
2095
2096 let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
2097
2098 let tx_data = PaymentTxData {
2101 tx_id: tx_id.clone(),
2102 timestamp: Some(utils::now()),
2103 is_confirmed: false,
2104 fees_sat,
2105 unblinding_data: None,
2106 };
2107 let tx_balance = PaymentTxBalance {
2108 amount: receiver_amount_sat,
2109 asset_id: asset_id.clone(),
2110 payment_type: PaymentType::Send,
2111 };
2112
2113 let description = address_data.message;
2114
2115 self.persister.insert_or_update_payment(
2116 tx_data.clone(),
2117 std::slice::from_ref(&tx_balance),
2118 Some(PaymentTxDetails {
2119 tx_id: tx_id.clone(),
2120 destination: destination.clone(),
2121 description: description.clone(),
2122 ..Default::default()
2123 }),
2124 false,
2125 )?;
2126 self.emit_payment_updated(Some(tx_id)).await?; let asset_info = self
2129 .persister
2130 .get_asset_metadata(&asset_id)?
2131 .map(|ref am| AssetInfo {
2132 name: am.name.clone(),
2133 ticker: am.ticker.clone(),
2134 amount: am.amount_from_sat(receiver_amount_sat),
2135 fees: None,
2136 });
2137 let payment_details = PaymentDetails::Liquid {
2138 asset_id,
2139 destination,
2140 description: description.unwrap_or("Liquid transfer".to_string()),
2141 asset_info,
2142 lnurl_info: None,
2143 bip353_address: None,
2144 payer_note: None,
2145 };
2146
2147 Ok(SendPaymentResponse {
2148 payment: Payment::from_tx_data(tx_data, tx_balance, None, payment_details),
2149 })
2150 }
2151
2152 async fn pay_sideswap(
2154 &self,
2155 req: PaySideSwapRequest,
2156 ) -> Result<SendPaymentResponse, PaymentError> {
2157 let PaySideSwapRequest {
2158 address_data,
2159 to_asset,
2160 amount,
2161 receiver_amount_sat,
2162 fees_sat,
2163 } = req;
2164
2165 let from_asset = AssetId::from_str(match amount {
2166 Some(PayAmount::Asset {
2167 from_asset: Some(ref from_asset),
2168 ..
2169 }) => from_asset,
2170 _ => &to_asset,
2171 })?;
2172 let to_asset = AssetId::from_str(&to_asset)?;
2173 let to_address = elements::Address::from_str(&address_data.address).map_err(|err| {
2174 PaymentError::generic(format!("Could not convert destination address: {err}"))
2175 })?;
2176
2177 let sideswap_service = SideSwapService::from_sdk(self).await;
2178
2179 let swap = sideswap_service
2180 .get_asset_swap(from_asset, to_asset, receiver_amount_sat)
2181 .await?;
2182
2183 ensure_sdk!(
2184 swap.fees_sat <= fees_sat,
2185 PaymentError::InvalidOrExpiredFees
2186 );
2187 swap.check_sufficient_balance(&self.get_info().await?.wallet_info)?;
2188
2189 let tx_id = sideswap_service
2190 .execute_swap(to_address.clone(), &swap)
2191 .await?;
2192
2193 self.persister.insert_or_update_payment(
2196 PaymentTxData {
2197 tx_id: tx_id.clone(),
2198 timestamp: Some(utils::now()),
2199 fees_sat: swap.fees_sat,
2200 is_confirmed: false,
2201 unblinding_data: None,
2202 },
2203 &[PaymentTxBalance {
2204 asset_id: swap.from_asset.to_string(),
2205 amount: swap.payer_amount_sat,
2206 payment_type: PaymentType::Send,
2207 }],
2208 Some(PaymentTxDetails {
2209 tx_id: tx_id.clone(),
2210 destination: to_address.to_string(),
2211 description: address_data.message,
2212 ..Default::default()
2213 }),
2214 false,
2215 )?;
2216 self.emit_payment_updated(Some(tx_id.clone())).await?; let payment = self
2219 .persister
2220 .get_payment(&tx_id)?
2221 .context("Payment not found")?;
2222 Ok(SendPaymentResponse { payment })
2223 }
2224
2225 async fn pay_liquid_payjoin(
2227 &self,
2228 address_data: LiquidAddressData,
2229 receiver_amount_sat: u64,
2230 ) -> Result<SendPaymentResponse, PaymentError> {
2231 let destination = address_data
2232 .to_uri()
2233 .unwrap_or(address_data.address.clone());
2234 let Some(asset_id) = address_data.asset_id else {
2235 return Err(PaymentError::asset_error(
2236 "Asset must be set when paying to a Liquid address",
2237 ));
2238 };
2239
2240 let (tx, asset_fees) = self
2241 .payjoin_service
2242 .build_payjoin_tx(&address_data.address, &asset_id, receiver_amount_sat)
2243 .await
2244 .inspect_err(|e| error!("Error building payjoin tx: {e}"))?;
2245 let tx_id = tx.txid().to_string();
2246 let fees_sat = tx.all_fees().values().sum::<u64>();
2247
2248 info!(
2249 "Built payjoin Liquid tx with receiver_amount_sat = {receiver_amount_sat}, asset_fees = {asset_fees}, fees_sat = {fees_sat} and txid = {tx_id}"
2250 );
2251
2252 let tx_id = self.liquid_chain_service.broadcast(&tx).await?.to_string();
2253
2254 let tx_data = PaymentTxData {
2257 tx_id: tx_id.clone(),
2258 fees_sat,
2259 timestamp: Some(utils::now()),
2260 is_confirmed: false,
2261 unblinding_data: None,
2262 };
2263 let tx_balance = PaymentTxBalance {
2264 asset_id: asset_id.clone(),
2265 amount: receiver_amount_sat + asset_fees,
2266 payment_type: PaymentType::Send,
2267 };
2268
2269 let description = address_data.message;
2270
2271 self.persister.insert_or_update_payment(
2272 tx_data.clone(),
2273 std::slice::from_ref(&tx_balance),
2274 Some(PaymentTxDetails {
2275 tx_id: tx_id.clone(),
2276 destination: destination.clone(),
2277 description: description.clone(),
2278 asset_fees: Some(asset_fees),
2279 ..Default::default()
2280 }),
2281 false,
2282 )?;
2283 self.emit_payment_updated(Some(tx_id)).await?; let asset_info = self
2286 .persister
2287 .get_asset_metadata(&asset_id)?
2288 .map(|ref am| AssetInfo {
2289 name: am.name.clone(),
2290 ticker: am.ticker.clone(),
2291 amount: am.amount_from_sat(receiver_amount_sat),
2292 fees: Some(am.amount_from_sat(asset_fees)),
2293 });
2294 let payment_details = PaymentDetails::Liquid {
2295 asset_id,
2296 destination,
2297 description: description.unwrap_or("Liquid transfer".to_string()),
2298 asset_info,
2299 lnurl_info: None,
2300 bip353_address: None,
2301 payer_note: None,
2302 };
2303
2304 Ok(SendPaymentResponse {
2305 payment: Payment::from_tx_data(tx_data, tx_balance, None, payment_details),
2306 })
2307 }
2308
2309 async fn send_payment_via_swap(
2313 &self,
2314 req: SendPaymentViaSwapRequest,
2315 timeout_sec: u64,
2316 ) -> Result<SendPaymentResponse, PaymentError> {
2317 let SendPaymentViaSwapRequest {
2318 invoice,
2319 bolt12_offer,
2320 payment_hash,
2321 description,
2322 receiver_amount_sat,
2323 fees_sat,
2324 } = req;
2325 let lbtc_pair = self.validate_submarine_pairs(receiver_amount_sat).await?;
2326 let boltz_fees_total = lbtc_pair.fees.total(receiver_amount_sat);
2327 let user_lockup_amount_sat = receiver_amount_sat + boltz_fees_total;
2328 let lockup_tx_fees_sat = self
2329 .estimate_lockup_tx_or_drain_tx_fee(user_lockup_amount_sat)
2330 .await?;
2331 ensure_sdk!(
2332 fees_sat == boltz_fees_total + lockup_tx_fees_sat,
2333 PaymentError::InvalidOrExpiredFees
2334 );
2335
2336 let swap = match self.persister.fetch_send_swap_by_invoice(&invoice)? {
2337 Some(swap) => match swap.state {
2338 Created => swap,
2339 TimedOut => {
2340 self.send_swap_handler.update_swap_info(
2341 &swap.id,
2342 PaymentState::Created,
2343 None,
2344 None,
2345 None,
2346 )?;
2347 swap
2348 }
2349 Pending => return Err(PaymentError::PaymentInProgress),
2350 Complete => return Err(PaymentError::AlreadyPaid),
2351 RefundPending | Refundable | Failed => {
2352 return Err(PaymentError::invalid_invoice(
2353 "Payment has already failed. Please try with another invoice",
2354 ))
2355 }
2356 WaitingFeeAcceptance => {
2357 return Err(PaymentError::Generic {
2358 err: "Send swap payment cannot be in state WaitingFeeAcceptance"
2359 .to_string(),
2360 })
2361 }
2362 },
2363 None => {
2364 let keypair = utils::generate_keypair();
2365 let refund_public_key = boltz_client::PublicKey {
2366 compressed: true,
2367 inner: keypair.public_key(),
2368 };
2369 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2370 url,
2371 hash_swap_id: Some(true),
2372 status: Some(vec![
2373 SubSwapStates::InvoiceFailedToPay,
2374 SubSwapStates::SwapExpired,
2375 SubSwapStates::TransactionClaimPending,
2376 SubSwapStates::TransactionLockupFailed,
2377 ]),
2378 });
2379 let create_response = self
2380 .swapper
2381 .create_send_swap(CreateSubmarineRequest {
2382 from: "L-BTC".to_string(),
2383 to: "BTC".to_string(),
2384 invoice: invoice.to_string(),
2385 refund_public_key,
2386 pair_hash: Some(lbtc_pair.hash.clone()),
2387 referral_id: None,
2388 webhook,
2389 })
2390 .await?;
2391
2392 let swap_id = &create_response.id;
2393 let create_response_json =
2394 SendSwap::from_boltz_struct_to_json(&create_response, swap_id)?;
2395 let destination_pubkey =
2396 utils::get_invoice_destination_pubkey(&invoice, bolt12_offer.is_some())?;
2397
2398 let payer_amount_sat = fees_sat + receiver_amount_sat;
2399 let swap = SendSwap {
2400 id: swap_id.to_string(),
2401 invoice: invoice.to_string(),
2402 bolt12_offer,
2403 payment_hash: Some(payment_hash.to_string()),
2404 destination_pubkey: Some(destination_pubkey),
2405 timeout_block_height: create_response.timeout_block_height,
2406 description,
2407 preimage: None,
2408 payer_amount_sat,
2409 receiver_amount_sat,
2410 pair_fees_json: serde_json::to_string(&lbtc_pair).map_err(|e| {
2411 PaymentError::generic(format!("Failed to serialize SubmarinePair: {e:?}"))
2412 })?,
2413 create_response_json,
2414 lockup_tx_id: None,
2415 refund_address: None,
2416 refund_tx_id: None,
2417 created_at: utils::now(),
2418 state: PaymentState::Created,
2419 refund_private_key: keypair.display_secret().to_string(),
2420 metadata: Default::default(),
2421 };
2422 self.persister.insert_or_update_send_swap(&swap)?;
2423 swap
2424 }
2425 };
2426 self.status_stream.track_swap_id(&swap.id)?;
2427
2428 let create_response = swap.get_boltz_create_response()?;
2429 self.send_swap_handler
2430 .try_lockup(&swap, &create_response)
2431 .await?;
2432
2433 self.wait_for_payment_with_timeout(
2434 Swap::Send(swap),
2435 create_response.accept_zero_conf,
2436 timeout_sec,
2437 )
2438 .await
2439 .map(|payment| SendPaymentResponse { payment })
2440 }
2441
2442 pub async fn fetch_lightning_limits(
2444 &self,
2445 ) -> Result<LightningPaymentLimitsResponse, PaymentError> {
2446 self.ensure_is_started().await?;
2447
2448 let submarine_pair = self
2449 .swapper
2450 .get_submarine_pairs()
2451 .await?
2452 .ok_or(PaymentError::PairsNotFound)?;
2453 let send_limits = submarine_pair.limits;
2454
2455 let reverse_pair = self
2456 .swapper
2457 .get_reverse_swap_pairs()
2458 .await?
2459 .ok_or(PaymentError::PairsNotFound)?;
2460 let receive_limits = reverse_pair.limits;
2461
2462 let res = LightningPaymentLimitsResponse {
2463 send: Limits {
2464 min_sat: send_limits.minimal_batched.unwrap_or(send_limits.minimal),
2465 max_sat: send_limits.maximal,
2466 max_zero_conf_sat: send_limits.maximal_zero_conf,
2467 },
2468 receive: Limits {
2469 min_sat: receive_limits.minimal,
2470 max_sat: receive_limits.maximal,
2471 max_zero_conf_sat: self.config.zero_conf_max_amount_sat(),
2472 },
2473 };
2474 debug!("fetch_lightning_limits returned: {res:?}");
2475 Ok(res)
2476 }
2477
2478 pub async fn fetch_onchain_limits(&self) -> Result<OnchainPaymentLimitsResponse, PaymentError> {
2480 self.ensure_is_started().await?;
2481
2482 let (pair_outgoing, pair_incoming) = self.swapper.get_chain_pairs().await?;
2483 let send_limits = pair_outgoing
2484 .ok_or(PaymentError::PairsNotFound)
2485 .map(|pair| pair.limits)?;
2486 let receive_limits = pair_incoming
2487 .ok_or(PaymentError::PairsNotFound)
2488 .map(|pair| pair.limits)?;
2489
2490 Ok(OnchainPaymentLimitsResponse {
2491 send: Limits {
2492 min_sat: send_limits.minimal,
2493 max_sat: send_limits.maximal,
2494 max_zero_conf_sat: send_limits.maximal_zero_conf,
2495 },
2496 receive: Limits {
2497 min_sat: receive_limits.minimal,
2498 max_sat: receive_limits.maximal,
2499 max_zero_conf_sat: receive_limits.maximal_zero_conf,
2500 },
2501 })
2502 }
2503
2504 pub async fn prepare_pay_onchain(
2513 &self,
2514 req: &PreparePayOnchainRequest,
2515 ) -> Result<PreparePayOnchainResponse, PaymentError> {
2516 self.ensure_is_started().await?;
2517
2518 let get_info_res = self.get_info().await?;
2519 let pair = self.get_chain_pair(Direction::Outgoing).await?;
2520 let claim_fees_sat = match req.fee_rate_sat_per_vbyte {
2521 Some(sat_per_vbyte) => ESTIMATED_BTC_CLAIM_TX_VSIZE * sat_per_vbyte as u64,
2522 None => pair.clone().fees.claim_estimate(),
2523 };
2524 let server_fees_sat = pair.fees.server();
2525
2526 info!("Preparing for onchain payment of kind: {:?}", req.amount);
2527 let (payer_amount_sat, receiver_amount_sat, total_fees_sat) = match req.amount {
2528 PayAmount::Bitcoin {
2529 receiver_amount_sat: amount_sat,
2530 } => {
2531 let receiver_amount_sat = amount_sat;
2532
2533 let user_lockup_amount_sat_without_service_fee =
2534 receiver_amount_sat + claim_fees_sat + server_fees_sat;
2535
2536 let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64
2539 * 100.0
2540 / (100.0 - pair.fees.percentage))
2541 .ceil() as u64;
2542 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2543
2544 let lockup_fees_sat = self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?;
2545
2546 let boltz_fees_sat =
2547 user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2548 let total_fees_sat =
2549 boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2550 let payer_amount_sat = receiver_amount_sat + total_fees_sat;
2551
2552 (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2553 }
2554 PayAmount::Drain => {
2555 ensure_sdk!(
2556 get_info_res.wallet_info.pending_receive_sat == 0
2557 && get_info_res.wallet_info.pending_send_sat == 0,
2558 PaymentError::Generic {
2559 err: "Cannot drain while there are pending payments".to_string(),
2560 }
2561 );
2562 let payer_amount_sat = get_info_res.wallet_info.balance_sat;
2563 let lockup_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
2564
2565 let user_lockup_amount_sat = payer_amount_sat - lockup_fees_sat;
2566 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2567
2568 let boltz_fees_sat = pair.fees.boltz(user_lockup_amount_sat);
2569 let total_fees_sat =
2570 boltz_fees_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat;
2571 let receiver_amount_sat = payer_amount_sat - total_fees_sat;
2572
2573 (payer_amount_sat, receiver_amount_sat, total_fees_sat)
2574 }
2575 PayAmount::Asset { .. } => {
2576 return Err(PaymentError::asset_error(
2577 "Cannot send an asset to a Bitcoin address",
2578 ))
2579 }
2580 };
2581
2582 let res = PreparePayOnchainResponse {
2583 receiver_amount_sat,
2584 claim_fees_sat,
2585 total_fees_sat,
2586 };
2587
2588 ensure_sdk!(
2589 payer_amount_sat <= get_info_res.wallet_info.balance_sat,
2590 PaymentError::InsufficientFunds
2591 );
2592
2593 info!("Prepared onchain payment: {res:?}");
2594 Ok(res)
2595 }
2596
2597 pub async fn pay_onchain(
2614 &self,
2615 req: &PayOnchainRequest,
2616 ) -> Result<SendPaymentResponse, PaymentError> {
2617 self.ensure_is_started().await?;
2618 info!("Paying onchain, request = {req:?}");
2619
2620 let timeout_sec = self.config.payment_timeout_sec;
2621
2622 let claim_address = self.validate_bitcoin_address(&req.address).await?;
2623 let balance_sat = self.get_info().await?.wallet_info.balance_sat;
2624 let receiver_amount_sat = req.prepare_response.receiver_amount_sat;
2625 let pair = self.get_chain_pair(Direction::Outgoing).await?;
2626 let claim_fees_sat = req.prepare_response.claim_fees_sat;
2627 let server_fees_sat = pair.fees.server();
2628 let server_lockup_amount_sat = receiver_amount_sat + claim_fees_sat;
2629
2630 let user_lockup_amount_sat_without_service_fee =
2631 receiver_amount_sat + claim_fees_sat + server_fees_sat;
2632
2633 let user_lockup_amount_sat = (user_lockup_amount_sat_without_service_fee as f64 * 100.0
2636 / (100.0 - pair.fees.percentage))
2637 .ceil() as u64;
2638 let boltz_fee_sat = user_lockup_amount_sat - user_lockup_amount_sat_without_service_fee;
2639 self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?;
2640
2641 let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2642
2643 let lockup_fees_sat = match payer_amount_sat == balance_sat {
2644 true => self.estimate_drain_tx_fee(None, None).await?,
2645 false => self.estimate_lockup_tx_fee(user_lockup_amount_sat).await?,
2646 };
2647
2648 ensure_sdk!(
2649 req.prepare_response.total_fees_sat
2650 == boltz_fee_sat + lockup_fees_sat + claim_fees_sat + server_fees_sat,
2651 PaymentError::InvalidOrExpiredFees
2652 );
2653
2654 ensure_sdk!(
2655 payer_amount_sat <= balance_sat,
2656 PaymentError::InsufficientFunds
2657 );
2658
2659 let preimage = Preimage::new();
2660 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
2661
2662 let claim_keypair = utils::generate_keypair();
2663 let claim_public_key = boltz_client::PublicKey {
2664 compressed: true,
2665 inner: claim_keypair.public_key(),
2666 };
2667 let refund_keypair = utils::generate_keypair();
2668 let refund_public_key = boltz_client::PublicKey {
2669 compressed: true,
2670 inner: refund_keypair.public_key(),
2671 };
2672 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
2673 url,
2674 hash_swap_id: Some(true),
2675 status: Some(vec![
2676 ChainSwapStates::TransactionFailed,
2677 ChainSwapStates::TransactionLockupFailed,
2678 ChainSwapStates::TransactionServerConfirmed,
2679 ]),
2680 });
2681 let create_response = self
2682 .swapper
2683 .create_chain_swap(CreateChainRequest {
2684 from: "L-BTC".to_string(),
2685 to: "BTC".to_string(),
2686 preimage_hash: preimage.sha256,
2687 claim_public_key: Some(claim_public_key),
2688 refund_public_key: Some(refund_public_key),
2689 user_lock_amount: None,
2690 server_lock_amount: Some(server_lockup_amount_sat),
2691 pair_hash: Some(pair.hash.clone()),
2692 referral_id: None,
2693 webhook,
2694 })
2695 .await?;
2696
2697 let create_response_json =
2698 ChainSwap::from_boltz_struct_to_json(&create_response, &create_response.id)?;
2699 let swap_id = create_response.id;
2700
2701 let accept_zero_conf = server_lockup_amount_sat <= pair.limits.maximal_zero_conf;
2702 let payer_amount_sat = req.prepare_response.total_fees_sat + receiver_amount_sat;
2703
2704 let swap = ChainSwap {
2705 id: swap_id.clone(),
2706 direction: Direction::Outgoing,
2707 claim_address: Some(claim_address),
2708 lockup_address: create_response.lockup_details.lockup_address,
2709 refund_address: None,
2710 timeout_block_height: create_response.lockup_details.timeout_block_height,
2711 claim_timeout_block_height: create_response.claim_details.timeout_block_height,
2712 preimage: preimage_str,
2713 description: Some("Bitcoin transfer".to_string()),
2714 payer_amount_sat,
2715 actual_payer_amount_sat: None,
2716 receiver_amount_sat,
2717 accepted_receiver_amount_sat: None,
2718 claim_fees_sat,
2719 pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
2720 PaymentError::generic(format!("Failed to serialize outgoing ChainPair: {e:?}"))
2721 })?,
2722 accept_zero_conf,
2723 create_response_json,
2724 claim_private_key: claim_keypair.display_secret().to_string(),
2725 refund_private_key: refund_keypair.display_secret().to_string(),
2726 server_lockup_tx_id: None,
2727 user_lockup_tx_id: None,
2728 claim_tx_id: None,
2729 refund_tx_id: None,
2730 created_at: utils::now(),
2731 state: PaymentState::Created,
2732 auto_accepted_fees: false,
2733 user_lockup_spent: false,
2734 metadata: Default::default(),
2735 };
2736 self.persister.insert_or_update_chain_swap(&swap)?;
2737 self.status_stream.track_swap_id(&swap_id)?;
2738
2739 self.wait_for_payment_with_timeout(Swap::Chain(swap), accept_zero_conf, timeout_sec)
2740 .await
2741 .map(|payment| SendPaymentResponse { payment })
2742 }
2743
2744 async fn wait_for_payment_with_timeout(
2745 &self,
2746 swap: Swap,
2747 accept_zero_conf: bool,
2748 timeout_sec: u64,
2749 ) -> Result<Payment, PaymentError> {
2750 let timeout_fut = tokio::time::sleep(Duration::from_secs(timeout_sec));
2751 tokio::pin!(timeout_fut);
2752
2753 let expected_swap_id = swap.id();
2754 let mut events_stream = self.event_manager.subscribe();
2755 let mut maybe_payment: Option<Payment> = None;
2756
2757 loop {
2758 tokio::select! {
2759 _ = &mut timeout_fut => match maybe_payment {
2760 Some(payment) => return Ok(payment),
2761 None => {
2762 debug!("Timeout occurred without payment, set swap to timed out");
2763 let update_res = match swap {
2764 Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None),
2765 Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
2766 swap_id: expected_swap_id.clone(),
2767 to_state: TimedOut,
2768 ..Default::default()
2769 }),
2770 _ => Ok(())
2771 };
2772 return match update_res {
2773 Ok(_) => Err(PaymentError::PaymentTimeout),
2774 Err(_) => {
2775 self.persister.get_payment(&expected_swap_id).ok().flatten().ok_or(PaymentError::generic("Payment not found"))
2778 }
2779 }
2780 },
2781 },
2782 event = events_stream.recv() => match event {
2783 Ok(SdkEvent::PaymentPending { details: payment }) => {
2784 let maybe_payment_swap_id = payment.details.get_swap_id();
2785 if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2786 match accept_zero_conf {
2787 true => {
2788 debug!("Received Send Payment pending event with zero-conf accepted");
2789 return Ok(payment)
2790 }
2791 false => {
2792 debug!("Received Send Payment pending event, waiting for confirmation");
2793 maybe_payment = Some(payment);
2794 }
2795 }
2796 };
2797 },
2798 Ok(SdkEvent::PaymentSucceeded { details: payment }) => {
2799 let maybe_payment_swap_id = payment.details.get_swap_id();
2800 if matches!(maybe_payment_swap_id, Some(swap_id) if swap_id == expected_swap_id) {
2801 debug!("Received Send Payment succeed event");
2802 return Ok(payment);
2803 }
2804 },
2805 Ok(event) => debug!("Unhandled event waiting for payment: {event:?}"),
2806 Err(e) => debug!("Received error waiting for payment: {e:?}"),
2807 }
2808 }
2809 }
2810 }
2811
2812 pub async fn prepare_receive_payment(
2822 &self,
2823 req: &PrepareReceiveRequest,
2824 ) -> Result<PrepareReceiveResponse, PaymentError> {
2825 self.ensure_is_started().await?;
2826
2827 let result = match req.payment_method.clone() {
2828 #[allow(deprecated)]
2829 PaymentMethod::Bolt11Invoice => {
2830 let payer_amount_sat = match req.amount {
2831 Some(ReceiveAmount::Asset { .. }) => {
2832 let err = PaymentError::asset_error(
2833 "Cannot receive an asset for this payment method",
2834 );
2835 error!("prepare_receive_payment returned error: {err:?}");
2836 return Err(err);
2837 }
2838 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
2839 None => {
2840 let err = PaymentError::generic(
2841 "Bitcoin payer amount must be set for this payment method",
2842 );
2843 error!("prepare_receive_payment returned error: {err:?}");
2844 return Err(err);
2845 }
2846 };
2847 let reverse_pair = self
2848 .swapper
2849 .get_reverse_swap_pairs()
2850 .await?
2851 .ok_or(PaymentError::PairsNotFound)?;
2852
2853 let fees_sat = reverse_pair.fees.total(payer_amount_sat);
2854
2855 reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
2856 PaymentError::AmountOutOfRange {
2857 min: reverse_pair.limits.minimal,
2858 max: reverse_pair.limits.maximal,
2859 }
2860 })?;
2861
2862 let min_payer_amount_sat = Some(reverse_pair.limits.minimal);
2863 let max_payer_amount_sat = Some(reverse_pair.limits.maximal);
2864 let swapper_feerate = Some(reverse_pair.fees.percentage);
2865
2866 debug!(
2867 "Preparing Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat"
2868 );
2869
2870 Ok(PrepareReceiveResponse {
2871 payment_method: req.payment_method.clone(),
2872 amount: req.amount.clone(),
2873 fees_sat,
2874 min_payer_amount_sat,
2875 max_payer_amount_sat,
2876 swapper_feerate,
2877 })
2878 }
2879 PaymentMethod::Bolt12Offer => {
2880 if req.amount.is_some() {
2881 let err = PaymentError::generic("Amount cannot be set for this payment method");
2882 error!("prepare_receive_payment returned error: {err:?}");
2883 return Err(err);
2884 }
2885
2886 let reverse_pair = self
2887 .swapper
2888 .get_reverse_swap_pairs()
2889 .await?
2890 .ok_or(PaymentError::PairsNotFound)?;
2891
2892 let fees_sat = reverse_pair.fees.total(0);
2893 debug!("Preparing Bolt12Offer Receive Swap with: min fees_sat {fees_sat}");
2894
2895 Ok(PrepareReceiveResponse {
2896 payment_method: req.payment_method.clone(),
2897 amount: req.amount.clone(),
2898 fees_sat,
2899 min_payer_amount_sat: Some(reverse_pair.limits.minimal),
2900 max_payer_amount_sat: Some(reverse_pair.limits.maximal),
2901 swapper_feerate: Some(reverse_pair.fees.percentage),
2902 })
2903 }
2904 PaymentMethod::BitcoinAddress => {
2905 let payer_amount_sat = match req.amount {
2906 Some(ReceiveAmount::Asset { .. }) => {
2907 let err = PaymentError::asset_error(
2908 "Asset cannot be received for this payment method",
2909 );
2910 error!("prepare_receive_payment returned error: {err:?}");
2911 return Err(err);
2912 }
2913 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
2914 None => None,
2915 };
2916 let pair = self
2917 .get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)
2918 .await?;
2919 let claim_fees_sat = pair.fees.claim_estimate();
2920 let server_fees_sat = pair.fees.server();
2921 let service_fees_sat = payer_amount_sat
2922 .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
2923 .unwrap_or_default();
2924
2925 let fees_sat = service_fees_sat + claim_fees_sat + server_fees_sat;
2926 debug!("Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}");
2927
2928 Ok(PrepareReceiveResponse {
2929 payment_method: req.payment_method.clone(),
2930 amount: req.amount.clone(),
2931 fees_sat,
2932 min_payer_amount_sat: Some(pair.limits.minimal),
2933 max_payer_amount_sat: Some(pair.limits.maximal),
2934 swapper_feerate: Some(pair.fees.percentage),
2935 })
2936 }
2937 PaymentMethod::LiquidAddress => {
2938 let (asset_id, payer_amount, payer_amount_sat) = match req.amount.clone() {
2939 Some(ReceiveAmount::Asset {
2940 payer_amount,
2941 asset_id,
2942 }) => (asset_id, payer_amount, None),
2943 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
2944 (self.config.lbtc_asset_id(), None, Some(payer_amount_sat))
2945 }
2946 None => (self.config.lbtc_asset_id(), None, None),
2947 };
2948
2949 debug!("Preparing Liquid Receive with: asset_id {asset_id}, amount {payer_amount:?}, amount_sat {payer_amount_sat:?}");
2950
2951 Ok(PrepareReceiveResponse {
2952 payment_method: req.payment_method.clone(),
2953 amount: req.amount.clone(),
2954 fees_sat: 0,
2955 min_payer_amount_sat: None,
2956 max_payer_amount_sat: None,
2957 swapper_feerate: None,
2958 })
2959 }
2960 };
2961 result
2962 .inspect(|res| debug!("prepare_receive_payment returned: {res:?}"))
2963 .inspect_err(|e| error!("prepare_receive_payment returned error: {e:?}"))
2964 }
2965
2966 pub async fn receive_payment(
2987 &self,
2988 req: &ReceivePaymentRequest,
2989 ) -> Result<ReceivePaymentResponse, PaymentError> {
2990 self.ensure_is_started().await?;
2991
2992 let PrepareReceiveResponse {
2993 payment_method,
2994 amount,
2995 fees_sat,
2996 ..
2997 } = req.prepare_response.clone();
2998
2999 let result = match payment_method {
3000 #[allow(deprecated)]
3001 PaymentMethod::Bolt11Invoice => {
3002 let amount_sat = match amount.clone() {
3003 Some(ReceiveAmount::Asset { .. }) => {
3004 let err = PaymentError::asset_error(
3005 "Asset cannot be received for this payment method",
3006 );
3007 error!("receive_payment returned error: {err:?}");
3008 return Err(err);
3009 }
3010 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => payer_amount_sat,
3011 None => {
3012 let err = PaymentError::generic(
3013 "Bitcoin payer amount must be set for this payment method",
3014 );
3015 error!("receive_payment returned error: {err:?}");
3016 return Err(err);
3017 }
3018 };
3019
3020 let (description, description_hash) = match (
3021 req.description.clone(),
3022 req.description_hash.clone(),
3023 ) {
3024 (None, Some(description_hash)) => match description_hash {
3025 DescriptionHash::UseDescription => {
3026 let err = PaymentError::InvalidDescription { err: "Cannot calculate payment description hash: no description provided".to_string() };
3027 error!("receive_payment returned error: {err:?}");
3028 return Err(err);
3029 }
3030 DescriptionHash::Custom { hash } => (None, Some(hash)),
3031 },
3032 (Some(description), Some(description_hash)) => {
3033 let calculated_hash = sha256::Hash::hash(description.as_bytes()).to_hex();
3034 match description_hash {
3035 DescriptionHash::UseDescription => (None, Some(calculated_hash)),
3036 DescriptionHash::Custom { hash } => {
3037 ensure_sdk!(
3038 calculated_hash == *hash,
3039 PaymentError::InvalidDescription {
3040 err: "Payment description hash mismatch".to_string()
3041 }
3042 );
3043 (None, Some(calculated_hash))
3044 }
3045 }
3046 }
3047 (description, None) => (description, None),
3048 };
3049 self.create_bolt11_receive_swap(
3050 amount_sat,
3051 fees_sat,
3052 description,
3053 description_hash,
3054 req.payer_note.clone(),
3055 )
3056 .await
3057 }
3058 PaymentMethod::Bolt12Offer => {
3059 let description = req.description.clone().unwrap_or("".to_string());
3060 match self
3061 .persister
3062 .fetch_bolt12_offer_by_description(&description)?
3063 {
3064 Some(bolt12_offer) => Ok(ReceivePaymentResponse {
3065 destination: bolt12_offer.id,
3066 liquid_expiration_blockheight: None,
3067 bitcoin_expiration_blockheight: None,
3068 }),
3069 None => self.create_bolt12_offer(description).await,
3070 }
3071 }
3072 PaymentMethod::BitcoinAddress => {
3073 let amount_sat = match amount.clone() {
3074 Some(ReceiveAmount::Asset { .. }) => {
3075 let err = PaymentError::asset_error(
3076 "Asset cannot be received for this payment method",
3077 );
3078 error!("receive_payment returned error: {err:?}");
3079 return Err(err);
3080 }
3081 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => Some(payer_amount_sat),
3082 None => None,
3083 };
3084 self.receive_onchain(amount_sat, fees_sat).await
3085 }
3086 PaymentMethod::LiquidAddress => {
3087 let lbtc_asset_id = self.config.lbtc_asset_id();
3088 let (asset_id, amount, amount_sat) = match amount.clone() {
3089 Some(ReceiveAmount::Asset {
3090 asset_id,
3091 payer_amount,
3092 }) => (asset_id, payer_amount, None),
3093 Some(ReceiveAmount::Bitcoin { payer_amount_sat }) => {
3094 (lbtc_asset_id.clone(), None, Some(payer_amount_sat))
3095 }
3096 None => (lbtc_asset_id.clone(), None, None),
3097 };
3098
3099 let address = self.onchain_wallet.next_unused_address().await?.to_string();
3100 let receive_destination =
3101 if asset_id.ne(&lbtc_asset_id) || amount.is_some() || amount_sat.is_some() {
3102 LiquidAddressData {
3103 address: address.to_string(),
3104 network: self.config.network.into(),
3105 amount,
3106 amount_sat,
3107 asset_id: Some(asset_id),
3108 label: None,
3109 message: req.description.clone(),
3110 }
3111 .to_uri()
3112 .map_err(|e| PaymentError::Generic {
3113 err: format!("Could not build BIP21 URI: {e:?}"),
3114 })?
3115 } else {
3116 address
3117 };
3118
3119 Ok(ReceivePaymentResponse {
3120 destination: receive_destination,
3121 liquid_expiration_blockheight: None,
3122 bitcoin_expiration_blockheight: None,
3123 })
3124 }
3125 };
3126 result
3127 .inspect(|res| debug!("receive_payment returned: {res:?}"))
3128 .inspect_err(|e| error!("receive_payment returned error: {e:?}"))
3129 }
3130
3131 async fn create_bolt11_receive_swap(
3132 &self,
3133 payer_amount_sat: u64,
3134 fees_sat: u64,
3135 description: Option<String>,
3136 description_hash: Option<String>,
3137 payer_note: Option<String>,
3138 ) -> Result<ReceivePaymentResponse, PaymentError> {
3139 let reverse_pair = self
3140 .swapper
3141 .get_reverse_swap_pairs()
3142 .await?
3143 .ok_or(PaymentError::PairsNotFound)?;
3144 let new_fees_sat = reverse_pair.fees.total(payer_amount_sat);
3145 ensure_sdk!(fees_sat == new_fees_sat, PaymentError::InvalidOrExpiredFees);
3146
3147 debug!("Creating BOLT11 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
3148
3149 let keypair = utils::generate_keypair();
3150
3151 let preimage = Preimage::new();
3152 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3153 let preimage_hash = preimage.sha256.to_string();
3154
3155 let mrh_addr = self.onchain_wallet.next_unused_address().await?;
3157 let mrh_addr_str = mrh_addr.to_string();
3159 let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
3160
3161 let receiver_amount_sat = payer_amount_sat - fees_sat;
3162 let webhook_claim_status =
3163 match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3164 true => RevSwapStates::TransactionConfirmed,
3165 false => RevSwapStates::TransactionMempool,
3166 };
3167 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3168 url,
3169 hash_swap_id: Some(true),
3170 status: Some(vec![webhook_claim_status]),
3171 });
3172
3173 let v2_req = CreateReverseRequest {
3174 from: "BTC".to_string(),
3175 to: "L-BTC".to_string(),
3176 invoice: None,
3177 invoice_amount: Some(payer_amount_sat),
3178 preimage_hash: Some(preimage.sha256),
3179 claim_public_key: keypair.public_key().into(),
3180 description,
3181 description_hash,
3182 address: Some(mrh_addr_str.clone()),
3183 address_signature: Some(mrh_addr_hash_sig.to_hex()),
3184 referral_id: None,
3185 webhook,
3186 };
3187 let create_response = self.swapper.create_receive_swap(v2_req).await?;
3188 let invoice_str = create_response
3189 .invoice
3190 .clone()
3191 .ok_or(PaymentError::receive_error("Invoice not found"))?;
3192
3193 self.persister.insert_or_update_reserved_address(
3195 &mrh_addr_str,
3196 create_response.timeout_block_height,
3197 )?;
3198
3199 let (bip21_lbtc_address, _bip21_amount_btc) = self
3201 .swapper
3202 .check_for_mrh(&invoice_str)
3203 .await?
3204 .ok_or(PaymentError::receive_error("Invoice has no MRH"))?;
3205 ensure_sdk!(
3206 bip21_lbtc_address == mrh_addr_str,
3207 PaymentError::receive_error("Invoice has incorrect address in MRH")
3208 );
3209
3210 let swap_id = create_response.id.clone();
3211 let invoice = Bolt11Invoice::from_str(&invoice_str)
3212 .map_err(|err| PaymentError::invalid_invoice(err.to_string()))?;
3213 let payer_amount_sat =
3214 invoice
3215 .amount_milli_satoshis()
3216 .ok_or(PaymentError::invalid_invoice(
3217 "Invoice does not contain an amount",
3218 ))?
3219 / 1000;
3220 let destination_pubkey = invoice_pubkey(&invoice);
3221
3222 ensure_sdk!(
3225 invoice.payment_hash().to_string() == preimage_hash,
3226 PaymentError::invalid_invoice("Invalid preimage returned by swapper")
3227 );
3228
3229 let create_response_json = ReceiveSwap::from_boltz_struct_to_json(
3230 &create_response,
3231 &swap_id,
3232 Some(&invoice.to_string()),
3233 )?;
3234 let invoice_description = match invoice.description() {
3235 Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
3236 Bolt11InvoiceDescription::Hash(_) => None,
3237 };
3238
3239 self.persister
3240 .insert_or_update_receive_swap(&ReceiveSwap {
3241 id: swap_id.clone(),
3242 preimage: preimage_str,
3243 create_response_json,
3244 claim_private_key: keypair.display_secret().to_string(),
3245 invoice: invoice.to_string(),
3246 bolt12_offer: None,
3247 payment_hash: Some(preimage_hash),
3248 destination_pubkey: Some(destination_pubkey),
3249 timeout_block_height: create_response.timeout_block_height,
3250 description: invoice_description,
3251 payer_note,
3252 payer_amount_sat,
3253 receiver_amount_sat,
3254 pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3255 PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3256 })?,
3257 claim_fees_sat: reverse_pair.fees.claim_estimate(),
3258 lockup_tx_id: None,
3259 claim_address: None,
3260 claim_tx_id: None,
3261 mrh_address: mrh_addr_str,
3262 mrh_tx_id: None,
3263 created_at: utils::now(),
3264 state: PaymentState::Created,
3265 metadata: Default::default(),
3266 })
3267 .map_err(|e| {
3268 error!("Failed to insert or update receive swap: {e:?}");
3269 PaymentError::PersistError
3270 })?;
3271 self.status_stream.track_swap_id(&swap_id)?;
3272
3273 Ok(ReceivePaymentResponse {
3274 destination: invoice.to_string(),
3275 liquid_expiration_blockheight: Some(create_response.timeout_block_height),
3276 bitcoin_expiration_blockheight: None,
3277 })
3278 }
3279
3280 pub async fn create_bolt12_invoice(
3293 &self,
3294 req: &CreateBolt12InvoiceRequest,
3295 ) -> Result<CreateBolt12InvoiceResponse, PaymentError> {
3296 debug!("Started create BOLT12 invoice");
3297 let bolt12_offer =
3298 self.persister
3299 .fetch_bolt12_offer_by_id(&req.offer)?
3300 .ok_or(PaymentError::generic(format!(
3301 "Bolt12 offer not found: {}",
3302 req.offer
3303 )))?;
3304 let offer = Offer::try_from(bolt12_offer.clone())?;
3306 let cln_node_public_key = offer
3307 .paths()
3308 .iter()
3309 .find_map(|path| match path.introduction_node().clone() {
3310 IntroductionNode::NodeId(node_id) => Some(node_id),
3311 IntroductionNode::DirectedShortChannelId(_, _) => None,
3312 })
3313 .ok_or(PaymentError::generic(format!(
3314 "No BTC CLN node found: {}",
3315 req.offer
3316 )))?;
3317 let invoice_request = utils::bolt12::decode_invoice_request(&req.invoice_request)?;
3318 let payer_amount_sat = invoice_request
3319 .amount_msats()
3320 .map(|msats| msats / 1_000)
3321 .ok_or(PaymentError::amount_missing(
3322 "Invoice request must contain an amount",
3323 ))?;
3324 let (params, maybe_reverse_pair) = tokio::try_join!(
3326 self.swapper.get_bolt12_params(),
3327 self.swapper.get_reverse_swap_pairs()
3328 )?;
3329 let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3330 reverse_pair.limits.within(payer_amount_sat).map_err(|_| {
3331 PaymentError::AmountOutOfRange {
3332 min: reverse_pair.limits.minimal,
3333 max: reverse_pair.limits.maximal,
3334 }
3335 })?;
3336 let fees_sat = reverse_pair.fees.total(payer_amount_sat);
3337 debug!("Creating BOLT12 Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat");
3338
3339 let secp = Secp256k1::new();
3340 let keypair = bolt12_offer.get_keypair()?;
3341 let preimage = Preimage::new();
3342 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3343 let preimage_hash = preimage.sha256.to_byte_array();
3344
3345 let mrh_addr = self.onchain_wallet.next_unused_address().await?;
3347 let mrh_addr_str = mrh_addr.to_string();
3349 let mrh_addr_hash_sig = utils::sign_message_hash(&mrh_addr_str, &keypair)?;
3350
3351 let entropy_source = RandomBytes::new(utils::generate_entropy());
3352 let nonce = Nonce::from_entropy_source(&entropy_source);
3353 let payer_note = invoice_request.payer_note().map(|s| s.to_string());
3354 let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
3355 offer_id: Offer::try_from(bolt12_offer)?.id(),
3356 invoice_request: InvoiceRequestFields {
3357 payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
3358 quantity: invoice_request.quantity(),
3359 payer_note_truncated: payer_note.clone().map(UntrustedString),
3360 human_readable_name: invoice_request.offer_from_hrn().clone(),
3361 },
3362 });
3363 let expanded_key = ExpandedKey::new(keypair.secret_key().secret_bytes());
3364 let payee_tlvs = UnauthenticatedReceiveTlvs {
3365 payment_secret: PaymentSecret(utils::generate_entropy()),
3366 payment_constraints: PaymentConstraints {
3367 max_cltv_expiry: 1_000_000,
3368 htlc_minimum_msat: 1,
3369 },
3370 payment_context,
3371 }
3372 .authenticate(nonce, &expanded_key);
3373
3374 let payment_path = BlindedPaymentPath::one_hop(
3376 cln_node_public_key,
3377 payee_tlvs.clone(),
3378 params.min_cltv as u16,
3379 &entropy_source,
3380 &secp,
3381 )
3382 .map_err(|_| {
3383 PaymentError::generic(
3384 "Failed to create BOLT12 invoice: Error creating blinded payment path",
3385 )
3386 })?;
3387
3388 let invoice = invoice_request
3390 .respond_with_no_std(
3391 vec![payment_path],
3392 PaymentHash(preimage_hash),
3393 SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| {
3394 PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3395 })?,
3396 )?
3397 .build()?
3398 .sign(|unsigned_invoice: &UnsignedBolt12Invoice| {
3399 Ok(secp.sign_schnorr_no_aux_rand(unsigned_invoice.as_ref().as_digest(), &keypair))
3400 })
3401 .map_err(|e| {
3402 PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3403 })?;
3404 let invoice_str = encode_invoice(&invoice).map_err(|e| {
3405 PaymentError::generic(format!("Failed to create BOLT12 invoice: {e:?}"))
3406 })?;
3407 debug!("Created BOLT12 invoice: {invoice_str}");
3408
3409 let claim_keypair = utils::generate_keypair();
3410 let receiver_amount_sat = payer_amount_sat - fees_sat;
3411 let webhook_claim_status =
3412 match receiver_amount_sat > self.config.zero_conf_max_amount_sat() {
3413 true => RevSwapStates::TransactionConfirmed,
3414 false => RevSwapStates::TransactionMempool,
3415 };
3416 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3417 url,
3418 hash_swap_id: Some(true),
3419 status: Some(vec![webhook_claim_status]),
3420 });
3421
3422 let v2_req = CreateReverseRequest {
3423 from: "BTC".to_string(),
3424 to: "L-BTC".to_string(),
3425 invoice: Some(invoice_str.clone()),
3426 invoice_amount: None,
3427 preimage_hash: None,
3428 claim_public_key: claim_keypair.public_key().into(),
3429 description: None,
3430 description_hash: None,
3431 address: Some(mrh_addr_str.clone()),
3432 address_signature: Some(mrh_addr_hash_sig.to_hex()),
3433 referral_id: None,
3434 webhook,
3435 };
3436 let create_response = self.swapper.create_receive_swap(v2_req).await?;
3437
3438 self.persister.insert_or_update_reserved_address(
3440 &mrh_addr_str,
3441 create_response.timeout_block_height,
3442 )?;
3443
3444 let swap_id = create_response.id.clone();
3445 let destination_pubkey = cln_node_public_key.to_hex();
3446 debug!("Created receive swap: {swap_id}");
3447
3448 let create_response_json =
3449 ReceiveSwap::from_boltz_struct_to_json(&create_response, &swap_id, None)?;
3450 let invoice_description = invoice.description().map(|s| s.to_string());
3451
3452 self.persister
3453 .insert_or_update_receive_swap(&ReceiveSwap {
3454 id: swap_id.clone(),
3455 preimage: preimage_str,
3456 create_response_json,
3457 claim_private_key: claim_keypair.display_secret().to_string(),
3458 invoice: invoice_str.clone(),
3459 bolt12_offer: Some(req.offer.clone()),
3460 payment_hash: Some(preimage.sha256.to_string()),
3461 destination_pubkey: Some(destination_pubkey),
3462 timeout_block_height: create_response.timeout_block_height,
3463 description: invoice_description,
3464 payer_note,
3465 payer_amount_sat,
3466 receiver_amount_sat,
3467 pair_fees_json: serde_json::to_string(&reverse_pair).map_err(|e| {
3468 PaymentError::generic(format!("Failed to serialize ReversePair: {e:?}"))
3469 })?,
3470 claim_fees_sat: reverse_pair.fees.claim_estimate(),
3471 lockup_tx_id: None,
3472 claim_address: None,
3473 claim_tx_id: None,
3474 mrh_address: mrh_addr_str,
3475 mrh_tx_id: None,
3476 created_at: utils::now(),
3477 state: PaymentState::Created,
3478 metadata: Default::default(),
3479 })
3480 .map_err(|e| {
3481 error!("Failed to insert or update receive swap: {e:?}");
3482 PaymentError::PersistError
3483 })?;
3484 self.status_stream.track_swap_id(&swap_id)?;
3485 debug!("Finished create BOLT12 invoice");
3486
3487 Ok(CreateBolt12InvoiceResponse {
3488 invoice: invoice_str,
3489 })
3490 }
3491
3492 async fn create_bolt12_offer(
3493 &self,
3494 description: String,
3495 ) -> Result<ReceivePaymentResponse, PaymentError> {
3496 let webhook_url = self.persister.get_webhook_url()?;
3497 let (nodes, maybe_reverse_pair) = tokio::try_join!(
3499 self.swapper.get_nodes(),
3500 self.swapper.get_reverse_swap_pairs()
3501 )?;
3502 let cln_node = nodes
3503 .get_btc_cln_node()
3504 .ok_or(PaymentError::generic("No BTC CLN node found"))?;
3505 debug!("Creating BOLT12 offer for description: {description}");
3506 let reverse_pair = maybe_reverse_pair.ok_or(PaymentError::PairsNotFound)?;
3507 let min_amount_sat = reverse_pair.limits.minimal;
3508 let keypair = utils::generate_keypair();
3509 let entropy_source = RandomBytes::new(utils::generate_entropy());
3510 let secp = Secp256k1::new();
3511 let message_context = MessageContext::Offers(OffersContext::InvoiceRequest {
3512 nonce: Nonce::from_entropy_source(&entropy_source),
3513 });
3514
3515 let offer = OfferBuilder::new(keypair.public_key())
3517 .chain(self.config.network.into())
3518 .amount_msats(min_amount_sat * 1_000)
3519 .description(description.clone())
3520 .path(
3521 BlindedMessagePath::one_hop(
3522 cln_node.public_key,
3523 message_context,
3524 &entropy_source,
3525 &secp,
3526 )
3527 .map_err(|_| {
3528 PaymentError::generic(
3529 "Error creating Bolt12 Offer: Could not create a one-hop blinded path",
3530 )
3531 })?,
3532 )
3533 .build()?;
3534 let offer_str = utils::bolt12::encode_offer(&offer)?;
3535 info!("Created BOLT12 offer: {offer_str}");
3536 self.swapper
3537 .create_bolt12_offer(CreateBolt12OfferRequest {
3538 offer: offer_str.clone(),
3539 url: webhook_url.clone(),
3540 })
3541 .await?;
3542 self.persister.insert_or_update_bolt12_offer(&Bolt12Offer {
3544 id: offer_str.clone(),
3545 description,
3546 private_key: keypair.display_secret().to_string(),
3547 webhook_url,
3548 created_at: utils::now(),
3549 })?;
3550 let subscribe_hash_sig = utils::sign_message_hash("SUBSCRIBE", &keypair)?;
3552 self.status_stream
3553 .track_offer(&offer_str, &subscribe_hash_sig.to_hex())?;
3554
3555 Ok(ReceivePaymentResponse {
3556 destination: offer_str,
3557 liquid_expiration_blockheight: None,
3558 bitcoin_expiration_blockheight: None,
3559 })
3560 }
3561
3562 async fn create_receive_chain_swap(
3563 &self,
3564 user_lockup_amount_sat: Option<u64>,
3565 fees_sat: u64,
3566 ) -> Result<ChainSwap, PaymentError> {
3567 let pair = self
3568 .get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)
3569 .await?;
3570 let claim_fees_sat = pair.fees.claim_estimate();
3571 let server_fees_sat = pair.fees.server();
3572 let service_fees_sat = user_lockup_amount_sat
3574 .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat))
3575 .unwrap_or_default();
3576
3577 ensure_sdk!(
3578 fees_sat == service_fees_sat + claim_fees_sat + server_fees_sat,
3579 PaymentError::InvalidOrExpiredFees
3580 );
3581
3582 let preimage = Preimage::new();
3583 let preimage_str = preimage.to_string().ok_or(PaymentError::InvalidPreimage)?;
3584
3585 let claim_keypair = utils::generate_keypair();
3586 let claim_public_key = boltz_client::PublicKey {
3587 compressed: true,
3588 inner: claim_keypair.public_key(),
3589 };
3590 let refund_keypair = utils::generate_keypair();
3591 let refund_public_key = boltz_client::PublicKey {
3592 compressed: true,
3593 inner: refund_keypair.public_key(),
3594 };
3595 let webhook = self.persister.get_webhook_url()?.map(|url| Webhook {
3596 url,
3597 hash_swap_id: Some(true),
3598 status: Some(vec![
3599 ChainSwapStates::TransactionFailed,
3600 ChainSwapStates::TransactionLockupFailed,
3601 ChainSwapStates::TransactionServerConfirmed,
3602 ]),
3603 });
3604 let create_response = self
3605 .swapper
3606 .create_chain_swap(CreateChainRequest {
3607 from: "BTC".to_string(),
3608 to: "L-BTC".to_string(),
3609 preimage_hash: preimage.sha256,
3610 claim_public_key: Some(claim_public_key),
3611 refund_public_key: Some(refund_public_key),
3612 user_lock_amount: user_lockup_amount_sat,
3613 server_lock_amount: None,
3614 pair_hash: Some(pair.hash.clone()),
3615 referral_id: None,
3616 webhook,
3617 })
3618 .await?;
3619
3620 let swap_id = create_response.id.clone();
3621 let create_response_json =
3622 ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?;
3623
3624 let accept_zero_conf = user_lockup_amount_sat
3625 .map(|user_lockup_amount_sat| user_lockup_amount_sat <= pair.limits.maximal_zero_conf)
3626 .unwrap_or(false);
3627 let receiver_amount_sat = user_lockup_amount_sat
3628 .map(|user_lockup_amount_sat| user_lockup_amount_sat - fees_sat)
3629 .unwrap_or(0);
3630
3631 let swap = ChainSwap {
3632 id: swap_id.clone(),
3633 direction: Direction::Incoming,
3634 claim_address: None,
3635 lockup_address: create_response.lockup_details.lockup_address,
3636 refund_address: None,
3637 timeout_block_height: create_response.lockup_details.timeout_block_height,
3638 claim_timeout_block_height: create_response.claim_details.timeout_block_height,
3639 preimage: preimage_str,
3640 description: Some("Bitcoin transfer".to_string()),
3641 payer_amount_sat: user_lockup_amount_sat.unwrap_or(0),
3642 actual_payer_amount_sat: None,
3643 receiver_amount_sat,
3644 accepted_receiver_amount_sat: None,
3645 claim_fees_sat,
3646 pair_fees_json: serde_json::to_string(&pair).map_err(|e| {
3647 PaymentError::generic(format!("Failed to serialize incoming ChainPair: {e:?}"))
3648 })?,
3649 accept_zero_conf,
3650 create_response_json,
3651 claim_private_key: claim_keypair.display_secret().to_string(),
3652 refund_private_key: refund_keypair.display_secret().to_string(),
3653 server_lockup_tx_id: None,
3654 user_lockup_tx_id: None,
3655 claim_tx_id: None,
3656 refund_tx_id: None,
3657 created_at: utils::now(),
3658 state: PaymentState::Created,
3659 auto_accepted_fees: false,
3660 user_lockup_spent: false,
3661 metadata: Default::default(),
3662 };
3663 self.persister.insert_or_update_chain_swap(&swap)?;
3664 self.status_stream.track_swap_id(&swap.id)?;
3665 Ok(swap)
3666 }
3667
3668 async fn receive_onchain(
3673 &self,
3674 user_lockup_amount_sat: Option<u64>,
3675 fees_sat: u64,
3676 ) -> Result<ReceivePaymentResponse, PaymentError> {
3677 self.ensure_is_started().await?;
3678
3679 let swap = self
3680 .create_receive_chain_swap(user_lockup_amount_sat, fees_sat)
3681 .await?;
3682 let create_response = swap.get_boltz_create_response()?;
3683 let address = create_response.lockup_details.lockup_address;
3684
3685 let amount = create_response.lockup_details.amount as f64 / 100_000_000.0;
3686 let bip21 = create_response.lockup_details.bip21.unwrap_or(format!(
3687 "bitcoin:{address}?amount={amount}&label=Send%20to%20L-BTC%20address"
3688 ));
3689
3690 Ok(ReceivePaymentResponse {
3691 destination: bip21,
3692 liquid_expiration_blockheight: Some(swap.claim_timeout_block_height),
3693 bitcoin_expiration_blockheight: Some(swap.timeout_block_height),
3694 })
3695 }
3696
3697 pub async fn list_refundables(&self) -> SdkResult<Vec<RefundableSwap>> {
3700 let chain_swaps = self.persister.list_refundable_chain_swaps()?;
3701
3702 let mut chain_swaps_with_scripts = vec![];
3703 for swap in &chain_swaps {
3704 let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
3705 chain_swaps_with_scripts.push((swap, script_pubkey));
3706 }
3707
3708 let lockup_scripts: Vec<&boltz_client::bitcoin::Script> = chain_swaps_with_scripts
3709 .iter()
3710 .map(|(_, script_pubkey)| script_pubkey.as_script())
3711 .collect();
3712 let scripts_utxos = self
3713 .bitcoin_chain_service
3714 .get_scripts_utxos(&lockup_scripts)
3715 .await?;
3716
3717 let mut script_to_utxos_map = std::collections::HashMap::new();
3718 for script_utxos in scripts_utxos {
3719 if let Some(first_utxo) = script_utxos.first() {
3720 if let Some((_, txo)) = first_utxo.as_bitcoin() {
3721 let script_pubkey: boltz_client::bitcoin::ScriptBuf = txo.script_pubkey.clone();
3722 script_to_utxos_map.insert(script_pubkey, script_utxos);
3723 }
3724 }
3725 }
3726
3727 let mut refundables = vec![];
3728
3729 for (chain_swap, script_pubkey) in chain_swaps_with_scripts {
3730 if let Some(script_utxos) = script_to_utxos_map.get(&script_pubkey) {
3731 let swap_id = &chain_swap.id;
3732 let amount_sat: u64 = script_utxos
3733 .iter()
3734 .filter_map(|utxo| utxo.as_bitcoin().cloned())
3735 .map(|(_, txo)| txo.value.to_sat())
3736 .sum();
3737 info!("Incoming Chain Swap {swap_id} is refundable with {amount_sat} sats");
3738
3739 refundables.push(chain_swap.to_refundable(amount_sat));
3740 }
3741 }
3742
3743 Ok(refundables)
3744 }
3745
3746 pub async fn prepare_refund(
3755 &self,
3756 req: &PrepareRefundRequest,
3757 ) -> SdkResult<PrepareRefundResponse> {
3758 let refund_address = self
3759 .validate_bitcoin_address(&req.refund_address)
3760 .await
3761 .map_err(|e| SdkError::Generic {
3762 err: format!("Failed to validate refund address: {e}"),
3763 })?;
3764
3765 let (tx_vsize, tx_fee_sat, refund_tx_id) = self
3766 .chain_swap_handler
3767 .prepare_refund(
3768 &req.swap_address,
3769 &refund_address,
3770 req.fee_rate_sat_per_vbyte,
3771 )
3772 .await?;
3773 Ok(PrepareRefundResponse {
3774 tx_vsize,
3775 tx_fee_sat,
3776 last_refund_tx_id: refund_tx_id,
3777 })
3778 }
3779
3780 pub async fn refund(&self, req: &RefundRequest) -> Result<RefundResponse, PaymentError> {
3789 let refund_address = self
3790 .validate_bitcoin_address(&req.refund_address)
3791 .await
3792 .map_err(|e| SdkError::Generic {
3793 err: format!("Failed to validate refund address: {e}"),
3794 })?;
3795
3796 let refund_tx_id = self
3797 .chain_swap_handler
3798 .refund_incoming_swap(
3799 &req.swap_address,
3800 &refund_address,
3801 req.fee_rate_sat_per_vbyte,
3802 true,
3803 )
3804 .or_else(|e| {
3805 warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}");
3806 self.chain_swap_handler.refund_incoming_swap(
3807 &req.swap_address,
3808 &refund_address,
3809 req.fee_rate_sat_per_vbyte,
3810 false,
3811 )
3812 })
3813 .await?;
3814
3815 Ok(RefundResponse { refund_tx_id })
3816 }
3817
3818 pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> {
3826 let t0 = Instant::now();
3827 let mut rescannable_swaps: Vec<Swap> = self
3828 .persister
3829 .list_chain_swaps()?
3830 .into_iter()
3831 .map(Into::into)
3832 .collect();
3833 self.recoverer
3834 .recover_from_onchain(&mut rescannable_swaps, None)
3835 .await?;
3836 let scanned_len = rescannable_swaps.len();
3837 for swap in rescannable_swaps {
3838 let swap_id = &swap.id();
3839 if let Swap::Chain(chain_swap) = swap {
3840 if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
3841 error!("Error persisting rescanned Chain Swap {swap_id}: {e}");
3842 }
3843 }
3844 }
3845 info!(
3846 "Rescanned {} chain swaps in {} seconds",
3847 scanned_len,
3848 t0.elapsed().as_millis()
3849 );
3850 Ok(())
3851 }
3852
3853 fn validate_buy_bitcoin(&self, amount_sat: u64) -> Result<(), PaymentError> {
3854 ensure_sdk!(
3855 self.config.network == LiquidNetwork::Mainnet,
3856 PaymentError::invalid_network("Can only buy bitcoin on Mainnet")
3857 );
3858 ensure_sdk!(
3860 amount_sat.is_multiple_of(1_000),
3861 PaymentError::generic("Can only buy sat amounts that are multiples of 1000")
3862 );
3863 Ok(())
3864 }
3865
3866 pub async fn prepare_buy_bitcoin(
3874 &self,
3875 req: &PrepareBuyBitcoinRequest,
3876 ) -> Result<PrepareBuyBitcoinResponse, PaymentError> {
3877 self.validate_buy_bitcoin(req.amount_sat)?;
3878
3879 let res = self
3880 .prepare_receive_payment(&PrepareReceiveRequest {
3881 payment_method: PaymentMethod::BitcoinAddress,
3882 amount: Some(ReceiveAmount::Bitcoin {
3883 payer_amount_sat: req.amount_sat,
3884 }),
3885 })
3886 .await?;
3887
3888 let Some(ReceiveAmount::Bitcoin {
3889 payer_amount_sat: amount_sat,
3890 }) = res.amount
3891 else {
3892 return Err(PaymentError::Generic {
3893 err: format!(
3894 "Error preparing receive payment, got amount: {:?}",
3895 res.amount
3896 ),
3897 });
3898 };
3899
3900 Ok(PrepareBuyBitcoinResponse {
3901 provider: req.provider,
3902 amount_sat,
3903 fees_sat: res.fees_sat,
3904 })
3905 }
3906
3907 pub async fn buy_bitcoin(&self, req: &BuyBitcoinRequest) -> Result<String, PaymentError> {
3915 self.validate_buy_bitcoin(req.prepare_response.amount_sat)?;
3916
3917 let swap = self
3918 .create_receive_chain_swap(
3919 Some(req.prepare_response.amount_sat),
3920 req.prepare_response.fees_sat,
3921 )
3922 .await?;
3923
3924 Ok(self
3925 .buy_bitcoin_service
3926 .buy_bitcoin(
3927 req.prepare_response.provider,
3928 &swap,
3929 req.redirect_url.clone(),
3930 )
3931 .await?)
3932 }
3933
3934 pub(crate) async fn get_monitored_swaps_list(
3938 &self,
3939 only_receive_swaps: bool,
3940 include_expired_incoming_chain_swaps: bool,
3941 chain_tips: ChainTips,
3942 ) -> Result<Vec<Swap>> {
3943 let receive_swaps = self
3944 .persister
3945 .list_recoverable_receive_swaps()?
3946 .into_iter()
3947 .map(Into::into)
3948 .collect();
3949
3950 if only_receive_swaps {
3951 return Ok(receive_swaps);
3952 }
3953
3954 let send_swaps = self
3955 .persister
3956 .list_recoverable_send_swaps()?
3957 .into_iter()
3958 .map(Into::into)
3959 .collect();
3960
3961 let Some(bitcoin_tip) = chain_tips.bitcoin_tip else {
3962 return Ok([receive_swaps, send_swaps].concat());
3963 };
3964
3965 let final_swap_states: [PaymentState; 2] = [PaymentState::Complete, PaymentState::Failed];
3966
3967 let chain_swaps: Vec<Swap> = self
3968 .persister
3969 .list_chain_swaps()?
3970 .into_iter()
3971 .filter(|swap| match swap.direction {
3972 Direction::Incoming => {
3973 if include_expired_incoming_chain_swaps {
3974 bitcoin_tip
3975 <= swap.timeout_block_height
3976 + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS
3977 && chain_tips.liquid_tip
3978 <= swap.claim_timeout_block_height
3979 + CHAIN_SWAP_MONITORING_PERIOD_LIQUID_BLOCKS
3980 } else {
3981 bitcoin_tip <= swap.timeout_block_height
3982 && chain_tips.liquid_tip <= swap.claim_timeout_block_height
3983 }
3984 }
3985 Direction::Outgoing => {
3986 !final_swap_states.contains(&swap.state)
3987 && chain_tips.liquid_tip <= swap.timeout_block_height
3988 && bitcoin_tip <= swap.claim_timeout_block_height
3989 }
3990 })
3991 .map(Into::into)
3992 .collect();
3993
3994 Ok([receive_swaps, send_swaps, chain_swaps].concat())
3995 }
3996
3997 async fn sync_payments_with_chain_data(
4000 &self,
4001 mut recoverable_swaps: Vec<Swap>,
4002 chain_tips: ChainTips,
4003 ) -> Result<()> {
4004 debug!("LiquidSdk::sync_payments_with_chain_data: start");
4005 debug!(
4006 "LiquidSdk::sync_payments_with_chain_data: called with {} recoverable swaps",
4007 recoverable_swaps.len()
4008 );
4009 let mut wallet_tx_map = self
4010 .recoverer
4011 .recover_from_onchain(&mut recoverable_swaps, Some(chain_tips))
4012 .await?;
4013
4014 let all_wallet_tx_ids: HashSet<String> =
4015 wallet_tx_map.keys().map(|txid| txid.to_string()).collect();
4016
4017 for swap in recoverable_swaps {
4018 let swap_id = &swap.id();
4019
4020 match swap {
4022 Swap::Receive(receive_swap) => {
4023 let history_updates = vec![&receive_swap.claim_tx_id, &receive_swap.mrh_tx_id];
4024 for tx_id in history_updates
4025 .into_iter()
4026 .flatten()
4027 .collect::<Vec<&String>>()
4028 {
4029 if let Some(tx) =
4030 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4031 {
4032 self.persister
4033 .insert_or_update_payment_with_wallet_tx(&tx)?;
4034 }
4035 }
4036 if let Err(e) = self.receive_swap_handler.update_swap(receive_swap) {
4037 error!("Error persisting recovered receive swap {swap_id}: {e}");
4038 }
4039 }
4040 Swap::Send(send_swap) => {
4041 let history_updates = vec![&send_swap.lockup_tx_id, &send_swap.refund_tx_id];
4042 for tx_id in history_updates
4043 .into_iter()
4044 .flatten()
4045 .collect::<Vec<&String>>()
4046 {
4047 if let Some(tx) =
4048 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4049 {
4050 self.persister
4051 .insert_or_update_payment_with_wallet_tx(&tx)?;
4052 }
4053 }
4054 if let Err(e) = self.send_swap_handler.update_swap(send_swap) {
4055 error!("Error persisting recovered send swap {swap_id}: {e}");
4056 }
4057 }
4058 Swap::Chain(chain_swap) => {
4059 let history_updates = match chain_swap.direction {
4060 Direction::Incoming => vec![&chain_swap.claim_tx_id],
4061 Direction::Outgoing => {
4062 vec![&chain_swap.user_lockup_tx_id, &chain_swap.refund_tx_id]
4063 }
4064 };
4065 for tx_id in history_updates
4066 .into_iter()
4067 .flatten()
4068 .collect::<Vec<&String>>()
4069 {
4070 if let Some(tx) =
4071 wallet_tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?)
4072 {
4073 self.persister
4074 .insert_or_update_payment_with_wallet_tx(&tx)?;
4075 }
4076 }
4077 if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) {
4078 error!("Error persisting recovered Chain Swap {swap_id}: {e}");
4079 }
4080 }
4081 };
4082 }
4083
4084 let non_swap_wallet_tx_map = wallet_tx_map;
4085
4086 let payments = self
4087 .persister
4088 .get_payments_by_tx_id(&ListPaymentsRequest::default())?;
4089
4090 let unconfirmed_payment_txs_data = self.persister.list_unconfirmed_payment_txs_data()?;
4092 let unconfirmed_txs_by_id: HashMap<String, PaymentTxData> = unconfirmed_payment_txs_data
4093 .into_iter()
4094 .map(|tx| (tx.tx_id.clone(), tx))
4095 .collect::<HashMap<String, PaymentTxData>>();
4096
4097 debug!(
4098 "Found {} unconfirmed payment txs",
4099 unconfirmed_txs_by_id.len()
4100 );
4101 for tx in non_swap_wallet_tx_map.values() {
4102 let tx_id = tx.txid.to_string();
4103 let maybe_payment = payments.get(&tx_id);
4104 let mut updated = false;
4105 match maybe_payment {
4106 None
4108 | Some(Payment {
4109 details: PaymentDetails::Liquid { .. },
4110 ..
4111 }) => {
4112 let updated_needed = maybe_payment
4113 .is_none_or(|payment| payment.status == Pending && tx.height.is_some());
4114 if updated_needed {
4115 self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
4118 self.emit_payment_updated(Some(tx_id.clone())).await?;
4119 updated = true
4120 }
4121 }
4122
4123 _ => {}
4124 }
4125 if !updated && unconfirmed_txs_by_id.contains_key(&tx_id) && tx.height.is_some() {
4126 self.persister.insert_or_update_payment_with_wallet_tx(tx)?;
4128 }
4129 }
4130
4131 let unknown_unconfirmed_txs: Vec<_> = unconfirmed_txs_by_id
4132 .iter()
4133 .filter(|(txid, _)| !all_wallet_tx_ids.contains(*txid))
4134 .map(|(_, tx)| tx)
4135 .collect();
4136
4137 debug!(
4138 "Found {} unknown unconfirmed txs",
4139 unknown_unconfirmed_txs.len()
4140 );
4141 for unknown_unconfirmed_tx in unknown_unconfirmed_txs {
4142 if unknown_unconfirmed_tx.timestamp.is_some_and(|t| {
4143 (utils::now().saturating_sub(t)) > NETWORK_PROPAGATION_GRACE_PERIOD.as_secs() as u32
4144 }) {
4145 self.persister
4146 .delete_payment_tx_data(&unknown_unconfirmed_tx.tx_id)?;
4147 info!(
4148 "Found an unknown unconfirmed tx and deleted it. Txid: {}",
4149 unknown_unconfirmed_tx.tx_id
4150 );
4151 } else {
4152 debug!(
4153 "Found an unknown unconfirmed tx that was inserted at {:?}. \
4154 Keeping it to allow propagation through the network. Txid: {}",
4155 unknown_unconfirmed_tx.timestamp, unknown_unconfirmed_tx.tx_id
4156 )
4157 }
4158 }
4159
4160 self.update_wallet_info().await?;
4161 debug!("LiquidSdk::sync_payments_with_chain_data: end");
4162 Ok(())
4163 }
4164
4165 async fn update_wallet_info(&self) -> Result<()> {
4166 let asset_metadata: HashMap<String, AssetMetadata> = self
4167 .persister
4168 .list_asset_metadata()?
4169 .into_iter()
4170 .map(|am| (am.asset_id.clone(), am))
4171 .collect();
4172 let transactions = self.onchain_wallet.transactions().await?;
4173 let tx_ids = transactions
4174 .iter()
4175 .map(|tx| tx.txid.to_string())
4176 .collect::<Vec<_>>();
4177 let asset_balances = transactions
4178 .into_iter()
4179 .fold(BTreeMap::<AssetId, i64>::new(), |mut acc, tx| {
4180 tx.balance.into_iter().for_each(|(asset_id, balance)| {
4181 if tx.height.is_some() || balance < 0 {
4183 *acc.entry(asset_id).or_default() += balance;
4184 }
4185 });
4186 acc
4187 })
4188 .into_iter()
4189 .map(|(asset_id, balance)| {
4190 let asset_id = asset_id.to_hex();
4191 let balance_sat = balance.unsigned_abs();
4192 let maybe_asset_metadata = asset_metadata.get(&asset_id);
4193 AssetBalance {
4194 asset_id,
4195 balance_sat,
4196 name: maybe_asset_metadata.map(|am| am.name.clone()),
4197 ticker: maybe_asset_metadata.map(|am| am.ticker.clone()),
4198 balance: maybe_asset_metadata.map(|am| am.amount_from_sat(balance_sat)),
4199 }
4200 })
4201 .collect::<Vec<AssetBalance>>();
4202 let mut balance_sat = asset_balances
4203 .clone()
4204 .into_iter()
4205 .find(|ab| ab.asset_id.eq(&self.config.lbtc_asset_id()))
4206 .map_or(0, |ab| ab.balance_sat);
4207
4208 let mut pending_send_sat = 0;
4209 let mut pending_receive_sat = 0;
4210 let payments = self.persister.get_payments(&ListPaymentsRequest {
4211 states: Some(vec![
4212 PaymentState::Pending,
4213 PaymentState::RefundPending,
4214 PaymentState::WaitingFeeAcceptance,
4215 ]),
4216 ..Default::default()
4217 })?;
4218
4219 for payment in payments {
4220 let is_lbtc_asset_id = payment.details.is_lbtc_asset_id(self.config.network);
4221 match payment.payment_type {
4222 PaymentType::Send => match payment.details.get_refund_tx_amount_sat() {
4223 Some(refund_tx_amount_sat) => pending_receive_sat += refund_tx_amount_sat,
4224 None => {
4225 let total_sat = if is_lbtc_asset_id {
4226 payment.amount_sat + payment.fees_sat
4227 } else {
4228 payment.fees_sat
4229 };
4230 if let Some(tx_id) = payment.tx_id {
4231 if !tx_ids.contains(&tx_id) {
4232 debug!("Deducting {total_sat} sats from balance");
4233 balance_sat = balance_sat.saturating_sub(total_sat);
4234 }
4235 }
4236 pending_send_sat += total_sat
4237 }
4238 },
4239 PaymentType::Receive => {
4240 if is_lbtc_asset_id && payment.status != RefundPending {
4241 pending_receive_sat += payment.amount_sat;
4242 }
4243 }
4244 }
4245 }
4246
4247 debug!("Onchain wallet balance: {balance_sat} sats");
4248 let info_response = WalletInfo {
4249 balance_sat,
4250 pending_send_sat,
4251 pending_receive_sat,
4252 fingerprint: self.onchain_wallet.fingerprint()?,
4253 pubkey: self.onchain_wallet.pubkey()?,
4254 asset_balances,
4255 };
4256 self.persister.set_wallet_info(&info_response)
4257 }
4258
4259 pub async fn list_payments(
4262 &self,
4263 req: &ListPaymentsRequest,
4264 ) -> Result<Vec<Payment>, PaymentError> {
4265 self.ensure_is_started().await?;
4266
4267 Ok(self.persister.get_payments(req)?)
4268 }
4269
4270 pub async fn get_payment(
4281 &self,
4282 req: &GetPaymentRequest,
4283 ) -> Result<Option<Payment>, PaymentError> {
4284 self.ensure_is_started().await?;
4285
4286 Ok(self.persister.get_payment_by_request(req)?)
4287 }
4288
4289 pub async fn fetch_payment_proposed_fees(
4294 &self,
4295 req: &FetchPaymentProposedFeesRequest,
4296 ) -> SdkResult<FetchPaymentProposedFeesResponse> {
4297 let chain_swap =
4298 self.persister
4299 .fetch_chain_swap_by_id(&req.swap_id)?
4300 .ok_or(SdkError::Generic {
4301 err: format!("Could not find Swap {}", req.swap_id),
4302 })?;
4303
4304 ensure_sdk!(
4305 chain_swap.state == WaitingFeeAcceptance,
4306 SdkError::Generic {
4307 err: "Payment is not WaitingFeeAcceptance".to_string()
4308 }
4309 );
4310
4311 let server_lockup_quote = self
4312 .swapper
4313 .get_zero_amount_chain_swap_quote(&req.swap_id)
4314 .await?;
4315
4316 let actual_payer_amount_sat =
4317 chain_swap
4318 .actual_payer_amount_sat
4319 .ok_or(SdkError::Generic {
4320 err: "No actual payer amount found when state is WaitingFeeAcceptance"
4321 .to_string(),
4322 })?;
4323 let fees_sat =
4324 actual_payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat;
4325
4326 Ok(FetchPaymentProposedFeesResponse {
4327 swap_id: req.swap_id.clone(),
4328 fees_sat,
4329 payer_amount_sat: actual_payer_amount_sat,
4330 receiver_amount_sat: actual_payer_amount_sat - fees_sat,
4331 })
4332 }
4333
4334 pub async fn accept_payment_proposed_fees(
4338 &self,
4339 req: &AcceptPaymentProposedFeesRequest,
4340 ) -> Result<(), PaymentError> {
4341 let FetchPaymentProposedFeesResponse {
4342 swap_id,
4343 fees_sat,
4344 payer_amount_sat,
4345 ..
4346 } = req.clone().response;
4347
4348 let chain_swap =
4349 self.persister
4350 .fetch_chain_swap_by_id(&swap_id)?
4351 .ok_or(SdkError::Generic {
4352 err: format!("Could not find Swap {swap_id}"),
4353 })?;
4354
4355 ensure_sdk!(
4356 chain_swap.state == WaitingFeeAcceptance,
4357 PaymentError::Generic {
4358 err: "Payment is not WaitingFeeAcceptance".to_string()
4359 }
4360 );
4361
4362 let server_lockup_quote = self
4363 .swapper
4364 .get_zero_amount_chain_swap_quote(&swap_id)
4365 .await?;
4366
4367 ensure_sdk!(
4368 fees_sat == payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat,
4369 PaymentError::InvalidOrExpiredFees
4370 );
4371
4372 self.persister
4373 .update_accepted_receiver_amount(&swap_id, Some(payer_amount_sat - fees_sat))?;
4374 self.swapper
4375 .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())
4376 .inspect_err(|e| {
4377 error!("Failed to accept zero-amount swap {swap_id} quote: {e} - trying to erase the accepted receiver amount...");
4378 let _ = self
4379 .persister
4380 .update_accepted_receiver_amount(&swap_id, None);
4381 }).await?;
4382 self.chain_swap_handler.update_swap_info(&ChainSwapUpdate {
4383 swap_id,
4384 to_state: Pending,
4385 ..Default::default()
4386 })
4387 }
4388
4389 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4391 pub fn empty_wallet_cache(&self) -> Result<()> {
4392 let mut path = PathBuf::from(self.config.working_dir.clone());
4393 path.push(Into::<lwk_wollet::ElementsNetwork>::into(self.config.network).as_str());
4394 path.push("enc_cache");
4395
4396 std::fs::remove_dir_all(&path)?;
4397 std::fs::create_dir_all(path)?;
4398
4399 Ok(())
4400 }
4401
4402 pub async fn sync(&self, partial_sync: bool) -> SdkResult<()> {
4404 let blockchain_info = self.get_info().await?.blockchain_info;
4405 let sync_context = self
4406 .get_sync_context(GetSyncContextRequest {
4407 partial_sync: Some(partial_sync),
4408 last_liquid_tip: blockchain_info.liquid_tip,
4409 last_bitcoin_tip: blockchain_info.bitcoin_tip,
4410 })
4411 .await?;
4412
4413 self.sync_inner(
4414 sync_context.recoverable_swaps,
4415 ChainTips {
4416 liquid_tip: sync_context.maybe_liquid_tip.ok_or(SdkError::Generic {
4417 err: "Liquid tip not available".to_string(),
4418 })?,
4419 bitcoin_tip: sync_context.maybe_bitcoin_tip,
4420 },
4421 )
4422 .await
4423 }
4424
4425 async fn get_sync_context(&self, req: GetSyncContextRequest) -> SdkResult<SyncContext> {
4441 let t0 = Instant::now();
4443 let liquid_tip = match self.liquid_chain_service.tip().await {
4444 Ok(tip) => Some(tip),
4445 Err(e) => {
4446 error!("Failed to fetch liquid tip: {e}");
4447 None
4448 }
4449 };
4450 let duration_ms = Instant::now().duration_since(t0).as_millis();
4451 if liquid_tip.is_some() {
4452 info!("Fetched liquid tip in ({duration_ms} ms)");
4453 }
4454
4455 let is_new_liquid_block = liquid_tip.is_some_and(|lt| lt > req.last_liquid_tip);
4456
4457 let mut recoverable_swaps = self
4459 .get_monitored_swaps_list(
4460 req.partial_sync.unwrap_or(false),
4461 true,
4462 ChainTips {
4463 liquid_tip: liquid_tip.unwrap_or(req.last_liquid_tip),
4464 bitcoin_tip: Some(req.last_bitcoin_tip),
4465 },
4466 )
4467 .await?;
4468
4469 let bitcoin_tip = if !is_new_liquid_block {
4472 debug!("No new liquid block, skipping bitcoin tip fetch");
4473 None
4474 } else if recoverable_swaps
4475 .iter()
4476 .any(|s| matches!(s, Swap::Chain(_)))
4477 .not()
4478 {
4479 debug!("No chain swaps being monitored, skipping bitcoin tip fetch");
4480 None
4481 } else {
4482 let t0 = Instant::now();
4484 let bitcoin_tip = match self.bitcoin_chain_service.tip().await {
4485 Ok(tip) => Some(tip),
4486 Err(e) => {
4487 error!("Failed to fetch bitcoin tip: {e}");
4488 None
4489 }
4490 };
4491 let duration_ms = Instant::now().duration_since(t0).as_millis();
4492 if bitcoin_tip.is_some() {
4493 info!("Fetched bitcoin tip in ({duration_ms} ms)");
4494 } else {
4495 recoverable_swaps.retain(|s| !matches!(s, Swap::Chain(_)));
4496 }
4497 bitcoin_tip
4498 };
4499
4500 let is_new_bitcoin_block = bitcoin_tip.is_some_and(|bt| bt > req.last_bitcoin_tip);
4501
4502 if let Some(liquid_tip) = liquid_tip {
4505 if req.partial_sync.is_none() {
4506 let only_receive_swaps = !is_new_liquid_block && !is_new_bitcoin_block;
4507 let include_expired_incoming_chain_swaps = is_new_bitcoin_block;
4508
4509 recoverable_swaps = self
4510 .get_monitored_swaps_list(
4511 only_receive_swaps,
4512 include_expired_incoming_chain_swaps,
4513 ChainTips {
4514 liquid_tip,
4515 bitcoin_tip,
4516 },
4517 )
4518 .await?;
4519 }
4520 } else {
4521 recoverable_swaps = Vec::new();
4522 }
4523
4524 Ok(SyncContext {
4525 maybe_liquid_tip: liquid_tip,
4526 maybe_bitcoin_tip: bitcoin_tip,
4527 recoverable_swaps,
4528 is_new_liquid_block,
4529 is_new_bitcoin_block,
4530 })
4531 }
4532
4533 async fn sync_inner(
4534 &self,
4535 recoverable_swaps: Vec<Swap>,
4536 chain_tips: ChainTips,
4537 ) -> SdkResult<()> {
4538 debug!(
4539 "LiquidSdk::sync_inner called with {} recoverable swaps",
4540 recoverable_swaps.len()
4541 );
4542 self.ensure_is_started().await?;
4543
4544 let t0 = Instant::now();
4545
4546 self.onchain_wallet.full_scan().await.map_err(|err| {
4547 error!("Failed to scan wallet: {err:?}");
4548 SdkError::generic(err.to_string())
4549 })?;
4550
4551 let is_first_sync = !self
4552 .persister
4553 .get_is_first_sync_complete()?
4554 .unwrap_or(false);
4555 match is_first_sync {
4556 true => {
4557 self.event_manager.pause_notifications();
4558 self.sync_payments_with_chain_data(recoverable_swaps, chain_tips)
4559 .await?;
4560 self.event_manager.resume_notifications();
4561 self.persister.set_is_first_sync_complete(true)?;
4562 }
4563 false => {
4564 self.sync_payments_with_chain_data(recoverable_swaps, chain_tips)
4565 .await?;
4566 }
4567 }
4568 let duration_ms = Instant::now().duration_since(t0).as_millis();
4569 info!("Synchronized with mempool and onchain data ({duration_ms} ms)");
4570
4571 self.notify_event_listeners(SdkEvent::Synced).await;
4572 Ok(())
4573 }
4574
4575 pub fn backup(&self, req: BackupRequest) -> Result<()> {
4582 let backup_path = req
4583 .backup_path
4584 .map(PathBuf::from)
4585 .unwrap_or(self.persister.get_default_backup_path());
4586 self.persister.backup(backup_path)
4587 }
4588
4589 pub fn restore(&self, req: RestoreRequest) -> Result<()> {
4596 let backup_path = req
4597 .backup_path
4598 .map(PathBuf::from)
4599 .unwrap_or(self.persister.get_default_backup_path());
4600 ensure_sdk!(
4601 backup_path.exists(),
4602 SdkError::generic("Backup file does not exist").into()
4603 );
4604 self.persister.restore_from_backup(backup_path)
4605 }
4606
4607 pub async fn prepare_lnurl_pay(
4640 &self,
4641 req: PrepareLnUrlPayRequest,
4642 ) -> Result<PrepareLnUrlPayResponse, LnUrlPayError> {
4643 let amount_msat = match req.amount {
4644 PayAmount::Drain => {
4645 let get_info_res = self
4646 .get_info()
4647 .await
4648 .map_err(|e| LnUrlPayError::Generic { err: e.to_string() })?;
4649 ensure_sdk!(
4650 get_info_res.wallet_info.pending_receive_sat == 0
4651 && get_info_res.wallet_info.pending_send_sat == 0,
4652 LnUrlPayError::Generic {
4653 err: "Cannot drain while there are pending payments".to_string(),
4654 }
4655 );
4656 let lbtc_pair = self
4657 .swapper
4658 .get_submarine_pairs()
4659 .await?
4660 .ok_or(PaymentError::PairsNotFound)?;
4661 let drain_fees_sat = self.estimate_drain_tx_fee(None, None).await?;
4662 let drain_amount_sat = get_info_res.wallet_info.balance_sat - drain_fees_sat;
4663 let dummy_fees_sat = lbtc_pair.fees.total(drain_amount_sat);
4665 let dummy_amount_sat = drain_amount_sat - dummy_fees_sat;
4666 let receiver_amount_sat = utils::increment_receiver_amount_up_to_drain_amount(
4667 dummy_amount_sat,
4668 &lbtc_pair,
4669 drain_amount_sat,
4670 );
4671 lbtc_pair
4672 .limits
4673 .within(receiver_amount_sat)
4674 .map_err(|e| LnUrlPayError::Generic { err: e.message() })?;
4675 let pair_fees_sat = lbtc_pair.fees.total(receiver_amount_sat);
4677 ensure_sdk!(
4678 receiver_amount_sat + pair_fees_sat == drain_amount_sat,
4679 LnUrlPayError::Generic {
4680 err: "Cannot drain without leaving a remainder".to_string(),
4681 }
4682 );
4683
4684 receiver_amount_sat * 1000
4685 }
4686 PayAmount::Bitcoin {
4687 receiver_amount_sat,
4688 } => receiver_amount_sat * 1000,
4689 PayAmount::Asset { .. } => {
4690 return Err(LnUrlPayError::Generic {
4691 err: "Cannot send an asset to a Bitcoin address".to_string(),
4692 })
4693 }
4694 };
4695
4696 match validate_lnurl_pay(
4697 self.rest_client.as_ref(),
4698 amount_msat,
4699 &req.comment,
4700 &req.data,
4701 self.config.network.into(),
4702 req.validate_success_action_url,
4703 )
4704 .await?
4705 {
4706 ValidatedCallbackResponse::EndpointError { data } => {
4707 Err(LnUrlPayError::Generic { err: data.reason })
4708 }
4709 ValidatedCallbackResponse::EndpointSuccess { data } => {
4710 let prepare_response = self
4711 .prepare_send_payment(&PrepareSendRequest {
4712 destination: data.pr.clone(),
4713 amount: Some(req.amount.clone()),
4714 disable_mrh: None,
4715 payment_timeout_sec: None,
4716 })
4717 .await?;
4718
4719 let destination = match prepare_response.destination {
4720 SendDestination::Bolt11 { invoice, .. } => SendDestination::Bolt11 {
4721 invoice,
4722 bip353_address: req.bip353_address,
4723 },
4724 SendDestination::LiquidAddress { address_data, .. } => {
4725 SendDestination::LiquidAddress {
4726 address_data,
4727 bip353_address: req.bip353_address,
4728 }
4729 }
4730 destination => destination,
4731 };
4732 let fees_sat = prepare_response
4733 .fees_sat
4734 .ok_or(PaymentError::InsufficientFunds)?;
4735
4736 Ok(PrepareLnUrlPayResponse {
4737 destination,
4738 fees_sat,
4739 data: req.data,
4740 amount: req.amount,
4741 comment: req.comment,
4742 success_action: data.success_action,
4743 })
4744 }
4745 }
4746 }
4747
4748 pub async fn lnurl_pay(
4761 &self,
4762 req: model::LnUrlPayRequest,
4763 ) -> Result<LnUrlPayResult, LnUrlPayError> {
4764 let prepare_response = req.prepare_response;
4765 let mut payment = self
4766 .send_payment(&SendPaymentRequest {
4767 prepare_response: PrepareSendResponse {
4768 destination: prepare_response.destination.clone(),
4769 fees_sat: Some(prepare_response.fees_sat),
4770 estimated_asset_fees: None,
4771 exchange_amount_sat: None,
4772 amount: Some(prepare_response.amount),
4773 disable_mrh: None,
4774 payment_timeout_sec: None,
4775 },
4776 use_asset_fees: None,
4777 payer_note: prepare_response.comment.clone(),
4778 })
4779 .await?
4780 .payment;
4781
4782 let maybe_sa_processed: Option<SuccessActionProcessed> = match prepare_response
4783 .success_action
4784 .clone()
4785 {
4786 Some(sa) => {
4787 match sa {
4788 SuccessAction::Aes { data } => {
4790 let PaymentDetails::Lightning {
4791 swap_id, preimage, ..
4792 } = &payment.details
4793 else {
4794 return Err(LnUrlPayError::Generic {
4795 err: format!("Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.", payment.details),
4796 });
4797 };
4798
4799 match preimage {
4800 Some(preimage_str) => {
4801 debug!(
4802 "Decrypting AES success action with preimage for Send Swap {swap_id}"
4803 );
4804 let preimage =
4805 sha256::Hash::from_str(preimage_str).map_err(|_| {
4806 LnUrlPayError::Generic {
4807 err: "Invalid preimage".to_string(),
4808 }
4809 })?;
4810 let preimage_arr = preimage.to_byte_array();
4811 let result = match (data, &preimage_arr).try_into() {
4812 Ok(data) => AesSuccessActionDataResult::Decrypted { data },
4813 Err(e) => AesSuccessActionDataResult::ErrorStatus {
4814 reason: e.to_string(),
4815 },
4816 };
4817 Some(SuccessActionProcessed::Aes { result })
4818 }
4819 None => {
4820 debug!("Preimage not yet available to decrypt AES success action for Send Swap {swap_id}");
4821 None
4822 }
4823 }
4824 }
4825 SuccessAction::Message { data } => {
4826 Some(SuccessActionProcessed::Message { data })
4827 }
4828 SuccessAction::Url { data } => Some(SuccessActionProcessed::Url { data }),
4829 }
4830 }
4831 None => None,
4832 };
4833
4834 let description = payment
4835 .details
4836 .get_description()
4837 .or_else(|| extract_description_from_metadata(&prepare_response.data));
4838
4839 let lnurl_pay_domain = match prepare_response.data.ln_address {
4840 Some(_) => None,
4841 None => Some(prepare_response.data.domain),
4842 };
4843 if let (Some(tx_id), Some(destination)) =
4844 (payment.tx_id.clone(), payment.destination.clone())
4845 {
4846 self.persister
4847 .insert_or_update_payment_details(PaymentTxDetails {
4848 tx_id: tx_id.clone(),
4849 destination,
4850 description,
4851 lnurl_info: Some(LnUrlInfo {
4852 ln_address: prepare_response.data.ln_address,
4853 lnurl_pay_comment: prepare_response.comment,
4854 lnurl_pay_domain,
4855 lnurl_pay_metadata: Some(prepare_response.data.metadata_str),
4856 lnurl_pay_success_action: maybe_sa_processed.clone(),
4857 lnurl_pay_unprocessed_success_action: prepare_response.success_action,
4858 lnurl_withdraw_endpoint: None,
4859 }),
4860 ..Default::default()
4861 })?;
4862 payment = self.persister.get_payment(&tx_id)?.unwrap_or(payment);
4864 }
4865
4866 Ok(LnUrlPayResult::EndpointSuccess {
4867 data: model::LnUrlPaySuccessData {
4868 payment,
4869 success_action: maybe_sa_processed,
4870 },
4871 })
4872 }
4873
4874 pub async fn lnurl_withdraw(
4881 &self,
4882 req: LnUrlWithdrawRequest,
4883 ) -> Result<LnUrlWithdrawResult, LnUrlWithdrawError> {
4884 let prepare_response = self
4885 .prepare_receive_payment(&{
4886 PrepareReceiveRequest {
4887 payment_method: PaymentMethod::Bolt11Invoice,
4888 amount: Some(ReceiveAmount::Bitcoin {
4889 payer_amount_sat: req.amount_msat / 1_000,
4890 }),
4891 }
4892 })
4893 .await?;
4894 let receive_res = self
4895 .receive_payment(&ReceivePaymentRequest {
4896 prepare_response,
4897 description: req.description.clone(),
4898 description_hash: None,
4899 payer_note: None,
4900 })
4901 .await?;
4902
4903 let Ok(invoice) = parse_invoice(&receive_res.destination) else {
4904 return Err(LnUrlWithdrawError::Generic {
4905 err: "Received unexpected output from receive request".to_string(),
4906 });
4907 };
4908
4909 let res =
4910 validate_lnurl_withdraw(self.rest_client.as_ref(), req.data.clone(), invoice.clone())
4911 .await?;
4912 if let LnUrlWithdrawResult::Ok { data: _ } = res {
4913 if let Some(ReceiveSwap {
4914 claim_tx_id: Some(tx_id),
4915 ..
4916 }) = self
4917 .persister
4918 .fetch_receive_swap_by_invoice(&invoice.bolt11)?
4919 {
4920 self.persister
4921 .insert_or_update_payment_details(PaymentTxDetails {
4922 tx_id,
4923 destination: receive_res.destination,
4924 description: req.description,
4925 lnurl_info: Some(LnUrlInfo {
4926 lnurl_withdraw_endpoint: Some(req.data.callback),
4927 ..Default::default()
4928 }),
4929 ..Default::default()
4930 })?;
4931 }
4932 }
4933 Ok(res)
4934 }
4935
4936 pub async fn lnurl_auth(
4942 &self,
4943 req_data: LnUrlAuthRequestData,
4944 ) -> Result<LnUrlCallbackStatus, LnUrlAuthError> {
4945 Ok(perform_lnurl_auth(
4946 self.rest_client.as_ref(),
4947 &req_data,
4948 &SdkLnurlAuthSigner::new(self.signer.clone()),
4949 )
4950 .await?)
4951 }
4952
4953 pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> {
4961 info!("Registering for webhook notifications");
4962 self.persister.set_webhook_url(webhook_url.clone())?;
4963
4964 let bolt12_offers = self.persister.list_bolt12_offers()?;
4966 for mut bolt12_offer in bolt12_offers {
4967 if bolt12_offer
4968 .webhook_url
4969 .clone()
4970 .is_none_or(|url| url != webhook_url)
4971 {
4972 let keypair = bolt12_offer.get_keypair()?;
4973 let webhook_url_hash_sig = utils::sign_message_hash(&webhook_url, &keypair)?;
4974 self.swapper
4975 .update_bolt12_offer(UpdateBolt12OfferRequest {
4976 offer: bolt12_offer.id.clone(),
4977 url: Some(webhook_url.clone()),
4978 signature: webhook_url_hash_sig.to_hex(),
4979 })
4980 .await?;
4981 bolt12_offer.webhook_url = Some(webhook_url.clone());
4982 self.persister
4983 .insert_or_update_bolt12_offer(&bolt12_offer)?;
4984 }
4985 }
4986
4987 Ok(())
4988 }
4989
4990 pub async fn unregister_webhook(&self) -> SdkResult<()> {
4997 info!("Unregistering for webhook notifications");
4998 let maybe_old_webhook_url = self.persister.get_webhook_url()?;
4999
5000 self.persister.remove_webhook_url()?;
5001
5002 if let Some(old_webhook_url) = maybe_old_webhook_url {
5004 let bolt12_offers = self
5005 .persister
5006 .list_bolt12_offers_by_webhook_url(&old_webhook_url)?;
5007 for mut bolt12_offer in bolt12_offers {
5008 let keypair = bolt12_offer.get_keypair()?;
5009 let update_hash_sig = utils::sign_message_hash("UPDATE", &keypair)?;
5010 self.swapper
5011 .update_bolt12_offer(UpdateBolt12OfferRequest {
5012 offer: bolt12_offer.id.clone(),
5013 url: None,
5014 signature: update_hash_sig.to_hex(),
5015 })
5016 .await?;
5017 bolt12_offer.webhook_url = None;
5018 self.persister
5019 .insert_or_update_bolt12_offer(&bolt12_offer)?;
5020 }
5021 }
5022
5023 Ok(())
5024 }
5025
5026 pub async fn fetch_fiat_rates(&self) -> Result<Vec<Rate>, SdkError> {
5028 self.fiat_api.fetch_fiat_rates().await.map_err(Into::into)
5029 }
5030
5031 pub async fn list_fiat_currencies(&self) -> Result<Vec<FiatCurrency>, SdkError> {
5034 self.fiat_api
5035 .list_fiat_currencies()
5036 .await
5037 .map_err(Into::into)
5038 }
5039
5040 pub async fn recommended_fees(&self) -> Result<RecommendedFees, SdkError> {
5042 Ok(self.bitcoin_chain_service.recommended_fees().await?)
5043 }
5044
5045 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
5046 pub fn default_config(
5048 network: LiquidNetwork,
5049 breez_api_key: Option<String>,
5050 ) -> Result<Config, SdkError> {
5051 let config = match network {
5052 LiquidNetwork::Mainnet => Config::mainnet_esplora(breez_api_key),
5053 LiquidNetwork::Testnet => {
5054 return Err(SdkError::network_not_supported(network));
5055 }
5056 LiquidNetwork::Regtest => Config::regtest_esplora(),
5057 };
5058
5059 Ok(config)
5060 }
5061
5062 pub async fn parse(&self, input: &str) -> Result<InputType, PaymentError> {
5066 let external_parsers = &self.external_input_parsers;
5067 let input_type =
5068 parse_with_rest_client(self.rest_client.as_ref(), input, Some(external_parsers))
5069 .await
5070 .map_err(|e| PaymentError::generic(e.to_string()))?;
5071
5072 let res = match input_type {
5073 InputType::LiquidAddress { ref address } => match &address.asset_id {
5074 Some(asset_id) if asset_id.ne(&self.config.lbtc_asset_id()) => {
5075 let asset_metadata = self.persister.get_asset_metadata(asset_id)?.ok_or(
5076 PaymentError::AssetError {
5077 err: format!("Asset {asset_id} is not supported"),
5078 },
5079 )?;
5080 let mut address = address.clone();
5081 address.set_amount_precision(asset_metadata.precision.into());
5082 InputType::LiquidAddress { address }
5083 }
5084 _ => input_type,
5085 },
5086 _ => input_type,
5087 };
5088 Ok(res)
5089 }
5090
5091 pub fn parse_invoice(input: &str) -> Result<LNInvoice, PaymentError> {
5093 parse_invoice(input).map_err(|e| PaymentError::invalid_invoice(e.to_string()))
5094 }
5095
5096 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
5120 pub fn init_logging(log_dir: &str, app_logger: Option<Box<dyn log::Log>>) -> Result<()> {
5121 crate::logger::init_logging(log_dir, app_logger)
5122 }
5123
5124 async fn start_plugin_inner(self: &Arc<Self>, plugin: &Arc<dyn Plugin>) -> SdkResult<()> {
5125 let plugin_id = plugin.id();
5126 let plugin_passphrase = self
5127 .signer
5128 .hmac_sha256(plugin_id.as_bytes().to_vec(), "m/49'/1'/0'/0/0".to_string())
5129 .map_err(|err| {
5130 SdkError::generic(format!("Could not generate plugin passphrase: {err}"))
5131 })?;
5132 let storage = PluginStorage::new(
5133 Arc::downgrade(&self.persister),
5134 &plugin_passphrase,
5135 plugin.id(),
5136 )?;
5137 plugin
5138 .on_start(PluginSdk::new(Arc::downgrade(self)), storage)
5139 .await;
5140 Ok(())
5141 }
5142
5143 pub async fn start_plugin(self: &Arc<Self>, plugin: Arc<dyn Plugin>) -> SdkResult<()> {
5144 let plugin_id = plugin.id();
5145 let mut plugins = self.plugins.lock().await;
5146 if plugins.get(&plugin_id).is_some() {
5147 return Err(SdkError::generic(format!(
5148 "Plugin {plugin_id} is already running"
5149 )));
5150 }
5151 plugins.insert(plugin_id, plugin.clone());
5152 self.start_plugin_inner(&plugin).await?;
5153 Ok(())
5154 }
5155}
5156
5157fn extract_description_from_metadata(request_data: &LnUrlPayRequestData) -> Option<String> {
5159 let metadata = request_data.metadata_vec().ok()?;
5160 metadata
5161 .iter()
5162 .find(|item| item.key == "text/plain")
5163 .map(|item| {
5164 info!("Extracted payment description: '{}'", item.value);
5165 item.value.clone()
5166 })
5167}
5168
5169#[cfg(test)]
5170mod tests {
5171 use std::time::Duration;
5172 use std::{str::FromStr, sync::Arc};
5173
5174 use anyhow::{anyhow, Result};
5175 use boltz_client::{
5176 boltz::{self, TransactionInfo},
5177 swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates},
5178 Secp256k1,
5179 };
5180 use lwk_wollet::{bitcoin::Network, hashes::hex::DisplayHex as _};
5181 use sdk_common::{
5182 bitcoin::hashes::hex::ToHex,
5183 lightning_with_bolt12::{
5184 ln::{channelmanager::PaymentId, inbound_payment::ExpandedKey},
5185 offers::{nonce::Nonce, offer::Offer},
5186 sign::RandomBytes,
5187 util::ser::Writeable,
5188 },
5189 };
5190 use tokio_with_wasm::alias as tokio;
5191
5192 use crate::test_utils::swapper::ZeroAmountSwapMockConfig;
5193 use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX;
5194 use crate::utils;
5195 use crate::{
5196 bitcoin, elements,
5197 model::{BtcHistory, Direction, LBtcHistory, PaymentState, Swap},
5198 sdk::LiquidSdk,
5199 test_utils::{
5200 chain::{MockBitcoinChainService, MockLiquidChainService},
5201 chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX},
5202 persist::{create_persister, new_receive_swap, new_send_swap},
5203 sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services},
5204 status_stream::MockStatusStream,
5205 swapper::MockSwapper,
5206 },
5207 };
5208 use crate::{
5209 model::CreateBolt12InvoiceRequest,
5210 test_utils::chain_swap::{
5211 TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX, TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX,
5212 TEST_LIQUID_OUTGOING_USER_LOCKUP_TX,
5213 },
5214 };
5215 use paste::paste;
5216
5217 #[cfg(feature = "browser-tests")]
5218 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
5219
5220 struct NewSwapArgs {
5221 direction: Direction,
5222 accepts_zero_conf: bool,
5223 initial_payment_state: Option<PaymentState>,
5224 receiver_amount_sat: Option<u64>,
5225 user_lockup_tx_id: Option<String>,
5226 zero_amount: bool,
5227 set_actual_payer_amount: bool,
5228 }
5229
5230 impl Default for NewSwapArgs {
5231 fn default() -> Self {
5232 Self {
5233 accepts_zero_conf: false,
5234 initial_payment_state: None,
5235 direction: Direction::Outgoing,
5236 receiver_amount_sat: None,
5237 user_lockup_tx_id: None,
5238 zero_amount: false,
5239 set_actual_payer_amount: false,
5240 }
5241 }
5242 }
5243
5244 impl NewSwapArgs {
5245 pub fn set_direction(mut self, direction: Direction) -> Self {
5246 self.direction = direction;
5247 self
5248 }
5249
5250 pub fn set_accepts_zero_conf(mut self, accepts_zero_conf: bool) -> Self {
5251 self.accepts_zero_conf = accepts_zero_conf;
5252 self
5253 }
5254
5255 pub fn set_receiver_amount_sat(mut self, receiver_amount_sat: Option<u64>) -> Self {
5256 self.receiver_amount_sat = receiver_amount_sat;
5257 self
5258 }
5259
5260 pub fn set_user_lockup_tx_id(mut self, user_lockup_tx_id: Option<String>) -> Self {
5261 self.user_lockup_tx_id = user_lockup_tx_id;
5262 self
5263 }
5264
5265 pub fn set_initial_payment_state(mut self, payment_state: PaymentState) -> Self {
5266 self.initial_payment_state = Some(payment_state);
5267 self
5268 }
5269
5270 pub fn set_zero_amount(mut self, zero_amount: bool) -> Self {
5271 self.zero_amount = zero_amount;
5272 self
5273 }
5274
5275 pub fn set_set_actual_payer_amount(mut self, set_actual_payer_amount: bool) -> Self {
5276 self.set_actual_payer_amount = set_actual_payer_amount;
5277 self
5278 }
5279 }
5280
5281 macro_rules! trigger_swap_update {
5282 (
5283 $type:literal,
5284 $args:expr,
5285 $persister:expr,
5286 $status_stream:expr,
5287 $status:expr,
5288 $transaction:expr,
5289 $zero_conf_rejected:expr
5290 ) => {{
5291 let swap = match $type {
5292 "chain" => {
5293 let swap = new_chain_swap(
5294 $args.direction,
5295 $args.initial_payment_state,
5296 $args.accepts_zero_conf,
5297 $args.user_lockup_tx_id,
5298 $args.zero_amount,
5299 $args.set_actual_payer_amount,
5300 $args.receiver_amount_sat,
5301 );
5302 $persister.insert_or_update_chain_swap(&swap).unwrap();
5303 Swap::Chain(swap)
5304 }
5305 "send" => {
5306 let swap =
5307 new_send_swap($args.initial_payment_state, $args.receiver_amount_sat);
5308 $persister.insert_or_update_send_swap(&swap).unwrap();
5309 Swap::Send(swap)
5310 }
5311 "receive" => {
5312 let swap =
5313 new_receive_swap($args.initial_payment_state, $args.receiver_amount_sat);
5314 $persister.insert_or_update_receive_swap(&swap).unwrap();
5315 Swap::Receive(swap)
5316 }
5317 _ => panic!(),
5318 };
5319
5320 $status_stream
5321 .clone()
5322 .send_mock_update(boltz::SwapStatus {
5323 id: swap.id(),
5324 status: $status.to_string(),
5325 transaction: $transaction,
5326 zero_conf_rejected: $zero_conf_rejected,
5327 ..Default::default()
5328 })
5329 .await
5330 .unwrap();
5331
5332 paste! {
5333 $persister.[<fetch _ $type _swap_by_id>](&swap.id())
5334 .unwrap()
5335 .ok_or(anyhow!("Could not retrieve {} swap", $type))
5336 .unwrap()
5337 }
5338 }};
5339 }
5340
5341 #[sdk_macros::async_test_all]
5342 async fn test_receive_swap_update_tracking() -> Result<()> {
5343 create_persister!(persister);
5344 let swapper = Arc::new(MockSwapper::default());
5345 let status_stream = Arc::new(MockStatusStream::new());
5346 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5347 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5348
5349 let sdk = new_liquid_sdk_with_chain_services(
5350 persister.clone(),
5351 swapper.clone(),
5352 status_stream.clone(),
5353 liquid_chain_service.clone(),
5354 bitcoin_chain_service.clone(),
5355 None,
5356 )
5357 .await?;
5358
5359 LiquidSdk::track_swap_updates(&sdk);
5360
5361 tokio::spawn(async move {
5363 let unrecoverable_states: [RevSwapStates; 4] = [
5365 RevSwapStates::SwapExpired,
5366 RevSwapStates::InvoiceExpired,
5367 RevSwapStates::TransactionFailed,
5368 RevSwapStates::TransactionRefunded,
5369 ];
5370
5371 for status in unrecoverable_states {
5372 let persisted_swap = trigger_swap_update!(
5373 "receive",
5374 NewSwapArgs::default(),
5375 persister,
5376 status_stream,
5377 status,
5378 None,
5379 None
5380 );
5381 assert_eq!(persisted_swap.state, PaymentState::Failed);
5382 }
5383
5384 for status in [
5387 RevSwapStates::TransactionMempool,
5388 RevSwapStates::TransactionConfirmed,
5389 ] {
5390 let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
5391 let mock_tx_id = mock_tx.txid();
5392 let height = (serde_json::to_string(&status).unwrap()
5393 == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
5394 as i32;
5395 liquid_chain_service.set_history(vec![LBtcHistory {
5396 txid: mock_tx_id,
5397 height,
5398 }]);
5399
5400 let persisted_swap = trigger_swap_update!(
5401 "receive",
5402 NewSwapArgs::default(),
5403 persister,
5404 status_stream,
5405 status,
5406 Some(TransactionInfo {
5407 id: mock_tx_id.to_string(),
5408 hex: Some(
5409 lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
5410 ),
5411 eta: None,
5412 }),
5413 None
5414 );
5415 assert!(persisted_swap.claim_tx_id.is_some());
5416 }
5417
5418 for status in [
5421 RevSwapStates::TransactionMempool,
5422 RevSwapStates::TransactionConfirmed,
5423 ] {
5424 let mock_tx = TEST_LIQUID_RECEIVE_LOCKUP_TX.clone();
5425 let mock_tx_id = mock_tx.txid();
5426 let height = (serde_json::to_string(&status).unwrap()
5427 == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
5428 as i32;
5429 liquid_chain_service.set_history(vec![LBtcHistory {
5430 txid: mock_tx_id,
5431 height,
5432 }]);
5433
5434 let persisted_swap = trigger_swap_update!(
5435 "receive",
5436 NewSwapArgs::default().set_receiver_amount_sat(Some(1000)),
5437 persister,
5438 status_stream,
5439 status,
5440 Some(TransactionInfo {
5441 id: mock_tx_id.to_string(),
5442 hex: Some(
5443 lwk_wollet::elements::encode::serialize(&mock_tx).to_lower_hex_string()
5444 ),
5445 eta: None
5446 }),
5447 None
5448 );
5449 assert!(persisted_swap.claim_tx_id.is_none());
5450 }
5451 })
5452 .await
5453 .unwrap();
5454
5455 Ok(())
5456 }
5457
5458 #[sdk_macros::async_test_all]
5459 async fn test_send_swap_update_tracking() -> Result<()> {
5460 create_persister!(persister);
5461 let swapper = Arc::new(MockSwapper::default());
5462 let status_stream = Arc::new(MockStatusStream::new());
5463
5464 let sdk = Arc::new(
5465 new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?,
5466 );
5467
5468 LiquidSdk::track_swap_updates(&sdk);
5469
5470 tokio::spawn(async move {
5472 let unrecoverable_states: [SubSwapStates; 3] = [
5474 SubSwapStates::TransactionLockupFailed,
5475 SubSwapStates::InvoiceFailedToPay,
5476 SubSwapStates::SwapExpired,
5477 ];
5478
5479 for status in unrecoverable_states {
5480 let persisted_swap = trigger_swap_update!(
5481 "send",
5482 NewSwapArgs::default(),
5483 persister,
5484 status_stream,
5485 status,
5486 None,
5487 None
5488 );
5489 assert_eq!(persisted_swap.state, PaymentState::Failed);
5490 }
5491
5492 let persisted_swap = trigger_swap_update!(
5495 "send",
5496 NewSwapArgs::default(),
5497 persister,
5498 status_stream,
5499 SubSwapStates::TransactionClaimPending,
5500 None,
5501 None
5502 );
5503 assert_eq!(persisted_swap.state, PaymentState::Complete);
5504 assert!(persisted_swap.preimage.is_some());
5505 })
5506 .await
5507 .unwrap();
5508
5509 Ok(())
5510 }
5511
5512 #[sdk_macros::async_test_all]
5513 async fn test_chain_swap_update_tracking() -> Result<()> {
5514 create_persister!(persister);
5515 let swapper = Arc::new(MockSwapper::default());
5516 let status_stream = Arc::new(MockStatusStream::new());
5517 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5518 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5519
5520 let sdk = new_liquid_sdk_with_chain_services(
5521 persister.clone(),
5522 swapper.clone(),
5523 status_stream.clone(),
5524 liquid_chain_service.clone(),
5525 bitcoin_chain_service.clone(),
5526 None,
5527 )
5528 .await?;
5529
5530 LiquidSdk::track_swap_updates(&sdk);
5531
5532 tokio::spawn(async move {
5534 let trigger_failed: [ChainSwapStates; 3] = [
5535 ChainSwapStates::TransactionFailed,
5536 ChainSwapStates::SwapExpired,
5537 ChainSwapStates::TransactionRefunded,
5538 ];
5539
5540 for direction in [Direction::Incoming, Direction::Outgoing] {
5542 for status in &trigger_failed {
5544 let persisted_swap = trigger_swap_update!(
5545 "chain",
5546 NewSwapArgs::default().set_direction(direction),
5547 persister,
5548 status_stream,
5549 status,
5550 None,
5551 None
5552 );
5553 assert_eq!(persisted_swap.state, PaymentState::Failed);
5554 }
5555
5556 let (mock_user_lockup_tx_hex, mock_user_lockup_tx_id) = match direction {
5557 Direction::Outgoing => {
5558 let tx = TEST_LIQUID_OUTGOING_USER_LOCKUP_TX.clone();
5559 (
5560 lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5561 tx.txid().to_string(),
5562 )
5563 }
5564 Direction::Incoming => {
5565 let tx = TEST_BITCOIN_INCOMING_USER_LOCKUP_TX.clone();
5566 (
5567 sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5568 tx.txid().to_string(),
5569 )
5570 }
5571 };
5572
5573 let (mock_server_lockup_tx_hex, mock_server_lockup_tx_id) = match direction {
5574 Direction::Incoming => {
5575 let tx = TEST_LIQUID_INCOMING_SERVER_LOCKUP_TX.clone();
5576 (
5577 lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(),
5578 tx.txid().to_string(),
5579 )
5580 }
5581 Direction::Outgoing => {
5582 let tx = TEST_BITCOIN_OUTGOING_SERVER_LOCKUP_TX.clone();
5583 (
5584 sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(),
5585 tx.txid().to_string(),
5586 )
5587 }
5588 };
5589
5590 for user_lockup_tx_id in &[None, Some(mock_user_lockup_tx_id.clone())] {
5594 if let Some(user_lockup_tx_id) = user_lockup_tx_id {
5595 match direction {
5596 Direction::Incoming => {
5597 bitcoin_chain_service.set_history(vec![BtcHistory {
5598 txid: bitcoin::Txid::from_str(user_lockup_tx_id).unwrap(),
5599 height: 0,
5600 }]);
5601 }
5602 Direction::Outgoing => {
5603 liquid_chain_service.set_history(vec![LBtcHistory {
5604 txid: elements::Txid::from_str(user_lockup_tx_id).unwrap(),
5605 height: 0,
5606 }]);
5607 }
5608 }
5609 }
5610 let persisted_swap = trigger_swap_update!(
5611 "chain",
5612 NewSwapArgs::default()
5613 .set_direction(direction)
5614 .set_initial_payment_state(PaymentState::Pending)
5615 .set_user_lockup_tx_id(user_lockup_tx_id.clone()),
5616 persister,
5617 status_stream,
5618 ChainSwapStates::TransactionLockupFailed,
5619 None,
5620 None
5621 );
5622 let expected_state = if user_lockup_tx_id.is_some() {
5623 match direction {
5624 Direction::Incoming => PaymentState::Refundable,
5625 Direction::Outgoing => PaymentState::RefundPending,
5626 }
5627 } else {
5628 PaymentState::Failed
5629 };
5630 assert_eq!(persisted_swap.state, expected_state);
5631 }
5632
5633 for status in [
5636 ChainSwapStates::TransactionMempool,
5637 ChainSwapStates::TransactionConfirmed,
5638 ] {
5639 if direction == Direction::Incoming {
5640 bitcoin_chain_service.set_history(vec![BtcHistory {
5641 txid: bitcoin::Txid::from_str(&mock_user_lockup_tx_id).unwrap(),
5642 height: 0,
5643 }]);
5644 bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]);
5645 }
5646 let persisted_swap = trigger_swap_update!(
5647 "chain",
5648 NewSwapArgs::default().set_direction(direction),
5649 persister,
5650 status_stream,
5651 status,
5652 Some(TransactionInfo {
5653 id: mock_user_lockup_tx_id.clone(),
5654 hex: Some(mock_user_lockup_tx_hex.clone()),
5655 eta: None
5656 }), Some(true) );
5659 assert_eq!(
5660 persisted_swap.user_lockup_tx_id,
5661 Some(mock_user_lockup_tx_id.clone())
5662 );
5663 assert!(!persisted_swap.accept_zero_conf);
5664 }
5665
5666 for accepts_zero_conf in [false, true] {
5672 let persisted_swap = trigger_swap_update!(
5673 "chain",
5674 NewSwapArgs::default()
5675 .set_direction(direction)
5676 .set_accepts_zero_conf(accepts_zero_conf)
5677 .set_set_actual_payer_amount(true),
5678 persister,
5679 status_stream,
5680 ChainSwapStates::TransactionServerMempool,
5681 Some(TransactionInfo {
5682 id: mock_server_lockup_tx_id.clone(),
5683 hex: Some(mock_server_lockup_tx_hex.clone()),
5684 eta: None,
5685 }),
5686 None
5687 );
5688 match accepts_zero_conf {
5689 false => {
5690 assert_eq!(persisted_swap.state, PaymentState::Pending);
5691 assert!(persisted_swap.server_lockup_tx_id.is_some());
5692 }
5693 true => {
5694 assert_eq!(persisted_swap.state, PaymentState::Pending);
5695 assert!(persisted_swap.claim_tx_id.is_some());
5696 }
5697 };
5698 }
5699
5700 let persisted_swap = trigger_swap_update!(
5703 "chain",
5704 NewSwapArgs::default()
5705 .set_direction(direction)
5706 .set_set_actual_payer_amount(true),
5707 persister,
5708 status_stream,
5709 ChainSwapStates::TransactionServerConfirmed,
5710 Some(TransactionInfo {
5711 id: mock_server_lockup_tx_id,
5712 hex: Some(mock_server_lockup_tx_hex),
5713 eta: None,
5714 }),
5715 None
5716 );
5717 assert_eq!(persisted_swap.state, PaymentState::Pending);
5718 assert!(persisted_swap.claim_tx_id.is_some());
5719 }
5720
5721 let persisted_swap = trigger_swap_update!(
5724 "chain",
5725 NewSwapArgs::default().set_direction(Direction::Outgoing),
5726 persister,
5727 status_stream,
5728 ChainSwapStates::Created,
5729 None,
5730 None
5731 );
5732 assert_eq!(persisted_swap.state, PaymentState::Pending);
5733 assert!(persisted_swap.user_lockup_tx_id.is_some());
5734 })
5735 .await
5736 .unwrap();
5737
5738 Ok(())
5739 }
5740
5741 #[sdk_macros::async_test_all]
5742 async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> {
5743 let user_lockup_sat = 50_000;
5744
5745 create_persister!(persister);
5746 let swapper = Arc::new(MockSwapper::new());
5747 let status_stream = Arc::new(MockStatusStream::new());
5748 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5749 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5750
5751 let sdk = new_liquid_sdk_with_chain_services(
5752 persister.clone(),
5753 swapper.clone(),
5754 status_stream.clone(),
5755 liquid_chain_service.clone(),
5756 bitcoin_chain_service.clone(),
5757 Some(0),
5758 )
5759 .await?;
5760
5761 LiquidSdk::track_swap_updates(&sdk);
5762
5763 tokio::spawn(async move {
5765 for fee_increase in [0, 1] {
5769 swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5770 user_lockup_sat,
5771 onchain_fee_increase_sat: fee_increase,
5772 });
5773 bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5774 let persisted_swap = trigger_swap_update!(
5775 "chain",
5776 NewSwapArgs::default()
5777 .set_direction(Direction::Incoming)
5778 .set_accepts_zero_conf(false)
5779 .set_zero_amount(true),
5780 persister,
5781 status_stream,
5782 ChainSwapStates::TransactionLockupFailed,
5783 None,
5784 None
5785 );
5786 match fee_increase {
5787 0 => {
5788 assert_eq!(persisted_swap.state, PaymentState::Created);
5789 }
5790 1 => {
5791 assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5792 }
5793 _ => panic!("Unexpected fee_increase"),
5794 }
5795 }
5796 })
5797 .await?;
5798
5799 Ok(())
5800 }
5801
5802 #[sdk_macros::async_test_all]
5803 async fn test_zero_amount_chain_swap_with_leeway() -> Result<()> {
5804 let user_lockup_sat = 50_000;
5805 let onchain_fee_rate_leeway_sat = 500;
5806
5807 create_persister!(persister);
5808 let swapper = Arc::new(MockSwapper::new());
5809 let status_stream = Arc::new(MockStatusStream::new());
5810 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5811 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5812
5813 let sdk = new_liquid_sdk_with_chain_services(
5814 persister.clone(),
5815 swapper.clone(),
5816 status_stream.clone(),
5817 liquid_chain_service.clone(),
5818 bitcoin_chain_service.clone(),
5819 Some(onchain_fee_rate_leeway_sat),
5820 )
5821 .await?;
5822
5823 LiquidSdk::track_swap_updates(&sdk);
5824
5825 tokio::spawn(async move {
5827 for fee_increase in [onchain_fee_rate_leeway_sat, onchain_fee_rate_leeway_sat + 1] {
5831 swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig {
5832 user_lockup_sat,
5833 onchain_fee_increase_sat: fee_increase,
5834 });
5835 bitcoin_chain_service.set_script_balance_sat(user_lockup_sat);
5836 let persisted_swap = trigger_swap_update!(
5837 "chain",
5838 NewSwapArgs::default()
5839 .set_direction(Direction::Incoming)
5840 .set_accepts_zero_conf(false)
5841 .set_zero_amount(true),
5842 persister,
5843 status_stream,
5844 ChainSwapStates::TransactionLockupFailed,
5845 None,
5846 None
5847 );
5848 match fee_increase {
5849 val if val == onchain_fee_rate_leeway_sat => {
5850 assert_eq!(persisted_swap.state, PaymentState::Created);
5851 }
5852 val if val == (onchain_fee_rate_leeway_sat + 1) => {
5853 assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance);
5854 }
5855 _ => panic!("Unexpected fee_increase"),
5856 }
5857 }
5858 })
5859 .await?;
5860
5861 Ok(())
5862 }
5863
5864 #[sdk_macros::async_test_all]
5865 async fn test_background_tasks() -> Result<()> {
5866 create_persister!(persister);
5867 let swapper = Arc::new(MockSwapper::new());
5868 let status_stream = Arc::new(MockStatusStream::new());
5869 let liquid_chain_service = Arc::new(MockLiquidChainService::new());
5870 let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new());
5871
5872 let sdk = new_liquid_sdk_with_chain_services(
5873 persister.clone(),
5874 swapper.clone(),
5875 status_stream.clone(),
5876 liquid_chain_service.clone(),
5877 bitcoin_chain_service.clone(),
5878 None,
5879 )
5880 .await?;
5881
5882 sdk.start().await?;
5883
5884 tokio::time::sleep(Duration::from_secs(3)).await;
5885
5886 sdk.disconnect().await?;
5887
5888 Ok(())
5889 }
5890
5891 #[sdk_macros::async_test_all]
5892 async fn test_create_bolt12_offer() -> Result<()> {
5893 create_persister!(persister);
5894
5895 let swapper = Arc::new(MockSwapper::default());
5896 let status_stream = Arc::new(MockStatusStream::new());
5897 let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5898
5899 let webhook_url = "https://example.com/webhook";
5901 persister.set_webhook_url(webhook_url.to_string())?;
5902
5903 let description = "test offer".to_string();
5905 let response = sdk.create_bolt12_offer(description.clone()).await?;
5906
5907 assert!(!response.destination.is_empty());
5909
5910 let offers = persister.list_bolt12_offers_by_webhook_url(webhook_url)?;
5912 assert_eq!(offers.len(), 1);
5913
5914 let offer = &offers[0];
5916 assert_eq!(offer.description, description);
5917 assert_eq!(offer.webhook_url, Some(webhook_url.to_string()));
5918 assert_eq!(offer.id, response.destination);
5919
5920 assert!(!offer.private_key.is_empty());
5922
5923 Ok(())
5924 }
5925
5926 #[sdk_macros::async_test_all]
5927 async fn test_create_bolt12_receive_swap() -> Result<()> {
5928 create_persister!(persister);
5929
5930 let swapper = Arc::new(MockSwapper::default());
5931 let status_stream = Arc::new(MockStatusStream::new());
5932 let sdk = new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?;
5933
5934 let webhook_url = "https://example.com/webhook";
5936 persister.set_webhook_url(webhook_url.to_string())?;
5937
5938 let description = "test offer".to_string();
5940 let response = sdk.create_bolt12_offer(description.clone()).await?;
5941 let offer = persister
5942 .fetch_bolt12_offer_by_id(&response.destination)?
5943 .unwrap();
5944
5945 let expanded_key = ExpandedKey::new([42; 32]);
5947 let entropy_source = RandomBytes::new(utils::generate_entropy());
5948 let nonce = Nonce::from_entropy_source(&entropy_source);
5949 let secp = Secp256k1::new();
5950 let payment_id = PaymentId([1; 32]);
5951 let invoice_request = TryInto::<Offer>::try_into(offer.clone())?
5952 .request_invoice(&expanded_key, nonce, &secp, payment_id)
5953 .unwrap()
5954 .amount_msats(1_000_000)
5955 .unwrap()
5956 .chain(Network::Regtest)
5957 .unwrap()
5958 .build_and_sign()
5959 .unwrap();
5960 let mut buffer = Vec::new();
5961 invoice_request.write(&mut buffer).unwrap();
5962
5963 let create_res = sdk
5965 .create_bolt12_invoice(&CreateBolt12InvoiceRequest {
5966 offer: offer.id,
5967 invoice_request: buffer.to_hex(),
5968 })
5969 .await
5970 .unwrap();
5971 assert!(create_res.invoice.starts_with("lni"));
5972
5973 Ok(())
5974 }
5975}