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