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