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