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