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