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