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