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