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